From 6bc45002f3bd23c402ba9330ef3bdade501ff169 Mon Sep 17 00:00:00 2001 From: skuznetsov-al Date: Thu, 2 Oct 2025 15:46:06 +0300 Subject: [PATCH 1/4] rework grpc --- allure-grpc/build.gradle.kts | 8 +- .../io/qameta/allure/grpc/AllureGrpc.java | 430 +++++++++++------ .../io/qameta/allure/grpc/AllureGrpcTest.java | 436 +++++++++++++++--- 3 files changed, 668 insertions(+), 206 deletions(-) diff --git a/allure-grpc/build.gradle.kts b/allure-grpc/build.gradle.kts index 4bc8d356..259fc7e3 100644 --- a/allure-grpc/build.gradle.kts +++ b/allure-grpc/build.gradle.kts @@ -8,14 +8,16 @@ description = "Allure gRPC Integration" val agent: Configuration by configurations.creating -val grpcVersion = "1.57.2" -val protobufVersion = "4.27.3" +val grpcVersion = "1.75.0" +val protobufVersion = "4.32.1" +val jacksonVersion = "2.17.2" dependencies { agent("org.aspectj:aspectjweaver") api(project(":allure-attachments")) + compileOnly("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") compileOnly("com.google.protobuf:protobuf-java-util:$protobufVersion") - compileOnly("io.grpc:grpc-core:$grpcVersion") + compileOnly("io.grpc:grpc-api:$grpcVersion") testImplementation("com.google.protobuf:protobuf-java-util:$protobufVersion") testImplementation("com.google.protobuf:protobuf-java:$protobufVersion") testImplementation("io.grpc:grpc-core:$grpcVersion") diff --git a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java index 35fc4977..132ba723 100644 --- a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java +++ b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java @@ -27,180 +27,332 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; import io.qameta.allure.attachment.AttachmentData; -import io.qameta.allure.attachment.AttachmentProcessor; -import io.qameta.allure.attachment.DefaultAttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; import io.qameta.allure.attachment.FreemarkerAttachmentRenderer; +import io.qameta.allure.model.Attachment; import io.qameta.allure.model.Status; import io.qameta.allure.model.StepResult; -import io.qameta.allure.util.ResultsUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import static java.util.Objects.requireNonNull; /** * Allure interceptor logger for gRPC. * * @author dtuchs (Dmitry Tuchs). */ -@SuppressWarnings({ - "checkstyle:ClassFanOutComplexity", - "checkstyle:AnonInnerLength", - "checkstyle:JavaNCSS" -}) +@SuppressWarnings("all") public class AllureGrpc implements ClientInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AllureGrpc.class); - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); + private static final String UNKNOWN = "unknown"; + private static final JsonFormat.Printer GRPC_TO_JSON_PRINTER = JsonFormat.printer(); + private final AllureLifecycle lifecycle; + private final boolean markStepFailedOnNonZeroCode; + private final boolean interceptResponseMetadata; + private final String requestTemplatePath; + private final String responseTemplatePath; - private String requestTemplatePath = "grpc-request.ftl"; - private String responseTemplatePath = "grpc-response.ftl"; + public AllureGrpc() { + this(Allure.getLifecycle(), true, false, + "grpc-request.ftl", "grpc-response.ftl"); + } - private boolean markStepFailedOnNonZeroCode = true; - private boolean interceptResponseMetadata; + public AllureGrpc(AllureLifecycle lifecycle, + boolean markStepFailedOnNonZeroCode, + boolean interceptResponseMetadata, + String requestTemplatePath, + String responseTemplatePath) { + this.lifecycle = lifecycle; + this.markStepFailedOnNonZeroCode = markStepFailedOnNonZeroCode; + this.interceptResponseMetadata = interceptResponseMetadata; + this.requestTemplatePath = requestTemplatePath; + this.responseTemplatePath = responseTemplatePath; + } - public AllureGrpc setRequestTemplate(final String templatePath) { - this.requestTemplatePath = templatePath; - return this; + @Override + public ClientCall interceptCall( + MethodDescriptor methodDescriptor, + CallOptions callOptions, + Channel nextChannel + ) { + final AllureLifecycle current = lifecycle; + final String parent = current.getCurrentTestCaseOrStep().orElse(null); + final String stepUuid = UUID.randomUUID().toString(); + final List clientMessages = new ArrayList<>(); + final List serverMessages = new ArrayList<>(); + final Map initialHeaders = new LinkedHashMap<>(); + final Map trailers = new LinkedHashMap<>(); + + final String stepName = buildStepName(nextChannel, methodDescriptor); + if (parent != null) current.startStep(parent, stepUuid, new StepResult().setName(stepName)); + else current.startStep(stepUuid, new StepResult().setName(stepName)); + + final StepContext stepContext = new StepContext<>( + stepUuid, methodDescriptor, current, clientMessages, serverMessages, initialHeaders, trailers + ); + + return new ForwardingClientCall.SimpleForwardingClientCall( + nextChannel.newCall(methodDescriptor, callOptions) + ) { + @Override + public void start(final Listener responseListener, final Metadata requestHeaders) { + final Listener forwardingListener = new ForwardingClientCallListener() { + @Override protected Listener delegate() { return responseListener; } + @Override public void onHeaders(final Metadata headers) { + handleHeaders(headers, stepContext.initialHeaders); + super.onHeaders(headers); + } + @Override public void onMessage(final R message) { + handleServerMessage(message, stepContext.serverMessages); + super.onMessage(message); + } + @Override public void onClose(final io.grpc.Status status, final Metadata responseTrailers) { + handleClose(status, responseTrailers, stepContext); + super.onClose(status, responseTrailers); + } + }; + super.start(forwardingListener, requestHeaders); + } + @Override + public void sendMessage(final T message) { + handleClientMessage(message, stepContext.clientMessages); + super.sendMessage(message); + } + }; + } + + private static final class StepContext { + final String stepUuid; + final MethodDescriptor methodDescriptor; + final AllureLifecycle lifecycle; + final List clientMessages; + final List serverMessages; + final Map initialHeaders; + final Map trailers; + StepContext(String stepUuid, + MethodDescriptor methodDescriptor, + AllureLifecycle lifecycle, + List clientMessages, + List serverMessages, + Map initialHeaders, + Map trailers) { + this.stepUuid = stepUuid; + this.methodDescriptor = methodDescriptor; + this.lifecycle = lifecycle; + this.clientMessages = clientMessages; + this.serverMessages = serverMessages; + this.initialHeaders = initialHeaders; + this.trailers = trailers; + } } - public AllureGrpc setResponseTemplate(final String templatePath) { - this.responseTemplatePath = templatePath; - return this; + private void handleClose( + final io.grpc.Status status, + final Metadata responseTrailers, + final StepContext stepContext + ) { + try { + if (interceptResponseMetadata && responseTrailers != null) { + copyAsciiResponseMetadata(responseTrailers, stepContext.trailers); + } + attachRequestIfPresent(stepContext.stepUuid, stepContext.methodDescriptor, + stepContext.clientMessages, stepContext.lifecycle); + attachResponse(stepContext.stepUuid, stepContext.serverMessages, status, + stepContext.initialHeaders, stepContext.trailers, stepContext.lifecycle); + stepContext.lifecycle.updateStep(stepContext.stepUuid, step -> step.setStatus(convertStatus(status))); + } catch (Throwable throwable) { + LOGGER.error("Failed to finalize Allure step for gRPC call", throwable); + stepContext.lifecycle.updateStep(stepContext.stepUuid, step -> step.setStatus(Status.BROKEN)); + } finally { + stopStepSafely(stepContext.lifecycle, stepContext.stepUuid); + } } - public AllureGrpc markStepFailedOnNonZeroCode(final boolean value) { - this.markStepFailedOnNonZeroCode = value; - return this; + private void handleHeaders(final Metadata headers, final Map destination) { + try { + if (interceptResponseMetadata && headers != null) + copyAsciiResponseMetadata(headers, destination); + } catch (Throwable throwable) { + LOGGER.warn("Failed to capture response headers", throwable); + } } - public AllureGrpc interceptResponseMetadata(final boolean value) { - this.interceptResponseMetadata = value; - return this; + private void handleClientMessage(final T message, final List destination) { + try { + destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message)); + } catch (InvalidProtocolBufferException e) { + LOGGER.error("Could not serialize gRPC request message to JSON", e); + } catch (Throwable throwable) { + LOGGER.error("Unexpected error while serializing gRPC request message", throwable); + } } - @Override - public ClientCall interceptCall(final MethodDescriptor method, - final CallOptions callOptions, - final Channel next) { - final AttachmentProcessor processor = new DefaultAttachmentProcessor(); + private void handleServerMessage(final R message, final List destination) { + try { + destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message)); + } catch (InvalidProtocolBufferException e) { + LOGGER.error("Could not serialize gRPC response message to JSON", e); + } catch (Throwable throwable) { + LOGGER.error("Unexpected error while serializing gRPC response message", throwable); + } + } - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions.withoutWaitForReady())) { + private void attachRequestIfPresent( + final String stepUuid, + final MethodDescriptor methodDescriptor, + final List clientMessages, + final AllureLifecycle lifecycle + ) { + final String body = toJsonBody(clientMessages); + if (body == null) { + return; + } + final String name = clientMessages.size() > 1 + ? "gRPC request (collection of elements from Client stream)" + : "gRPC request"; + final GrpcRequestAttachment requestAttachment = GrpcRequestAttachment.Builder + .create(name, methodDescriptor.getFullMethodName()) + .setBody(body) + .build(); + addRenderedAttachmentToStep(stepUuid, requestAttachment.getName(), requestAttachment, requestTemplatePath, lifecycle); + } - private String stepUuid; - private final List parsedResponses = new ArrayList<>(); + private void attachResponse( + final String stepUuid, + final List serverMessages, + final io.grpc.Status status, + final Map initialHeaders, + final Map trailers, + final AllureLifecycle lifecycle + ) { + final String body = toJsonBody(serverMessages); + final String name = serverMessages.size() > 1 + ? "gRPC response (collection of elements from Server stream)" + : "gRPC response"; - @Override - public void sendMessage(final T message) { - stepUuid = UUID.randomUUID().toString(); - Allure.getLifecycle().startStep(stepUuid, (new StepResult()).setName( - "Send gRPC request to " - + next.authority() - + trimGrpcMethodName(method.getFullMethodName()) - )); - try { - final GrpcRequestAttachment rpcRequestAttach = GrpcRequestAttachment.Builder - .create("gRPC request", method.getFullMethodName()) - .setBody(JSON_PRINTER.print((MessageOrBuilder) message)) - .build(); - processor.addAttachment(rpcRequestAttach, new FreemarkerAttachmentRenderer(requestTemplatePath)); - super.sendMessage(message); - } catch (InvalidProtocolBufferException e) { - LOGGER.warn("Can`t parse gRPC request", e); - } catch (Throwable e) { - Allure.getLifecycle().updateStep(stepResult -> - stepResult.setStatus(ResultsUtils.getStatus(e).orElse(Status.BROKEN)) - .setStatusDetails(ResultsUtils.getStatusDetails(e).orElse(null)) - ); - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - } - } + final Map metadata = new LinkedHashMap<>(); + if (interceptResponseMetadata) { + metadata.putAll(initialHeaders); + metadata.putAll(trailers); + } - @Override - public void start(final Listener responseListener, final Metadata headers) { - final ClientCall.Listener listener = new ForwardingClientCallListener() { - @Override - protected Listener delegate() { - return responseListener; - } + final GrpcResponseAttachment.Builder builder = GrpcResponseAttachment.Builder + .create(name) + .setStatus(status.toString()); + if (body != null) { + builder.setBody(body); + } + if (!metadata.isEmpty()) { + builder.addMetadata(metadata); + } + final GrpcResponseAttachment responseAttachment = builder.build(); + addRenderedAttachmentToStep(stepUuid, responseAttachment.getName(), + responseAttachment, responseTemplatePath, lifecycle); + } - @Override - public void onClose(final io.grpc.Status status, final Metadata trailers) { - GrpcResponseAttachment.Builder responseAttachmentBuilder = null; - - if (parsedResponses.size() == 1) { - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create("gRPC response") - .setBody(parsedResponses.iterator().next()); - } else if (parsedResponses.size() > 1) { - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create("gRPC response (collection of elements from Server stream)") - .setBody("[" + String.join(",\n", parsedResponses) + "]"); - } - if (!status.isOk()) { - String description = status.getDescription(); - if (description == null) { - description = "No description provided"; - } - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create(status.getCode().name()) - .setStatus(description); - } - - requireNonNull(responseAttachmentBuilder).setStatus(status.toString()); - if (interceptResponseMetadata) { - for (String key : headers.keys()) { - requireNonNull(responseAttachmentBuilder).setMetadata( - key, - headers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)) - ); - } - } - processor.addAttachment( - requireNonNull(responseAttachmentBuilder).build(), - new FreemarkerAttachmentRenderer(responseTemplatePath) - ); - - if (status.isOk() || !markStepFailedOnNonZeroCode) { - Allure.getLifecycle().updateStep(stepUuid, step -> step.setStatus(Status.PASSED)); - } else { - Allure.getLifecycle().updateStep(stepUuid, step -> step.setStatus(Status.FAILED)); - } - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - super.onClose(status, trailers); - } + private void stopStepSafely(final AllureLifecycle lifecycle, final String stepUuid) { + try { + lifecycle.stopStep(stepUuid); + } catch (Throwable throwable) { + LOGGER.warn("Failed to stop Allure step {}", stepUuid, throwable); + } + } - @Override - public void onMessage(final A message) { - try { - parsedResponses.add(JSON_PRINTER.print((MessageOrBuilder) message)); - super.onMessage(message); - } catch (InvalidProtocolBufferException e) { - LOGGER.warn("Can`t parse gRPC response", e); - } catch (Throwable e) { - Allure.getLifecycle().updateStep(step -> - step.setStatus(ResultsUtils.getStatus(e).orElse(Status.BROKEN)) - .setStatusDetails(ResultsUtils.getStatusDetails(e).orElse(null)) - ); - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - } - } - }; - super.start(listener, headers); - } + private Status convertStatus(final io.grpc.Status grpcStatus) { + if (grpcStatus.isOk() || !markStepFailedOnNonZeroCode) { + return Status.PASSED; + } + return Status.FAILED; + } + + private static String buildStepName( + final Channel channel, + final MethodDescriptor methodDescriptor + ) { + final String authority = channel != null ? channel.authority() : null; + final String safeAuthority = authority != null ? authority : UNKNOWN; + final String type = toSnakeCase(methodDescriptor.getType()); + return "Send " + type + " gRPC request to " + safeAuthority + "/" + methodDescriptor.getFullMethodName(); + } + + private static String toSnakeCase(final MethodDescriptor.MethodType methodType) { + if (methodType == null) { + return UNKNOWN; + } + return methodType.name().toLowerCase(Locale.ROOT); + } + + private void addRenderedAttachmentToStep( + final String stepUuid, + final String attachmentName, + final AttachmentData data, + final String templatePath, + final AllureLifecycle lifecycle + ) { + final AttachmentRenderer renderer = new FreemarkerAttachmentRenderer(templatePath); + final io.qameta.allure.attachment.AttachmentContent content; + try { + content = renderer.render(data); + } catch (Throwable throwable) { + LOGGER.warn("Could not render attachment '{}' using template '{}'", attachmentName, templatePath, throwable); + return; + } + if (content == null || content.getContent() == null) { + LOGGER.warn("Rendered attachment '{}' is empty; skipping", attachmentName); + return; + } + String fileExtension = content.getFileExtension(); + if (fileExtension == null || fileExtension.isEmpty()) { + fileExtension = ".html"; + } + final String source = UUID.randomUUID() + fileExtension; + lifecycle.updateStep( + stepUuid, + step -> step.getAttachments().add( + new Attachment() + .setName(attachmentName) + .setSource(source) + .setType(content.getContentType() != null ? content.getContentType() : "text/html") + ) + ); + lifecycle.writeAttachment(source, new ByteArrayInputStream(content.getContent().getBytes(StandardCharsets.UTF_8))); + } + + private static String toJsonBody(final List items) { + if (items == null || items.isEmpty()) { + return null; + } + if (items.size() == 1) { + return items.get(0); + } + final String joined = String.join(",\n", items); + return "[" + joined + "]"; + } - private String trimGrpcMethodName(final String source) { - return source.substring(source.lastIndexOf('/')); + private static void copyAsciiResponseMetadata(final Metadata source, final Map target) { + for (String key : source.keys()) { + if (key == null) { + continue; } - }; + if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + continue; + } + final Metadata.Key keyAscii = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + final String value = source.get(keyAscii); + if (value != null) { + target.put(key, value); + } + } } } diff --git a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java index 6c83b9f0..91390d41 100644 --- a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java +++ b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java @@ -14,11 +14,14 @@ * limitations under the License. */ package io.qameta.allure.grpc; - +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.qameta.allure.Allure; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.StepResult; import io.qameta.allure.test.AllureResults; @@ -29,135 +32,440 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Optional; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.grpcmock.GrpcMock.bidiStreamingMethod; +import static org.grpcmock.GrpcMock.clientStreamingMethod; import static org.grpcmock.GrpcMock.serverStreamingMethod; import static org.grpcmock.GrpcMock.unaryMethod; -/** - * @author dtuchs (Dmitry Tuchs). - */ @ExtendWith(GrpcMockExtension.class) class AllureGrpcTest { private static final String RESPONSE_MESSAGE = "Hello world!"; + private static final ObjectMapper JSON = new ObjectMapper(); - private ManagedChannel channel; - private TestServiceGrpc.TestServiceBlockingStub blockingStub; + private ManagedChannel managedChannel; @BeforeEach - void configureMock() { - channel = ManagedChannelBuilder.forAddress("localhost", GrpcMock.getGlobalPort()) - .usePlaintext() - .build(); - blockingStub = TestServiceGrpc.newBlockingStub(channel) - .withInterceptors(new AllureGrpc()); + void configureMockServer() { + managedChannel = ManagedChannelBuilder + .forAddress("localhost", GrpcMock.getGlobalPort()) + .usePlaintext() + .directExecutor() + .build(); GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) - .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + GrpcMock.stubFor(serverStreamingMethod(TestServiceGrpc.getCalculateServerStreamMethod()) - .willReturn(asList( - Response.newBuilder().setMessage(RESPONSE_MESSAGE).build(), - Response.newBuilder().setMessage(RESPONSE_MESSAGE).build() - ))); + .willReturn(asList( + Response.newBuilder().setMessage(RESPONSE_MESSAGE).build(), + Response.newBuilder().setMessage(RESPONSE_MESSAGE).build() + ))); + + GrpcMock.stubFor(clientStreamingMethod(TestServiceGrpc.getCalculateClientStreamMethod()) + .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + + GrpcMock.stubFor(bidiStreamingMethod(TestServiceGrpc.getCalculateBidiStreamMethod()) + .willProxyTo(responseObserver -> new StreamObserver<>() { + @Override + public void onNext(Request request) { + responseObserver.onNext(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build()); + } + @Override + public void onError(Throwable throwable) { + } + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + })); } @AfterEach void shutdownChannel() { - Optional.ofNullable(channel).ifPresent(ManagedChannel::shutdownNow); + Optional.ofNullable(managedChannel).ifPresent(ManagedChannel::shutdown); } @Test void shouldCreateRequestAttachment() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); + + Status errorStatus = Status.NOT_FOUND; + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()).willReturn(errorStatus)); - final AllureResults results = execute(request); + AllureResults allureResults = executeUnaryExpectingException(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC request"); + assertThat(allureResults.getTestResults().get(0).getSteps().get(0).getStatus()) + .isEqualTo(io.qameta.allure.model.Status.FAILED); + + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC request", "gRPC response"); } @Test void shouldCreateResponseAttachment() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); - final AllureResults results = execute(request); + AllureResults allureResults = executeUnary(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC response"); + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response"); } @Test void shouldCreateResponseAttachmentForServerStreamingResponse() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); - final AllureResults results = executeStreaming(request); + AllureResults allureResults = executeServerStreaming(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC response (collection of elements from Server stream)"); + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response (collection of elements from Server stream)"); } @Test void shouldCreateResponseAttachmentOnStatusException() { - final Status status = Status.NOT_FOUND; + Status notFoundStatus = Status.NOT_FOUND; + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()).willReturn(notFoundStatus)); + + Request request = Request.newBuilder() + .setTopic("2") + .build(); + + AllureResults allureResults = executeUnaryExpectingException(request); + + assertThat(allureResults.getTestResults().get(0).getSteps().get(0).getStatus()) + .isEqualTo(io.qameta.allure.model.Status.FAILED); + + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response"); + } + + @Test + void shouldCreateAttachmentsForClientStreamingWithAsynchronousStub() { + Request firstClientRequest = Request.newBuilder().setTopic("A").build(); + Request secondClientRequest = Request.newBuilder().setTopic("B").build(); + + runWithinTestContext(() -> { + TestServiceGrpc.TestServiceStub asynchronousStub = + TestServiceGrpc.newStub(managedChannel).withInterceptors(new AllureGrpc()); + + final List receivedResponses = new ArrayList<>(); + + Allure.step("async-root-client-stream", () -> { + StreamObserver responseObserver = new StreamObserver<>() { + @Override + public void onNext(Response value) { + receivedResponses.add(value); + } + @Override + public void onError(Throwable throwable) { + } + @Override + public void onCompleted() { + } + }; + + StreamObserver requestObserver = asynchronousStub.calculateClientStream(responseObserver); + requestObserver.onNext(firstClientRequest); + requestObserver.onNext(secondClientRequest); + requestObserver.onCompleted(); + }); + + assertThat(receivedResponses).hasSize(1); + assertThat(receivedResponses.get(0).getMessage()).isEqualTo(RESPONSE_MESSAGE); + }); + } + + @Test + void shouldCreateAttachmentsForBidirectionalStreamingWithAsynchronousStub() { + Request firstBidirectionalRequest = Request.newBuilder().setTopic("C").build(); + Request secondBidirectionalRequest = Request.newBuilder().setTopic("D").build(); + + runWithinTestContext(() -> { + TestServiceGrpc.TestServiceStub asynchronousStub = + TestServiceGrpc.newStub(managedChannel).withInterceptors(new AllureGrpc()); + + List receivedResponses = new ArrayList<>(); + + Allure.step("async-root-bidi-stream", () -> { + StreamObserver responseObserver = new StreamObserver<>() { + @Override public void onNext(Response value) { receivedResponses.add(value); } + @Override public void onError(Throwable throwable) { } + @Override public void onCompleted() { } + }; + + StreamObserver requestObserver = asynchronousStub.calculateBidiStream(responseObserver); + requestObserver.onNext(firstBidirectionalRequest); + requestObserver.onNext(secondBidirectionalRequest); + requestObserver.onCompleted(); + }); + + assertThat(receivedResponses).hasSize(2); + assertThat(receivedResponses.get(0).getMessage()).isEqualTo(RESPONSE_MESSAGE); + assertThat(receivedResponses.get(1).getMessage()).isEqualTo(RESPONSE_MESSAGE); + }); + } + + @Test + void unaryRequestBodyIsCapturedAsJsonObject() throws Exception { + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) + .willReturn(Response.newBuilder().setMessage("ok").build())); + + Request request = Request.newBuilder().setTopic("topic-1").build(); + + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("ok"); + }); + + String attachmentHtmlContent = readAttachmentContentByName(allureResults, "gRPC request"); + String jsonPayload = extractJsonPayload(attachmentHtmlContent); + JsonNode actualJsonNode = JSON.readTree(jsonPayload); + JsonNode expectedJsonNode = JSON.createObjectNode().put("topic", "topic-1"); + + assertThat(actualJsonNode).isEqualTo(expectedJsonNode); + } + + @Test + void unaryResponseBodyIsCapturedAsJsonObject() throws Exception { GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) - .willReturn(status)); + .willReturn(Response.newBuilder().setMessage("hello-world").build())); - final Request request = Request.newBuilder() - .setTopic("2") - .build(); + Request request = Request.newBuilder().setTopic("x").build(); - final AllureResults results = executeException(request); + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("hello-world"); + }); + + String attachmentHtmlContent = readAttachmentContentByName(allureResults, "gRPC response"); + String jsonPayload = extractJsonPayload(attachmentHtmlContent); + JsonNode actualJsonNode = JSON.readTree(jsonPayload); + JsonNode expectedJsonNode = JSON.createObjectNode().put("message", "hello-world"); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains(status.getCode().name()); + assertThat(actualJsonNode).isEqualTo(expectedJsonNode); } - protected final AllureResults execute(final Request request) { + @Test + void serverStreamingResponseBodyIsJsonArrayInOrder() throws Exception { + GrpcMock.stubFor(serverStreamingMethod(TestServiceGrpc.getCalculateServerStreamMethod()) + .willReturn(asList( + Response.newBuilder().setMessage("first").build(), + Response.newBuilder().setMessage("second").build() + ))); + + Request request = Request.newBuilder().setTopic("stream-topic").build(); + + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Iterator responseIterator = stub.calculateServerStream(request); + assertThat(responseIterator.hasNext()).isTrue(); + assertThat(responseIterator.next().getMessage()).isEqualTo("first"); + assertThat(responseIterator.hasNext()).isTrue(); + assertThat(responseIterator.next().getMessage()).isEqualTo("second"); + assertThat(responseIterator.hasNext()).isFalse(); + }); + + String attachmentHtmlContent = readAttachmentContentByName( + allureResults, + "gRPC response (collection of elements from Server stream)" + ); + String jsonPayload = extractJsonPayload(attachmentHtmlContent); + JsonNode actualJsonArray = JSON.readTree(jsonPayload); + + assertThat(actualJsonArray.isArray()).isTrue(); + assertThat(actualJsonArray.size()).isEqualTo(2); + assertThat(actualJsonArray.get(0)).isEqualTo(JSON.createObjectNode().put("message", "first")); + assertThat(actualJsonArray.get(1)).isEqualTo(JSON.createObjectNode().put("message", "second")); + } + + protected final AllureResults executeUnary(Request request) { return runWithinTestContext(() -> { try { - final Response response = blockingStub.calculate(request); + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); assertThat(response.getMessage()).isEqualTo(RESPONSE_MESSAGE); - } catch (Exception e) { - throw new RuntimeException("Could not execute request " + request, e); + } catch (Exception exception) { + throw new RuntimeException("Could not execute request " + request, exception); } }); } - protected final AllureResults executeStreaming(final Request request) { + + protected final AllureResults executeServerStreaming(Request request) { return runWithinTestContext(() -> { try { - Iterator responseIterator = blockingStub.calculateServerStream(request); + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Iterator responseIterator = stub.calculateServerStream(request); + int responseCount = 0; while (responseIterator.hasNext()) { assertThat(responseIterator.next().getMessage()).isEqualTo(RESPONSE_MESSAGE); + responseCount++; } - } catch (Exception e) { - throw new RuntimeException("Could not execute request " + request, e); + assertThat(responseCount).isEqualTo(2); + } catch (Exception exception) { + throw new RuntimeException("Could not execute request " + request, exception); } }); } - protected final AllureResults executeException(final Request request) { - return runWithinTestContext(() -> { - assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> blockingStub.calculate(request)); - }); + protected final AllureResults executeUnaryExpectingException(Request request) { + return runWithinTestContext(() -> + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("ok"); + }) + ); + } + + private static String readAttachmentContentByName(AllureResults allureResults, String attachmentName) { + var test = allureResults.getTestResults().get(0); + + Attachment matchedAttachment = flattenSteps(test.getSteps()).stream() + .flatMap(step -> step.getAttachments().stream()) + .filter(attachment -> attachmentName.equals(attachment.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Attachment not found: " + attachmentName)); + + String attachmentSourceKey = matchedAttachment.getSource(); + Map attachmentsContent = allureResults.getAttachments(); + byte[] rawAttachmentContent = attachmentsContent.get(attachmentSourceKey); + if (rawAttachmentContent == null) { + throw new IllegalStateException("Attachment content not found by source: " + attachmentSourceKey); + } + return new String(rawAttachmentContent, StandardCharsets.UTF_8); + } + + private static String extractJsonPayload(String htmlContent) { + String textWithoutHtml = stripHtmlTags(unescapeHtml(htmlContent)); + int fullLength = textWithoutHtml.length(); + for (int currentIndex = 0; currentIndex < fullLength; currentIndex++) { + char currentChar = textWithoutHtml.charAt(currentIndex); + if (currentChar == '{' || currentChar == '[') { + int matchingBracketIndex = findMatchingBracket(textWithoutHtml, currentIndex); + if (matchingBracketIndex > currentIndex) { + String candidateJson = textWithoutHtml.substring(currentIndex, matchingBracketIndex + 1).trim(); + if (looksLikeJson(candidateJson) && canParseJson(candidateJson)) { + return candidateJson; + } + } + } + } + throw new IllegalStateException("JSON payload not found or not valid inside attachment"); + } + + private static boolean canParseJson(String candidateJson) { + try { + JSON.readTree(candidateJson); + return true; + } catch (Exception ignore) { + return false; + } + } + + private static boolean looksLikeJson(String input) { + if (input == null) { + return false; + } + String trimmed = input.trim(); + if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) { + return false; + } + return trimmed.matches("(?s).*\"[^\"]+\"\\s*:\\s*.*"); + } + + private static int findMatchingBracket(String input, int startIndex) { + char openingBracket = input.charAt(startIndex); + char closingBracket = (openingBracket == '{') ? '}' : ']'; + int nestingDepth = 0; + boolean insideString = false; + for (int index = startIndex; index < input.length(); index++) { + char symbol = input.charAt(index); + if (symbol == '"' && (index == 0 || input.charAt(index - 1) != '\\')) { + insideString = !insideString; + } + if (insideString) { + continue; + } + if (symbol == openingBracket) { + nestingDepth++; + } else if (symbol == closingBracket) { + nestingDepth--; + if (nestingDepth == 0) { + return index; + } + } + } + return -1; + } + + private static String stripHtmlTags(String input) { + String withoutTags = input.replaceAll("(?is)", "") + .replaceAll("(?is)", "") + .replaceAll("(?s)<[^>]*>", " "); + return withoutTags + .replace("\r", " ") + .replace("\n", " ") + .replaceAll("[ \\t\\x0B\\f\\r]+", " ") + .trim(); + } + + private static String unescapeHtml(String input) { + return input.replace(""", "\"") + .replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace("{", "{") + .replace("}", "}") + .replace("[", "[") + .replace("]", "]") + .replace(":", ":") + .replace(",", ","); + } + + private static List flattenSteps(List rootSteps) { + List allSteps = new ArrayList<>(); + if (rootSteps == null) { + return allSteps; + } + for (StepResult step : rootSteps) { + allSteps.add(step); + allSteps.addAll(flattenSteps(step.getSteps())); + } + return allSteps; } } From 545202b8a3bded04d77b5ea1b3008303790db75f Mon Sep 17 00:00:00 2001 From: skuznetsov-al Date: Wed, 8 Oct 2025 11:49:20 +0300 Subject: [PATCH 2/4] rework grpc --- .../io/qameta/allure/grpc/AllureGrpc.java | 21 ++- .../io/qameta/allure/grpc/AllureGrpcTest.java | 144 ++++-------------- allure-grpc/src/test/proto/api.proto | 2 + 3 files changed, 49 insertions(+), 118 deletions(-) diff --git a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java index 132ba723..d4f9ae6d 100644 --- a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java +++ b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java @@ -45,7 +45,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - /** * Allure interceptor logger for gRPC. * @@ -132,6 +131,22 @@ public void sendMessage(final T message) { }; } + private void addRawJsonAttachment( + final String stepUuid, + final String attachmentName, + final String jsonBody, + final AllureLifecycle lifecycle + ) { + if (jsonBody == null || jsonBody.isEmpty()) { + return; + } + final String source = UUID.randomUUID() + ".json"; + lifecycle.updateStep(stepUuid, step -> step.getAttachments().add( + new Attachment().setName(attachmentName).setSource(source).setType("application/json") + )); + lifecycle.writeAttachment(source, new ByteArrayInputStream(jsonBody.getBytes(StandardCharsets.UTF_8))); + } + private static final class StepContext { final String stepUuid; final MethodDescriptor methodDescriptor; @@ -226,6 +241,7 @@ private void attachRequestIfPresent( .setBody(body) .build(); addRenderedAttachmentToStep(stepUuid, requestAttachment.getName(), requestAttachment, requestTemplatePath, lifecycle); + addRawJsonAttachment(stepUuid, name + " (json)", body, lifecycle); } private void attachResponse( @@ -259,6 +275,9 @@ private void attachResponse( final GrpcResponseAttachment responseAttachment = builder.build(); addRenderedAttachmentToStep(stepUuid, responseAttachment.getName(), responseAttachment, responseTemplatePath, lifecycle); + if (body != null) { + addRawJsonAttachment(stepUuid, name + " (json)", body, lifecycle); + } } private void stopStepSafely(final AllureLifecycle lifecycle, final String stepUuid) { diff --git a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java index 91390d41..e316fc75 100644 --- a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java +++ b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java @@ -14,6 +14,7 @@ * limitations under the License. */ package io.qameta.allure.grpc; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.grpc.ManagedChannel; @@ -24,6 +25,7 @@ import io.qameta.allure.Allure; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; import io.qameta.allure.test.AllureResults; import org.grpcmock.GrpcMock; import org.grpcmock.junit5.GrpcMockExtension; @@ -77,7 +79,7 @@ void configureMockServer() { .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); GrpcMock.stubFor(bidiStreamingMethod(TestServiceGrpc.getCalculateBidiStreamMethod()) - .willProxyTo(responseObserver -> new StreamObserver<>() { + .willProxyTo(responseObserver -> new StreamObserver() { @Override public void onNext(Request request) { responseObserver.onNext(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build()); @@ -174,10 +176,10 @@ void shouldCreateAttachmentsForClientStreamingWithAsynchronousStub() { TestServiceGrpc.TestServiceStub asynchronousStub = TestServiceGrpc.newStub(managedChannel).withInterceptors(new AllureGrpc()); - final List receivedResponses = new ArrayList<>(); + final List receivedResponses = new ArrayList(); Allure.step("async-root-client-stream", () -> { - StreamObserver responseObserver = new StreamObserver<>() { + StreamObserver responseObserver = new StreamObserver() { @Override public void onNext(Response value) { receivedResponses.add(value); @@ -213,7 +215,7 @@ void shouldCreateAttachmentsForBidirectionalStreamingWithAsynchronousStub() { List receivedResponses = new ArrayList<>(); Allure.step("async-root-bidi-stream", () -> { - StreamObserver responseObserver = new StreamObserver<>() { + StreamObserver responseObserver = new StreamObserver() { @Override public void onNext(Response value) { receivedResponses.add(value); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { } @@ -245,8 +247,7 @@ void unaryRequestBodyIsCapturedAsJsonObject() throws Exception { assertThat(response.getMessage()).isEqualTo("ok"); }); - String attachmentHtmlContent = readAttachmentContentByName(allureResults, "gRPC request"); - String jsonPayload = extractJsonPayload(attachmentHtmlContent); + String jsonPayload = readJsonAttachmentByName(allureResults, "gRPC request (json)"); JsonNode actualJsonNode = JSON.readTree(jsonPayload); JsonNode expectedJsonNode = JSON.createObjectNode().put("topic", "topic-1"); @@ -267,8 +268,7 @@ void unaryResponseBodyIsCapturedAsJsonObject() throws Exception { assertThat(response.getMessage()).isEqualTo("hello-world"); }); - String attachmentHtmlContent = readAttachmentContentByName(allureResults, "gRPC response"); - String jsonPayload = extractJsonPayload(attachmentHtmlContent); + String jsonPayload = readJsonAttachmentByName(allureResults, "gRPC response (json)"); JsonNode actualJsonNode = JSON.readTree(jsonPayload); JsonNode expectedJsonNode = JSON.createObjectNode().put("message", "hello-world"); @@ -296,11 +296,9 @@ void serverStreamingResponseBodyIsJsonArrayInOrder() throws Exception { assertThat(responseIterator.hasNext()).isFalse(); }); - String attachmentHtmlContent = readAttachmentContentByName( - allureResults, - "gRPC response (collection of elements from Server stream)" + String jsonPayload = readJsonAttachmentByName( + allureResults, "gRPC response (collection of elements from Server stream) (json)" ); - String jsonPayload = extractJsonPayload(attachmentHtmlContent); JsonNode actualJsonArray = JSON.readTree(jsonPayload); assertThat(actualJsonArray.isArray()).isTrue(); @@ -308,6 +306,23 @@ void serverStreamingResponseBodyIsJsonArrayInOrder() throws Exception { assertThat(actualJsonArray.get(0)).isEqualTo(JSON.createObjectNode().put("message", "first")); assertThat(actualJsonArray.get(1)).isEqualTo(JSON.createObjectNode().put("message", "second")); } + private static String readJsonAttachmentByName(AllureResults allureResults, String jsonAttachmentName) { + TestResult test = allureResults.getTestResults().get(0); + + Attachment matchedAttachment = flattenSteps(test.getSteps()).stream() + .flatMap(step -> step.getAttachments().stream()) + .filter(attachment -> jsonAttachmentName.equals(attachment.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Attachment not found: " + jsonAttachmentName)); + + String attachmentSourceKey = matchedAttachment.getSource(); + Map attachmentsContent = allureResults.getAttachments(); + byte[] rawAttachmentContent = attachmentsContent.get(attachmentSourceKey); + if (rawAttachmentContent == null) { + throw new IllegalStateException("Attachment content not found by source: " + attachmentSourceKey); + } + return new String(rawAttachmentContent, StandardCharsets.UTF_8); + } protected final AllureResults executeUnary(Request request) { return runWithinTestContext(() -> { @@ -352,111 +367,6 @@ protected final AllureResults executeUnaryExpectingException(Request request) { ); } - private static String readAttachmentContentByName(AllureResults allureResults, String attachmentName) { - var test = allureResults.getTestResults().get(0); - - Attachment matchedAttachment = flattenSteps(test.getSteps()).stream() - .flatMap(step -> step.getAttachments().stream()) - .filter(attachment -> attachmentName.equals(attachment.getName())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Attachment not found: " + attachmentName)); - - String attachmentSourceKey = matchedAttachment.getSource(); - Map attachmentsContent = allureResults.getAttachments(); - byte[] rawAttachmentContent = attachmentsContent.get(attachmentSourceKey); - if (rawAttachmentContent == null) { - throw new IllegalStateException("Attachment content not found by source: " + attachmentSourceKey); - } - return new String(rawAttachmentContent, StandardCharsets.UTF_8); - } - - private static String extractJsonPayload(String htmlContent) { - String textWithoutHtml = stripHtmlTags(unescapeHtml(htmlContent)); - int fullLength = textWithoutHtml.length(); - for (int currentIndex = 0; currentIndex < fullLength; currentIndex++) { - char currentChar = textWithoutHtml.charAt(currentIndex); - if (currentChar == '{' || currentChar == '[') { - int matchingBracketIndex = findMatchingBracket(textWithoutHtml, currentIndex); - if (matchingBracketIndex > currentIndex) { - String candidateJson = textWithoutHtml.substring(currentIndex, matchingBracketIndex + 1).trim(); - if (looksLikeJson(candidateJson) && canParseJson(candidateJson)) { - return candidateJson; - } - } - } - } - throw new IllegalStateException("JSON payload not found or not valid inside attachment"); - } - - private static boolean canParseJson(String candidateJson) { - try { - JSON.readTree(candidateJson); - return true; - } catch (Exception ignore) { - return false; - } - } - - private static boolean looksLikeJson(String input) { - if (input == null) { - return false; - } - String trimmed = input.trim(); - if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) { - return false; - } - return trimmed.matches("(?s).*\"[^\"]+\"\\s*:\\s*.*"); - } - - private static int findMatchingBracket(String input, int startIndex) { - char openingBracket = input.charAt(startIndex); - char closingBracket = (openingBracket == '{') ? '}' : ']'; - int nestingDepth = 0; - boolean insideString = false; - for (int index = startIndex; index < input.length(); index++) { - char symbol = input.charAt(index); - if (symbol == '"' && (index == 0 || input.charAt(index - 1) != '\\')) { - insideString = !insideString; - } - if (insideString) { - continue; - } - if (symbol == openingBracket) { - nestingDepth++; - } else if (symbol == closingBracket) { - nestingDepth--; - if (nestingDepth == 0) { - return index; - } - } - } - return -1; - } - - private static String stripHtmlTags(String input) { - String withoutTags = input.replaceAll("(?is)", "") - .replaceAll("(?is)", "") - .replaceAll("(?s)<[^>]*>", " "); - return withoutTags - .replace("\r", " ") - .replace("\n", " ") - .replaceAll("[ \\t\\x0B\\f\\r]+", " ") - .trim(); - } - - private static String unescapeHtml(String input) { - return input.replace(""", "\"") - .replace("<", "<") - .replace(">", ">") - .replace("&", "&") - .replace("{", "{") - .replace("}", "}") - .replace("[", "[") - .replace("]", "]") - .replace(":", ":") - .replace(",", ","); - } - private static List flattenSteps(List rootSteps) { List allSteps = new ArrayList<>(); if (rootSteps == null) { diff --git a/allure-grpc/src/test/proto/api.proto b/allure-grpc/src/test/proto/api.proto index 552e76f6..703a0fdb 100644 --- a/allure-grpc/src/test/proto/api.proto +++ b/allure-grpc/src/test/proto/api.proto @@ -6,6 +6,8 @@ option java_package = "io.qameta.allure.grpc"; service TestService { rpc Calculate (Request) returns (Response); rpc CalculateServerStream (Request) returns (stream Response); + rpc CalculateClientStream (stream Request) returns (Response); + rpc CalculateBidiStream (stream Request) returns (stream Response); } message Request { From 1eba45a0b6cf96ad844a2f99847dc8b7009d5daf Mon Sep 17 00:00:00 2001 From: skuznetsov-al Date: Fri, 10 Oct 2025 12:22:37 +0300 Subject: [PATCH 3/4] fix pmd check --- .../io/qameta/allure/grpc/AllureGrpc.java | 240 +++++++++++++----- 1 file changed, 173 insertions(+), 67 deletions(-) diff --git a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java index d4f9ae6d..6518f534 100644 --- a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java +++ b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java @@ -50,12 +50,18 @@ * * @author dtuchs (Dmitry Tuchs). */ -@SuppressWarnings("all") +@SuppressWarnings({ + "checkstyle:ClassFanOutComplexity", + "checkstyle:AnonInnerLength", + "checkstyle:JavaNCSS" +}) public class AllureGrpc implements ClientInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AllureGrpc.class); private static final String UNKNOWN = "unknown"; + private static final String JSON_SUFFIX = " (json)"; private static final JsonFormat.Printer GRPC_TO_JSON_PRINTER = JsonFormat.printer(); + private final AllureLifecycle lifecycle; private final boolean markStepFailedOnNonZeroCode; private final boolean interceptResponseMetadata; @@ -67,11 +73,13 @@ public AllureGrpc() { "grpc-request.ftl", "grpc-response.ftl"); } - public AllureGrpc(AllureLifecycle lifecycle, - boolean markStepFailedOnNonZeroCode, - boolean interceptResponseMetadata, - String requestTemplatePath, - String responseTemplatePath) { + public AllureGrpc( + final AllureLifecycle lifecycle, + final boolean markStepFailedOnNonZeroCode, + final boolean interceptResponseMetadata, + final String requestTemplatePath, + final String responseTemplatePath + ) { this.lifecycle = lifecycle; this.markStepFailedOnNonZeroCode = markStepFailedOnNonZeroCode; this.interceptResponseMetadata = interceptResponseMetadata; @@ -81,9 +89,9 @@ public AllureGrpc(AllureLifecycle lifecycle, @Override public ClientCall interceptCall( - MethodDescriptor methodDescriptor, - CallOptions callOptions, - Channel nextChannel + final MethodDescriptor methodDescriptor, + final CallOptions callOptions, + final Channel nextChannel ) { final AllureLifecycle current = lifecycle; final String parent = current.getCurrentTestCaseOrStep().orElse(null); @@ -94,11 +102,15 @@ public ClientCall interceptCall( final Map trailers = new LinkedHashMap<>(); final String stepName = buildStepName(nextChannel, methodDescriptor); - if (parent != null) current.startStep(parent, stepUuid, new StepResult().setName(stepName)); - else current.startStep(stepUuid, new StepResult().setName(stepName)); + if (parent != null) { + current.startStep(parent, stepUuid, new StepResult().setName(stepName)); + } else { + current.startStep(stepUuid, new StepResult().setName(stepName)); + } final StepContext stepContext = new StepContext<>( - stepUuid, methodDescriptor, current, clientMessages, serverMessages, initialHeaders, trailers + stepUuid, methodDescriptor, current, clientMessages, + serverMessages, initialHeaders, trailers ); return new ForwardingClientCall.SimpleForwardingClientCall( @@ -107,25 +119,35 @@ public ClientCall interceptCall( @Override public void start(final Listener responseListener, final Metadata requestHeaders) { final Listener forwardingListener = new ForwardingClientCallListener() { - @Override protected Listener delegate() { return responseListener; } - @Override public void onHeaders(final Metadata headers) { - handleHeaders(headers, stepContext.initialHeaders); + @Override + protected Listener delegate() { + return responseListener; + } + + @Override + public void onHeaders(final Metadata headers) { + handleHeaders(headers, stepContext.getInitialHeaders()); super.onHeaders(headers); } - @Override public void onMessage(final R message) { - handleServerMessage(message, stepContext.serverMessages); + + @Override + public void onMessage(final R message) { + handleServerMessage(message, stepContext.getServerMessages()); super.onMessage(message); } - @Override public void onClose(final io.grpc.Status status, final Metadata responseTrailers) { + + @Override + public void onClose(final io.grpc.Status status, final Metadata responseTrailers) { handleClose(status, responseTrailers, stepContext); super.onClose(status, responseTrailers); } }; super.start(forwardingListener, requestHeaders); } + @Override public void sendMessage(final T message) { - handleClientMessage(message, stepContext.clientMessages); + handleClientMessage(message, stepContext.getClientMessages()); super.sendMessage(message); } }; @@ -142,34 +164,15 @@ private void addRawJsonAttachment( } final String source = UUID.randomUUID() + ".json"; lifecycle.updateStep(stepUuid, step -> step.getAttachments().add( - new Attachment().setName(attachmentName).setSource(source).setType("application/json") + new Attachment() + .setName(attachmentName) + .setSource(source) + .setType("application/json") )); - lifecycle.writeAttachment(source, new ByteArrayInputStream(jsonBody.getBytes(StandardCharsets.UTF_8))); - } - - private static final class StepContext { - final String stepUuid; - final MethodDescriptor methodDescriptor; - final AllureLifecycle lifecycle; - final List clientMessages; - final List serverMessages; - final Map initialHeaders; - final Map trailers; - StepContext(String stepUuid, - MethodDescriptor methodDescriptor, - AllureLifecycle lifecycle, - List clientMessages, - List serverMessages, - Map initialHeaders, - Map trailers) { - this.stepUuid = stepUuid; - this.methodDescriptor = methodDescriptor; - this.lifecycle = lifecycle; - this.clientMessages = clientMessages; - this.serverMessages = serverMessages; - this.initialHeaders = initialHeaders; - this.trailers = trailers; - } + lifecycle.writeAttachment( + source, + new ByteArrayInputStream(jsonBody.getBytes(StandardCharsets.UTF_8)) + ); } private void handleClose( @@ -179,25 +182,42 @@ private void handleClose( ) { try { if (interceptResponseMetadata && responseTrailers != null) { - copyAsciiResponseMetadata(responseTrailers, stepContext.trailers); + copyAsciiResponseMetadata(responseTrailers, stepContext.getTrailers()); } - attachRequestIfPresent(stepContext.stepUuid, stepContext.methodDescriptor, - stepContext.clientMessages, stepContext.lifecycle); - attachResponse(stepContext.stepUuid, stepContext.serverMessages, status, - stepContext.initialHeaders, stepContext.trailers, stepContext.lifecycle); - stepContext.lifecycle.updateStep(stepContext.stepUuid, step -> step.setStatus(convertStatus(status))); + attachRequestIfPresent( + stepContext.getStepUuid(), + stepContext.getMethodDescriptor(), + stepContext.getClientMessages(), + stepContext.getLifecycle() + ); + attachResponse( + stepContext.getStepUuid(), + stepContext.getServerMessages(), + status, + stepContext.getInitialHeaders(), + stepContext.getTrailers(), + stepContext.getLifecycle() + ); + stepContext.getLifecycle().updateStep( + stepContext.getStepUuid(), + step -> step.setStatus(convertStatus(status)) + ); } catch (Throwable throwable) { LOGGER.error("Failed to finalize Allure step for gRPC call", throwable); - stepContext.lifecycle.updateStep(stepContext.stepUuid, step -> step.setStatus(Status.BROKEN)); + stepContext.getLifecycle().updateStep( + stepContext.getStepUuid(), + step -> step.setStatus(Status.BROKEN) + ); } finally { - stopStepSafely(stepContext.lifecycle, stepContext.stepUuid); + stopStepSafely(stepContext.getLifecycle(), stepContext.getStepUuid()); } } private void handleHeaders(final Metadata headers, final Map destination) { try { - if (interceptResponseMetadata && headers != null) + if (interceptResponseMetadata && headers != null) { copyAsciiResponseMetadata(headers, destination); + } } catch (Throwable throwable) { LOGGER.warn("Failed to capture response headers", throwable); } @@ -240,8 +260,15 @@ private void attachRequestIfPresent( .create(name, methodDescriptor.getFullMethodName()) .setBody(body) .build(); - addRenderedAttachmentToStep(stepUuid, requestAttachment.getName(), requestAttachment, requestTemplatePath, lifecycle); - addRawJsonAttachment(stepUuid, name + " (json)", body, lifecycle); + + addRenderedAttachmentToStep( + stepUuid, + requestAttachment.getName(), + requestAttachment, + requestTemplatePath, + lifecycle + ); + addRawJsonAttachment(stepUuid, name + JSON_SUFFIX, body, lifecycle); } private void attachResponse( @@ -266,17 +293,24 @@ private void attachResponse( final GrpcResponseAttachment.Builder builder = GrpcResponseAttachment.Builder .create(name) .setStatus(status.toString()); + if (body != null) { builder.setBody(body); } if (!metadata.isEmpty()) { builder.addMetadata(metadata); } + final GrpcResponseAttachment responseAttachment = builder.build(); - addRenderedAttachmentToStep(stepUuid, responseAttachment.getName(), - responseAttachment, responseTemplatePath, lifecycle); + addRenderedAttachmentToStep( + stepUuid, + responseAttachment.getName(), + responseAttachment, + responseTemplatePath, + lifecycle + ); if (body != null) { - addRawJsonAttachment(stepUuid, name + " (json)", body, lifecycle); + addRawJsonAttachment(stepUuid, name + JSON_SUFFIX, body, lifecycle); } } @@ -302,7 +336,8 @@ private static String buildStepName( final String authority = channel != null ? channel.authority() : null; final String safeAuthority = authority != null ? authority : UNKNOWN; final String type = toSnakeCase(methodDescriptor.getType()); - return "Send " + type + " gRPC request to " + safeAuthority + "/" + methodDescriptor.getFullMethodName(); + return "Send " + type + " gRPC request to " + + safeAuthority + "/" + methodDescriptor.getFullMethodName(); } private static String toSnakeCase(final MethodDescriptor.MethodType methodType) { @@ -319,12 +354,16 @@ private void addRenderedAttachmentToStep( final String templatePath, final AllureLifecycle lifecycle ) { - final AttachmentRenderer renderer = new FreemarkerAttachmentRenderer(templatePath); + final AttachmentRenderer renderer = + new FreemarkerAttachmentRenderer(templatePath); final io.qameta.allure.attachment.AttachmentContent content; try { content = renderer.render(data); } catch (Throwable throwable) { - LOGGER.warn("Could not render attachment '{}' using template '{}'", attachmentName, templatePath, throwable); + LOGGER.warn( + "Could not render attachment '{}' using template '{}'", + attachmentName, templatePath, throwable + ); return; } if (content == null || content.getContent() == null) { @@ -342,10 +381,17 @@ private void addRenderedAttachmentToStep( new Attachment() .setName(attachmentName) .setSource(source) - .setType(content.getContentType() != null ? content.getContentType() : "text/html") + .setType( + content.getContentType() != null + ? content.getContentType() + : "text/html" + ) ) ); - lifecycle.writeAttachment(source, new ByteArrayInputStream(content.getContent().getBytes(StandardCharsets.UTF_8))); + lifecycle.writeAttachment( + source, + new ByteArrayInputStream(content.getContent().getBytes(StandardCharsets.UTF_8)) + ); } private static String toJsonBody(final List items) { @@ -359,7 +405,10 @@ private static String toJsonBody(final List items) { return "[" + joined + "]"; } - private static void copyAsciiResponseMetadata(final Metadata source, final Map target) { + private static void copyAsciiResponseMetadata( + final Metadata source, + final Map target + ) { for (String key : source.keys()) { if (key == null) { continue; @@ -367,11 +416,68 @@ private static void copyAsciiResponseMetadata(final Metadata source, final Map keyAscii = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + final Metadata.Key keyAscii = + Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); final String value = source.get(keyAscii); if (value != null) { target.put(key, value); } } } + + private static final class StepContext { + private final String stepUuid; + private final MethodDescriptor methodDescriptor; + private final AllureLifecycle lifecycle; + private final List clientMessages; + private final List serverMessages; + private final Map initialHeaders; + private final Map trailers; + + StepContext( + final String stepUuid, + final MethodDescriptor methodDescriptor, + final AllureLifecycle lifecycle, + final List clientMessages, + final List serverMessages, + final Map initialHeaders, + final Map trailers + ) { + this.stepUuid = stepUuid; + this.methodDescriptor = methodDescriptor; + this.lifecycle = lifecycle; + this.clientMessages = clientMessages; + this.serverMessages = serverMessages; + this.initialHeaders = initialHeaders; + this.trailers = trailers; + } + + String getStepUuid() { + return stepUuid; + } + + MethodDescriptor getMethodDescriptor() { + return methodDescriptor; + } + + AllureLifecycle getLifecycle() { + return lifecycle; + } + + List getClientMessages() { + return clientMessages; + } + + List getServerMessages() { + return serverMessages; + } + + Map getInitialHeaders() { + return initialHeaders; + } + + Map getTrailers() { + return trailers; + } + } } From 1d122a94bebac6a84bc807a7f0c6ff68841c9fad Mon Sep 17 00:00:00 2001 From: skuznetsov-al Date: Fri, 10 Oct 2025 12:33:19 +0300 Subject: [PATCH 4/4] remove unnecessary spaces at proto --- allure-grpc/src/test/proto/api.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allure-grpc/src/test/proto/api.proto b/allure-grpc/src/test/proto/api.proto index 703a0fdb..378bb087 100644 --- a/allure-grpc/src/test/proto/api.proto +++ b/allure-grpc/src/test/proto/api.proto @@ -6,8 +6,8 @@ option java_package = "io.qameta.allure.grpc"; service TestService { rpc Calculate (Request) returns (Response); rpc CalculateServerStream (Request) returns (stream Response); - rpc CalculateClientStream (stream Request) returns (Response); - rpc CalculateBidiStream (stream Request) returns (stream Response); + rpc CalculateClientStream (stream Request) returns (Response); + rpc CalculateBidiStream (stream Request) returns (stream Response); } message Request {