diff --git a/changelog/@unreleased/pr-1764.v2.yml b/changelog/@unreleased/pr-1764.v2.yml new file mode 100644 index 000000000..dac15bd63 --- /dev/null +++ b/changelog/@unreleased/pr-1764.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: Allow other plugins to register tasks to produce junit reports + links: + - https://github.com/palantir/gradle-baseline/pull/1764 diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineCircleCi.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineCircleCi.java index 8421a7bc2..732b2cae4 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineCircleCi.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineCircleCi.java @@ -19,7 +19,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.palantir.gradle.junit.JunitReportsExtension; -import com.palantir.gradle.junit.JunitReportsPlugin; +import com.palantir.gradle.junit.JunitReportsRootPlugin; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -39,7 +39,7 @@ public final class BaselineCircleCi implements Plugin { @Override public void apply(Project project) { - project.getPluginManager().apply(JunitReportsPlugin.class); + project.getPluginManager().apply(JunitReportsRootPlugin.class); configurePluginsForReports(project); configurePluginsForArtifacts(project); diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineCircleCiJavaIntegrationTests.java b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineCircleCiJavaIntegrationTests.java index bdc37ffb9..128b90433 100644 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineCircleCiJavaIntegrationTests.java +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineCircleCiJavaIntegrationTests.java @@ -70,7 +70,7 @@ public void javacIntegrationTest() throws IOException { .contains("b = 2") .contains("uses unchecked or unsafe operations"); - File report = new File(reportsDir, "javac/foobar-compileJava.xml"); + File report = new File(reportsDir, "/foobar-compileJava.xml"); assertThat(report).exists(); String reportXml = Files.asCharSource(report, StandardCharsets.UTF_8).read(); assertThat(reportXml) @@ -134,7 +134,7 @@ public void checkstyleIntegrationTest() throws IOException { .buildAndFail(); assertThat(result.getOutput()).contains("Checkstyle rule violations were found"); - File report = new File(reportsDir, "checkstyle/foobar-checkstyleMain.xml"); + File report = new File(reportsDir, "foobar-checkstyleMain.xml"); assertThat(report).exists(); String reportXml = Files.asCharSource(report, StandardCharsets.UTF_8).read(); assertThat(reportXml).contains("Name 'a_constant' must match pattern"); diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineIntegrationTest.groovy b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineIntegrationTest.groovy index 6498147a7..51e6b77aa 100644 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineIntegrationTest.groovy +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineIntegrationTest.groovy @@ -44,6 +44,6 @@ class BaselineIntegrationTest extends AbstractPluginTest { with().withArguments('-s').withGradleVersion(gradleVersion).build() where: - gradleVersion << ['5.4', '6.2'] + gradleVersion << ['6.1', '6.2'] } } diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/JunitReportsPluginSpec.groovy b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/JunitReportsPluginSpec.groovy index a4fc9fe77..26efd78bc 100644 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/JunitReportsPluginSpec.groovy +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/JunitReportsPluginSpec.groovy @@ -24,7 +24,7 @@ import spock.lang.Unroll @Unroll @IgnoreIf({ Integer.parseInt(jvm.javaSpecificationVersion) >= 14 }) class JunitReportsPluginSpec extends IntegrationSpec { - private static final List GRADLE_TEST_VERSIONS = ['5.6.4', '6.1'] + private static final List GRADLE_TEST_VERSIONS = ['6.1'] def '#gradleVersionNumber: configures the checkstlye plugin correctly'() { setup: diff --git a/gradle-junit-reports/build.gradle b/gradle-junit-reports/build.gradle index 2358c428a..403e08c03 100644 --- a/gradle-junit-reports/build.gradle +++ b/gradle-junit-reports/build.gradle @@ -5,12 +5,18 @@ dependencies { compile gradleApi() compile 'com.google.guava:guava' + annotationProcessor 'org.immutables:value' annotationProcessor 'org.inferred:freebuilder' + + compileOnly 'org.immutables:value::annotations' compileOnly 'org.inferred:freebuilder' - testCompile 'com.google.guava:guava' - testCompile 'junit:junit' - testCompile 'org.assertj:assertj-core' - testCompile 'org.mockito:mockito-core' + testImplementation 'com.google.guava:guava' testImplementation 'com.netflix.nebula:nebula-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/BuildFailureListener.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/BuildFailureListener.java index 26dd341a1..235d871ce 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/BuildFailureListener.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/BuildFailureListener.java @@ -19,28 +19,32 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; import org.gradle.api.Task; import org.gradle.api.execution.TaskExecutionListener; import org.gradle.api.tasks.TaskExecutionException; import org.gradle.api.tasks.TaskState; -import org.gradle.api.tasks.testing.Test; public final class BuildFailureListener implements TaskExecutionListener { private final List testCases = new ArrayList<>(); + private final Predicate isTracked; + + public BuildFailureListener(Predicate isTracked) { + this.isTracked = isTracked; + } @Override - @SuppressWarnings("StrictUnusedVariable") - public void beforeExecute(Task task) {} + public void beforeExecute(Task _task) {} @Override public synchronized void afterExecute(Task task, TaskState state) { - if (isUntracked(task)) { + if (!isTracked.test(task)) { Report.TestCase.Builder testCase = new Report.TestCase.Builder().name(":" + task.getProject().getName() + ":" + task.getName()); Throwable failure = state.getFailure(); - if (failure != null && isUntracked(task)) { + if (failure != null) { if (failure instanceof TaskExecutionException && failure.getCause() != null) { failure = failure.getCause(); } @@ -67,8 +71,4 @@ private static String getMessage(Throwable throwable) { return throwable.getClass().getSimpleName() + ": " + throwable.getMessage(); } } - - private static boolean isUntracked(Task task) { - return !(task instanceof Test) && !StyleTaskTimer.isStyleTask(task); - } } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/CheckstyleReportHandler.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/CheckstyleReportHandler.java index b9703f3db..78da3a1c5 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/CheckstyleReportHandler.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/CheckstyleReportHandler.java @@ -41,7 +41,7 @@ public void startElement(String uri, String localName, String qName, Attributes break; case "error": - failures.add(new Failure.Builder() + failures.add(Failure.builder() .source(attributes.getValue("source")) .severity(attributes.getValue("severity").toUpperCase()) .file(file) diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/DefaultTaskTimer.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/DefaultTaskTimer.java new file mode 100644 index 000000000..3ce29bb96 --- /dev/null +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/DefaultTaskTimer.java @@ -0,0 +1,75 @@ +/* + * (c) Copyright 2017 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.gradle.junit; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.function.Predicate; +import org.gradle.api.Task; +import org.gradle.api.execution.TaskExecutionListener; +import org.gradle.api.tasks.TaskState; + +public final class DefaultTaskTimer implements TaskTimer, TaskExecutionListener { + + private final Map taskTimers = new LinkedHashMap<>(); + private final Predicate isTrackedTask; + + public DefaultTaskTimer(Predicate isTrackedTask) { + this.isTrackedTask = isTrackedTask; + } + + @Override + public long getTaskTimeNanos(Task task) { + return Optional.ofNullable(taskTimers.get(task)) + .map(Timer::getElapsed) + .orElseThrow(() -> new IllegalArgumentException("No time available for task " + task.getName())); + } + + @Override + public void beforeExecute(Task task) { + if (isTrackedTask.test(task)) { + taskTimers.put(task, new Timer()); + } + } + + @Override + public void afterExecute(Task task, TaskState _taskState) { + Optional.ofNullable(taskTimers.get(task)).ifPresent(Timer::stop); + } + + static final class Timer { + private final long startTime; + private OptionalLong endTime; + + Timer() { + this.startTime = System.nanoTime(); + this.endTime = OptionalLong.empty(); + } + + void stop() { + endTime = OptionalLong.of(System.nanoTime()); + } + + long getElapsed() { + if (endTime.isPresent()) { + return endTime.getAsLong() - startTime; + } + return 0L; + } + } +} diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Failure.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Failure.java index 4e66fbcfe..c93a37fcd 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Failure.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Failure.java @@ -16,12 +16,16 @@ package com.palantir.gradle.junit; import java.io.File; -import org.inferred.freebuilder.FreeBuilder; +import org.immutables.value.Value.Default; +import org.immutables.value.Value.Immutable; -@FreeBuilder -interface Failure { +@Immutable +public interface Failure { - String source(); + @Default + default String source() { + return ""; + } File file(); @@ -31,12 +35,14 @@ interface Failure { String message(); - String details(); + @Default + default String details() { + return ""; + } + + class Builder extends ImmutableFailure.Builder {} - class Builder extends Failure_Builder { - Builder() { - source(""); - details(""); - } + static Builder builder() { + return new Builder(); } } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/FailuresSupplier.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/FailuresSupplier.java index 7cee6b9a7..88584001b 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/FailuresSupplier.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/FailuresSupplier.java @@ -19,7 +19,7 @@ import java.nio.file.Path; import java.util.List; -interface FailuresSupplier { +public interface FailuresSupplier { List getFailures() throws IOException; RuntimeException handleInternalFailure(Path reportDir, RuntimeException ex); diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JavacFailuresSupplier.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JavacFailuresSupplier.java index 50f449ab9..aed799239 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JavacFailuresSupplier.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JavacFailuresSupplier.java @@ -61,7 +61,7 @@ public List getFailures() { } Matcher matcher = ERROR_LINE.matcher(line); if (matcher.matches()) { - failureBuilder = new Failure.Builder() + failureBuilder = Failure.builder() .file(new File(matcher.group(1))) .line(Integer.parseInt(matcher.group(2))) .severity("ERROR") diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsExtension.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsExtension.java index 783e86f05..375ffd886 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsExtension.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsExtension.java @@ -16,19 +16,35 @@ package com.palantir.gradle.junit; +import java.util.function.Predicate; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.file.DirectoryProperty; public class JunitReportsExtension { + private static final String EXT_JUNIT_REPORTS = "junitReports"; private final DirectoryProperty reportsDirectory; + private final TaskTimer taskTimer; - public JunitReportsExtension(Project project) { - this.reportsDirectory = project.getObjects().directoryProperty(); - reportsDirectory.set(project.getLayout().getBuildDirectory().dir("junit-reports")); + static JunitReportsExtension register(Project project, Predicate isTaskRegistered) { + DefaultTaskTimer timer = new DefaultTaskTimer(isTaskRegistered); + project.getGradle().addListener(timer); + return project.getExtensions().create(EXT_JUNIT_REPORTS, JunitReportsExtension.class, project, timer); + } + + public JunitReportsExtension(Project project, TaskTimer taskTimer) { + this.reportsDirectory = project.getObjects() + .directoryProperty() + .value(project.getLayout().getBuildDirectory().dir("junit-reports")); + this.taskTimer = taskTimer; } public final DirectoryProperty getReportsDirectory() { return reportsDirectory; } + + public final TaskTimer getTaskTimer() { + return taskTimer; + } } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsFinalizer.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsFinalizer.java index 63e96680c..4019ffa68 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsFinalizer.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsFinalizer.java @@ -21,67 +21,63 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; -import javax.inject.Inject; import javax.xml.transform.TransformerException; import org.gradle.api.DefaultTask; +import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.util.GUtil; import org.w3c.dom.Document; -public class JunitReportsFinalizer extends DefaultTask { +public abstract class JunitReportsFinalizer extends DefaultTask { public static void registerFinalizer( - Task task, TaskTimer timer, FailuresSupplier failuresSupplier, Provider reportDir) { - JunitReportsFinalizer finalizer = Tasks.createTask( - task.getProject().getTasks(), task.getName() + "CircleFinalizer", JunitReportsFinalizer.class); - if (finalizer == null) { - // Already registered (happens if the user applies us to the root project and subprojects) - return; - } - finalizer.setStyleTask(task); - finalizer.setTaskTimer(timer); - finalizer.setFailuresSupplier(failuresSupplier); - finalizer - .getTargetFile() - .set(reportDir.map(dir -> dir.file(task.getProject().getName() + "-" + task.getName() + ".xml"))); - finalizer.getReportDir().set(reportDir); - - task.finalizedBy(finalizer); + Project project, + String taskName, + TaskTimer taskTimer, + FailuresSupplier failuresSupplier, + Provider reportDir) { + TaskProvider wrappedTask = project.getTasks().named(taskName); + TaskProvider finalizer = project.getTasks() + .register( + GUtil.toLowerCamelCase(taskName + " junitReportsFinalizer"), + JunitReportsFinalizer.class, + task -> { + task.getWrappedDidWork() + .set(project.provider( + () -> wrappedTask.get().getDidWork())); + task.getWrappedTaskName().set(taskName); + task.getDurationNanos() + .set(project.provider(() -> taskTimer.getTaskTimeNanos(wrappedTask.get()))); + task.setFailuresSupplier(failuresSupplier); + task.getTargetFile() + .set(reportDir.map(dir -> dir.file(project.getName() + "-" + taskName + ".xml"))); + task.getReportDir().set(reportDir); + }); + + wrappedTask.configure(task -> { + task.finalizedBy(finalizer); + }); } - private Task styleTask; - private TaskTimer taskTimer; private FailuresSupplier failuresSupplier; - private final RegularFileProperty targetFile = getProject().getObjects().fileProperty(); - private final DirectoryProperty reportDir = getProject().getObjects().directoryProperty(); - - @Inject - public JunitReportsFinalizer() {} @Input - public final Task getStyleTask() { - return styleTask; - } - - public final void setStyleTask(Task styleTask) { - this.styleTask = styleTask; - } + public abstract Property getWrappedDidWork(); @Input - public final TaskTimer getTaskTimer() { - return taskTimer; - } + abstract Property getWrappedTaskName(); - public final void setTaskTimer(TaskTimer taskTimer) { - this.taskTimer = taskTimer; - } - - @Input + @Internal public final FailuresSupplier getFailuresSupplier() { return failuresSupplier; } @@ -90,19 +86,18 @@ public final void setFailuresSupplier(FailuresSupplier failuresSupplier) { this.failuresSupplier = failuresSupplier; } - @Input - public final RegularFileProperty getTargetFile() { - return targetFile; - } + @Internal + public abstract Property getDurationNanos(); - @Input - public final DirectoryProperty getReportDir() { - return reportDir; - } + @OutputFile + public abstract RegularFileProperty getTargetFile(); + + @Internal + public abstract DirectoryProperty getReportDir(); @TaskAction public final void createCircleReport() throws IOException, TransformerException { - if (!styleTask.getDidWork()) { + if (!getWrappedDidWork().get()) { setDidWork(false); return; } @@ -111,12 +106,12 @@ public final void createCircleReport() throws IOException, TransformerException File rootDir = getProject().getRootProject().getProjectDir(); String projectName = getProject().getName(); List failures = failuresSupplier.getFailures(); - long taskTimeNanos = taskTimer.getTaskTimeNanos(styleTask); + long taskTimeNanos = getDurationNanos().get(); Document report = JunitReportCreator.reportToXml(FailuresReportGenerator.failuresReport( - rootDir, projectName, styleTask.getName(), taskTimeNanos, failures)); + rootDir, projectName, getWrappedTaskName().get(), taskTimeNanos, failures)); - File target = targetFile.getAsFile().get(); + File target = getTargetFile().getAsFile().get(); Files.createDirectories(target.getParentFile().toPath()); try (Writer writer = Files.newBufferedWriter(target.toPath(), StandardCharsets.UTF_8)) { XmlUtils.write(writer, report); @@ -125,7 +120,7 @@ public final void createCircleReport() throws IOException, TransformerException RuntimeException modified; try { modified = failuresSupplier.handleInternalFailure( - reportDir.getAsFile().get().toPath(), e); + getReportDir().getAsFile().get().toPath(), e); } catch (RuntimeException x) { e.addSuppressed(x); throw e; diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsPlugin.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsPlugin.java index f52ffd348..a5c85d038 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsPlugin.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsPlugin.java @@ -18,10 +18,8 @@ import com.google.common.base.Splitter; import java.io.File; -import java.nio.file.Path; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFile; import org.gradle.api.plugins.quality.Checkstyle; @@ -31,50 +29,33 @@ public final class JunitReportsPlugin implements Plugin { - public static final String EXT_JUNIT_REPORTS = "junitReports"; - @Override @SuppressWarnings("Slf4jLogsafeArgs") public void apply(Project project) { - if (project != project.getRootProject()) { - project.getLogger() - .warn( - "com.palantir.junit-reports should be applied to the root project only, not '{}'", - project.getName()); - } - - JunitReportsExtension reportsExtension = - project.getExtensions().create(EXT_JUNIT_REPORTS, JunitReportsExtension.class, project); + JunitReportsExtension rootExt = project.getRootProject().getExtensions().getByType(JunitReportsExtension.class); + JunitTaskResultExtension ext = JunitTaskResultExtension.register(project); - configureBuildFailureFinalizer(project.getRootProject(), reportsExtension.getReportsDirectory()); + project.getTasks().withType(Test.class, test -> { + test.getReports().getJunitXml().setEnabled(true); + test.getReports().getJunitXml().setDestination(junitPath(rootExt.getReportsDirectory(), test.getPath())); + }); - TaskTimer timer = new StyleTaskTimer(); - project.getRootProject().getGradle().addListener(timer); + project.getTasks().withType(Checkstyle.class, checkstyle -> { + ext.registerTask( + checkstyle.getName(), XmlReportFailuresSupplier.create(checkstyle, new CheckstyleReportHandler())); + }); - project.getRootProject().allprojects(proj -> { - proj.getTasks().withType(Test.class, test -> { - test.getReports().getJunitXml().setEnabled(true); - test.getReports() - .getJunitXml() - .setDestination(junitPath(reportsExtension.getReportsDirectory(), test.getPath())); - }); - proj.getTasks() - .withType( - Checkstyle.class, - checkstyle -> JunitReportsFinalizer.registerFinalizer( - checkstyle, - timer, - XmlReportFailuresSupplier.create(checkstyle, new CheckstyleReportHandler()), - reportsExtension.getReportsDirectory().map(dir -> dir.dir("checkstyle")))); - proj.getTasks() - .withType( - JavaCompile.class, - javac -> JunitReportsFinalizer.registerFinalizer( - javac, - timer, - JavacFailuresSupplier.create(javac), - reportsExtension.getReportsDirectory().map(dir -> dir.dir("javac")))); + project.getTasks().withType(JavaCompile.class, javac -> { + ext.registerTask(javac.getName(), JavacFailuresSupplier.create(javac)); }); + + ext.getTaskEntries() + .configureEach(entry -> JunitReportsFinalizer.registerFinalizer( + project, + entry.name(), + rootExt.getTaskTimer(), + entry.failuresSupplier(), + rootExt.getReportsDirectory())); } private static Provider junitPath(DirectoryProperty basePath, String testPath) { @@ -83,20 +64,4 @@ private static Provider junitPath(DirectoryProperty basePath, String testP dir.file(String.join(File.separator, Splitter.on(':').splitToList(testPath.substring(1))))) .map(RegularFile::getAsFile); } - - private static void configureBuildFailureFinalizer(Project rootProject, Provider reportsDir) { - Provider targetFileProvider = reportsDir.map(dir -> { - int attemptNumber = 1; - Path targetFile = dir.getAsFile().toPath().resolve("gradle").resolve("build.xml"); - while (targetFile.toFile().exists()) { - targetFile = dir.getAsFile().toPath().resolve("gradle").resolve("build" + ++attemptNumber + ".xml"); - } - return dir.file(targetFile.toAbsolutePath().toString()); - }); - - BuildFailureListener listener = new BuildFailureListener(); - BuildFinishedAction action = new BuildFinishedAction(targetFileProvider, listener); - rootProject.getGradle().addListener(listener); - rootProject.getGradle().buildFinished(action); - } } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsRootPlugin.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsRootPlugin.java new file mode 100644 index 000000000..139dc4efa --- /dev/null +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitReportsRootPlugin.java @@ -0,0 +1,61 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.gradle.junit; + +import com.google.common.base.Preconditions; +import java.nio.file.Path; +import java.util.function.Predicate; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.Provider; + +public final class JunitReportsRootPlugin implements Plugin { + @Override + public void apply(Project project) { + Preconditions.checkState( + project.getRootProject().equals(project), "Plugin must only be applied to root project"); + + Predicate isTaskRegistered = task -> task.getProject() + .getExtensions() + .getByType(JunitTaskResultExtension.class) + .getTaskEntries() + .findByName(task.getName()) + != null; + + JunitReportsExtension reportsExtension = JunitReportsExtension.register(project, isTaskRegistered); + Provider targetFileProvider = reportsExtension + .getReportsDirectory() + .map(dir -> { + int attemptNumber = 1; + Path targetFile = dir.getAsFile().toPath().resolve("gradle").resolve("build.xml"); + while (targetFile.toFile().exists()) { + targetFile = + dir.getAsFile().toPath().resolve("gradle").resolve("build" + ++attemptNumber + ".xml"); + } + return dir.file(targetFile.toAbsolutePath().toString()); + }); + + BuildFailureListener listener = new BuildFailureListener(isTaskRegistered); + BuildFinishedAction action = new BuildFinishedAction(targetFileProvider, listener); + project.getGradle().addListener(listener); + project.getGradle().buildFinished(action); + + project.allprojects(proj -> proj.getPluginManager().apply(JunitReportsPlugin.class)); + } +} diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitTaskResultExtension.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitTaskResultExtension.java new file mode 100644 index 000000000..c61946c66 --- /dev/null +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/JunitTaskResultExtension.java @@ -0,0 +1,75 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.gradle.junit; + +import java.nio.file.Path; +import java.util.List; +import org.gradle.api.NamedDomainObjectSet; +import org.gradle.api.Project; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Provider; +import org.immutables.value.Value.Immutable; + +public class JunitTaskResultExtension { + static final String NAME = "junitTaskResults"; + private final NamedDomainObjectSet taskEntries; + + static JunitTaskResultExtension register(Project project) { + return project.getExtensions().create(NAME, JunitTaskResultExtension.class); + } + + public JunitTaskResultExtension(ObjectFactory objects) { + this.taskEntries = objects.namedDomainObjectSet(TaskEntry.class); + } + + public final NamedDomainObjectSet getTaskEntries() { + return taskEntries; + } + + public final void registerTask(String taskName, Provider> failures) { + registerTask(taskName, new FailuresSupplier() { + @Override + public List getFailures() { + return failures.get(); + } + + @Override + public RuntimeException handleInternalFailure(Path _reportDir, RuntimeException ex) { + return ex; + } + }); + } + + public final void registerTask(String taskName, FailuresSupplier failuresSupplier) { + taskEntries.add(TaskEntry.of(taskName, failuresSupplier)); + } + + @Immutable + interface TaskEntry { + + String name(); + + FailuresSupplier failuresSupplier(); + + static TaskEntry of(String name, FailuresSupplier failuresSupplier) { + return ImmutableTaskEntry.builder() + .name(name) + .failuresSupplier(failuresSupplier) + .build(); + } + } +} diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/StyleTaskTimer.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/StyleTaskTimer.java deleted file mode 100644 index 3f13cb617..000000000 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/StyleTaskTimer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * (c) Copyright 2017 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.gradle.junit; - -import java.util.LinkedHashMap; -import java.util.Map; -import org.gradle.api.Task; -import org.gradle.api.plugins.quality.Checkstyle; -import org.gradle.api.tasks.TaskState; -import org.gradle.api.tasks.compile.JavaCompile; - -public final class StyleTaskTimer implements TaskTimer { - - private final Map taskTimeNanosByTask = new LinkedHashMap<>(); - private long lastStartTime; - - @Override - @SuppressWarnings("PreferSafeLoggableExceptions") - public long getTaskTimeNanos(Task styleTask) { - if (!isStyleTask(styleTask)) { - throw new ClassCastException("not a style task"); - } - Long taskTimeNanos = taskTimeNanosByTask.get(styleTask); - if (taskTimeNanos == null) { - throw new IllegalArgumentException("no time available for task"); - } - return taskTimeNanos; - } - - @Override - public void beforeExecute(Task task) { - if (isStyleTask(task)) { - lastStartTime = System.nanoTime(); - } - } - - @Override - @SuppressWarnings("StrictUnusedVariable") - public void afterExecute(Task task, TaskState taskState) { - if (isStyleTask(task)) { - taskTimeNanosByTask.put(task, System.nanoTime() - lastStartTime); - } - } - - public static boolean isStyleTask(Task task) { - return task instanceof Checkstyle || task instanceof JavaCompile; - } -} diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/TaskTimer.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/TaskTimer.java index a8e9ee020..243fdc3c6 100644 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/TaskTimer.java +++ b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/TaskTimer.java @@ -17,8 +17,7 @@ package com.palantir.gradle.junit; import org.gradle.api.Task; -import org.gradle.api.execution.TaskExecutionListener; -public interface TaskTimer extends TaskExecutionListener { - long getTaskTimeNanos(Task styleTask); +public interface TaskTimer { + long getTaskTimeNanos(Task task); } diff --git a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Tasks.java b/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Tasks.java deleted file mode 100644 index 06150eb13..000000000 --- a/gradle-junit-reports/src/main/java/com/palantir/gradle/junit/Tasks.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * (c) Copyright 2018 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.gradle.junit; - -import org.gradle.api.Task; -import org.gradle.api.UnknownTaskException; -import org.gradle.api.tasks.TaskContainer; - -public final class Tasks { - - static T createTask(TaskContainer tasks, String preferredName, Class type) { - String name = preferredName; - int count = 1; - while (true) { - try { - Task existingTask = tasks.getByName(name); - if (type.isInstance(existingTask)) { - return null; - } - } catch (UnknownTaskException e) { - return tasks.create(name, type); - } - count++; - name = preferredName + count; - } - } - - private Tasks() {} -} diff --git a/gradle-junit-reports/src/test/groovy/com/palantir/gradle/junit/JunitReportsPluginIntegrationSpec.groovy b/gradle-junit-reports/src/test/groovy/com/palantir/gradle/junit/JunitReportsPluginIntegrationSpec.groovy new file mode 100644 index 000000000..6307c52e4 --- /dev/null +++ b/gradle-junit-reports/src/test/groovy/com/palantir/gradle/junit/JunitReportsPluginIntegrationSpec.groovy @@ -0,0 +1,67 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.gradle.junit + + +import nebula.test.IntegrationSpec + +class JunitReportsPluginIntegrationSpec extends IntegrationSpec { + + + def setup() { + buildFile << """ + ${applyPlugin(JunitReportsRootPlugin)} + + task foo { + doLast { + throw new RuntimeException("Failure") + } + } + """.stripIndent() + } + + def 'captures specific failures for registered tasks'() { + when: + buildFile << """ + import com.palantir.gradle.junit.Failure + + junitTaskResults { + registerTask 'foo', project.provider({ -> [ + new Failure.Builder() + .severity("error") + .file(file('build.gradle')) + .line(1) + .message("some failure") + .build() + ]}) + } + """.stripIndent() + + then: + def result = runTasksWithFailure('foo') + result.wasExecuted('fooJunitReportsFinalizer') + fileExists("build/junit-reports/${moduleName}-foo.xml") + } + + def 'captures failure for non-registered tasks'() { + expect: + def result = runTasksWithFailure('foo') + !result.wasExecuted('fooJunitReportsFinalizer') + !fileExists("build/junit-reports/${moduleName}-foo.xml") + file('build/junit-reports/gradle/build.xml').text.contains "" + } +} diff --git a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/BuildFailureListenerTests.java b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/BuildFailureListenerTests.java index ba47b102f..935951e61 100644 --- a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/BuildFailureListenerTests.java +++ b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/BuildFailureListenerTests.java @@ -16,16 +16,23 @@ package com.palantir.gradle.junit; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.palantir.gradle.junit.Report.TestCase; +import java.util.function.Predicate; import org.gradle.api.Task; import org.gradle.api.plugins.quality.Checkstyle; import org.gradle.api.tasks.TaskState; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) public class BuildFailureListenerTests { private static final String PROJECT_1_NAME = "project1"; @@ -33,7 +40,15 @@ public class BuildFailureListenerTests { private static final String PROJECT_2_NAME = "project2"; private static final String TASK_2_NAME = "task2"; - private final BuildFailureListener listener = new BuildFailureListener(); + @Mock + Predicate isTracked; + + private BuildFailureListener listener; + + @BeforeEach + void beforeEach() { + listener = new BuildFailureListener(isTracked); + } @Test public void noTasks() { @@ -41,14 +56,16 @@ public void noTasks() { } @Test - public void onlyTestAndStyleTasks() { + public void ignoresTrackedTasks() { + when(isTracked.test(any())).thenReturn(true); listener.afterExecute(mock(org.gradle.api.tasks.testing.Test.class), succeeded()); listener.afterExecute(mock(Checkstyle.class), succeeded()); assertThat(listener.getTestCases()).isEmpty(); } @Test - public void successfulTasks() { + public void includesSuccessfulUntrackedTasks() { + when(isTracked.test(any())).thenReturn(false); listener.afterExecute(task(PROJECT_1_NAME, TASK_1_NAME), succeeded()); listener.afterExecute(task(PROJECT_2_NAME, TASK_2_NAME), succeeded()); assertThat(listener.getTestCases()) @@ -63,6 +80,7 @@ public void successfulTasks() { @Test public void failedTasks() { + when(isTracked.test(any())).thenReturn(false); listener.afterExecute(task(PROJECT_1_NAME, TASK_1_NAME), failed("task 1 failed")); listener.afterExecute(task(PROJECT_2_NAME, TASK_2_NAME), failed("task 2 failed")); diff --git a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/FailuresReportGeneratorTests.java b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/FailuresReportGeneratorTests.java index 6853b217c..f4899df79 100644 --- a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/FailuresReportGeneratorTests.java +++ b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/FailuresReportGeneratorTests.java @@ -51,7 +51,7 @@ public void testTwoErrors() { @Test public void testJavacErrors() { List failures = ImmutableList.of( - new Failure.Builder() + Failure.builder() .file(new File(ROOT, "src/main/java/com/example/MyClass.java")) .line(8) .severity("ERROR") @@ -59,7 +59,7 @@ public void testJavacErrors() { .details("\n private final int a = \"hello\"; " + "\n ^") .build(), - new Failure.Builder() + Failure.builder() .file(new File(ROOT, "src/main/java/com/example/MyClass.java")) .line(12) .severity("ERROR") diff --git a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JavacFailuresSupplierTest.java b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JavacFailuresSupplierTest.java index 6f4119de5..daab7db75 100644 --- a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JavacFailuresSupplierTest.java +++ b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JavacFailuresSupplierTest.java @@ -70,14 +70,14 @@ public void twoFailuresInOutputWithNoWarnings() { JavacFailuresSupplier supplier = new JavacFailuresSupplier(new StringBuilder(javacOutput)); assertThat(supplier.getFailures()) .containsExactly( - new Failure.Builder() + Failure.builder() .file(new File(CLASS_FILE)) .line(LINE_1) .severity("ERROR") .message(ERROR_1) .details(DETAIL_1) .build(), - new Failure.Builder() + Failure.builder() .file(new File(CLASS_FILE)) .line(12) .severity("ERROR") diff --git a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JunitReportsFinalizerTests.java b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JunitReportsFinalizerTests.java index 88bd29675..0d53fb71a 100644 --- a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JunitReportsFinalizerTests.java +++ b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/JunitReportsFinalizerTests.java @@ -20,8 +20,6 @@ import static com.palantir.gradle.junit.TestCommon.readTestFile; import static com.palantir.gradle.junit.TestCommon.testFile; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; @@ -53,15 +51,13 @@ public void translatesCheckstyleReport() throws IOException, TransformerExceptio checkstyle.setDidWork(true); - TaskTimer timer = mock(TaskTimer.class); - when(timer.getTaskTimeNanos(checkstyle)).thenReturn(FAILED_CHECKSTYLE_TIME_NANOS); - File targetFile = new File(projectDir.getRoot(), "reports/report.xml"); JunitReportsFinalizer finalizer = (JunitReportsFinalizer) project.task(ImmutableMap.of("type", JunitReportsFinalizer.class), "checkstyleTestCircleFinalizer"); - finalizer.setStyleTask(checkstyle); - finalizer.setTaskTimer(timer); + finalizer.getWrappedDidWork().set(true); + finalizer.getWrappedTaskName().set(checkstyle.getName()); + finalizer.getDurationNanos().set(FAILED_CHECKSTYLE_TIME_NANOS); finalizer.setFailuresSupplier(XmlReportFailuresSupplier.create(checkstyle, new CheckstyleReportHandler())); finalizer.getTargetFile().set(targetFile); @@ -83,17 +79,13 @@ public void doesNothingIfTaskSkipped() throws IOException, TransformerException .build(); Checkstyle checkstyle = createCheckstyleTask(project); - checkstyle.setDidWork(false); - - TaskTimer timer = mock(TaskTimer.class); - when(timer.getTaskTimeNanos(checkstyle)).thenReturn(FAILED_CHECKSTYLE_TIME_NANOS); - File targetFile = new File(projectDir.getRoot(), "reports/report.xml"); JunitReportsFinalizer finalizer = (JunitReportsFinalizer) project.task(ImmutableMap.of("type", JunitReportsFinalizer.class), "checkstyleTestCircleFinalizer"); - finalizer.setStyleTask(checkstyle); - finalizer.setTaskTimer(timer); + finalizer.getWrappedDidWork().set(false); + finalizer.getWrappedTaskName().set(checkstyle.getName()); + finalizer.getDurationNanos().set(FAILED_CHECKSTYLE_TIME_NANOS); finalizer.setFailuresSupplier(XmlReportFailuresSupplier.create(checkstyle, new CheckstyleReportHandler())); finalizer.getTargetFile().set(targetFile); diff --git a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/TestCommon.java b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/TestCommon.java index b91b935f7..6b46b297d 100644 --- a/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/TestCommon.java +++ b/gradle-junit-reports/src/test/java/com/palantir/gradle/junit/TestCommon.java @@ -30,14 +30,14 @@ public final class TestCommon { private static final String MESSAGE_2 = "Parameter name 'c' must match pattern '^[a-z][a-zA-Z0-9][a-zA-Z0-9]*$'."; private static final String MESSAGE_1 = "Parameter name 'b' must match pattern '^[a-z][a-zA-Z0-9][a-zA-Z0-9]*$'."; public static final ImmutableList CHECKSTYLE_FAILURES = ImmutableList.of( - new Failure.Builder() + Failure.builder() .source(SOURCE) .severity("ERROR") .file(new File(ROOT, CLASSFILE)) .line(135) .message(MESSAGE_1) .build(), - new Failure.Builder() + Failure.builder() .source(SOURCE) .severity("ERROR") .file(new File(ROOT, CLASSFILE)) diff --git a/versions.lock b/versions.lock index 22a81e14c..70c889ac6 100644 --- a/versions.lock +++ b/versions.lock @@ -45,7 +45,7 @@ com.typesafe:config:1.2.0 (1 constraints: d60cb924) commons-io:commons-io:2.6 (2 constraints: 1e213039) io.dropwizard.metrics:metrics-core:3.2.5 (1 constraints: 94108aa5) io.github.java-diff-utils:java-diff-utils:4.0 (1 constraints: 811205f6) -junit:junit:4.13.2 (9 constraints: 5f81550b) +junit:junit:4.13.2 (10 constraints: ae9100f0) net.bytebuddy:byte-buddy:1.10.20 (1 constraints: 6e0ba2e9) net.bytebuddy:byte-buddy-agent:1.10.20 (1 constraints: 6e0ba2e9) net.ltgt.gradle:gradle-errorprone-plugin:1.3.0 (1 constraints: 0605f935) @@ -74,7 +74,7 @@ org.hamcrest:hamcrest-core:2.2 (4 constraints: 2b2b319e) org.hamcrest:hamcrest-library:1.3 (1 constraints: fc138c38) org.immutables:value:2.8.8 (1 constraints: 14051536) org.inferred:freebuilder:1.14.6 (1 constraints: 3e053b3b) -org.mockito:mockito-core:3.8.0 (2 constraints: c6123d28) +org.mockito:mockito-core:3.8.0 (3 constraints: 8b214d98) org.mockito:mockito-errorprone:3.8.0 (1 constraints: 0d051236) org.objenesis:objenesis:3.1 (2 constraints: 9917f357) org.ow2.asm:asm:9.1 (3 constraints: 4223aa30) @@ -106,16 +106,18 @@ javax.xml.bind:jaxb-api:2.3.1 (1 constraints: c0069559) junit:junit-dep:4.11 (1 constraints: ba1063b3) net.lingala.zip4j:zip4j:1.3.2 (1 constraints: 0805fb35) one.util:streamex:0.7.3 (1 constraints: 0c050336) -org.apiguardian:apiguardian-api:1.1.0 (6 constraints: 18697c5a) +org.apiguardian:apiguardian-api:1.1.0 (7 constraints: 8f790357) org.jooq:jooq:3.14.8 (1 constraints: 4205493b) -org.junit:junit-bom:5.7.1 (7 constraints: 6377a0d6) +org.junit:junit-bom:5.7.1 (8 constraints: e5874073) org.junit.jupiter:junit-jupiter:5.7.1 (2 constraints: 220e3a59) -org.junit.jupiter:junit-jupiter-api:5.7.1 (5 constraints: a04df979) +org.junit.jupiter:junit-jupiter-api:5.7.1 (6 constraints: 675c6b92) org.junit.jupiter:junit-jupiter-engine:5.7.1 (2 constraints: 1d177a3c) org.junit.jupiter:junit-jupiter-migrationsupport:5.7.1 (2 constraints: 220e3a59) org.junit.jupiter:junit-jupiter-params:5.7.1 (2 constraints: 1d177a3c) org.junit.platform:junit-platform-commons:1.7.1 (3 constraints: e829c52a) -org.junit.platform:junit-platform-engine:1.7.1 (2 constraints: b81977f3) +org.junit.platform:junit-platform-engine:1.7.1 (3 constraints: 362a9f5c) +org.junit.vintage:junit-vintage-engine:5.7.1 (1 constraints: 14098995) +org.mockito:mockito-junit-jupiter:3.8.0 (1 constraints: 0d051236) org.opentest4j:opentest4j:1.2.0 (2 constraints: cd205b49) org.reactivestreams:reactive-streams:1.0.2 (1 constraints: bd068859) org.spockframework:spock-core:1.3-groovy-2.4 (1 constraints: 7c10f3af)