diff --git a/buildSrc/src/main/java/org/springframework/boot/build/testing/TestFailuresPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/testing/TestFailuresPlugin.java index 806e563b1b9..6fdb9bec493 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/testing/TestFailuresPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/testing/TestFailuresPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,11 @@ package org.springframework.boot.build.testing; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import org.gradle.BuildResult; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.testing.Test; import org.gradle.api.tasks.testing.TestDescriptor; import org.gradle.api.tasks.testing.TestListener; @@ -39,46 +36,37 @@ public class TestFailuresPlugin implements Plugin { @Override public void apply(Project project) { - TestResultsExtension testResults = getOrCreateTestResults(project); + Provider testResultsOverview = project.getGradle().getSharedServices() + .registerIfAbsent("testResultsOverview", TestResultsOverview.class, (spec) -> { + }); project.getTasks().withType(Test.class, - (test) -> test.addTestListener(new FailureRecordingTestListener(testResults, test))); - } - - private TestResultsExtension getOrCreateTestResults(Project project) { - TestResultsExtension testResults = project.getRootProject().getExtensions() - .findByType(TestResultsExtension.class); - if (testResults == null) { - testResults = project.getRootProject().getExtensions().create("testResults", TestResultsExtension.class); - project.getRootProject().getGradle().buildFinished(testResults::buildFinished); - } - return testResults; + (test) -> test.addTestListener(new FailureRecordingTestListener(testResultsOverview, test))); } private final class FailureRecordingTestListener implements TestListener { - private final List failures = new ArrayList<>(); + private final List failures = new ArrayList<>(); - private final TestResultsExtension testResults; + private final Provider testResultsOverview; private final Test test; - private FailureRecordingTestListener(TestResultsExtension testResults, Test test) { - this.testResults = testResults; + private FailureRecordingTestListener(Provider testResultOverview, Test test) { + this.testResultsOverview = testResultOverview; this.test = test; } @Override public void afterSuite(TestDescriptor descriptor, TestResult result) { if (!this.failures.isEmpty()) { - Collections.sort(this.failures); - this.testResults.addFailures(this.test, this.failures); + this.testResultsOverview.get().addFailures(this.test, this.failures); } } @Override public void afterTest(TestDescriptor descriptor, TestResult result) { if (result.getFailedTestCount() > 0) { - this.failures.add(new TestFailure(descriptor)); + this.failures.add(descriptor); } } @@ -94,55 +82,4 @@ public class TestFailuresPlugin implements Plugin { } - private static final class TestFailure implements Comparable { - - private final TestDescriptor descriptor; - - private TestFailure(TestDescriptor descriptor) { - this.descriptor = descriptor; - } - - @Override - public int compareTo(TestFailure other) { - int comparison = this.descriptor.getClassName().compareTo(other.descriptor.getClassName()); - if (comparison == 0) { - comparison = this.descriptor.getName().compareTo(other.descriptor.getName()); - } - return comparison; - } - - } - - public static class TestResultsExtension { - - private final Map> testFailures = new TreeMap<>( - (one, two) -> one.getPath().compareTo(two.getPath())); - - private final Object monitor = new Object(); - - void addFailures(Test test, List testFailures) { - synchronized (this.monitor) { - this.testFailures.put(test, testFailures); - } - } - - public void buildFinished(BuildResult result) { - synchronized (this.monitor) { - if (this.testFailures.isEmpty()) { - return; - } - System.err.println(); - System.err.println("Found test failures in " + this.testFailures.size() + " test task" - + ((this.testFailures.size() == 1) ? ":" : "s:")); - this.testFailures.forEach((task, failures) -> { - System.err.println(); - System.err.println(task.getPath()); - failures.forEach((failure) -> System.err.println( - " " + failure.descriptor.getClassName() + " > " + failure.descriptor.getName())); - }); - } - } - - } - } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/testing/TestResultsOverview.java b/buildSrc/src/main/java/org/springframework/boot/build/testing/TestResultsOverview.java new file mode 100644 index 00000000000..5a45fd1c0ac --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/testing/TestResultsOverview.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.testing; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.gradle.api.services.BuildService; +import org.gradle.api.services.BuildServiceParameters; +import org.gradle.api.tasks.testing.Test; +import org.gradle.api.tasks.testing.TestDescriptor; +import org.gradle.tooling.events.FinishEvent; +import org.gradle.tooling.events.OperationCompletionListener; + +/** + * {@link BuildService} that provides an overview of all of the test failures in the + * build. + * + * @author Andy Wilkinson + */ +public abstract class TestResultsOverview + implements BuildService, OperationCompletionListener, AutoCloseable { + + private final Map> testFailures = new TreeMap<>( + (one, two) -> one.getPath().compareTo(two.getPath())); + + private final Object monitor = new Object(); + + void addFailures(Test test, List failureDescriptors) { + List testFailures = failureDescriptors.stream().map(TestFailure::new).sorted() + .collect(Collectors.toList()); + synchronized (this.monitor) { + this.testFailures.put(test, testFailures); + } + } + + @Override + public void onFinish(FinishEvent event) { + // OperationCompletionListener is implemented to defer close until the build ends + } + + @Override + public void close() { + synchronized (this.monitor) { + if (this.testFailures.isEmpty()) { + return; + } + System.err.println(); + System.err.println("Found test failures in " + this.testFailures.size() + " test task" + + ((this.testFailures.size() == 1) ? ":" : "s:")); + this.testFailures.forEach((task, failures) -> { + System.err.println(); + System.err.println(task.getPath()); + failures.forEach((failure) -> System.err + .println(" " + failure.descriptor.getClassName() + " > " + failure.descriptor.getName())); + }); + } + } + + private static final class TestFailure implements Comparable { + + private final TestDescriptor descriptor; + + private TestFailure(TestDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public int compareTo(TestFailure other) { + int comparison = this.descriptor.getClassName().compareTo(other.descriptor.getClassName()); + if (comparison == 0) { + comparison = this.descriptor.getName().compareTo(other.descriptor.getName()); + } + return comparison; + } + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/testing/TestFailuresPluginIntegrationTests.java b/buildSrc/src/test/java/org/springframework/boot/build/testing/TestFailuresPluginIntegrationTests.java index f0d914319aa..38dd5b4de60 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/testing/TestFailuresPluginIntegrationTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/testing/TestFailuresPluginIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ class TestFailuresPluginIntegrationTests { void multiProjectParallel() throws IOException { createMultiProjectBuild(); BuildResult result = GradleRunner.create().withDebug(true).withProjectDir(this.projectDir) - .withArguments("build", "--parallel").withPluginClasspath().buildAndFail(); + .withArguments("build", "--parallel", "--stacktrace").withPluginClasspath().buildAndFail(); assertThat(readLines(result.getOutput())).containsSequence("Found test failures in 2 test tasks:", "", ":project-one:test", " example.ExampleTests > bad()", " example.ExampleTests > fail()", " example.MoreTests > bad()", " example.MoreTests > fail()", "", ":project-two:test",