diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index 45a5e949bdbd..feeab7ed25f5 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -147,7 +147,7 @@ tasks { val consoleLauncherTestReportsDir = project.layout.buildDirectory.dir("console-launcher-test-results") val consoleLauncherTestEventXmlFiles = - files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("junit-platform-events-*.xml") } }) + files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("**/open-test-report.xml") } }) val consoleLauncherTest by registering(RunConsoleLauncher::class) { args.addAll("execute") @@ -157,7 +157,6 @@ tasks { argumentProviders.add(CommandLineArgumentProvider { listOf( "--reports-dir=${consoleLauncherTestReportsDir.get()}", - "--config=junit.platform.reporting.output.dir=${consoleLauncherTestReportsDir.get()}", ) }) args.addAll("--include-classname", ".*Tests") diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 33fb2881d2b5..4010e54b7085 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -40,6 +40,7 @@ endif::[] :DiscoverySelectors_selectPackage: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectPackage(java.lang.String)[selectPackage] :DiscoverySelectors_selectUniqueId: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUniqueId(java.lang.String)[selectUniqueId] :DiscoverySelectors_selectUri: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUri(java.lang.String)[selectUri] +:EngineDiscoveryRequest: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/EngineDiscoveryRequest.html[EngineDiscoveryRequest] :FileSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/FileSelector.html[FileSelector] :HierarchicalTestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.html[HierarchicalTestEngine] :IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector] @@ -47,6 +48,7 @@ endif::[] :ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector] :NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector] :NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector] +:OutputDirectoryProvider: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider] :PackageSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/PackageSelector.html[PackageSelector] :ParallelExecutionConfigurationStrategy: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.html[ParallelExecutionConfigurationStrategy] :UniqueIdSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/UniqueIdSelector.html[UniqueIdSelector] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index cd9cc2eab135..fe84142c29d7 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -40,11 +40,16 @@ JUnit repository on GitHub. `--select-file` and `--select-resource`. * `ConsoleLauncher` now accepts multiple values for all `--select` options. * Add `--select-unique-id` support to ConsoleLauncher. +* Add `getOutputDirectoryProvider()` method to `EngineDiscoveryRequest` and `TestPlan` to + allow test engines to publish/attach files to containers and tests by calling + `EngineExecutionListener.fileEntryPublished(...)`. Registered `TestExecutionListeners` + can then access these files by overriding the `fileEntryPublished(...)` method. * The following improvements have been made to the open-test-reporting XML output: - Information about the Git repository, the current branch, the commit hash, and the current worktree status are now included in the XML report, if applicable. - A section containing JUnit-specific metadata about each test/container to the HTML report is now written by open-test-reporting when added to the classpath/module path + - Information about published files is now included as attachments. [[release-notes-5.12.0-M1-junit-jupiter]] @@ -112,6 +117,8 @@ JUnit repository on GitHub. * When enabled via the `junit.jupiter.execution.timeout.threaddump.enabled` configuration parameter, an implementation of `PreInterruptCallback` is registered that writes a thread dump to `System.out` prior to interrupting a test thread due to a timeout. +* `TestReporter` now allows publishing files for a test method or test class which can be + used to include them in test reports, such as the Open Test Reporting format. [[release-notes-5.12.0-M1-junit-vintage]] diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc index 287917904eea..310788bd1812 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc @@ -3,41 +3,46 @@ The `junit-platform-reporting` artifact contains `{TestExecutionListener}` implementations that generate XML test reports in two flavors: -<> and -<>. +<> and +<>. NOTE: The module also contains other `TestExecutionListener` implementations that can be used to build custom reporting. See <> for details. -[[junit-platform-reporting-legacy-xml]] -==== Legacy XML format +[[junit-platform-reporting-output-directory]] +==== Output Directory -`{LegacyXmlReportGeneratingListener}` generates a separate XML report for each root in the -`{TestPlan}`. Note that the generated XML format is compatible with the de facto standard -for JUnit 4 based test reports that was made popular by the Ant build system. +The JUnit Platform provides an `{OutputDirectoryProvider}` via +`{EngineDiscoveryRequest}` and `{TestPlan}` to registered <> +and <>, respectively. Its root directory can be +configured via the following <>: -The `LegacyXmlReportGeneratingListener` is used by the <> -as well. +`junit.platform.reporting.output.dir=`:: + Configure the output directory for reporting. By default, `build` is used if a Gradle + build script is found, and `target` if a Maven POM is found; otherwise, the current + working directory is used. + +To create a unique output directory per test run, you can use the `\{uniqueNumber}` +placeholder in the path. For example, `reports/junit-\{uniqueNumber}` will create +directories like `reports/junit-8803697269315188212`. This can be useful when using +Gradle's or Maven's parallel execution capabilities which create multiple JVM forks +that run concurrently. [[junit-platform-reporting-open-test-reporting]] -==== Open Test Reporting XML format +==== Open Test Reporting `{OpenTestReportGeneratingListener}` writes an XML report for the entire execution in the event-based format specified by {OpenTestReporting} which supports all features of the JUnit Platform such as hierarchical test structures, display names, tags, etc. The listener is auto-registered and can be configured via the following -<>: +<>: `junit.platform.reporting.open.xml.enabled=true|false`:: Enable/disable writing the report. -`junit.platform.reporting.output.dir=`:: - Configure the output directory for the reports. By default, `build` is used if a Gradle - build script is found, and `target` if a Maven POM is found; otherwise, the current - working directory is used. -If enabled, the listener creates an XML report file named -`junit-platform-events-.xml` per test run in the configured output directory. +If enabled, the listener creates an XML report file named `open-test-report.xml` in the +configured <>. TIP: The {OpenTestReportingCliTool} can be used to convert from the event-based format to the hierarchical format which is more human-readable. @@ -145,3 +150,13 @@ via the `--config-resource` option: $ java -jar junit-platform-console-standalone-{platform-version}.jar \ --config-resource=configuration.properties ---- + +[[junit-platform-reporting-legacy-xml]] +==== Legacy XML format + +`{LegacyXmlReportGeneratingListener}` generates a separate XML report for each root in the +`{TestPlan}`. Note that the generated XML format is compatible with the de facto standard +for JUnit 4 based test reports that was made popular by the Ant build system. + +The `LegacyXmlReportGeneratingListener` is used by the <> +as well. diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 200673894b7a..25a442d6c295 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1077,8 +1077,9 @@ include::{testDir}/example/TestInfoDemo.java[tags=user_guide] * `{TestReporterParameterResolver}`: if a constructor or method parameter is of type `{TestReporter}`, the `TestReporterParameterResolver` will supply an instance of `TestReporter`. The `TestReporter` can be used to publish additional data about the - current test run. The data can be consumed via the `reportingEntryPublished()` method in - a `{TestExecutionListener}`, allowing it to be viewed in IDEs or included in reports. + current test run or attach files to it. The data can be consumed in a + `{TestExecutionListener}` via the `reportingEntryPublished()` or `fileEntryPublished()` + method, respectively. This allows them to be viewed in IDEs or included in reports. + In JUnit Jupiter you should use `TestReporter` where you used to print information to `stdout` or `stderr` in JUnit 4. Using `@RunWith(JUnitPlatform.class)` will output all diff --git a/documentation/src/test/java/example/TestReporterDemo.java b/documentation/src/test/java/example/TestReporterDemo.java index 7d9a3f35c206..bafc00f83a3e 100644 --- a/documentation/src/test/java/example/TestReporterDemo.java +++ b/documentation/src/test/java/example/TestReporterDemo.java @@ -10,11 +10,16 @@ package example; +import static java.util.Collections.singletonList; + +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -41,5 +46,18 @@ void reportMultipleKeyValuePairs(TestReporter testReporter) { testReporter.publishEntry(values); } + @Test + void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception { + + testReporter.publishFile("test1.txt", file -> Files.write(file, singletonList("Test 1"))); + + Path existingFile = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2")); + testReporter.publishFile(existingFile); + + testReporter.publishFile("test3", dir -> { + Path nestedFile = Files.createDirectory(dir).resolve("nested.txt"); + Files.write(nestedFile, singletonList("Nested content")); + }); + } } // end::user_guide[] diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts index e87b590297d7..169d5756a10c 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts @@ -5,7 +5,6 @@ import org.gradle.api.tasks.PathSensitivity.RELATIVE import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED import org.gradle.internal.os.OperatingSystem -import java.nio.file.Files plugins { `java-library` @@ -31,7 +30,7 @@ val generateOpenTestHtmlReport by tasks.registering(JavaExec::class) { eventXmlFiles.from(tasks.withType().map { objects.fileTree() .from(it.reports.junitXml.outputLocation) - .include("junit-platform-events-*.xml") + .include("junit-*/open-test-report.xml") }) outputLocation = layout.buildDirectory.file("reports/open-test-report.html") } @@ -119,7 +118,7 @@ tasks.withType().configureEach { jvmArgumentProviders += CommandLineArgumentProvider { listOf( "-Djunit.platform.reporting.open.xml.enabled=true", - "-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}" + "-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}/junit-{uniqueNumber}", ) } @@ -127,10 +126,12 @@ tasks.withType().configureEach { classpath.from(javaAgentClasspath) } - val reportFiles = objects.fileTree().from(reports.junitXml.outputLocation).matching { include("junit-platform-events-*.xml") } + val reportDirTree = objects.fileTree().from(reports.junitXml.outputLocation) doFirst { - reportFiles.files.forEach { - Files.delete(it.toPath()) + reportDirTree.visit { + if (name.startsWith("junit-")) { + file.deleteRecursively() + } } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java index ffcf580603ff..3039692b083f 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java @@ -10,12 +10,17 @@ package org.junit.jupiter.api; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.Map; import org.apiguardian.api.API; +import org.junit.jupiter.api.function.ThrowingConsumer; /** * Parameters of type {@code TestReporter} can be injected into @@ -77,4 +82,36 @@ default void publishEntry(String value) { this.publishEntry("value", value); } + /** + * Publish the supplied file and attach it to the current test or container. + *

