Log some information about all test failures when the build completes
Closes gh-19696pull/19790/head
parent
986bef9cba
commit
2ac931cacb
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.ArrayList;
|
||||||
|
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.tasks.testing.Test;
|
||||||
|
import org.gradle.api.tasks.testing.TestDescriptor;
|
||||||
|
import org.gradle.api.tasks.testing.TestListener;
|
||||||
|
import org.gradle.api.tasks.testing.TestResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin for recording test failures and reporting them at the end of the build.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class TestFailuresPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(Project project) {
|
||||||
|
TestResultsExtension testResults = getOrCreateTestResults(project);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FailureRecordingTestListener implements TestListener {
|
||||||
|
|
||||||
|
private List<TestFailure> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
private final TestResultsExtension testResults;
|
||||||
|
|
||||||
|
private final Test test;
|
||||||
|
|
||||||
|
private FailureRecordingTestListener(TestResultsExtension testResults, Test test) {
|
||||||
|
this.testResults = testResults;
|
||||||
|
this.test = test;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterSuite(TestDescriptor descriptor, TestResult result) {
|
||||||
|
if (!this.failures.isEmpty()) {
|
||||||
|
this.testResults.addFailures(this.test, this.failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTest(TestDescriptor descriptor, TestResult result) {
|
||||||
|
if (result.getFailedTestCount() > 0) {
|
||||||
|
this.failures.add(new TestFailure(descriptor, result.getExceptions()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeSuite(TestDescriptor descriptor) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTest(TestDescriptor descriptor) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestFailure implements Comparable<TestFailure> {
|
||||||
|
|
||||||
|
private final TestDescriptor descriptor;
|
||||||
|
|
||||||
|
private final List<Throwable> exceptions;
|
||||||
|
|
||||||
|
private TestFailure(TestDescriptor descriptor, List<Throwable> exceptions) {
|
||||||
|
this.descriptor = descriptor;
|
||||||
|
this.exceptions = exceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.getClassName());
|
||||||
|
}
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestResultsExtension {
|
||||||
|
|
||||||
|
private final Map<Test, List<TestFailure>> testFailures = new TreeMap<>(
|
||||||
|
(one, two) -> one.getPath().compareTo(two.getPath()));
|
||||||
|
|
||||||
|
private final Object monitor = new Object();
|
||||||
|
|
||||||
|
void addFailures(Test test, List<TestFailure> 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()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.BuildResult;
|
||||||
|
import org.gradle.testkit.runner.GradleRunner;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrations tests for {@link TestFailuresPlugin}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class TestFailuresPluginIntegrationTests {
|
||||||
|
|
||||||
|
private File projectDir;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir File projectDir) throws IOException {
|
||||||
|
this.projectDir = projectDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void singleProject() throws IOException {
|
||||||
|
createProject(this.projectDir);
|
||||||
|
BuildResult result = GradleRunner.create().withDebug(true).withProjectDir(this.projectDir)
|
||||||
|
.withArguments("build").withPluginClasspath().buildAndFail();
|
||||||
|
assertThat(readLines(result.getOutput())).containsSequence("Found test failures in 1 test task:", "", ":test",
|
||||||
|
" example.ExampleTests > bad()", " example.ExampleTests > fail()",
|
||||||
|
" example.MoreTests > bad()", " example.MoreTests > fail()", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiProject() throws IOException {
|
||||||
|
createMultiProjectBuild();
|
||||||
|
BuildResult result = GradleRunner.create().withDebug(true).withProjectDir(this.projectDir)
|
||||||
|
.withArguments("build").withPluginClasspath().buildAndFail();
|
||||||
|
assertThat(readLines(result.getOutput())).containsSequence("Found test failures in 1 test task:", "",
|
||||||
|
":project-one:test", " example.ExampleTests > bad()", " example.ExampleTests > fail()",
|
||||||
|
" example.MoreTests > bad()", " example.MoreTests > fail()", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiProjectContinue() throws IOException {
|
||||||
|
createMultiProjectBuild();
|
||||||
|
BuildResult result = GradleRunner.create().withDebug(true).withProjectDir(this.projectDir)
|
||||||
|
.withArguments("build", "--continue").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",
|
||||||
|
" example.ExampleTests > bad()", " example.ExampleTests > fail()",
|
||||||
|
" example.MoreTests > bad()", " example.MoreTests > fail()", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiProjectParallel() throws IOException {
|
||||||
|
createMultiProjectBuild();
|
||||||
|
BuildResult result = GradleRunner.create().withDebug(true).withProjectDir(this.projectDir)
|
||||||
|
.withArguments("build", "--parallel").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",
|
||||||
|
" example.ExampleTests > bad()", " example.ExampleTests > fail()",
|
||||||
|
" example.MoreTests > bad()", " example.MoreTests > fail()", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createProject(File dir) {
|
||||||
|
File examplePackage = new File(dir, "src/test/java/example");
|
||||||
|
examplePackage.mkdirs();
|
||||||
|
createTestSource("ExampleTests", examplePackage);
|
||||||
|
createTestSource("MoreTests", examplePackage);
|
||||||
|
createBuildScript(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMultiProjectBuild() {
|
||||||
|
createProject(new File(this.projectDir, "project-one"));
|
||||||
|
createProject(new File(this.projectDir, "project-two"));
|
||||||
|
withPrintWriter(new File(this.projectDir, "settings.gradle"), (writer) -> {
|
||||||
|
writer.println("include 'project-one'");
|
||||||
|
writer.println("include 'project-two'");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTestSource(String name, File dir) {
|
||||||
|
withPrintWriter(new File(dir, name + ".java"), (writer) -> {
|
||||||
|
writer.println("package example;");
|
||||||
|
writer.println();
|
||||||
|
writer.println("import org.junit.jupiter.api.Test;");
|
||||||
|
writer.println();
|
||||||
|
writer.println("import static org.assertj.core.api.Assertions.assertThat;");
|
||||||
|
writer.println();
|
||||||
|
writer.println("class " + name + "{");
|
||||||
|
writer.println();
|
||||||
|
writer.println(" @Test");
|
||||||
|
writer.println(" void fail() {");
|
||||||
|
writer.println(" assertThat(true).isFalse();");
|
||||||
|
writer.println(" }");
|
||||||
|
writer.println();
|
||||||
|
writer.println(" @Test");
|
||||||
|
writer.println(" void bad() {");
|
||||||
|
writer.println(" assertThat(5).isLessThan(4);");
|
||||||
|
writer.println(" }");
|
||||||
|
writer.println();
|
||||||
|
writer.println(" @Test");
|
||||||
|
writer.println(" void ok() {");
|
||||||
|
writer.println(" }");
|
||||||
|
writer.println();
|
||||||
|
writer.println("}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBuildScript(File dir) {
|
||||||
|
withPrintWriter(new File(dir, "build.gradle"), (writer) -> {
|
||||||
|
writer.println("plugins {");
|
||||||
|
writer.println(" id 'java'");
|
||||||
|
writer.println(" id 'org.springframework.boot.test-failures'");
|
||||||
|
writer.println("}");
|
||||||
|
writer.println();
|
||||||
|
writer.println("repositories {");
|
||||||
|
writer.println(" mavenCentral()");
|
||||||
|
writer.println("}");
|
||||||
|
writer.println();
|
||||||
|
writer.println("dependencies {");
|
||||||
|
writer.println(" testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'");
|
||||||
|
writer.println(" testImplementation 'org.assertj:assertj-core:3.11.1'");
|
||||||
|
writer.println("}");
|
||||||
|
writer.println();
|
||||||
|
writer.println("test {");
|
||||||
|
writer.println(" useJUnitPlatform()");
|
||||||
|
writer.println("}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withPrintWriter(File file, Consumer<PrintWriter> consumer) {
|
||||||
|
try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
|
||||||
|
consumer.accept(writer);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> readLines(String output) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new StringReader(output))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
lines.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue