From 46d983668d1da85003c23b4f25f75ea8e470fe8e Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 27 Apr 2026 16:23:34 +0800 Subject: [PATCH 1/2] feat: add Gradle test event streaming protocol Add an opt-in runBuild protocol flag for Gradle Tooling API test events and stream normalized test events from the Gradle server. Wire the extension client to request and receive the events without changing existing build behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extension/src/client/TaskServerClient.ts | 10 +- .../badsyntax/gradle/GradleBuildRunner.java | 14 ++- .../gradle/handlers/RunBuildHandler.java | 91 ++++++++++++++++++- .../badsyntax/gradle/GradleServerTest.java | 60 ++++++++++++ proto/gradle.proto | 31 +++++++ 5 files changed, 202 insertions(+), 4 deletions(-) diff --git a/extension/src/client/TaskServerClient.ts b/extension/src/client/TaskServerClient.ts index 55e8df06c..9587ad16a 100644 --- a/extension/src/client/TaskServerClient.ts +++ b/extension/src/client/TaskServerClient.ts @@ -10,6 +10,7 @@ import { GradleBuild, Environment, GradleConfig, + GradleTestEvent, RunBuildRequest, RunBuildReply, CancelBuildRequest, @@ -230,7 +231,8 @@ export class TaskServerClient implements vscode.Disposable { showOutputColors = true, additionalToolOptions = "", title?: string, - location?: vscode.ProgressLocation + location?: vscode.ProgressLocation, + onTestEvent?: (event: GradleTestEvent) => void ): Promise { await this.connectWaiter.wait(); this.statusBarItem.hide(); @@ -260,6 +262,7 @@ export class TaskServerClient implements vscode.Disposable { request.setJavaDebugPort(javaDebugPort); request.setInput(input); request.setAdditionalToolOptions(additionalToolOptions); + request.setStreamTestEvents(Boolean(onTestEvent)); if (javaDebugPort > 0) { const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(projectFolder)); @@ -285,6 +288,11 @@ export class TaskServerClient implements vscode.Disposable { case RunBuildReply.KindCase.CANCELLED: this.handleRunBuildCancelled(args, runBuildReply.getCancelled()!, task); break; + case RunBuildReply.KindCase.TEST_EVENT: + if (onTestEvent) { + onTestEvent(runBuildReply.getTestEvent()!); + } + break; } }) .on("error", reject) diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleBuildRunner.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleBuildRunner.java index 8c2335274..4e1186eef 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleBuildRunner.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleBuildRunner.java @@ -79,9 +79,11 @@ public class GradleBuildRunner { private ProgressListener progressListener; private Boolean javaDebugCleanOutputCache; private String additionalToolOptions; + private Boolean streamTestEvents; public GradleBuildRunner(String projectDir, List args, GradleConfig gradleConfig, String cancellationKey, - Boolean colorOutput, int javaDebugPort, Boolean javaDebugCleanOutputCache, String additionalToolOptions) { + Boolean colorOutput, int javaDebugPort, Boolean javaDebugCleanOutputCache, String additionalToolOptions, + Boolean streamTestEvents) { this.projectDir = projectDir; this.args = args; this.gradleConfig = gradleConfig; @@ -90,6 +92,13 @@ public GradleBuildRunner(String projectDir, List args, GradleConfig grad this.javaDebugPort = javaDebugPort; this.javaDebugCleanOutputCache = javaDebugCleanOutputCache; this.additionalToolOptions = additionalToolOptions; + this.streamTestEvents = streamTestEvents; + } + + public GradleBuildRunner(String projectDir, List args, GradleConfig gradleConfig, String cancellationKey, + Boolean colorOutput, int javaDebugPort, Boolean javaDebugCleanOutputCache, String additionalToolOptions) { + this(projectDir, args, gradleConfig, cancellationKey, colorOutput, javaDebugPort, javaDebugCleanOutputCache, + additionalToolOptions, false); } public GradleBuildRunner(String projectDir, List args, GradleConfig gradleConfig, String cancellationKey) { @@ -133,6 +142,9 @@ private void runBuild(ProjectConnection connection) throws GradleBuildRunnerExce progressEvents.add(OperationType.PROJECT_CONFIGURATION); progressEvents.add(OperationType.TASK); progressEvents.add(OperationType.TRANSFORM); + if (Boolean.TRUE.equals(streamTestEvents)) { + progressEvents.add(OperationType.TEST); + } CancellationToken cancellationToken = GradleBuildCancellation.buildToken(cancellationKey); diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java index 2fb6bccdf..08bb8e1db 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java @@ -4,6 +4,7 @@ import com.github.badsyntax.gradle.Cancelled; import com.github.badsyntax.gradle.ErrorMessageBuilder; import com.github.badsyntax.gradle.GradleBuildRunner; +import com.github.badsyntax.gradle.GradleTestEvent; import com.github.badsyntax.gradle.Output; import com.github.badsyntax.gradle.Progress; import com.github.badsyntax.gradle.RunBuildReply; @@ -20,6 +21,16 @@ import org.gradle.tooling.UnsupportedVersionException; import org.gradle.tooling.events.ProgressEvent; import org.gradle.tooling.events.ProgressListener; +import org.gradle.tooling.events.test.Destination; +import org.gradle.tooling.events.test.JvmTestOperationDescriptor; +import org.gradle.tooling.events.test.TestFailureResult; +import org.gradle.tooling.events.test.TestFinishEvent; +import org.gradle.tooling.events.test.TestOperationDescriptor; +import org.gradle.tooling.events.test.TestOutputDescriptor; +import org.gradle.tooling.events.test.TestOutputEvent; +import org.gradle.tooling.events.test.TestSkippedResult; +import org.gradle.tooling.events.test.TestStartEvent; +import org.gradle.tooling.events.test.TestSuccessResult; import org.gradle.tooling.exceptions.UnsupportedBuildArgumentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +49,11 @@ public RunBuildHandler(RunBuildRequest req, StreamObserver respon this.responseObserver = responseObserver; this.progressListener = (ProgressEvent event) -> { synchronized (RunBuildHandler.class) { - replyWithProgress(event); + if (req.getStreamTestEvents() && isTestEvent(event)) { + replyWithTestEvent(event); + } else { + replyWithProgress(event); + } } }; this.standardOutputListener = new ByteBufferOutputStream() { @@ -62,7 +77,7 @@ public void onFlush(byte[] bytes) { public void run() { GradleBuildRunner gradleRunner = new GradleBuildRunner(req.getProjectDir(), req.getArgsList(), req.getGradleConfig(), req.getCancellationKey(), req.getShowOutputColors(), req.getJavaDebugPort(), - req.getJavaDebugCleanOutputCache(), req.getAdditionalToolOptions()); + req.getJavaDebugCleanOutputCache(), req.getAdditionalToolOptions(), req.getStreamTestEvents()); gradleRunner.setProgressListener(progressListener).setStandardOutputStream(standardOutputListener) .setStandardErrorStream(standardErrorListener); @@ -104,6 +119,78 @@ private void replyWithProgress(ProgressEvent progressEvent) { .setProgress(Progress.newBuilder().setMessage(progressEvent.getDisplayName())).build()); } + private void replyWithTestEvent(ProgressEvent progressEvent) { + responseObserver.onNext(RunBuildReply.newBuilder().setTestEvent(convertTestEvent(progressEvent)).build()); + } + + private static boolean isTestEvent(ProgressEvent event) { + return event instanceof TestStartEvent || event instanceof TestFinishEvent || event instanceof TestOutputEvent; + } + + private static GradleTestEvent convertTestEvent(ProgressEvent event) { + GradleTestEvent.Builder builder = GradleTestEvent.newBuilder().setEventTime(event.getEventTime()) + .setDisplayName(event.getDisplayName()); + + if (event instanceof TestOutputEvent) { + TestOutputDescriptor descriptor = ((TestOutputEvent) event).getDescriptor(); + fillDescriptor(builder, descriptor); + builder.setEventType(GradleTestEvent.EventType.OUTPUT).setMessage(descriptor.getMessage()); + if (Destination.StdOut.equals(descriptor.getDestination())) { + builder.setOutputDestination(GradleTestEvent.OutputDestination.STDOUT); + } else if (Destination.StdErr.equals(descriptor.getDestination())) { + builder.setOutputDestination(GradleTestEvent.OutputDestination.STDERR); + } + return builder.build(); + } + + if (event instanceof TestStartEvent) { + builder.setEventType(GradleTestEvent.EventType.STARTED); + } else if (event instanceof TestFinishEvent) { + TestFinishEvent finishEvent = (TestFinishEvent) event; + if (finishEvent.getResult() instanceof TestSuccessResult) { + builder.setEventType(GradleTestEvent.EventType.SUCCEEDED); + } else if (finishEvent.getResult() instanceof TestSkippedResult) { + builder.setEventType(GradleTestEvent.EventType.SKIPPED); + } else if (finishEvent.getResult() instanceof TestFailureResult) { + builder.setEventType(GradleTestEvent.EventType.FAILED); + } + } + + fillDescriptor(builder, ((org.gradle.tooling.events.test.TestProgressEvent) event).getDescriptor()); + return builder.build(); + } + + private static void fillDescriptor(GradleTestEvent.Builder builder, + org.gradle.tooling.events.OperationDescriptor descriptor) { + builder.setId(descriptorPath(descriptor)).setName(descriptor.getName()) + .setDisplayName(descriptor.getDisplayName()); + if (descriptor.getParent() != null) { + builder.setParentId(descriptorPath(descriptor.getParent())); + } + if (descriptor instanceof TestOperationDescriptor) { + builder.setDisplayName(((TestOperationDescriptor) descriptor).getTestDisplayName()); + } + if (descriptor instanceof JvmTestOperationDescriptor) { + JvmTestOperationDescriptor jvmDescriptor = (JvmTestOperationDescriptor) descriptor; + if (jvmDescriptor.getClassName() != null) { + builder.setClassName(jvmDescriptor.getClassName()); + } + if (jvmDescriptor.getMethodName() != null) { + builder.setMethodName(jvmDescriptor.getMethodName()); + } + if (jvmDescriptor.getSuiteName() != null) { + builder.setSuiteName(jvmDescriptor.getSuiteName()); + } + } + } + + private static String descriptorPath(org.gradle.tooling.events.OperationDescriptor descriptor) { + if (descriptor.getParent() == null) { + return descriptor.getName(); + } + return descriptorPath(descriptor.getParent()) + "/" + descriptor.getName(); + } + private void replyWithStandardOutput(byte[] bytes) { ByteString byteString = ByteString.copyFrom(bytes); responseObserver.onNext(RunBuildReply.newBuilder() diff --git a/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java b/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java index c2a3fcd60..d6899d08c 100644 --- a/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java +++ b/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.powermock.api.mockito.PowerMockito.*; @@ -24,7 +25,11 @@ import java.util.HashMap; import java.util.List; import java.util.Set; +import org.gradle.tooling.events.OperationDescriptor; import org.gradle.tooling.events.OperationType; +import org.gradle.tooling.events.ProgressListener; +import org.gradle.tooling.events.test.JvmTestOperationDescriptor; +import org.gradle.tooling.events.test.TestStartEvent; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -475,4 +480,59 @@ public void runBuild_shouldStreamCorrectProgressEvents() throws IOException { assertTrue(onAddProgressListener.getValue().contains(OperationType.TASK)); assertTrue(onAddProgressListener.getValue().contains(OperationType.TRANSFORM)); } + + @Test + public void runBuild_shouldStreamTestProgressEventsWhenRequested() throws IOException { + StreamObserver mockResponseObserver = (StreamObserver) mock(StreamObserver.class); + + RunBuildRequest req = RunBuildRequest.newBuilder().setProjectDir(mockProjectDir.getAbsolutePath().toString()) + .addAllArgs(mockBuildArgs).setGradleConfig(GradleConfig.newBuilder().setWrapperEnabled(true)) + .setStreamTestEvents(true).build(); + + ArgumentCaptor> onAddProgressListener = ArgumentCaptor.forClass(Set.class); + + OperationDescriptor parentDescriptor = mock(OperationDescriptor.class); + when(parentDescriptor.getName()).thenReturn("test"); + when(parentDescriptor.getDisplayName()).thenReturn("test"); + + JvmTestOperationDescriptor descriptor = mock(JvmTestOperationDescriptor.class); + when(descriptor.getName()).thenReturn("testMethod"); + when(descriptor.getDisplayName()).thenReturn("testMethod()"); + when(descriptor.getTestDisplayName()).thenReturn("testMethod()"); + when(descriptor.getClassName()).thenReturn("com.example.FooTest"); + when(descriptor.getMethodName()).thenReturn("testMethod"); + when(descriptor.getParent()).thenReturn(parentDescriptor); + + TestStartEvent event = mock(TestStartEvent.class); + when(event.getEventTime()).thenReturn(123L); + when(event.getDisplayName()).thenReturn("testMethod() started"); + when(event.getDescriptor()).thenReturn(descriptor); + + when(mockBuildLauncher.addProgressListener(any(ProgressListener.class), + ArgumentMatchers.>any())).thenAnswer(invocation -> { + ProgressListener listener = invocation.getArgument(0); + listener.statusChanged(event); + return mockBuildLauncher; + }); + + stub.runBuild(req, mockResponseObserver); + verify(mockResponseObserver, never()).onError(any()); + verify(mockBuildLauncher).addProgressListener(any(ProgressListener.class), onAddProgressListener.capture()); + + assertEquals(4, onAddProgressListener.getValue().size()); + assertTrue(onAddProgressListener.getValue().contains(OperationType.PROJECT_CONFIGURATION)); + assertTrue(onAddProgressListener.getValue().contains(OperationType.TASK)); + assertTrue(onAddProgressListener.getValue().contains(OperationType.TRANSFORM)); + assertTrue(onAddProgressListener.getValue().contains(OperationType.TEST)); + + ArgumentCaptor replyCaptor = ArgumentCaptor.forClass(RunBuildReply.class); + verify(mockResponseObserver, times(2)).onNext(replyCaptor.capture()); + RunBuildReply testEventReply = replyCaptor.getAllValues().get(0); + assertEquals(RunBuildReply.KindCase.TEST_EVENT, testEventReply.getKindCase()); + assertEquals(GradleTestEvent.EventType.STARTED, testEventReply.getTestEvent().getEventType()); + assertEquals("test/testMethod", testEventReply.getTestEvent().getId()); + assertEquals("test", testEventReply.getTestEvent().getParentId()); + assertEquals("com.example.FooTest", testEventReply.getTestEvent().getClassName()); + assertEquals("testMethod", testEventReply.getTestEvent().getMethodName()); + } } diff --git a/proto/gradle.proto b/proto/gradle.proto index 84693fee3..240bb90b3 100644 --- a/proto/gradle.proto +++ b/proto/gradle.proto @@ -77,6 +77,7 @@ message RunBuildRequest { bool show_output_colors = 7; bool java_debug_clean_output_cache = 8; string additional_tool_options = 9; + bool stream_test_events = 10; } message RunBuildResult { @@ -89,9 +90,39 @@ message RunBuildReply { Progress progress = 2; Output output = 3; Cancelled cancelled = 4; + GradleTestEvent test_event = 5; } } +message GradleTestEvent { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + STARTED = 1; + SUCCEEDED = 2; + FAILED = 3; + SKIPPED = 4; + OUTPUT = 5; + } + + enum OutputDestination { + UNKNOWN = 0; + STDOUT = 1; + STDERR = 2; + } + + EventType event_type = 1; + string id = 2; + string parent_id = 3; + string name = 4; + string display_name = 5; + string class_name = 6; + string method_name = 7; + string suite_name = 8; + int64 event_time = 9; + string message = 10; + OutputDestination output_destination = 11; +} + message CancelBuildRequest { string cancellation_key = 1; } From cb84b8813227a8eae6838dc917f39f5794dbdcad Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 28 Apr 2026 13:38:23 +0800 Subject: [PATCH 2/2] fix: harden Gradle test event streaming Use a per-handler response lock instead of a global RunBuildHandler lock, include Gradle failure details in failed test events, and make descriptor path construction iterative. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gradle/handlers/RunBuildHandler.java | 91 ++++++++++++++----- .../badsyntax/gradle/GradleServerTest.java | 49 ++++++++++ 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java index 08bb8e1db..7f17feba5 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/handlers/RunBuildHandler.java @@ -16,8 +16,12 @@ import io.grpc.stub.StreamObserver; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.gradle.tooling.BuildCancelledException; import org.gradle.tooling.BuildException; +import org.gradle.tooling.Failure; import org.gradle.tooling.UnsupportedVersionException; import org.gradle.tooling.events.ProgressEvent; import org.gradle.tooling.events.ProgressListener; @@ -43,33 +47,28 @@ public class RunBuildHandler { private ProgressListener progressListener; private ByteBufferOutputStream standardOutputListener; private ByteBufferOutputStream standardErrorListener; + private final Object responseLock = new Object(); public RunBuildHandler(RunBuildRequest req, StreamObserver responseObserver) { this.req = req; this.responseObserver = responseObserver; this.progressListener = (ProgressEvent event) -> { - synchronized (RunBuildHandler.class) { - if (req.getStreamTestEvents() && isTestEvent(event)) { - replyWithTestEvent(event); - } else { - replyWithProgress(event); - } + if (req.getStreamTestEvents() && isTestEvent(event)) { + replyWithTestEvent(event); + } else { + replyWithProgress(event); } }; this.standardOutputListener = new ByteBufferOutputStream() { @Override public void onFlush(byte[] bytes) { - synchronized (RunBuildHandler.class) { - replyWithStandardOutput(bytes); - } + replyWithStandardOutput(bytes); } }; this.standardErrorListener = new ByteBufferOutputStream() { @Override public void onFlush(byte[] bytes) { - synchronized (RunBuildHandler.class) { - replyWithStandardError(bytes); - } + replyWithStandardError(bytes); } }; } @@ -88,10 +87,10 @@ public void run() { try { gradleRunner.run(); replyWithSuccess(); - responseObserver.onCompleted(); + completeResponse(); } catch (BuildCancelledException e) { replyWithCancelled(e); - responseObserver.onCompleted(); + completeResponse(); } catch (BuildException | UnsupportedVersionException | UnsupportedBuildArgumentException | IllegalStateException | IOException | GradleBuildRunnerException e) { logger.error(e.getMessage()); @@ -100,27 +99,41 @@ public void run() { } public void replyWithCancelled(BuildCancelledException e) { - responseObserver.onNext(RunBuildReply.newBuilder() + sendReply(RunBuildReply.newBuilder() .setCancelled(Cancelled.newBuilder().setMessage(e.getMessage()).setProjectDir(req.getProjectDir())) .build()); } public void replyWithError(Exception e) { - responseObserver.onError(ErrorMessageBuilder.build(e)); + synchronized (responseLock) { + responseObserver.onError(ErrorMessageBuilder.build(e)); + } } public void replyWithSuccess() { - responseObserver.onNext(RunBuildReply.newBuilder() + sendReply(RunBuildReply.newBuilder() .setRunBuildResult(RunBuildResult.newBuilder().setMessage("Successfully run build")).build()); } private void replyWithProgress(ProgressEvent progressEvent) { - responseObserver.onNext(RunBuildReply.newBuilder() + sendReply(RunBuildReply.newBuilder() .setProgress(Progress.newBuilder().setMessage(progressEvent.getDisplayName())).build()); } private void replyWithTestEvent(ProgressEvent progressEvent) { - responseObserver.onNext(RunBuildReply.newBuilder().setTestEvent(convertTestEvent(progressEvent)).build()); + sendReply(RunBuildReply.newBuilder().setTestEvent(convertTestEvent(progressEvent)).build()); + } + + private void sendReply(RunBuildReply reply) { + synchronized (responseLock) { + responseObserver.onNext(reply); + } + } + + private void completeResponse() { + synchronized (responseLock) { + responseObserver.onCompleted(); + } } private static boolean isTestEvent(ProgressEvent event) { @@ -153,6 +166,10 @@ private static GradleTestEvent convertTestEvent(ProgressEvent event) { builder.setEventType(GradleTestEvent.EventType.SKIPPED); } else if (finishEvent.getResult() instanceof TestFailureResult) { builder.setEventType(GradleTestEvent.EventType.FAILED); + String failureMessage = failureMessage((TestFailureResult) finishEvent.getResult()); + if (!failureMessage.isEmpty()) { + builder.setMessage(failureMessage); + } } } @@ -185,23 +202,49 @@ private static void fillDescriptor(GradleTestEvent.Builder builder, } private static String descriptorPath(org.gradle.tooling.events.OperationDescriptor descriptor) { - if (descriptor.getParent() == null) { - return descriptor.getName(); + List names = new ArrayList<>(); + org.gradle.tooling.events.OperationDescriptor current = descriptor; + while (current != null) { + names.add(current.getName()); + current = current.getParent(); } - return descriptorPath(descriptor.getParent()) + "/" + descriptor.getName(); + Collections.reverse(names); + return String.join("/", names); } private void replyWithStandardOutput(byte[] bytes) { ByteString byteString = ByteString.copyFrom(bytes); - responseObserver.onNext(RunBuildReply.newBuilder() + sendReply(RunBuildReply.newBuilder() .setOutput(Output.newBuilder().setOutputType(Output.OutputType.STDOUT).setOutputBytes(byteString)) .build()); } private void replyWithStandardError(byte[] bytes) { ByteString byteString = ByteString.copyFrom(bytes); - responseObserver.onNext(RunBuildReply.newBuilder() + sendReply(RunBuildReply.newBuilder() .setOutput(Output.newBuilder().setOutputType(Output.OutputType.STDERR).setOutputBytes(byteString)) .build()); } + + private static String failureMessage(TestFailureResult failureResult) { + List messages = new ArrayList<>(); + for (Failure failure : failureResult.getFailures()) { + collectFailureMessages(failure, messages); + } + return String.join("\n---\n", messages); + } + + private static void collectFailureMessages(Failure failure, List messages) { + String message = failure.getMessage(); + String description = failure.getDescription(); + if (!Strings.isNullOrEmpty(message)) { + messages.add(message); + } + if (!Strings.isNullOrEmpty(description) && !description.equals(message)) { + messages.add(description); + } + for (Failure cause : failure.getCauses()) { + collectFailureMessages(cause, messages); + } + } } diff --git a/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java b/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java index d6899d08c..eb19a9fa8 100644 --- a/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java +++ b/gradle-server/src/test/java/com/github/badsyntax/gradle/GradleServerTest.java @@ -25,10 +25,13 @@ import java.util.HashMap; import java.util.List; import java.util.Set; +import org.gradle.tooling.Failure; import org.gradle.tooling.events.OperationDescriptor; import org.gradle.tooling.events.OperationType; import org.gradle.tooling.events.ProgressListener; import org.gradle.tooling.events.test.JvmTestOperationDescriptor; +import org.gradle.tooling.events.test.TestFailureResult; +import org.gradle.tooling.events.test.TestFinishEvent; import org.gradle.tooling.events.test.TestStartEvent; import org.junit.After; import org.junit.Before; @@ -535,4 +538,50 @@ public void runBuild_shouldStreamTestProgressEventsWhenRequested() throws IOExce assertEquals("com.example.FooTest", testEventReply.getTestEvent().getClassName()); assertEquals("testMethod", testEventReply.getTestEvent().getMethodName()); } + + @Test + public void runBuild_shouldIncludeFailureMessageInFailedTestEvent() throws IOException { + StreamObserver mockResponseObserver = (StreamObserver) mock(StreamObserver.class); + + RunBuildRequest req = RunBuildRequest.newBuilder().setProjectDir(mockProjectDir.getAbsolutePath().toString()) + .addAllArgs(mockBuildArgs).setGradleConfig(GradleConfig.newBuilder().setWrapperEnabled(true)) + .setStreamTestEvents(true).build(); + + JvmTestOperationDescriptor descriptor = mock(JvmTestOperationDescriptor.class); + when(descriptor.getName()).thenReturn("testMethod"); + when(descriptor.getDisplayName()).thenReturn("testMethod()"); + when(descriptor.getTestDisplayName()).thenReturn("testMethod()"); + + Failure failure = mock(Failure.class); + when(failure.getMessage()).thenReturn("expected:<1> but was:<2>"); + when(failure.getDescription()).thenReturn("java.lang.AssertionError: expected:<1> but was:<2>"); + when(failure.getCauses()).thenReturn(List.of()); + + TestFailureResult failureResult = mock(TestFailureResult.class); + doReturn(List.of(failure)).when(failureResult).getFailures(); + + TestFinishEvent event = mock(TestFinishEvent.class); + when(event.getEventTime()).thenReturn(123L); + when(event.getDisplayName()).thenReturn("testMethod() failed"); + when(event.getDescriptor()).thenReturn(descriptor); + when(event.getResult()).thenReturn(failureResult); + + when(mockBuildLauncher.addProgressListener(any(ProgressListener.class), + ArgumentMatchers.>any())).thenAnswer(invocation -> { + ProgressListener listener = invocation.getArgument(0); + listener.statusChanged(event); + return mockBuildLauncher; + }); + + stub.runBuild(req, mockResponseObserver); + verify(mockResponseObserver, never()).onError(any()); + + ArgumentCaptor replyCaptor = ArgumentCaptor.forClass(RunBuildReply.class); + verify(mockResponseObserver, times(2)).onNext(replyCaptor.capture()); + RunBuildReply testEventReply = replyCaptor.getAllValues().get(0); + assertEquals(RunBuildReply.KindCase.TEST_EVENT, testEventReply.getKindCase()); + assertEquals(GradleTestEvent.EventType.FAILED, testEventReply.getTestEvent().getEventType()); + assertEquals("expected:<1> but was:<2>\n---\njava.lang.AssertionError: expected:<1> but was:<2>", + testEventReply.getTestEvent().getMessage()); + } }