+ * The file will be copied to the report output directory replacing any + * potentially existing file with the same name. + * + * @param file the file to be attached; never {@code null} or blank + * @since 5.12 + */ + @API(status = EXPERIMENTAL, since = "5.12") + default void publishFile(Path file) { + publishFile(file.getFileName().toString(), path -> Files.copy(file, path, REPLACE_EXISTING)); + } + + /** + * Publish a file with the supplied name written by the supplied action and + * attach it to the current test or container. + *

+ * The {@link Path} passed to the supplied action will be relative to the + * report output directory, but it's up to the action to write the file or + * directory. + * + * @param fileName the name of the file to be attached; never {@code null} or blank + * and must not contain any path separators + * @param action the action to be executed to write the file; never {@code null} + * @since 5.12 + */ + @API(status = EXPERIMENTAL, since = "5.12") + default void publishFile(String fileName, ThrowingConsumer action) { + throw new UnsupportedOperationException(); + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index 44a3447a4e7b..36ef064bc17b 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -10,10 +10,12 @@ package org.junit.jupiter.api.extension; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -25,6 +27,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.support.ReflectionSupport; @@ -364,6 +367,22 @@ default void publishReportEntry(String value) { this.publishReportEntry("value", value); } + /** + * Publish a file with the supplied name written by the supplied action and + * attach it to the current test or container. + *

+ * The file will be resolved in the report output directory prior to + * invoking the supplied action. + * + * @param fileName the name of the file to be attached; never {@code null} or blank + * and must not contain any path separators + * @param action the action to be executed to write the file; never {@code null} + * @since 5.12 + * @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished + */ + @API(status = EXPERIMENTAL, since = "5.12") + void publishFile(String fileName, ThrowingConsumer action); + /** * Get the {@link Store} for the supplied {@link Namespace}. * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java index 94b13afd4548..a4f5ac7fa454 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java @@ -63,8 +63,8 @@ public Optional getArtifactId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - JupiterConfiguration configuration = new CachingJupiterConfiguration( - new DefaultJupiterConfiguration(discoveryRequest.getConfigurationParameters())); + JupiterConfiguration configuration = new CachingJupiterConfiguration(new DefaultJupiterConfiguration( + discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider())); JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration); new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor); return engineDescriptor; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java index f2e24ba494dc..09a98cd3e4ef 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDirFactory; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * Caching implementation of the {@link JupiterConfiguration} API. @@ -138,4 +139,9 @@ public ExtensionContextScope getDefaultTestInstantiationExtensionContextScope() DEFAULT_TEST_INSTANTIATION_EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, __ -> delegate.getDefaultTestInstantiationExtensionContextScope()); } + + @Override + public OutputDirectoryProvider getOutputDirectoryProvider() { + return delegate.getOutputDirectoryProvider(); + } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java index b72b0362e638..11d4e46d0c9c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java @@ -33,6 +33,7 @@ import org.junit.platform.commons.util.ClassNamePatternFilterUtils; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * Default implementation of the {@link JupiterConfiguration} API. @@ -67,10 +68,13 @@ public class DefaultJupiterConfiguration implements JupiterConfiguration { new EnumConfigurationParameterConverter<>(ExtensionContextScope.class, "extension context scope"); private final ConfigurationParameters configurationParameters; + private final OutputDirectoryProvider outputDirectoryProvider; - public DefaultJupiterConfiguration(ConfigurationParameters configurationParameters) { + public DefaultJupiterConfiguration(ConfigurationParameters configurationParameters, + OutputDirectoryProvider outputDirectoryProvider) { this.configurationParameters = Preconditions.notNull(configurationParameters, "ConfigurationParameters must not be null"); + this.outputDirectoryProvider = outputDirectoryProvider; } @Override @@ -156,4 +160,9 @@ public ExtensionContextScope getDefaultTestInstantiationExtensionContextScope() return extensionContextScopeConverter.get(configurationParameters, DEFAULT_TEST_INSTANTIATION_EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME, ExtensionContextScope.DEFAULT); } + + @Override + public OutputDirectoryProvider getOutputDirectoryProvider() { + return outputDirectoryProvider; + } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java index b342054c5cbf..ebfe5939e6ca 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.io.TempDirFactory; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * @since 5.4 @@ -78,4 +79,5 @@ public interface JupiterConfiguration { ExtensionContextScope getDefaultTestInstantiationExtensionContextScope(); + OutputDirectoryProvider getOutputDirectoryProvider(); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java index 4ff9573a32b6..32fc7339582e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java @@ -13,6 +13,8 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toCollection; +import java.io.IOException; +import java.nio.file.Path; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -25,6 +27,7 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; @@ -33,9 +36,11 @@ import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.hierarchical.Node; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; @@ -109,6 +114,25 @@ public void publishReportEntry(Map values) { this.engineExecutionListener.reportingEntryPublished(this.testDescriptor, ReportEntry.from(values)); } + @Override + public void publishFile(String fileName, ThrowingConsumer action) { + try { + Path dir = configuration.getOutputDirectoryProvider().createOutputDirectory(this.testDescriptor); + try { + Path file = dir.resolve(fileName); + action.accept(file); + this.engineExecutionListener.fileEntryPublished(this.testDescriptor, FileEntry.from(file)); + } + catch (Throwable t) { + UnrecoverableExceptions.rethrowIfUnrecoverable(t); + throw new JUnitException("Failed to publish file", t); + } + } + catch (IOException e) { + throw new JUnitException("Failed to create output directory", e); + } + } + @Override public Optional getParent() { return Optional.ofNullable(this.parent); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java new file mode 100644 index 000000000000..b776a0fe3b94 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import java.nio.file.Path; +import java.util.Map; + +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.function.ThrowingConsumer; + +/** + * @since 1.12 + */ +class DefaultTestReporter implements TestReporter { + + private final ExtensionContext extensionContext; + + DefaultTestReporter(ExtensionContext extensionContext) { + this.extensionContext = extensionContext; + } + + @Override + public void publishEntry(Map map) { + extensionContext.publishReportEntry(map); + } + + @Override + public void publishFile(String fileName, ThrowingConsumer action) { + extensionContext.publishFile(fileName, action); + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TestReporterParameterResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TestReporterParameterResolver.java index c06536d448dc..5427d8029cf9 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TestReporterParameterResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TestReporterParameterResolver.java @@ -34,7 +34,7 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon @Override public TestReporter resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return extensionContext::publishReportEntry; + return new DefaultTestReporter(extensionContext); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java index 1cdddf814e10..d7dc8f8aacff 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java @@ -11,6 +11,8 @@ package org.junit.platform.console.tasks; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.console.tasks.DiscoveryRequestCreator.toDiscoveryRequestBuilder; +import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME; import java.io.PrintWriter; import java.net.URL; @@ -32,6 +34,7 @@ import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.launcher.core.LauncherFactory; import org.junit.platform.launcher.listeners.SummaryGeneratingListener; import org.junit.platform.launcher.listeners.TestExecutionSummary; @@ -75,7 +78,7 @@ private void discoverTests(PrintWriter out) { Launcher launcher = launcherSupplier.get(); Optional commandLineTestPrinter = createDetailsPrintingListener(out); - LauncherDiscoveryRequest discoveryRequest = new DiscoveryRequestCreator().toDiscoveryRequest(discoveryOptions); + LauncherDiscoveryRequest discoveryRequest = toDiscoveryRequestBuilder(discoveryOptions).build(); TestPlan testPlan = launcher.discover(discoveryRequest); commandLineTestPrinter.ifPresent(printer -> printer.listTests(testPlan)); @@ -98,8 +101,10 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report Launcher launcher = launcherSupplier.get(); SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher); - LauncherDiscoveryRequest discoveryRequest = new DiscoveryRequestCreator().toDiscoveryRequest(discoveryOptions); - launcher.execute(discoveryRequest); + LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions); + reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME, + dir.toAbsolutePath().toString())); + launcher.execute(discoveryRequestBuilder.build()); TestExecutionSummary summary = summaryListener.getSummary(); if (summary.getTotalFailureCount() > 0 || outputOptions.getDetails() != Details.NONE) { diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java index 6cbbfa039d6b..25af8a62a0d7 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java @@ -42,7 +42,6 @@ import org.junit.platform.engine.discovery.ClasspathRootSelector; import org.junit.platform.engine.discovery.IterationSelector; import org.junit.platform.engine.discovery.MethodSelector; -import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; /** @@ -50,7 +49,7 @@ */ class DiscoveryRequestCreator { - LauncherDiscoveryRequest toDiscoveryRequest(TestDiscoveryOptions options) { + static LauncherDiscoveryRequestBuilder toDiscoveryRequestBuilder(TestDiscoveryOptions options) { LauncherDiscoveryRequestBuilder requestBuilder = request(); List selectors = createDiscoverySelectors(options); requestBuilder.selectors(selectors); @@ -58,10 +57,10 @@ LauncherDiscoveryRequest toDiscoveryRequest(TestDiscoveryOptions options) { requestBuilder.configurationParameters(options.getConfigurationParameters()); requestBuilder.configurationParametersResources( options.getConfigurationParametersResources().toArray(new String[0])); - return requestBuilder.build(); + return requestBuilder; } - private List createDiscoverySelectors(TestDiscoveryOptions options) { + private static List createDiscoverySelectors(TestDiscoveryOptions options) { List explicitSelectors = options.getExplicitSelectors(); if (options.isScanClasspath()) { Preconditions.condition(explicitSelectors.isEmpty(), @@ -77,12 +76,12 @@ private List createDiscoverySelectors(TestDiscovery "Please specify an explicit selector option or use --scan-class-path or --scan-modules"); } - private List createClasspathRootSelectors(TestDiscoveryOptions options) { + private static List createClasspathRootSelectors(TestDiscoveryOptions options) { Set classpathRoots = determineClasspathRoots(options); return selectClasspathRoots(classpathRoots); } - private Set determineClasspathRoots(TestDiscoveryOptions options) { + private static Set determineClasspathRoots(TestDiscoveryOptions options) { if (options.getSelectedClasspathEntries().isEmpty()) { Set rootDirs = new LinkedHashSet<>(ReflectionUtils.getAllClasspathRootDirectories()); rootDirs.addAll(options.getExistingAdditionalClasspathEntries()); @@ -91,7 +90,7 @@ private Set determineClasspathRoots(TestDiscoveryOptions options) { return new LinkedHashSet<>(options.getSelectedClasspathEntries()); } - private void addFilters(LauncherDiscoveryRequestBuilder requestBuilder, TestDiscoveryOptions options, + private static void addFilters(LauncherDiscoveryRequestBuilder requestBuilder, TestDiscoveryOptions options, List selectors) { requestBuilder.filters(includedClassNamePatterns(options, selectors)); @@ -133,7 +132,7 @@ private void addFilters(LauncherDiscoveryRequestBuilder requestBuilder, TestDisc } } - private ClassNameFilter includedClassNamePatterns(TestDiscoveryOptions options, + private static ClassNameFilter includedClassNamePatterns(TestDiscoveryOptions options, List selectors) { Stream patternStreams = Stream.concat( // options.getIncludedClassNamePatterns().stream(), // diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java index 0cbe2abd3352..c6a50c6349c2 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/FlatPrintingListener.java @@ -14,6 +14,7 @@ import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; @@ -73,6 +74,12 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e printlnMessage(Style.REPORTED, "Reported values", entry.toString()); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + printlnTestDescriptor(Style.REPORTED, "Reported:", testIdentifier); + printlnMessage(Style.REPORTED, "Reported file", file.toString()); + } + private void printlnTestDescriptor(Style style, String message, TestIdentifier testIdentifier) { println(style, "%-10s %s (%s)", message, testIdentifier.getDisplayName(), testIdentifier.getUniqueId()); } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreeNode.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreeNode.java index 8c1217f8cf68..fb119bbfa0f0 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreeNode.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreeNode.java @@ -16,6 +16,7 @@ import org.junit.platform.commons.util.StringUtils; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestIdentifier; @@ -31,6 +32,7 @@ class TreeNode { private TestIdentifier identifier; private TestExecutionResult result; final Queue reports = new ConcurrentLinkedQueue<>(); + final Queue files = new ConcurrentLinkedQueue<>(); final Queue children = new ConcurrentLinkedQueue<>(); boolean visible; @@ -61,6 +63,11 @@ TreeNode addReportEntry(ReportEntry reportEntry) { return this; } + TreeNode addFileEntry(FileEntry file) { + files.add(file); + return this; + } + TreeNode setResult(TestExecutionResult result) { this.result = result; this.duration = System.currentTimeMillis() - creation; diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java index 8e171d48d3b2..dceaa952ce50 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrinter.java @@ -21,6 +21,7 @@ import org.junit.platform.console.options.Theme; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -88,6 +89,7 @@ private void printVisible(TreeNode node, String indent, boolean continuous) { node.reason().ifPresent(reason -> printMessage(Style.SKIPPED, tabbed, reason)); node.reports.forEach(e -> printReportEntry(tabbed, e)); out.println(); + node.files.forEach(e -> printFileEntry(tabbed, e)); } private String tab(TreeNode node, boolean continuous) { @@ -152,6 +154,14 @@ private void printReportEntry(String indent, Map.Entry mapEntry) out.print("`"); } + private void printFileEntry(String indent, FileEntry fileEntry) { + out.print(indent); + out.print(fileEntry.getTimestamp()); + out.print(" "); + out.print(color(Style.SUCCESSFUL, fileEntry.getFile().toUri().toString())); + out.println(); + } + private void printMessage(Style style, String indent, String message) { String[] lines = message.split("\\R"); out.print(" "); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java index 437f7b56733e..34a3b3300b49 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/TreePrintingListener.java @@ -17,6 +17,7 @@ import org.junit.platform.console.options.Theme; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; @@ -73,6 +74,11 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e getNode(testIdentifier).addReportEntry(entry); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + getNode(testIdentifier).addFileEntry(file); + } + @Override public void listTests(TestPlan testPlan) { root = new TreeNode(testPlan.toString()); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java index a8df758dc114..bf247ff5e1a1 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/VerboseTreePrintingListener.java @@ -19,6 +19,7 @@ import org.junit.platform.console.options.Theme; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; @@ -130,6 +131,11 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e printDetail(Style.REPORTED, "reports", entry.toString()); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + printDetail(Style.REPORTED, "reports", file.toString()); + } + /** * Print static information about the test identifier. */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java index 41f6e3b76f18..3aa9cd5256b5 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java @@ -10,11 +10,14 @@ package org.junit.platform.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.List; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * {@code EngineDiscoveryRequest} provides a {@link TestEngine} access to the @@ -72,10 +75,6 @@ public interface EngineDiscoveryRequest { /** * Get the {@link EngineDiscoveryListener} for this request. * - *

The default implementation returns a no-op listener that ignores all - * calls so that engines that call this methods can be used with an earlier - * version of the JUnit Platform that did not yet include this API. - * * @return the discovery listener; never {@code null} * @since 1.6 */ @@ -84,4 +83,15 @@ default EngineDiscoveryListener getDiscoveryListener() { return EngineDiscoveryListener.NOOP; } + /** + * Get the {@link OutputDirectoryProvider} for this request. + * + * @return the output directory provider; never {@code null} + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + default OutputDirectoryProvider getOutputDirectoryProvider() { + throw new JUnitException( + "OutputDirectoryProvider not available; probably due to unaligned versions of the junit-platform-engine and junit-platform-launcher jars on the classpath/module path."); + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineExecutionListener.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineExecutionListener.java index 80727af0e3c2..c5fe614fc6b6 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineExecutionListener.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineExecutionListener.java @@ -10,10 +10,12 @@ package org.junit.platform.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -137,4 +139,24 @@ default void executionFinished(TestDescriptor testDescriptor, TestExecutionResul default void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { } + /** + * Can be called for any {@link TestDescriptor} in order to attach a file to + * a test or container — for example: + * + *

    + *
  • Screenshots
  • + *
  • Logs
  • + *
  • Output files written by the code under test
  • + *
+ * + *

The current lifecycle state of the supplied {@code TestDescriptor} is + * not relevant: file events can occur at any time. + * + * @param testDescriptor the descriptor of the test or container to which + * the file entry belongs + * @param file a {@code FileEntry} instance to be attached + */ + @API(status = EXPERIMENTAL, since = "1.12") + default void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java index ce0b9e0c65c7..64bdc62cb37d 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java @@ -31,9 +31,7 @@ public class ExecutionRequest { private final TestDescriptor rootTestDescriptor; - private final EngineExecutionListener engineExecutionListener; - private final ConfigurationParameters configurationParameters; @API(status = INTERNAL, since = "1.0") diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/FileEntry.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/FileEntry.java new file mode 100644 index 000000000000..9dab655e5a1f --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/FileEntry.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.reporting; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.nio.file.Path; +import java.time.LocalDateTime; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ToStringBuilder; + +/** + * {@code FileEntry} encapsulates a file to be published to the reporting + * infrastructure. + * + * @since 1.12 + * @see #from(Path) + */ +@API(status = EXPERIMENTAL, since = "1.12") +public final class FileEntry { + + /** + * Factory for creating a new {@code FileEntry} from the supplied file. + * + * @param file the file to publish; never {@code null} + */ + public static FileEntry from(Path file) { + return new FileEntry(file); + } + + private final LocalDateTime timestamp = LocalDateTime.now(); + private final Path file; + + private FileEntry(Path file) { + this.file = Preconditions.notNull(file, "file must not be null"); + } + + /** + * Get the timestamp for when this {@code FileEntry} was created. + * + * @return when this entry was created; never {@code null} + */ + public LocalDateTime getTimestamp() { + return this.timestamp; + } + + /** + * Get the file to be published. + * + * @return the file to publish; never {@code null} + */ + public Path getFile() { + return file; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.append("timestamp", this.timestamp); + builder.append("file", this.file); + return builder.toString(); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/OutputDirectoryProvider.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/OutputDirectoryProvider.java new file mode 100644 index 000000000000..5bb83fc77dc9 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/OutputDirectoryProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.reporting; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.io.IOException; +import java.nio.file.Path; + +import org.apiguardian.api.API; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.TestDescriptor; + +/** + * Provider of output directories for test engines to write reports and other + * output files to. + * + * @since 1.12 + * @see EngineDiscoveryRequest#getOutputDirectoryProvider() + */ +@API(status = EXPERIMENTAL, since = "1.12") +public interface OutputDirectoryProvider { + + /** + * {@return the root directory for all output files; never {@code null}} + */ + Path getRootDirectory(); + + /** + * Create an output directory for the supplied test descriptor. + * + * @param testDescriptor the test descriptor for which to create an output + * directory; never {@code null} + * @return the output directory + * @throws IOException if the output directory could not be created + */ + Path createOutputDirectory(TestDescriptor testDescriptor) throws IOException; + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java index 740df190eaaa..f091e0984ecc 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java @@ -83,7 +83,7 @@ private void add(String key, String value) { * * @return a copy of the map of key-value pairs; never {@code null} */ - public final Map getKeyValuePairs() { + public Map getKeyValuePairs() { return Collections.unmodifiableMap(this.keyValuePairs); } @@ -94,7 +94,7 @@ public final Map getKeyValuePairs() { * * @return when this entry was created; never {@code null} */ - public final LocalDateTime getTimestamp() { + public LocalDateTime getTimestamp() { return this.timestamp; } diff --git a/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingExecutionListener.java b/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingExecutionListener.java index cef4753f01b2..04d72df4cb87 100644 --- a/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingExecutionListener.java +++ b/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingExecutionListener.java @@ -26,6 +26,7 @@ import org.apiguardian.api.API; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; @@ -97,6 +98,14 @@ public void reportingEntryPublished(TestIdentifier test, ReportEntry reportEntry } } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + FileEntryEvent event = new FileEntryEvent(); + event.uniqueId = testIdentifier.getUniqueId(); + event.path = file.getFile().toAbsolutePath().toString(); + event.commit(); + } + @Category({ "JUnit", "Execution" }) @StackTrace(false) abstract static class ExecutionEvent extends Event { @@ -159,4 +168,14 @@ static class ReportEntryEvent extends ExecutionEvent { @Label("Value") String value; } + + @Label("File Entry") + @Name("org.junit.FileEntry") + static class FileEntryEvent extends ExecutionEvent { + @UniqueId + @Label("Unique Id") + String uniqueId; + @Label("Path") + String path; + } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index 3a7aa079aed1..da3dac7fae74 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -191,6 +191,34 @@ public class LauncherConstants { @API(status = EXPERIMENTAL, since = "1.10") public static final String STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME = "junit.platform.stacktrace.pruning.enabled"; + /** + * Property name used to configure the output directory for reporting. + * + *

If set, value must be a valid path that will be created if it doesn't + * exist. If not set, the default output directory will be determined by the + * reporting engine based on the current working directory. + * + * @since 1.12 + * @see #OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER + * @see org.junit.platform.engine.reporting.OutputDirectoryProvider + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static final String OUTPUT_DIR_PROPERTY_NAME = "junit.platform.reporting.output.dir"; + + /** + * Placeholder for use in {@link #OUTPUT_DIR_PROPERTY_NAME} that will be + * replaced with a unique number. + * + *

This can be used to create a unique output directory for each test + * run. For example, if multiple forks are used, each fork can be configured + * to write its output to a separate directory. + * + * @since 1.12 + * @see #OUTPUT_DIR_PROPERTY_NAME + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static final String OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER = "{uniqueNumber}"; + private LauncherConstants() { /* no-op */ } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestExecutionListener.java index 9179705f3661..ad3c98105bb0 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestExecutionListener.java @@ -10,12 +10,14 @@ package org.junit.platform.launcher; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -184,4 +186,15 @@ default void executionFinished(TestIdentifier testIdentifier, TestExecutionResul default void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { } + /** + * Called when a file has been published for the supplied {@link TestIdentifier}. + * + *

Can be called at any time during the execution of a test plan. + * + * @param testIdentifier describes the test or container to which the entry pertains + * @param file the published {@code FileEntry} + */ + @API(status = EXPERIMENTAL, since = "1.12") + default void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java index c83f11124913..c236d1980216 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java @@ -34,6 +34,7 @@ import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * {@code TestPlan} describes the tree of tests and containers as discovered @@ -70,6 +71,7 @@ public class TestPlan { private final boolean containsTests; private final ConfigurationParameters configurationParameters; + private final OutputDirectoryProvider outputDirectoryProvider; /** * Construct a new {@code TestPlan} from the supplied collection of @@ -82,24 +84,28 @@ public class TestPlan { * plan should be created; never {@code null} * @param configurationParameters the {@code ConfigurationParameters} for * this test plan; never {@code null} + * @param outputDirectoryProvider the {@code OutputDirectoryProvider} for + * this test plan; never {@code null} * @return a new test plan */ - @API(status = INTERNAL, since = "1.0") + @API(status = INTERNAL, since = "1.12") public static TestPlan from(Collection engineDescriptors, - ConfigurationParameters configurationParameters) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { Preconditions.notNull(engineDescriptors, "Cannot create TestPlan from a null collection of TestDescriptors"); Preconditions.notNull(configurationParameters, "Cannot create TestPlan from null ConfigurationParameters"); TestPlan testPlan = new TestPlan(engineDescriptors.stream().anyMatch(TestDescriptor::containsTests), - configurationParameters); + configurationParameters, outputDirectoryProvider); TestDescriptor.Visitor visitor = descriptor -> testPlan.addInternal(TestIdentifier.from(descriptor)); engineDescriptors.forEach(engineDescriptor -> engineDescriptor.accept(visitor)); return testPlan; } @API(status = INTERNAL, since = "1.4") - protected TestPlan(boolean containsTests, ConfigurationParameters configurationParameters) { + protected TestPlan(boolean containsTests, ConfigurationParameters configurationParameters, + OutputDirectoryProvider outputDirectoryProvider) { this.containsTests = containsTests; this.configurationParameters = configurationParameters; + this.outputDirectoryProvider = outputDirectoryProvider; } /** @@ -291,6 +297,17 @@ public ConfigurationParameters getConfigurationParameters() { return this.configurationParameters; } + /** + * Get the {@link OutputDirectoryProvider} for this test plan. + * + * @return the output directory provider; never {@code null} + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public OutputDirectoryProvider getOutputDirectoryProvider() { + return this.outputDirectoryProvider; + } + /** * Accept the supplied {@link Visitor} for a depth-first traversal of the * test plan. diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeEngineExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeEngineExecutionListener.java index b68e59bee23c..b872c12b0f1a 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeEngineExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeEngineExecutionListener.java @@ -21,6 +21,7 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; class CompositeEngineExecutionListener implements EngineExecutionListener { @@ -67,6 +68,13 @@ public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry e () -> "reportingEntryPublished(" + testDescriptor + ", " + entry + ")"); } + @Override + public void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + notifyEach(engineExecutionListeners, IterationOrder.ORIGINAL, + listener -> listener.fileEntryPublished(testDescriptor, file), + () -> "fileEntryPublished(" + testDescriptor + ", " + file + ")"); + } + private static void notifyEach(List listeners, IterationOrder iterationOrder, Consumer consumer, Supplier description) { iterationOrder.forEach(listeners, listener -> { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeTestExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeTestExecutionListener.java index 9107865d8872..1eaeee172a07 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeTestExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/CompositeTestExecutionListener.java @@ -21,6 +21,7 @@ import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; @@ -95,6 +96,13 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e () -> "reportingEntryPublished(" + testIdentifier + ", " + entry + ")"); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + notifyEach(testExecutionListeners, IterationOrder.ORIGINAL, + listener -> listener.fileEntryPublished(testIdentifier, file), + () -> "fileEntryPublished(" + testIdentifier + ", " + file + ")"); + } + private static void notifyEach(List listeners, IterationOrder iterationOrder, Consumer consumer, Supplier description) { iterationOrder.forEach(listeners, listener -> { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java index 3ea9c295384a..fd59b0b447a4 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java @@ -20,6 +20,7 @@ import org.junit.platform.engine.DiscoveryFilter; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.launcher.EngineFilter; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -51,15 +52,19 @@ final class DefaultDiscoveryRequest implements LauncherDiscoveryRequest { // Listener for test discovery that may abort on errors. private final LauncherDiscoveryListener discoveryListener; + private final OutputDirectoryProvider outputDirectoryProvider; + DefaultDiscoveryRequest(List selectors, List engineFilters, List> discoveryFilters, List postDiscoveryFilters, - LauncherConfigurationParameters configurationParameters, LauncherDiscoveryListener discoveryListener) { + LauncherConfigurationParameters configurationParameters, LauncherDiscoveryListener discoveryListener, + OutputDirectoryProvider outputDirectoryProvider) { this.selectors = selectors; this.engineFilters = engineFilters; this.discoveryFilters = discoveryFilters; this.postDiscoveryFilters = postDiscoveryFilters; this.configurationParameters = configurationParameters; this.discoveryListener = discoveryListener; + this.outputDirectoryProvider = outputDirectoryProvider; } @Override @@ -91,7 +96,11 @@ public ConfigurationParameters getConfigurationParameters() { @Override public LauncherDiscoveryListener getDiscoveryListener() { - return discoveryListener; + return this.discoveryListener; } + @Override + public OutputDirectoryProvider getOutputDirectoryProvider() { + return this.outputDirectoryProvider; + } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingEngineExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingEngineExecutionListener.java index 18a95fabdb3b..2eca74628725 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingEngineExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingEngineExecutionListener.java @@ -13,6 +13,7 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -51,4 +52,8 @@ public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry e delegate.reportingEntryPublished(testDescriptor, entry); } + @Override + public void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + delegate.fileEntryPublished(testDescriptor, file); + } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java index 09cb6231b9a7..6e01d443cfb7 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java @@ -75,8 +75,7 @@ public EngineDiscoveryOrchestrator(Iterable testEngines, * {@linkplain TestDescriptor#prune() prunes} the resulting test tree. */ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase) { - Map result = discover(request, phase, UniqueId::forEngine); - return new LauncherDiscoveryResult(result, request.getConfigurationParameters()); + return discover(request, phase, UniqueId::forEngine); } /** @@ -94,17 +93,18 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase * will not emit start or emit events for engines without tests. */ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, UniqueId parentId) { - Map testEngines = discover(request, phase, parentId::appendEngine); - LauncherDiscoveryResult result = new LauncherDiscoveryResult(testEngines, request.getConfigurationParameters()); + LauncherDiscoveryResult result = discover(request, phase, parentId::appendEngine); return result.withRetainedEngines(TestDescriptor::containsTests); } - private Map discover(LauncherDiscoveryRequest request, Phase phase, + private LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, Function uniqueIdCreator) { LauncherDiscoveryListener listener = getLauncherDiscoveryListener(request); listener.launcherDiscoveryStarted(request); try { - return discoverSafely(request, phase, listener, uniqueIdCreator); + Map testEngines = discoverSafely(request, phase, listener, uniqueIdCreator); + return new LauncherDiscoveryResult(testEngines, request.getConfigurationParameters(), + request.getOutputDirectoryProvider()); } finally { listener.launcherDiscoveryFinished(request); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java index c000c09ce6cc..1ef05c02d820 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java @@ -195,7 +195,7 @@ private void execute(TestDescriptor engineDescriptor, EngineExecutionListener li OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener, engineDescriptor); try { - testEngine.execute(new ExecutionRequest(engineDescriptor, delayingListener, configurationParameters)); + testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters)); delayingListener.reportEngineOutcome(); } catch (Throwable throwable) { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ExecutionListenerAdapter.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ExecutionListenerAdapter.java index 54e3b7c2ae38..cd85e331dccb 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ExecutionListenerAdapter.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ExecutionListenerAdapter.java @@ -13,6 +13,7 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; @@ -61,6 +62,11 @@ public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry e this.testExecutionListener.reportingEntryPublished(getTestIdentifier(testDescriptor), entry); } + @Override + public void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + this.testExecutionListener.fileEntryPublished(getTestIdentifier(testDescriptor), file); + } + private TestIdentifier getTestIdentifier(TestDescriptor testDescriptor) { return this.testPlan.getTestIdentifier(testDescriptor.getUniqueId()); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProvider.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProvider.java new file mode 100644 index 000000000000..9296ca634468 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId.Segment; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; + +/** + * Hierarchical {@link OutputDirectoryProvider} that creates directories based on + * the unique ID segments of a {@link TestDescriptor}. + * + * @since 1.12 + */ +class HierarchicalOutputDirectoryProvider implements OutputDirectoryProvider { + + private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[^a-z0-9.,_\\-() ]", Pattern.CASE_INSENSITIVE); + private static final String REPLACEMENT = "_"; + + private final Supplier rootDirSupplier; + private volatile Path rootDir; + + HierarchicalOutputDirectoryProvider(Supplier rootDirSupplier) { + this.rootDirSupplier = rootDirSupplier; + } + + @Override + public Path createOutputDirectory(TestDescriptor testDescriptor) throws IOException { + Preconditions.notNull(testDescriptor, "testDescriptor must not be null"); + + List segments = testDescriptor.getUniqueId().getSegments(); + Path relativePath = segments.stream() // + .skip(1) // + .map(HierarchicalOutputDirectoryProvider::toSanitizedPath) // + .reduce(toSanitizedPath(segments.get(0)), Path::resolve); + return Files.createDirectories(getRootDirectory().resolve(relativePath)); + } + + @Override + public synchronized Path getRootDirectory() { + if (rootDir == null) { + rootDir = rootDirSupplier.get(); + } + return rootDir; + } + + private static Path toSanitizedPath(Segment segment) { + return Paths.get(sanitizeName(segment.getValue())); + } + + private static String sanitizeName(String value) { + return FORBIDDEN_CHARS.matcher(value).replaceAll(REPLACEMENT); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java index 9195eaef2e97..3297909c09ae 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java @@ -31,12 +31,12 @@ class InternalTestPlan extends TestPlan { static InternalTestPlan from(LauncherDiscoveryResult discoveryResult) { TestPlan delegate = TestPlan.from(discoveryResult.getEngineTestDescriptors(), - discoveryResult.getConfigurationParameters()); + discoveryResult.getConfigurationParameters(), discoveryResult.getOutputDirectoryProvider()); return new InternalTestPlan(discoveryResult, delegate); } private InternalTestPlan(LauncherDiscoveryResult discoveryResult, TestPlan delegate) { - super(delegate.containsTests(), delegate.getConfigurationParameters()); + super(delegate.containsTests(), delegate.getConfigurationParameters(), delegate.getOutputDirectoryProvider()); this.discoveryResult = discoveryResult; this.delegate = delegate; } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.java index cb480d2533e3..0d62e9a3b5c2 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.java @@ -11,7 +11,9 @@ package org.junit.platform.launcher.core; import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME; import java.util.ArrayList; import java.util.Arrays; @@ -27,11 +29,14 @@ import org.junit.platform.engine.DiscoveryFilter; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.Filter; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.launcher.EngineFilter; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.PostDiscoveryFilter; import org.junit.platform.launcher.core.LauncherConfigurationParameters.Builder; +import org.junit.platform.launcher.listeners.OutputDir; import org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners; /** @@ -107,6 +112,7 @@ public final class LauncherDiscoveryRequestBuilder { private final List discoveryListeners = new ArrayList<>(); private boolean implicitConfigurationParametersEnabled = true; private ConfigurationParameters parentConfigurationParameters; + private OutputDirectoryProvider outputDirectoryProvider; /** * Create a new {@code LauncherDiscoveryRequestBuilder}. @@ -283,6 +289,27 @@ public LauncherDiscoveryRequestBuilder listeners(LauncherDiscoveryListener... li return this; } + /** + * Set the {@link OutputDirectoryProvider} to use for the request. + * + *

If not specified, a default provider will be used that can be + * configured via the {@value LauncherConstants#OUTPUT_DIR_PROPERTY_NAME} + * configuration parameter. + * + * @param outputDirectoryProvider the output directory provider to use; + * never {@code null} + * @return this builder for method chaining + * @since 1.12 + * @see OutputDirectoryProvider + * @see LauncherConstants#OUTPUT_DIR_PROPERTY_NAME + */ + @API(status = EXPERIMENTAL, since = "1.12") + public LauncherDiscoveryRequestBuilder outputDirectoryProvider(OutputDirectoryProvider outputDirectoryProvider) { + this.outputDirectoryProvider = Preconditions.notNull(outputDirectoryProvider, + "outputDirectoryProvider must not be null"); + return this; + } + private void storeFilter(Filter filter) { if (filter instanceof EngineFilter) { this.engineFilters.add((EngineFilter) filter); @@ -307,8 +334,18 @@ else if (filter instanceof DiscoveryFilter) { public LauncherDiscoveryRequest build() { LauncherConfigurationParameters launcherConfigurationParameters = buildLauncherConfigurationParameters(); LauncherDiscoveryListener discoveryListener = getLauncherDiscoveryListener(launcherConfigurationParameters); + OutputDirectoryProvider outputDirectoryProvider = getOutputDirectoryProvider(launcherConfigurationParameters); return new DefaultDiscoveryRequest(this.selectors, this.engineFilters, this.discoveryFilters, - this.postDiscoveryFilters, launcherConfigurationParameters, discoveryListener); + this.postDiscoveryFilters, launcherConfigurationParameters, discoveryListener, outputDirectoryProvider); + } + + private OutputDirectoryProvider getOutputDirectoryProvider( + LauncherConfigurationParameters configurationParameters) { + if (this.outputDirectoryProvider != null) { + return this.outputDirectoryProvider; + } + return new HierarchicalOutputDirectoryProvider( + () -> OutputDir.create(configurationParameters.get(OUTPUT_DIR_PROPERTY_NAME)).toPath()); } private LauncherConfigurationParameters buildLauncherConfigurationParameters() { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java index d65b4c3fe4d1..af4c75db0e67 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java @@ -23,6 +23,7 @@ import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; /** * Represents the result of test discovery of the configured @@ -35,11 +36,13 @@ public class LauncherDiscoveryResult { private final Map testEngineDescriptors; private final ConfigurationParameters configurationParameters; + private final OutputDirectoryProvider outputDirectoryProvider; LauncherDiscoveryResult(Map testEngineDescriptors, - ConfigurationParameters configurationParameters) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { this.testEngineDescriptors = unmodifiableMap(new LinkedHashMap<>(testEngineDescriptors)); this.configurationParameters = configurationParameters; + this.outputDirectoryProvider = outputDirectoryProvider; } public TestDescriptor getEngineTestDescriptor(TestEngine testEngine) { @@ -47,7 +50,11 @@ public TestDescriptor getEngineTestDescriptor(TestEngine testEngine) { } ConfigurationParameters getConfigurationParameters() { - return configurationParameters; + return this.configurationParameters; + } + + OutputDirectoryProvider getOutputDirectoryProvider() { + return this.outputDirectoryProvider; } public Collection getTestEngines() { @@ -60,15 +67,16 @@ Collection getEngineTestDescriptors() { public LauncherDiscoveryResult withRetainedEngines(Predicate predicate) { Map prunedTestEngineDescriptors = retainEngines(predicate); - if (prunedTestEngineDescriptors.size() < testEngineDescriptors.size()) { - return new LauncherDiscoveryResult(prunedTestEngineDescriptors, configurationParameters); + if (prunedTestEngineDescriptors.size() < this.testEngineDescriptors.size()) { + return new LauncherDiscoveryResult(prunedTestEngineDescriptors, this.configurationParameters, + this.outputDirectoryProvider); } return this; } private Map retainEngines(Predicate predicate) { // @formatter:off - return testEngineDescriptors.entrySet() + return this.testEngineDescriptors.entrySet() .stream() .filter(entry -> predicate.test(entry.getValue())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/OutputDir.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/OutputDir.java index 31de20e55175..27856d5154b8 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/OutputDir.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/OutputDir.java @@ -11,15 +11,20 @@ package org.junit.platform.launcher.listeners; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.security.SecureRandom; import java.util.Optional; +import java.util.function.BiPredicate; import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.platform.commons.util.StringUtils; @@ -27,6 +32,9 @@ @API(status = INTERNAL, since = "1.9") public class OutputDir { + private static final Pattern OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER_PATTERN = Pattern.compile( + Pattern.quote(OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER)); + public static OutputDir create(Optional customDir) { try { return createSafely(customDir, () -> Paths.get(".").toAbsolutePath()); @@ -40,11 +48,21 @@ public static OutputDir create(Optional customDir) { * Package private for testing purposes. */ static OutputDir createSafely(Optional customDir, Supplier currentWorkingDir) throws IOException { + return createSafely(customDir, currentWorkingDir, new SecureRandom()); + } + + private static OutputDir createSafely(Optional customDir, Supplier currentWorkingDir, + SecureRandom random) throws IOException { Path cwd = currentWorkingDir.get(); Path outputDir; if (customDir.isPresent() && StringUtils.isNotBlank(customDir.get())) { - outputDir = cwd.resolve(customDir.get()); + String customPath = customDir.get(); + while (customPath.contains(OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER)) { + customPath = OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER_PATTERN.matcher(customPath) // + .replaceFirst(String.valueOf(Math.abs(random.nextLong()))); + } + outputDir = cwd.resolve(customPath); } else if (Files.exists(cwd.resolve("pom.xml"))) { outputDir = cwd.resolve("target"); @@ -60,13 +78,15 @@ else if (containsFilesWithExtensions(cwd, ".gradle", ".gradle.kts")) { Files.createDirectories(outputDir); } - return new OutputDir(outputDir); + return new OutputDir(outputDir.normalize(), random); } private final Path path; + private final SecureRandom random; - private OutputDir(Path path) { + private OutputDir(Path path, SecureRandom random) { this.path = path; + this.random = random; } public Path toPath() { @@ -74,7 +94,7 @@ public Path toPath() { } public Path createFile(String prefix, String extension) throws UncheckedIOException { - String filename = String.format("%s-%d.%s", prefix, Math.abs(new SecureRandom().nextLong()), extension); + String filename = String.format("%s-%d.%s", prefix, Math.abs(random.nextLong()), extension); Path outputFile = path.resolve(filename); try { @@ -93,18 +113,18 @@ public Path createFile(String prefix, String extension) throws UncheckedIOExcept * supplied extensions. */ private static boolean containsFilesWithExtensions(Path dir, String... extensions) throws IOException { - return Files.find(dir, 1, // - (path, basicFileAttributes) -> { - if (basicFileAttributes.isRegularFile()) { - for (String extension : extensions) { - if (path.getFileName().toString().endsWith(extension)) { - return true; - } + BiPredicate matcher = (path, basicFileAttributes) -> { + if (basicFileAttributes.isRegularFile()) { + for (String extension : extensions) { + if (path.getFileName().toString().endsWith(extension)) { + return true; } } - return false; - }) // - .findFirst() // - .isPresent(); + } + return false; + }; + try (Stream pathStream = Files.find(dir, 1, matcher)) { + return pathStream.findFirst().isPresent(); + } } } diff --git a/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/OutputDirectoryProviders.java b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/OutputDirectoryProviders.java new file mode 100644 index 000000000000..408d0740ce6a --- /dev/null +++ b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/OutputDirectoryProviders.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import java.nio.file.Path; + +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; + +public class OutputDirectoryProviders { + + public static OutputDirectoryProvider dummyOutputDirectoryProvider() { + return new HierarchicalOutputDirectoryProvider(() -> { + throw new JUnitException("This should not be called; use a real provider instead"); + }); + } + + public static OutputDirectoryProvider hierarchicalOutputDirectoryProvider(Path rootDir) { + return new HierarchicalOutputDirectoryProvider(() -> rootDir); + } +} diff --git a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java index 9381776a0d40..c7fbef603e88 100644 --- a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java +++ b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java @@ -19,6 +19,7 @@ import static org.opentest4j.reporting.events.core.CoreFactory.cpuCores; import static org.opentest4j.reporting.events.core.CoreFactory.data; import static org.opentest4j.reporting.events.core.CoreFactory.directorySource; +import static org.opentest4j.reporting.events.core.CoreFactory.file; import static org.opentest4j.reporting.events.core.CoreFactory.fileSource; import static org.opentest4j.reporting.events.core.CoreFactory.hostName; import static org.opentest4j.reporting.events.core.CoreFactory.infrastructure; @@ -73,6 +74,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; @@ -85,7 +87,6 @@ import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; -import org.junit.platform.launcher.listeners.OutputDir; import org.opentest4j.reporting.events.api.DocumentWriter; import org.opentest4j.reporting.events.api.NamespaceRegistry; import org.opentest4j.reporting.events.core.Infrastructure; @@ -103,11 +104,11 @@ public class OpenTestReportGeneratingListener implements TestExecutionListener { static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled"; - static final String OUTPUT_DIR_PROPERTY_NAME = "junit.platform.reporting.output.dir"; private final AtomicInteger idCounter = new AtomicInteger(); private final Map inProgressIds = new ConcurrentHashMap<>(); private DocumentWriter eventsFileWriter = DocumentWriter.noop(); + private Path outputDir; public OpenTestReportGeneratingListener() { } @@ -123,8 +124,8 @@ public void testPlanExecutionStarted(TestPlan testPlan) { .add("junit", JUnitFactory.NAMESPACE, "https://junit.org/junit5/schemas/open-test-reporting/junit-1.9.xsd") // .build(); - Path eventsXml = OutputDir.create(config.get(OUTPUT_DIR_PROPERTY_NAME)) // - .createFile("junit-platform-events", "xml"); + outputDir = testPlan.getOutputDirectoryProvider().getRootDirectory(); + Path eventsXml = outputDir.resolve("open-test-report.xml"); try { eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml); reportInfrastructure(); @@ -340,6 +341,14 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e data -> entry.getKeyValuePairs().forEach(data::addEntry)))); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry entry) { + String id = inProgressIds.get(testIdentifier.getUniqueIdObject()); + eventsFileWriter.append(reported(id, Instant.now()), // + reported -> reported.append(attachments(), attachments -> attachments.append(file(entry.getTimestamp()), // + file -> file.withPath(outputDir.relativize(entry.getFile()).toString())))); + } + @Override public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { String id = inProgressIds.remove(testIdentifier.getUniqueIdObject()); diff --git a/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatformRunnerListener.java b/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatformRunnerListener.java index e91045ee8422..2a51109e17c2 100644 --- a/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatformRunnerListener.java +++ b/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatformRunnerListener.java @@ -16,6 +16,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestExecutionResult.Status; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; @@ -85,6 +86,11 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e System.out.println(entry); } + @Override + public void fileEntryPublished(TestIdentifier testIdentifier, FileEntry file) { + System.out.println(file); + } + private Failure toFailure(TestExecutionResult testExecutionResult, Description description) { return new Failure(description, testExecutionResult.getThrowable().orElse(null)); } diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java index 610bc0524e8d..56fd410ce06e 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java @@ -41,6 +41,7 @@ import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.MethodSelector; import org.junit.platform.engine.discovery.PackageNameFilter; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.launcher.EngineFilter; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.TagFilter; @@ -261,6 +262,12 @@ public SuiteLauncherDiscoveryRequestBuilder enableImplicitConfigurationParameter return this; } + public SuiteLauncherDiscoveryRequestBuilder outputDirectoryProvider( + OutputDirectoryProvider outputDirectoryProvider) { + delegate.outputDirectoryProvider(outputDirectoryProvider); + return this; + } + /** * Apply a suite's annotation-based configuration, selectors, and filters to * this builder. diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java index ce9107310ecf..6e7dfffe05a9 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java @@ -25,6 +25,7 @@ import org.junit.platform.engine.UniqueId.Segment; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.engine.support.discovery.SelectorResolver; /** @@ -39,12 +40,14 @@ final class ClassSelectorResolver implements SelectorResolver { private final Predicate classNameFilter; private final SuiteEngineDescriptor suiteEngineDescriptor; private final ConfigurationParameters configurationParameters; + private final OutputDirectoryProvider outputDirectoryProvider; ClassSelectorResolver(Predicate classNameFilter, SuiteEngineDescriptor suiteEngineDescriptor, - ConfigurationParameters configurationParameters) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { this.classNameFilter = classNameFilter; this.suiteEngineDescriptor = suiteEngineDescriptor; this.configurationParameters = configurationParameters; + this.outputDirectoryProvider = outputDirectoryProvider; } @Override @@ -103,7 +106,7 @@ private Optional newSuiteDescriptor(Class suiteClass, Te return Optional.empty(); } - return Optional.of(new SuiteTestDescriptor(id, suiteClass, configurationParameters)); + return Optional.of(new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider)); } private static boolean containsCycle(UniqueId id) { diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java index 15d31d65ccc2..8fc2bbeef786 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java @@ -25,7 +25,8 @@ final class DiscoverySelectorResolver { .addSelectorResolver(context -> new ClassSelectorResolver( context.getClassNameFilter(), context.getEngineDescriptor(), - context.getDiscoveryRequest().getConfigurationParameters())) + context.getDiscoveryRequest().getConfigurationParameters(), + context.getDiscoveryRequest().getOutputDirectoryProvider())) .build(); // @formatter:on diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java index ded5d53aebc2..779c2a03bc3a 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java @@ -26,6 +26,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector; @@ -53,15 +54,18 @@ final class SuiteTestDescriptor extends AbstractTestDescriptor { private final SuiteLauncherDiscoveryRequestBuilder discoveryRequestBuilder = request(); private final ConfigurationParameters configurationParameters; + private final OutputDirectoryProvider outputDirectoryProvider; private final Boolean failIfNoTests; private final Class suiteClass; private LauncherDiscoveryResult launcherDiscoveryResult; private SuiteLauncher launcher; - SuiteTestDescriptor(UniqueId id, Class suiteClass, ConfigurationParameters configurationParameters) { + SuiteTestDescriptor(UniqueId id, Class suiteClass, ConfigurationParameters configurationParameters, + OutputDirectoryProvider outputDirectoryProvider) { super(id, getSuiteDisplayName(suiteClass), ClassSource.from(suiteClass)); this.configurationParameters = configurationParameters; + this.outputDirectoryProvider = outputDirectoryProvider; this.failIfNoTests = getFailIfNoTests(suiteClass); this.suiteClass = suiteClass; } @@ -99,6 +103,7 @@ void discover() { .enableImplicitConfigurationParameters(false) .parentConfigurationParameters(configurationParameters) .applyConfigurationParametersFromSuite(suiteClass) + .outputDirectoryProvider(outputDirectoryProvider) .build(); // @formatter:on this.launcher = SuiteLauncher.create(); diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java index 23884df7577b..0d2b634bb01e 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java @@ -13,15 +13,18 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; +import java.nio.file.Path; import java.util.Map; import java.util.ServiceLoader; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.CollectionUtils; import org.junit.platform.commons.util.Preconditions; @@ -34,6 +37,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator; import org.junit.platform.launcher.core.EngineExecutionOrchestrator; @@ -249,7 +253,7 @@ private static void executeDirectly(TestEngine testEngine, EngineDiscoveryReques EngineExecutionListener listener) { UniqueId engineUniqueId = UniqueId.forEngine(testEngine.getId()); TestDescriptor engineTestDescriptor = testEngine.discover(discoveryRequest, engineUniqueId); - ExecutionRequest request = new ExecutionRequest(engineTestDescriptor, listener, + ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters()); testEngine.execute(request); } @@ -295,7 +299,8 @@ private EngineTestKit() { public static final class Builder { private final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request() // - .enableImplicitConfigurationParameters(false); + .enableImplicitConfigurationParameters(false) // + .outputDirectoryProvider(DisabledOutputDirectoryProvider.INSTANCE); private final TestEngine testEngine; private Builder(TestEngine testEngine) { @@ -422,6 +427,25 @@ public Builder enableImplicitConfigurationParameters(boolean enabled) { return this; } + /** + * Set the {@link OutputDirectoryProvider} to use. + * + *

If not specified, a default provider will be used that throws an + * exception when attempting to create output directories. This is done + * to avoid accidentally writing output files to the file system. + * + * @param outputDirectoryProvider the output directory provider to use; + * never {@code null} + * @return this builder for method chaining + * @since 1.12 + * @see OutputDirectoryProvider + */ + @API(status = EXPERIMENTAL, since = "1.12") + public Builder outputDirectoryProvider(OutputDirectoryProvider outputDirectoryProvider) { + this.requestBuilder.outputDirectoryProvider(outputDirectoryProvider); + return this; + } + /** * Execute tests for the configured {@link TestEngine}, * {@linkplain DiscoverySelector discovery selectors}, @@ -441,6 +465,27 @@ public EngineExecutionResults execute() { return executionRecorder.getExecutionResults(); } + private static class DisabledOutputDirectoryProvider implements OutputDirectoryProvider { + + public static final OutputDirectoryProvider INSTANCE = new DisabledOutputDirectoryProvider(); + + private static final String FAILURE_MESSAGE = "Writing outputs is disabled by default when using EngineTestKit. " + + "To enable, configure a custom OutputDirectoryProvider via EngineTestKit#outputDirectoryProvider."; + + private DisabledOutputDirectoryProvider() { + } + + @Override + public Path getRootDirectory() { + throw new JUnitException(FAILURE_MESSAGE); + } + + @Override + public Path createOutputDirectory(TestDescriptor testDescriptor) { + throw new JUnitException(FAILURE_MESSAGE); + } + + } } } diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Event.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Event.java index 190a7aaf5510..08069dfeea17 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Event.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Event.java @@ -10,6 +10,7 @@ package org.junit.platform.testkit.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.junit.platform.commons.util.FunctionUtils.where; @@ -22,6 +23,7 @@ import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -51,6 +53,23 @@ public static Event reportingEntryPublished(TestDescriptor testDescriptor, Repor return new Event(EventType.REPORTING_ENTRY_PUBLISHED, testDescriptor, entry); } + /** + * Create an {@code Event} for a published file for the supplied + * {@link TestDescriptor} and {@link FileEntry}. + * + * @param testDescriptor the {@code TestDescriptor} associated with the + * event; never {@code null} + * @param file the {@code FileEntry} that was published; never {@code null} + * @return the newly created {@code Event} + * @since 1.12 + * @see EventType#FILE_ENTRY_PUBLISHED + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static Event fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + Preconditions.notNull(file, "FileEntry must not be null"); + return new Event(EventType.FILE_ENTRY_PUBLISHED, testDescriptor, file); + } + /** * Create an {@code Event} for the dynamic registration of the * supplied {@link TestDescriptor}. diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java index 3431252cbef2..339321e3c5b5 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java @@ -12,6 +12,7 @@ import static java.util.function.Predicate.isEqual; import static java.util.stream.Collectors.toList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; import static org.assertj.core.api.Assertions.allOf; @@ -27,6 +28,7 @@ import static org.junit.platform.testkit.engine.EventType.SKIPPED; import static org.junit.platform.testkit.engine.EventType.STARTED; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -42,6 +44,7 @@ import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestExecutionResult.Status; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.descriptor.EngineDescriptor; @@ -470,4 +473,18 @@ public static Condition reportEntry(Map keyValuePairs) { "event for report entry with key-value pairs %s", keyValuePairs); } + /** + * Create a new {@link Condition} that matches if and only if an + * {@link Event}'s {@linkplain Event#getPayload() payload} is an instance of + * {@link FileEntry} that contains a file that matches the supplied + * {@link Predicate}. + * + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static Condition fileEntry(Predicate filePredicate) { + return new Condition<>(byPayload(FileEntry.class, it -> filePredicate.test(it.getFile())), + "event for file entry with custom predicate"); + } + } diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventStatistics.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventStatistics.java index bd1cf3e06262..f052d2f2b486 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventStatistics.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventStatistics.java @@ -10,6 +10,7 @@ package org.junit.platform.testkit.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.junit.platform.testkit.engine.Assertions.assertEquals; @@ -127,6 +128,20 @@ public EventStatistics reportingEntryPublished(long expected) { return this; } + /** + * Specify the number of expected file entry publication events. + * + * @param expected the expected number of events + * @return this {@code EventStatistics} for method chaining + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public EventStatistics fileEntryPublished(long expected) { + this.executables.add( + () -> assertEquals(expected, this.events.fileEntryPublished().count(), "file entry published")); + return this; + } + /** * Specify the number of expected dynamic registration events. * diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventType.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventType.java index 09a6bdb59833..7bb029ad9929 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventType.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventType.java @@ -10,11 +10,13 @@ package org.junit.platform.testkit.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import org.apiguardian.api.API; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -60,6 +62,15 @@ public enum EventType { * * @see org.junit.platform.engine.EngineExecutionListener#reportingEntryPublished(TestDescriptor, ReportEntry) */ - REPORTING_ENTRY_PUBLISHED + REPORTING_ENTRY_PUBLISHED, + + /** + * Signals that a {@link TestDescriptor} published a file entry. + * + * @since 1.12 + * @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished(TestDescriptor, FileEntry) + */ + @API(status = EXPERIMENTAL, since = "1.12") + FILE_ENTRY_PUBLISHED } diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Events.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Events.java index 256ea9f2d2d3..9f688eb9d763 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Events.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/Events.java @@ -13,6 +13,7 @@ import static java.util.Collections.sort; import static java.util.function.Predicate.isEqual; import static java.util.stream.Collectors.toList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.junit.platform.commons.util.FunctionUtils.where; import static org.junit.platform.testkit.engine.Event.byPayload; @@ -207,6 +208,18 @@ public Events reportingEntryPublished() { this.category + " Reporting Entry Published"); } + /** + * Get the file entry publication {@link Events} contained in this + * {@code Events} object. + * + * @return the filtered {@code Events}; never {@code null} + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public Events fileEntryPublished() { + return new Events(eventsByType(EventType.FILE_ENTRY_PUBLISHED), this.category + " File Entry Published"); + } + /** * Get the dynamic registration {@link Events} contained in this * {@code Events} object. diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/ExecutionRecorder.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/ExecutionRecorder.java index c7f42f5693af..1f375f3a4e33 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/ExecutionRecorder.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/ExecutionRecorder.java @@ -10,6 +10,7 @@ package org.junit.platform.testkit.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import java.util.List; @@ -19,6 +20,7 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; /** @@ -82,6 +84,17 @@ public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry e this.events.add(Event.reportingEntryPublished(testDescriptor, entry)); } + /** + * Record an {@link Event} for a published {@link FileEntry}. + * + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + @Override + public void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + this.events.add(Event.fileEntryPublished(testDescriptor, file)); + } + /** * Get the state of the engine's execution in the form of {@link EngineExecutionResults}. * diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java index dee38a4fec62..531016e16b4f 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java @@ -46,7 +46,6 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.testkit.engine.EngineExecutionResults; @@ -397,14 +396,6 @@ public void executionSkipped(TestDescriptor testDescriptor, String reason) { PlainJUnit4TestCaseWithLifecycleMethods.EVENTS.add( "executionSkipped:" + testDescriptor.getDisplayName()); } - - @Override - public void dynamicTestRegistered(TestDescriptor testDescriptor) { - } - - @Override - public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { - } }; execute(testClass, listener); @@ -933,7 +924,7 @@ private static void execute(Class testClass, EngineExecutionListener listener var discoveryRequest = request(testClass); var engineTestDescriptor = testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId())); testEngine.execute( - new ExecutionRequest(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters())); + ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters())); } private static LauncherDiscoveryRequest request(Class testClass) { diff --git a/jupiter-tests/jupiter-tests.gradle.kts b/jupiter-tests/jupiter-tests.gradle.kts index a6557280218c..061920f4d96d 100644 --- a/jupiter-tests/jupiter-tests.gradle.kts +++ b/jupiter-tests/jupiter-tests.gradle.kts @@ -23,6 +23,7 @@ dependencies { testImplementation(libs.memoryfilesystem) testImplementation(testFixtures(projects.junitJupiterApi)) testImplementation(testFixtures(projects.junitJupiterEngine)) + testImplementation(testFixtures(projects.junitPlatformLauncher)) testImplementation(testFixtures(projects.junitPlatformReporting)) } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java index 209f07cc4767..b868af74f87f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java @@ -14,6 +14,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import java.util.Set; @@ -39,7 +40,7 @@ protected EngineExecutionResults executeTestsForClass(Class testClass) { } protected EngineExecutionResults executeTests(DiscoverySelector... selectors) { - return executeTests(request().selectors(selectors)); + return executeTests(request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider())); } protected EngineExecutionResults executeTests(LauncherDiscoveryRequestBuilder builder) { @@ -51,7 +52,8 @@ protected EngineExecutionResults executeTests(LauncherDiscoveryRequest request) } protected TestDescriptor discoverTests(DiscoverySelector... selectors) { - return discoverTests(request().selectors(selectors).build()); + return discoverTests( + request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider()).build()); } protected TestDescriptor discoverTests(LauncherDiscoveryRequest request) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ReportingTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ReportingTests.java index 19176dd10003..4cf84f61e430 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ReportingTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ReportingTests.java @@ -10,20 +10,28 @@ package org.junit.jupiter.engine; -import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; - +import static org.junit.platform.launcher.core.OutputDirectoryProviders.hierarchicalOutputDirectoryProvider; +import static org.junit.platform.testkit.engine.EventConditions.fileEntry; +import static org.junit.platform.testkit.engine.EventConditions.reportEntry; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.function.Predicate; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.platform.commons.PreconditionViolationException; @@ -35,20 +43,42 @@ class ReportingTests extends AbstractJupiterTestEngineTests { @ParameterizedTest @CsvSource(textBlock = """ - PER_CLASS, 7 - PER_METHOD, 9 + PER_CLASS, 1, 7, 5 + PER_METHOD, 0, 9, 7 """) - void reportEntriesArePublished(Lifecycle lifecycle, int expectedReportEntryCount) { + void reportAndFileEntriesArePublished(Lifecycle lifecycle, int containerEntries, int testReportEntries, + int testFileEntries, @TempDir Path tempDir) { var request = request() // .selectors(selectClass(MyReportingTestCase.class)) // - .configurationParameter(DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name()); - executeTests(request) // + .configurationParameter(DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name()) // + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(tempDir)); + + var results = executeTests(request); + + results // + .containerEvents() // + .assertStatistics(stats -> stats // + .started(2) // + .succeeded(2) // + .reportingEntryPublished(containerEntries) // + .fileEntryPublished(containerEntries)); + + results // .testEvents() // .assertStatistics(stats -> stats // .started(2) // .succeeded(2) // - .failed(0) // - .reportingEntryPublished(expectedReportEntryCount)); + .reportingEntryPublished(testReportEntries) // + .fileEntryPublished(testFileEntries)) // + .assertThatEvents() // + .haveExactly(2, reportEntry(Map.of("value", "@BeforeEach"))) // + .haveExactly(2, fileEntry(nameAndContent("beforeEach"))) // + .haveExactly(1, reportEntry(Map.of())) // + .haveExactly(1, reportEntry(Map.of("user name", "dk38"))) // + .haveExactly(1, reportEntry(Map.of("value", "message"))) // + .haveExactly(1, fileEntry(nameAndContent("succeedingTest"))) // + .haveExactly(2, reportEntry(Map.of("value", "@AfterEach"))) // + .haveExactly(2, fileEntry(nameAndContent("afterEach"))); } @SuppressWarnings("JUnitMalformedDeclaration") @@ -57,23 +87,27 @@ static class MyReportingTestCase { public MyReportingTestCase(TestReporter reporter) { // Reported on class-level for PER_CLASS lifecycle and on method-level for PER_METHOD lifecycle reporter.publishEntry("Constructor"); + reporter.publishFile("constructor", file -> Files.writeString(file, "constructor")); } @BeforeEach void beforeEach(TestReporter reporter) { reporter.publishEntry("@BeforeEach"); + reporter.publishFile("beforeEach", file -> Files.writeString(file, "beforeEach")); } @AfterEach void afterEach(TestReporter reporter) { reporter.publishEntry("@AfterEach"); + reporter.publishFile("afterEach", file -> Files.writeString(file, "afterEach")); } @Test void succeedingTest(TestReporter reporter) { - reporter.publishEntry(emptyMap()); + reporter.publishEntry(Map.of()); reporter.publishEntry("user name", "dk38"); reporter.publishEntry("message"); + reporter.publishFile("succeedingTest", file -> Files.writeString(file, "succeedingTest")); } @Test @@ -101,4 +135,15 @@ void invalidReportData(TestReporter reporter) { } + private static Predicate nameAndContent(String expectedName) { + return file -> { + try { + return Path.of(expectedName).equals(file.getFileName()) && expectedName.equals(Files.readString(file)); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java index a6c1d9919e56..2b329c1704c1 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -44,20 +45,20 @@ class DefaultJupiterConfigurationTests { @Test void getDefaultTestInstanceLifecyclePreconditions() { PreconditionViolationException exception = assertThrows(PreconditionViolationException.class, - () -> new DefaultJupiterConfiguration(null)); + () -> new DefaultJupiterConfiguration(null, dummyOutputDirectoryProvider())); assertThat(exception).hasMessage("ConfigurationParameters must not be null"); } @Test void getDefaultTestInstanceLifecycleWithNoConfigParamSet() { - JupiterConfiguration configuration = new DefaultJupiterConfiguration(mock()); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider()); Lifecycle lifecycle = configuration.getDefaultTestInstanceLifecycle(); assertThat(lifecycle).isEqualTo(PER_METHOD); } @Test void getDefaultTempDirCleanupModeWithNoConfigParamSet() { - JupiterConfiguration configuration = new DefaultJupiterConfiguration(mock()); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider()); CleanupMode cleanupMode = configuration.getDefaultTempDirCleanupMode(); assertThat(cleanupMode).isEqualTo(ALWAYS); } @@ -82,7 +83,8 @@ void shouldGetDefaultDisplayNameGeneratorWithConfigParamSet() { ConfigurationParameters parameters = mock(); String key = Constants.DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME; when(parameters.get(key)).thenReturn(Optional.of(CustomDisplayNameGenerator.class.getName())); - JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters, + dummyOutputDirectoryProvider()); DisplayNameGenerator defaultDisplayNameGenerator = configuration.getDefaultDisplayNameGenerator(); @@ -94,7 +96,8 @@ void shouldGetStandardAsDefaultDisplayNameGeneratorWithoutConfigParamSet() { ConfigurationParameters parameters = mock(); String key = Constants.DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME; when(parameters.get(key)).thenReturn(Optional.empty()); - JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters, + dummyOutputDirectoryProvider()); DisplayNameGenerator defaultDisplayNameGenerator = configuration.getDefaultDisplayNameGenerator(); @@ -106,7 +109,8 @@ void shouldGetNothingAsDefaultTestMethodOrderWithoutConfigParamSet() { ConfigurationParameters parameters = mock(); String key = Constants.DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME; when(parameters.get(key)).thenReturn(Optional.empty()); - JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters, + dummyOutputDirectoryProvider()); final Optional defaultTestMethodOrder = configuration.getDefaultTestMethodOrderer(); @@ -118,7 +122,8 @@ void shouldGetDefaultTempDirFactorySupplierWithConfigParamSet() { ConfigurationParameters parameters = mock(); String key = Constants.DEFAULT_TEMP_DIR_FACTORY_PROPERTY_NAME; when(parameters.get(key)).thenReturn(Optional.of(CustomFactory.class.getName())); - JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters, + dummyOutputDirectoryProvider()); Supplier supplier = configuration.getDefaultTempDirFactorySupplier(); @@ -138,7 +143,8 @@ void shouldGetStandardAsDefaultTempDirFactorySupplierWithoutConfigParamSet() { ConfigurationParameters parameters = mock(); String key = Constants.DEFAULT_TEMP_DIR_FACTORY_PROPERTY_NAME; when(parameters.get(key)).thenReturn(Optional.empty()); - JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters); + JupiterConfiguration configuration = new DefaultJupiterConfiguration(parameters, + dummyOutputDirectoryProvider()); Supplier supplier = configuration.getDefaultTempDirFactorySupplier(); @@ -148,7 +154,8 @@ void shouldGetStandardAsDefaultTempDirFactorySupplierWithoutConfigParamSet() { private void assertDefaultConfigParam(String configValue, Lifecycle expected) { ConfigurationParameters configParams = mock(); when(configParams.get(KEY)).thenReturn(Optional.ofNullable(configValue)); - Lifecycle lifecycle = new DefaultJupiterConfiguration(configParams).getDefaultTestInstanceLifecycle(); + Lifecycle lifecycle = new DefaultJupiterConfiguration(configParams, + dummyOutputDirectoryProvider()).getDefaultTestInstanceLifecycle(); assertThat(lifecycle).isEqualTo(expected); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index 779ff8620904..66caeeb639f5 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Named.named; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -280,7 +281,8 @@ void usingStore() { @ParameterizedTest @MethodSource("extensionContextFactories") void configurationParameter(Function extensionContextFactory) { - JupiterConfiguration echo = new DefaultJupiterConfiguration(new EchoParameters()); + JupiterConfiguration echo = new DefaultJupiterConfiguration(new EchoParameters(), + dummyOutputDirectoryProvider()); String key = "123"; Optional expected = Optional.of(key); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java index 2bbdf1321a95..4a6e6294d80a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME; import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,7 +51,8 @@ class TestInstanceLifecycleUtilsTests { @Test void getTestInstanceLifecyclePreconditions() { PreconditionViolationException exception = assertThrows(PreconditionViolationException.class, - () -> getTestInstanceLifecycle(null, new DefaultJupiterConfiguration(mock()))); + () -> getTestInstanceLifecycle(null, + new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider()))); assertThat(exception).hasMessage("testClass must not be null"); exception = assertThrows(PreconditionViolationException.class, @@ -60,7 +62,8 @@ void getTestInstanceLifecyclePreconditions() { @Test void getTestInstanceLifecycleWithNoConfigParamSet() { - Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), new DefaultJupiterConfiguration(mock())); + Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), + new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider())); assertThat(lifecycle).isEqualTo(PER_METHOD); } @@ -68,7 +71,8 @@ void getTestInstanceLifecycleWithNoConfigParamSet() { void getTestInstanceLifecycleWithConfigParamSet() { ConfigurationParameters configParams = mock(); when(configParams.get(KEY)).thenReturn(Optional.of(PER_CLASS.name().toLowerCase())); - Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), new DefaultJupiterConfiguration(configParams)); + Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), + new DefaultJupiterConfiguration(configParams, dummyOutputDirectoryProvider())); assertThat(lifecycle).isEqualTo(PER_CLASS); } @@ -76,21 +80,24 @@ void getTestInstanceLifecycleWithConfigParamSet() { void getTestInstanceLifecycleWithLocalConfigThatOverridesCustomDefaultSetViaConfigParam() { ConfigurationParameters configParams = mock(); when(configParams.get(KEY)).thenReturn(Optional.of(PER_CLASS.name().toLowerCase())); - Lifecycle lifecycle = getTestInstanceLifecycle(TestCase.class, new DefaultJupiterConfiguration(configParams)); + Lifecycle lifecycle = getTestInstanceLifecycle(TestCase.class, + new DefaultJupiterConfiguration(configParams, dummyOutputDirectoryProvider())); assertThat(lifecycle).isEqualTo(PER_METHOD); } @Test void getTestInstanceLifecycleFromMetaAnnotationWithNoConfigParamSet() { Class testClass = BaseMetaAnnotatedTestCase.class; - Lifecycle lifecycle = getTestInstanceLifecycle(testClass, new DefaultJupiterConfiguration(mock())); + Lifecycle lifecycle = getTestInstanceLifecycle(testClass, + new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider())); assertThat(lifecycle).isEqualTo(PER_CLASS); } @Test void getTestInstanceLifecycleFromSpecializedClassWithNoConfigParamSet() { Class testClass = SpecializedTestCase.class; - Lifecycle lifecycle = getTestInstanceLifecycle(testClass, new DefaultJupiterConfiguration(mock())); + Lifecycle lifecycle = getTestInstanceLifecycle(testClass, + new DefaultJupiterConfiguration(mock(), dummyOutputDirectoryProvider())); assertThat(lifecycle).isEqualTo(PER_CLASS); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java index 6a0645975813..11a55e711610 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java @@ -236,7 +236,7 @@ public Path createTempDirectory(AnnotatedElementContext elementContext, Extensio @Override public void close() throws IOException { - TempDirFactory.super.close(); + fileSystem.close(); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTestReporterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTestReporterTests.java new file mode 100644 index 000000000000..68fc7dbff8a7 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTestReporterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; + +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; + +@MockitoSettings +public class DefaultTestReporterTests { + + @TempDir + Path tempDir; + + @Mock + ExtensionContext extensionContext; + + @Captor + ArgumentCaptor fileNameCaptor; + + @Captor + ArgumentCaptor> actionCaptor; + + @InjectMocks + DefaultTestReporter testReporter; + + @Test + void copiesExistingFileToTarget() throws Throwable { + testReporter.publishFile(Files.writeString(tempDir.resolve("source"), "content")); + + verify(extensionContext).publishFile(fileNameCaptor.capture(), actionCaptor.capture()); + assertThat(fileNameCaptor.getValue()).isEqualTo("source"); + actionCaptor.getValue().accept(tempDir.resolve("target")); + + assertThat(tempDir.resolve("target")).hasContent("content"); + } + + @Test + void executesCustomActionWithTargetFile() throws Throwable { + testReporter.publishFile("target", file -> Files.writeString(file, "content")); + + verify(extensionContext).publishFile(fileNameCaptor.capture(), actionCaptor.capture()); + assertThat(fileNameCaptor.getValue()).isEqualTo("target"); + actionCaptor.getValue().accept(tempDir.resolve("target")); + + assertThat(tempDir.resolve("target")).hasContent("content"); + } + + @Test + void failsWhenPublishingMissingFile() { + testReporter.publishFile(tempDir.resolve("source")); + + verify(extensionContext).publishFile(fileNameCaptor.capture(), actionCaptor.capture()); + assertThat(fileNameCaptor.getValue()).isEqualTo("source"); + assertThatThrownBy(() -> actionCaptor.getValue().accept(tempDir.resolve("target"))) // + .isInstanceOf(NoSuchFileException.class); + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index fcdd5a0a7583..638cdbf95e1f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -22,6 +22,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -34,6 +35,7 @@ import org.junit.jupiter.api.extension.ExecutableInvoker; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.params.provider.Arguments; @@ -275,6 +277,10 @@ public Optional getConfigurationParameter(String key, Function public void publishReportEntry(Map map) { } + @Override + public void publishFile(String fileName, ThrowingConsumer action) { + } + @Override public Store getStore(Namespace namespace) { var store = new NamespaceAwareStore(this.store, namespace); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java index fce602b53011..576f2b3ed97b 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.io.PrintWriter; @@ -186,7 +187,7 @@ void flat_single_color() { private void demoTestRun(TestExecutionListener listener) { TestDescriptor testDescriptor = new TestDescriptorStub(UniqueId.forEngine("demo-engine"), "My Test"); - TestPlan testPlan = TestPlan.from(List.of(testDescriptor), mock()); + TestPlan testPlan = TestPlan.from(List.of(testDescriptor), mock(), dummyOutputDirectoryProvider()); listener.testPlanExecutionStarted(testPlan); listener.executionStarted(TestIdentifier.from(testDescriptor)); listener.executionFinished(TestIdentifier.from(testDescriptor), TestExecutionResult.successful()); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java index 95e7123423bc..3e560fa1fabc 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java @@ -359,8 +359,7 @@ void convertsConfigurationParametersResources() { } private LauncherDiscoveryRequest convert() { - var creator = new DiscoveryRequestCreator(); - return creator.toDiscoveryRequest(options); + return DiscoveryRequestCreator.toDiscoveryRequestBuilder(options).build(); } private void assertIncludes(Filter filter, String included) { diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java index 8b0c7a5a29b4..afff86ab473a 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/FlatPrintingListenerTests.java @@ -18,11 +18,13 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.file.Path; import org.assertj.core.util.Maps; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.fakes.TestDescriptorStub; import org.junit.platform.launcher.TestIdentifier; @@ -60,6 +62,19 @@ void reportingEntryPublished() { () -> assertTrue(lines[1].endsWith(", foo = 'bar']"))); } + @Test + void fileEntryPublished() { + var stringWriter = new StringWriter(); + listener(stringWriter).fileEntryPublished(newTestIdentifier(), FileEntry.from(Path.of("test.txt"))); + var lines = lines(stringWriter); + + assertEquals(2, lines.length); + assertAll("lines in the output", // + () -> assertEquals("Reported: demo-test ([engine:demo-engine])", lines[0]), // + () -> assertTrue(lines[1].startsWith(INDENTATION + "=> Reported file: FileEntry [timestamp =")), // + () -> assertTrue(lines[1].endsWith(", file = test.txt]"))); + } + @Test void executionFinishedWithFailure() { var stringWriter = new StringWriter(); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java index c6443a624f4e..953cda4a2d58 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertLinesMatch; import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.io.PrintWriter; @@ -44,7 +45,7 @@ void prepareListener() { "%c ool test"); engineDescriptor.addChild(testDescriptor); - testPlan = TestPlan.from(Collections.singleton(engineDescriptor), mock()); + testPlan = TestPlan.from(Collections.singleton(engineDescriptor), mock(), dummyOutputDirectoryProvider()); testIdentifier = testPlan.getTestIdentifier(testDescriptor.getUniqueId()); listener.testPlanExecutionStarted(testPlan); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/TreePrinterTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/TreePrinterTests.java index 8316437899d3..7cafb539423f 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/TreePrinterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/TreePrinterTests.java @@ -21,12 +21,14 @@ import java.io.PrintWriter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.platform.console.options.Theme; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.fakes.TestDescriptorStub; import org.junit.platform.launcher.TestIdentifier; @@ -92,7 +94,7 @@ void reportsAreTabbedCorrectly() { c1.addChild(m1); var m2 = new TreeNode(identifier("m-2", "method two")).setResult(successful()); - m2.addReportEntry(ReportEntry.from("key", "m-2")); + m2.addFileEntry(FileEntry.from(Path.of("test.txt"))); c1.addChild(m2); new TreePrinter(out, Theme.UNICODE, ColorPalette.NONE).print(root); @@ -105,7 +107,7 @@ void reportsAreTabbedCorrectly() { " ├─ method one ✔", // " │ ....-..-..T..:...* key = `m-1`", // " └─ method two ✔", // - " ....-..-..T..:...* key = `m-2`" // + " ....-..-..T..:...* file:.*" // ), // actual()); } diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreeListenerTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreePrintingListenerTests.java similarity index 87% rename from platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreeListenerTests.java rename to platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreePrintingListenerTests.java index 52c74188e08a..8da61de46ea8 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreeListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/VerboseTreePrintingListenerTests.java @@ -15,11 +15,13 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.platform.console.options.Theme; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.fakes.TestDescriptorStub; import org.junit.platform.launcher.TestIdentifier; @@ -27,7 +29,7 @@ /** * @since 1.3.2 */ -class VerboseTreeListenerTests { +class VerboseTreePrintingListenerTests { private static final String EOL = System.lineSeparator(); @@ -56,6 +58,15 @@ void reportingEntryPublished() { assertLinesMatch(List.of(" reports: ReportEntry \\[timestamp = .+, foo = 'bar'\\]"), List.of(lines)); } + @Test + void fileEntryPublished() { + var stringWriter = new StringWriter(); + listener(stringWriter).fileEntryPublished(newTestIdentifier(), FileEntry.from(Path.of("test.txt"))); + var lines = lines(stringWriter); + + assertLinesMatch(List.of(" reports: FileEntry \\[timestamp = .+, file = test.txt\\]"), List.of(lines)); + } + @Test void executionFinishedWithFailure() { var stringWriter = new StringWriter(); diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java index 11d1842ce4a5..057fb6a66826 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java @@ -77,7 +77,7 @@ void init() { private HierarchicalTestExecutor createExecutor( HierarchicalTestExecutorService executorService) { - var request = new ExecutionRequest(root, listener, null); + var request = ExecutionRequest.create(root, listener, null); return new HierarchicalTestExecutor<>(request, rootContext, executorService, OpenTest4JAwareThrowableCollector::new); } diff --git a/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingExecutionListenerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingExecutionListenerIntegrationTests.java index 7d3fe88736a4..cef08d696d9d 100644 --- a/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingExecutionListenerIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingExecutionListenerIntegrationTests.java @@ -12,13 +12,19 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.hierarchicalOutputDirectoryProvider; +import static org.junit.platform.reporting.testutil.FileUtils.findPath; import static org.moditect.jfrunit.ExpectedEvent.event; import static org.moditect.jfrunit.JfrEventsAssert.assertThat; +import java.nio.file.Files; +import java.nio.file.Path; + import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestReporter; import org.junit.jupiter.api.extension.DisabledOnOpenJ9; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly; import org.moditect.jfrunit.EnableEvent; @@ -33,15 +39,18 @@ public class FlightRecordingExecutionListenerIntegrationTests { @Test @EnableEvent("org.junit.*") - void reportsEvents() { + void reportsEvents(@TempDir Path tempDir) { var launcher = LauncherFactoryForTestingPurposesOnly.createLauncher(new JupiterTestEngine()); var request = request() // .selectors(selectClass(TestCase.class)) // + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(tempDir)) // .build(); launcher.execute(request, new FlightRecordingExecutionListener()); jfrEvents.awaitEvents(); + var testFile = findPath(tempDir, "glob:**/test.txt"); + assertThat(jfrEvents) // .contains(event("org.junit.TestPlanExecution") // .with("engineNames", "JUnit Jupiter")) // @@ -59,6 +68,8 @@ void reportsEvents() { .contains(event("org.junit.ReportEntry") // .with("key", "message") // .with("value", "Hello JFR!")) // + .contains(event("org.junit.FileEntry") // + .with("path", testFile.toAbsolutePath().toString())) // .contains(event("org.junit.SkippedTest") // .with("displayName", "skipped()") // .with("type", "TEST") // @@ -70,6 +81,7 @@ static class TestCase { @Test void test(TestReporter reporter) { reporter.publishEntry("message", "Hello JFR!"); + reporter.publishFile("test.txt", file -> Files.writeString(file, "test")); } @Test diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java index e62df473bb38..80c09b0bea04 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java @@ -11,6 +11,7 @@ package org.junit.platform.launcher; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -40,7 +41,7 @@ public Type getType() { } }); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); assertThat(testPlan.containsTests()).as("contains tests").isFalse(); } @@ -55,7 +56,7 @@ public Type getType() { } }); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); assertThat(testPlan.containsTests()).as("contains tests").isTrue(); } @@ -75,7 +76,7 @@ public boolean mayRegisterTests() { } }); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); assertThat(testPlan.containsTests()).as("contains tests").isTrue(); } @@ -93,7 +94,8 @@ void acceptsVisitorsInDepthFirstOrder() { engineDescriptor2.addChild(test2); engineDescriptor2.addChild(test3); - var testPlan = TestPlan.from(List.of(engineDescriptor, engineDescriptor2), configParams); + var testPlan = TestPlan.from(List.of(engineDescriptor, engineDescriptor2), configParams, + dummyOutputDirectoryProvider()); var visitor = mock(TestPlan.Visitor.class); testPlan.accept(visitor); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java index d56669e6eb80..46f26f12d7cf 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java @@ -28,6 +28,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.descriptor.DemoMethodTestDescriptor; import org.mockito.InOrder; @@ -171,6 +172,11 @@ public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { throw new RuntimeException("failed to invoke listener"); } + + @Override + public void fileEntryPublished(TestDescriptor testDescriptor, FileEntry file) { + throw new RuntimeException("failed to invoke listener"); + } } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java index cf3004e23966..cb2973165fff 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -206,7 +207,7 @@ private void assertThatTestListenerErrorLogged(LogRecordListener logRecordListen } private static TestPlan anyTestPlan() { - return TestPlan.from(Set.of(anyTestDescriptor()), mock()); + return TestPlan.from(Set.of(anyTestDescriptor()), mock(), dummyOutputDirectoryProvider()); } private static DemoMethodTestDescriptor anyTestDescriptor() { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java index 0d2d5786687d..d60b569abb98 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java @@ -11,6 +11,7 @@ package org.junit.platform.launcher.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.util.Map; @@ -34,7 +35,8 @@ class ExecutionListenerAdapterTests { void testReportingEntryPublished() { var testDescriptor = getSampleMethodTestDescriptor(); - var discoveryResult = new LauncherDiscoveryResult(Map.of(mock(), testDescriptor), mock()); + var discoveryResult = new LauncherDiscoveryResult(Map.of(mock(), testDescriptor), mock(), + dummyOutputDirectoryProvider()); var testPlan = InternalTestPlan.from(discoveryResult); var testIdentifier = testPlan.getTestIdentifier(testDescriptor.getUniqueId()); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProviderTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProviderTests.java new file mode 100644 index 000000000000..c5c23e3e6fa2 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/HierarchicalOutputDirectoryProviderTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.launcher.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; + +@MockitoSettings +public class HierarchicalOutputDirectoryProviderTests { + + @TempDir + Path tempDir; + + @Mock + Supplier rootDirSupplier; + + @Mock + TestDescriptor testDescriptor; + + @InjectMocks + HierarchicalOutputDirectoryProvider provider; + + @BeforeEach + void prepareMock() { + when(rootDirSupplier.get()).thenReturn(tempDir); + } + + @Test + void returnsConfiguredRootDir() { + assertThat(provider.getRootDirectory()).isEqualTo(tempDir); + assertThat(provider.getRootDirectory()).isEqualTo(tempDir); + verify(rootDirSupplier, times(1)).get(); + } + + @Test + void createsSubDirectoriesBasedOnUniqueId() throws Exception { + var uniqueId = UniqueId.forEngine("engine") // + .append("irrelevant", "foo") // + .append("irrelevant", "bar"); + when(testDescriptor.getUniqueId()).thenReturn(uniqueId); + + var outputDir = provider.createOutputDirectory(testDescriptor); + + assertThat(outputDir) // + .isEqualTo(tempDir.resolve(Path.of("engine", "foo", "bar"))) // + .exists(); + } + + @Test + void replacesForbiddenCharacters() throws Exception { + var uniqueId = UniqueId.forEngine("Engine<>") // + .append("irrelevant", "*/abc"); + when(testDescriptor.getUniqueId()).thenReturn(uniqueId); + + var outputDir = provider.createOutputDirectory(testDescriptor); + + assertThat(outputDir) // + .isEqualTo(tempDir.resolve(Path.of("Engine__", "__abc"))) // + .exists(); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/OutputDirTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/OutputDirTests.java index ecab5c1b90c5..d3ac3bea016b 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/OutputDirTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/OutputDirTests.java @@ -10,77 +10,92 @@ package org.junit.platform.launcher.listeners; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.platform.launcher.LauncherConstants; class OutputDirTests { + @TempDir + Path cwd; + @Test void getOutputDirUsesCustomOutputDir() throws Exception { - String customDir = "build/UniqueIdTrackingListenerIntegrationTests"; - Path outputDir = OutputDir.create(Optional.of(customDir)).toPath(); - assertThat(Files.isSameFile(Paths.get(customDir), outputDir)).isTrue(); + var customDir = cwd.resolve("custom-dir"); + var outputDir = OutputDir.create(Optional.of(customDir.toAbsolutePath().toString())).toPath(); + assertThat(Files.isSameFile(customDir, outputDir)).isTrue(); assertThat(outputDir).exists(); } + @Test + void getOutputDirUsesCustomOutputDirWithPlaceholder() { + var customDir = cwd.resolve("build").resolve("junit-" + LauncherConstants.OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER); + var outputDir = OutputDir.create(Optional.of(customDir.toAbsolutePath().toString())).toPath(); + assertThat(outputDir).exists() // + .hasParent(cwd.resolve("build")) // + .extracting(it -> it.getFileName().toString(), as(STRING)) // + .matches("junit-\\d+"); + } + @Test void getOutputDirFallsBackToCurrentWorkingDir() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking"; - String expected = cwd; + var expected = cwd; - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } @Test void getOutputDirDetectsMavenPom() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking/maven"; - String expected = cwd + "/target"; + Files.createFile(cwd.resolve("pom.xml")); + var expected = cwd.resolve("target"); - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } @Test void getOutputDirDetectsGradleGroovyDefaultBuildScript() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking/gradle/groovy"; - String expected = cwd + "/build"; + Files.createFile(cwd.resolve("build.gradle")); + var expected = cwd.resolve("build"); - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } @Test void getOutputDirDetectsGradleGroovyCustomBuildScript() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking/gradle/groovy/sub-project"; - String expected = cwd + "/build"; + Files.createFile(cwd.resolve("sub-project.gradle")); + var expected = cwd.resolve("build"); - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } @Test void getOutputDirDetectsGradleKotlinDefaultBuildScript() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking/gradle/kotlin"; - String expected = cwd + "/build"; + Files.createFile(cwd.resolve("build.gradle.kts")); + var expected = cwd.resolve("build"); - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } @Test void getOutputDirDetectsGradleKotlinCustomBuildScript() throws Exception { - String cwd = "src/test/resources/listeners/uidtracking/gradle/kotlin/sub-project"; - String expected = cwd + "/build"; + Files.createFile(cwd.resolve("sub-project.gradle.kts")); + var expected = cwd.resolve("build"); - assertOutputDirIsDetected(cwd, expected); + assertOutputDirIsDetected(expected); } - private void assertOutputDirIsDetected(String cwd, String expected) throws IOException { - Path outputDir = OutputDir.createSafely(Optional.empty(), () -> Paths.get(cwd)).toPath(); - assertThat(Files.isSameFile(Paths.get(expected), outputDir)).isTrue(); + private void assertOutputDirIsDetected(Path expected) throws IOException { + var outputDir = OutputDir.createSafely(Optional.empty(), () -> cwd).toPath(); + assertThat(Files.isSameFile(expected, outputDir)).isTrue(); assertThat(outputDir).exists(); } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java index 3eeb506f44f1..70c519e08167 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.platform.commons.test.ConcurrencyTestingUtils.executeConcurrently; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.io.PrintWriter; @@ -39,7 +40,7 @@ class SummaryGenerationTests { private final SummaryGeneratingListener listener = new SummaryGeneratingListener(); - private final TestPlan testPlan = TestPlan.from(List.of(), mock()); + private final TestPlan testPlan = TestPlan.from(List.of(), mock(), dummyOutputDirectoryProvider()); @Test void emptyReport() { diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java index 408a85ef9bc9..a9a11684c5b1 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java @@ -11,6 +11,7 @@ package org.junit.platform.reporting.legacy; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.util.Set; @@ -71,13 +72,13 @@ void legacyReportingClassNameForDescendantOfTestIdentifierWithClassSourceIsClass } private String getClassName(UniqueId uniqueId) { - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock()); + var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); return LegacyReportingUtils.getClassName(testPlan, testPlan.getTestIdentifier(uniqueId)); } @SuppressWarnings("deprecation") private String getClassNameFromOldLocation(UniqueId uniqueId) { - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock()); + var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); return org.junit.platform.launcher.listeners.LegacyReportingUtils.getClassName(testPlan, testPlan.getTestIdentifier(uniqueId)); } diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java index de3b42a79f2f..1f6eeb70dc63 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java @@ -19,6 +19,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.reporting.legacy.xml.XmlReportAssertions.assertValidAccordingToJenkinsSchema; import static org.mockito.Mockito.mock; @@ -370,7 +371,7 @@ void printsExceptionWhenReportsDirCannotBeCreated() throws Exception { var out = new StringWriter(); var listener = new LegacyXmlReportGeneratingListener(reportsDir, new PrintWriter(out)); - listener.testPlanExecutionStarted(TestPlan.from(Set.of(), mock())); + listener.testPlanExecutionStarted(TestPlan.from(Set.of(), mock(), dummyOutputDirectoryProvider())); assertThat(out.toString()).containsSubsequence("Could not create reports directory", "FileAlreadyExistsException", "at "); @@ -386,7 +387,8 @@ void printsExceptionWhenReportCouldNotBeWritten() throws Exception { var out = new StringWriter(); var listener = new LegacyXmlReportGeneratingListener(tempDirectory, new PrintWriter(out)); - listener.testPlanExecutionStarted(TestPlan.from(Set.of(engineDescriptor), mock())); + listener.testPlanExecutionStarted( + TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider())); listener.executionFinished(TestIdentifier.from(engineDescriptor), successful()); assertThat(out.toString()).containsSubsequence("Could not write XML report", "Exception", "at "); @@ -397,7 +399,7 @@ void writesReportEntriesToSystemOutElement() throws Exception { var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock()); + var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); var out = new StringWriter(); var listener = new LegacyXmlReportGeneratingListener(tempDirectory, new PrintWriter(out)); diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java index 8d685654c1fa..f2890b183884 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.engine.TestExecutionResult.failed; import static org.junit.platform.engine.TestExecutionResult.successful; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.mock; import java.time.Clock; @@ -37,7 +38,7 @@ void resultsOfTestIdentifierWithoutAnyReportedEventsAreEmpty() { var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var results = reportData.getResults(testPlan.getTestIdentifier(childUniqueId)); @@ -50,7 +51,7 @@ void resultsOfTestIdentifierWithoutReportedEventsContainsOnlyFailureOfAncestor() var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var failureOfAncestor = failed(new RuntimeException("failed!")); @@ -66,7 +67,7 @@ void resultsOfTestIdentifierWithoutReportedEventsContainsOnlySuccessOfAncestor() var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(engineDescriptor.getUniqueId()), successful()); diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java index ef81af430d6e..7f8a324c50de 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java @@ -19,6 +19,7 @@ import static org.junit.platform.engine.TestExecutionResult.successful; import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; import static org.junit.platform.launcher.LauncherConstants.STDOUT_REPORT_ENTRY_KEY; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.reporting.legacy.xml.XmlReportAssertions.assertValidAccordingToJenkinsSchema; import static org.mockito.Mockito.mock; @@ -54,7 +55,7 @@ class XmlReportWriterTests { @Test void writesTestsuiteElementsWithoutTestcaseElementsWithoutAnyTests() throws Exception { - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); @@ -72,7 +73,7 @@ void writesReportEntry() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); var testDescriptor = new TestDescriptorStub(uniqueId, "successfulTest"); engineDescriptor.addChild(testDescriptor); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.addReportEntry(TestIdentifier.from(testDescriptor), ReportEntry.from("myKey", "myValue")); @@ -90,7 +91,7 @@ void writesCapturedOutput() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); var testDescriptor = new TestDescriptorStub(uniqueId, "successfulTest"); engineDescriptor.addChild(testDescriptor); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var reportEntry = ReportEntry.from(Map.of( // @@ -119,7 +120,7 @@ void writesCapturedOutput() throws Exception { void writesEmptySkippedElementForSkippedTestWithoutReason() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "skippedTest")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markSkipped(testPlan.getTestIdentifier(uniqueId), null); @@ -149,7 +150,7 @@ public String getLegacyReportingName() { return "failedTest"; } }); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(uniqueId), failed(null)); @@ -169,7 +170,7 @@ public String getLegacyReportingName() { void omitsMessageAttributeForFailedTestWithThrowableWithoutMessage() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "failedTest")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(uniqueId), failed(new NullPointerException())); @@ -186,7 +187,7 @@ void omitsMessageAttributeForFailedTestWithThrowableWithoutMessage() throws Exce void writesValidXmlEvenIfExceptionMessageContainsCData() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var assertionError = new AssertionError(""); @@ -202,7 +203,7 @@ void writesValidXmlEvenIfExceptionMessageContainsCData() throws Exception { void escapesInvalidCharactersInSystemPropertiesAndExceptionMessages() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var assertionError = new AssertionError("expected: but was: "); @@ -231,7 +232,7 @@ void escapesInvalidCharactersInSystemPropertiesAndExceptionMessages() throws Exc void doesNotReopenCDataWithinCDataContent() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams); + var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var assertionError = new AssertionError(""); diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java index eb4a1df8cb2a..cd7792776187 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java @@ -15,14 +15,14 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.ENABLED_PROPERTY_NAME; -import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.OUTPUT_DIR_PROPERTY_NAME; +import static org.junit.platform.reporting.testutil.FileUtils.findPath; -import java.io.IOException; import java.net.URISyntaxException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.regex.Pattern; @@ -58,11 +58,14 @@ void writesValidXmlReport() throws Exception { executeTests(engine); - var xmlFile = findXmlReport(); + var xmlFile = findPath(tempDirectory, "glob:**/open-test-report.xml"); + assertThat(tempDirectory.relativize(xmlFile).toString()) // + .matches("junit-\\d+[/\\\\]open-test-report.xml"); assertThat(validate(xmlFile)).isEmpty(); var expected = """ - testClass) { var launcher = mock(Launcher.class); var captor = ArgumentCaptor.forClass(LauncherDiscoveryRequest.class); - when(launcher.discover(captor.capture())).thenReturn(TestPlan.from(Set.of(), mock())); + when(launcher.discover(captor.capture())).thenReturn( + TestPlan.from(Set.of(), mock(), dummyOutputDirectoryProvider())); new JUnitPlatform(testClass, launcher); diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java index 24d683c3397d..9675f662c5b8 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java @@ -10,9 +10,11 @@ package org.junit.platform.suite.engine; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.launcher.TagFilter.excludeTags; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.hierarchicalOutputDirectoryProvider; import static org.junit.platform.suite.engine.SuiteEngineDescriptor.ENGINE_ID; import static org.junit.platform.testkit.engine.EventConditions.container; import static org.junit.platform.testkit.engine.EventConditions.displayName; @@ -24,7 +26,10 @@ import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; @@ -65,11 +70,15 @@ */ class SuiteEngineTests { + @TempDir + private Path outputDir; + @Test void selectClasses() { // @formatter:off EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(SelectClassesSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .testEvents() .assertThatEvents() @@ -168,6 +177,7 @@ void suiteSuite() { // @formatter:off EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(SuiteSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .testEvents() .assertThatEvents() @@ -184,6 +194,7 @@ void selectClassesByUniqueId() { .append(SuiteTestDescriptor.SEGMENT_TYPE, SelectClassesSuite.class.getName()); EngineTestKit.engine(ENGINE_ID) .selectors(selectUniqueId(uniqId)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .testEvents() .assertThatEvents() @@ -239,6 +250,7 @@ void selectMethodAndSuiteInTestPlanByUniqueId() { EngineTestKit.engine(ENGINE_ID) .selectors(selectUniqueId(uniqueId)) .selectors(selectClass(SelectClassesSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .testEvents() .assertThatEvents() @@ -391,6 +403,7 @@ void cyclicSuite() { // @formatter:off EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(CyclicSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .allEvents() .assertThatEvents() @@ -417,6 +430,7 @@ void threePartCyclicSuite() { // @formatter:off EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(ThreePartCyclicSuite.PartA.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .allEvents() .assertThatEvents() @@ -429,6 +443,7 @@ void selectByIdentifier() { // @formatter:off EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(SelectByIdentifierSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) .execute() .testEvents() .assertThatEvents() @@ -437,6 +452,21 @@ void selectByIdentifier() { // @formatter:on } + @Test + void passesOutputDirectoryProviderToEnginesInSuite() { + // @formatter:off + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(SelectClassesSuite.class)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) + .execute() + .testEvents() + .assertThatEvents() + .haveExactly(1, event(test(SingleTestTestCase.class.getName()), finishedSuccessfully())); + // @formatter:on + + assertThat(outputDir).isDirectoryRecursivelyContaining("glob:**/test.txt"); + } + @Suite @SelectClasses(SingleTestTestCase.class) private static class PrivateSuite { diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java index 02ad36b35029..2f9507a01dc1 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java @@ -19,6 +19,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestReporter; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; @@ -26,6 +27,8 @@ import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.launcher.core.OutputDirectoryProviders; import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.engine.testcases.SingleTestTestCase; import org.junit.platform.suite.engine.testsuites.SelectClassesSuite; @@ -40,10 +43,13 @@ class SuiteTestDescriptorTests { final UniqueId jupiterEngineId = suiteId.append("engine", JupiterEngineDescriptor.ENGINE_ID); final UniqueId testClassId = jupiterEngineId.append(ClassTestDescriptor.SEGMENT_TYPE, SingleTestTestCase.class.getName()); - final UniqueId methodId = testClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + final UniqueId methodId = testClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, + "test(%s)".formatted(TestReporter.class.getName())); final ConfigurationParameters configurationParameters = new EmptyConfigurationParameters(); - final SuiteTestDescriptor suite = new SuiteTestDescriptor(suiteId, TestSuite.class, configurationParameters); + final OutputDirectoryProvider outputDirectoryProvider = OutputDirectoryProviders.dummyOutputDirectoryProvider(); + final SuiteTestDescriptor suite = new SuiteTestDescriptor(suiteId, TestSuite.class, configurationParameters, + outputDirectoryProvider); @Test void suiteIsEmptyBeforeDiscovery() { diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/SingleTestTestCase.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/SingleTestTestCase.java index fb0c0230e18d..eb6dc4734034 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/SingleTestTestCase.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/SingleTestTestCase.java @@ -10,7 +10,10 @@ package org.junit.platform.suite.engine.testcases; +import java.nio.file.Files; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestReporter; /** * @since 1.8 @@ -18,6 +21,7 @@ public class SingleTestTestCase { @Test - void test() { + void test(TestReporter testReporter) { + testReporter.publishFile("test.txt", file -> Files.writeString(file, "test")); } } diff --git a/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/build.gradle b/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/build.gradle deleted file mode 100644 index a4185e83967b..000000000000 --- a/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/build.gradle +++ /dev/null @@ -1 +0,0 @@ -// This file is used in tests for the UniqueIdTrackingListener diff --git a/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/sub-project/sub-project.gradle b/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/sub-project/sub-project.gradle deleted file mode 100644 index a4185e83967b..000000000000 --- a/platform-tests/src/test/resources/listeners/uidtracking/gradle/groovy/sub-project/sub-project.gradle +++ /dev/null @@ -1 +0,0 @@ -// This file is used in tests for the UniqueIdTrackingListener diff --git a/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/build.gradle.kts b/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/build.gradle.kts deleted file mode 100644 index a4185e83967b..000000000000 --- a/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/build.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -// This file is used in tests for the UniqueIdTrackingListener diff --git a/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/sub-project/sub-project.gradle.kts b/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/sub-project/sub-project.gradle.kts deleted file mode 100644 index a4185e83967b..000000000000 --- a/platform-tests/src/test/resources/listeners/uidtracking/gradle/kotlin/sub-project/sub-project.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -// This file is used in tests for the UniqueIdTrackingListener diff --git a/platform-tests/src/test/resources/listeners/uidtracking/maven/pom.xml b/platform-tests/src/test/resources/listeners/uidtracking/maven/pom.xml deleted file mode 100644 index 1a462ec9525d..000000000000 --- a/platform-tests/src/test/resources/listeners/uidtracking/maven/pom.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts index c83d5d840ada..398dabc34424 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts +++ b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts @@ -31,6 +31,8 @@ tasks.test { graalvmNative { binaries { named("test") { + // TODO #3040 Add to native-image.properties + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider") buildArgs.add("-H:+ReportExceptionStackTraces") } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java index 3f56478ef293..f9f66055fecd 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java @@ -10,9 +10,8 @@ package platform.tooling.support.tests; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; +import static org.junit.platform.reporting.testutil.FileUtils.findPath; + import java.nio.file.Path; import java.util.regex.Pattern; @@ -22,22 +21,18 @@ class XmlAssertions { static void verifyContainsExpectedStartedOpenTestReport(Path testResultsDir) { - try (var files = Files.list(testResultsDir)) { - Path xmlFile = files.filter(it -> it.getFileName().toString().startsWith("junit-platform-events-")) // - .findAny() // - .orElseThrow(() -> new AssertionError("Missing open-test-reporting XML file in " + testResultsDir)); - verifyContent(xmlFile); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } + var xmlFile = findPath(testResultsDir, "glob:**/open-test-report.xml"); + verifyContent(xmlFile); } private static void verifyContent(Path xmlFile) { var expected = """ - ${xmlunit.ignore}