Skip to content

Commit dbd4e2e

Browse files
authored
Allow attaching files to test results (#4138)
Being able to attach files such as screenshots or extra log files to a test is useful to diagnose the outcome of tests. This PR adds an API for Jupiter test authors to do so (`TestReporter.publishFile`) and includes it when writing the Open Test Reporting XML output (in `OpenTestReportGeneratingListener`) via a new method on `TestExecutionListener`. Moreover, it adds `OutputDirectoryProvider` to `EngineDiscoveryRequest` so other engines can also attach files and write them to the same output directory and makes it available to `TestExecutionListener` implementations via `TestPlan`. The default location of the XML output is changed from `OUTPUT_DIR/junit-platform-events-*.xml` to `OUTPUT_DIR/open-test-report.xml`. The output directory can be made unique by using the `{uniqueNumber}` placeholder in the `junit.platform.reporting.output.dir` configuration parameter.
1 parent e3f2a09 commit dbd4e2e

File tree

100 files changed

+1376
-233
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+1376
-233
lines changed

documentation/documentation.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ tasks {
147147

148148
val consoleLauncherTestReportsDir = project.layout.buildDirectory.dir("console-launcher-test-results")
149149
val consoleLauncherTestEventXmlFiles =
150-
files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("junit-platform-events-*.xml") } })
150+
files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("**/open-test-report.xml") } })
151151

152152
val consoleLauncherTest by registering(RunConsoleLauncher::class) {
153153
args.addAll("execute")
@@ -157,7 +157,6 @@ tasks {
157157
argumentProviders.add(CommandLineArgumentProvider {
158158
listOf(
159159
"--reports-dir=${consoleLauncherTestReportsDir.get()}",
160-
"--config=junit.platform.reporting.output.dir=${consoleLauncherTestReportsDir.get()}",
161160
)
162161
})
163162
args.addAll("--include-classname", ".*Tests")

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ endif::[]
4040
:DiscoverySelectors_selectPackage: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectPackage(java.lang.String)[selectPackage]
4141
:DiscoverySelectors_selectUniqueId: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUniqueId(java.lang.String)[selectUniqueId]
4242
:DiscoverySelectors_selectUri: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUri(java.lang.String)[selectUri]
43+
:EngineDiscoveryRequest: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/EngineDiscoveryRequest.html[EngineDiscoveryRequest]
4344
:FileSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/FileSelector.html[FileSelector]
4445
:HierarchicalTestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.html[HierarchicalTestEngine]
4546
:IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector]
4647
:MethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector]
4748
:ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector]
4849
:NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector]
4950
:NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector]
51+
:OutputDirectoryProvider: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider]
5052
:PackageSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/PackageSelector.html[PackageSelector]
5153
:ParallelExecutionConfigurationStrategy: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.html[ParallelExecutionConfigurationStrategy]
5254
:UniqueIdSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/UniqueIdSelector.html[UniqueIdSelector]

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,16 @@ JUnit repository on GitHub.
4040
`--select-file` and `--select-resource`.
4141
* `ConsoleLauncher` now accepts multiple values for all `--select` options.
4242
* Add `--select-unique-id` support to ConsoleLauncher.
43+
* Add `getOutputDirectoryProvider()` method to `EngineDiscoveryRequest` and `TestPlan` to
44+
allow test engines to publish/attach files to containers and tests by calling
45+
`EngineExecutionListener.fileEntryPublished(...)`. Registered `TestExecutionListeners`
46+
can then access these files by overriding the `fileEntryPublished(...)` method.
4347
* The following improvements have been made to the open-test-reporting XML output:
4448
- Information about the Git repository, the current branch, the commit hash, and the
4549
current worktree status are now included in the XML report, if applicable.
4650
- A section containing JUnit-specific metadata about each test/container to the HTML
4751
report is now written by open-test-reporting when added to the classpath/module path
52+
- Information about published files is now included as attachments.
4853

4954

5055
[[release-notes-5.12.0-M1-junit-jupiter]]
@@ -112,6 +117,8 @@ JUnit repository on GitHub.
112117
* When enabled via the `junit.jupiter.execution.timeout.threaddump.enabled` configuration
113118
parameter, an implementation of `PreInterruptCallback` is registered that writes a
114119
thread dump to `System.out` prior to interrupting a test thread due to a timeout.
120+
* `TestReporter` now allows publishing files for a test method or test class which can be
121+
used to include them in test reports, such as the Open Test Reporting format.
115122

116123

117124
[[release-notes-5.12.0-M1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,46 @@
33

44
The `junit-platform-reporting` artifact contains `{TestExecutionListener}` implementations
55
that generate XML test reports in two flavors:
6-
<<junit-platform-reporting-legacy-xml, legacy>> and
7-
<<junit-platform-reporting-open-test-reporting, Open Test Reporting>>.
6+
<<junit-platform-reporting-open-test-reporting, Open Test Reporting>> and
7+
<<junit-platform-reporting-legacy-xml, legacy>>.
88

99
NOTE: The module also contains other `TestExecutionListener` implementations that can be
1010
used to build custom reporting. See <<running-tests-listeners>> for details.
1111

12-
[[junit-platform-reporting-legacy-xml]]
13-
==== Legacy XML format
12+
[[junit-platform-reporting-output-directory]]
13+
==== Output Directory
1414

15-
`{LegacyXmlReportGeneratingListener}` generates a separate XML report for each root in the
16-
`{TestPlan}`. Note that the generated XML format is compatible with the de facto standard
17-
for JUnit 4 based test reports that was made popular by the Ant build system.
15+
The JUnit Platform provides an `{OutputDirectoryProvider}` via
16+
`{EngineDiscoveryRequest}` and `{TestPlan}` to registered <<test-engines, test engines>>
17+
and <<running-tests-listeners, listeners>>, respectively. Its root directory can be
18+
configured via the following <<running-tests-config-params, configuration parameter>>:
1819

19-
The `LegacyXmlReportGeneratingListener` is used by the <<running-tests-console-launcher>>
20-
as well.
20+
`junit.platform.reporting.output.dir=<path>`::
21+
Configure the output directory for reporting. By default, `build` is used if a Gradle
22+
build script is found, and `target` if a Maven POM is found; otherwise, the current
23+
working directory is used.
24+
25+
To create a unique output directory per test run, you can use the `\{uniqueNumber}`
26+
placeholder in the path. For example, `reports/junit-\{uniqueNumber}` will create
27+
directories like `reports/junit-8803697269315188212`. This can be useful when using
28+
Gradle's or Maven's parallel execution capabilities which create multiple JVM forks
29+
that run concurrently.
2130

2231
[[junit-platform-reporting-open-test-reporting]]
23-
==== Open Test Reporting XML format
32+
==== Open Test Reporting
2433

2534
`{OpenTestReportGeneratingListener}` writes an XML report for the entire execution in the
2635
event-based format specified by {OpenTestReporting} which supports all features of the
2736
JUnit Platform such as hierarchical test structures, display names, tags, etc.
2837

2938
The listener is auto-registered and can be configured via the following
30-
<<running-tests-config-params>>:
39+
<<running-tests-config-params, configuration parameter>>:
3140

3241
`junit.platform.reporting.open.xml.enabled=true|false`::
3342
Enable/disable writing the report.
34-
`junit.platform.reporting.output.dir=<path>`::
35-
Configure the output directory for the reports. By default, `build` is used if a Gradle
36-
build script is found, and `target` if a Maven POM is found; otherwise, the current
37-
working directory is used.
3843

39-
If enabled, the listener creates an XML report file named
40-
`junit-platform-events-<random-id>.xml` per test run in the configured output directory.
44+
If enabled, the listener creates an XML report file named `open-test-report.xml` in the
45+
configured <<junit-platform-reporting-output-directory, output directory>>.
4146

4247
TIP: The {OpenTestReportingCliTool} can be used to convert from the event-based format to
4348
the hierarchical format which is more human-readable.
@@ -145,3 +150,13 @@ via the `--config-resource` option:
145150
$ java -jar junit-platform-console-standalone-{platform-version}.jar <OPTIONS> \
146151
--config-resource=configuration.properties
147152
----
153+
154+
[[junit-platform-reporting-legacy-xml]]
155+
==== Legacy XML format
156+
157+
`{LegacyXmlReportGeneratingListener}` generates a separate XML report for each root in the
158+
`{TestPlan}`. Note that the generated XML format is compatible with the de facto standard
159+
for JUnit 4 based test reports that was made popular by the Ant build system.
160+
161+
The `LegacyXmlReportGeneratingListener` is used by the <<running-tests-console-launcher>>
162+
as well.

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,8 +1077,9 @@ include::{testDir}/example/TestInfoDemo.java[tags=user_guide]
10771077
* `{TestReporterParameterResolver}`: if a constructor or method parameter is of type
10781078
`{TestReporter}`, the `TestReporterParameterResolver` will supply an instance of
10791079
`TestReporter`. The `TestReporter` can be used to publish additional data about the
1080-
current test run. The data can be consumed via the `reportingEntryPublished()` method in
1081-
a `{TestExecutionListener}`, allowing it to be viewed in IDEs or included in reports.
1080+
current test run or attach files to it. The data can be consumed in a
1081+
`{TestExecutionListener}` via the `reportingEntryPublished()` or `fileEntryPublished()`
1082+
method, respectively. This allows them to be viewed in IDEs or included in reports.
10821083
+
10831084
In JUnit Jupiter you should use `TestReporter` where you used to print information to
10841085
`stdout` or `stderr` in JUnit 4. Using `@RunWith(JUnitPlatform.class)` will output all

documentation/src/test/java/example/TestReporterDemo.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010

1111
package example;
1212

13+
import static java.util.Collections.singletonList;
14+
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
1317
import java.util.HashMap;
1418
import java.util.Map;
1519

1620
import org.junit.jupiter.api.Test;
1721
import org.junit.jupiter.api.TestReporter;
22+
import org.junit.jupiter.api.io.TempDir;
1823
import org.junit.jupiter.api.parallel.Execution;
1924
import org.junit.jupiter.api.parallel.ExecutionMode;
2025

@@ -41,5 +46,18 @@ void reportMultipleKeyValuePairs(TestReporter testReporter) {
4146
testReporter.publishEntry(values);
4247
}
4348

49+
@Test
50+
void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {
51+
52+
testReporter.publishFile("test1.txt", file -> Files.write(file, singletonList("Test 1")));
53+
54+
Path existingFile = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2"));
55+
testReporter.publishFile(existingFile);
56+
57+
testReporter.publishFile("test3", dir -> {
58+
Path nestedFile = Files.createDirectory(dir).resolve("nested.txt");
59+
Files.write(nestedFile, singletonList("Nested content"));
60+
});
61+
}
4462
}
4563
// end::user_guide[]

gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import org.gradle.api.tasks.PathSensitivity.RELATIVE
55
import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
66
import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED
77
import org.gradle.internal.os.OperatingSystem
8-
import java.nio.file.Files
98

109
plugins {
1110
`java-library`
@@ -31,7 +30,7 @@ val generateOpenTestHtmlReport by tasks.registering(JavaExec::class) {
3130
eventXmlFiles.from(tasks.withType<Test>().map {
3231
objects.fileTree()
3332
.from(it.reports.junitXml.outputLocation)
34-
.include("junit-platform-events-*.xml")
33+
.include("junit-*/open-test-report.xml")
3534
})
3635
outputLocation = layout.buildDirectory.file("reports/open-test-report.html")
3736
}
@@ -119,18 +118,20 @@ tasks.withType<Test>().configureEach {
119118
jvmArgumentProviders += CommandLineArgumentProvider {
120119
listOf(
121120
"-Djunit.platform.reporting.open.xml.enabled=true",
122-
"-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}"
121+
"-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}/junit-{uniqueNumber}",
123122
)
124123
}
125124

126125
jvmArgumentProviders += objects.newInstance(JavaAgentArgumentProvider::class).apply {
127126
classpath.from(javaAgentClasspath)
128127
}
129128

130-
val reportFiles = objects.fileTree().from(reports.junitXml.outputLocation).matching { include("junit-platform-events-*.xml") }
129+
val reportDirTree = objects.fileTree().from(reports.junitXml.outputLocation)
131130
doFirst {
132-
reportFiles.files.forEach {
133-
Files.delete(it.toPath())
131+
reportDirTree.visit {
132+
if (name.startsWith("junit-")) {
133+
file.deleteRecursively()
134+
}
134135
}
135136
}
136137

junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010

1111
package org.junit.jupiter.api;
1212

13+
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
14+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1315
import static org.apiguardian.api.API.Status.STABLE;
1416

17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
1519
import java.util.Collections;
1620
import java.util.Map;
1721

1822
import org.apiguardian.api.API;
23+
import org.junit.jupiter.api.function.ThrowingConsumer;
1924

2025
/**
2126
* Parameters of type {@code TestReporter} can be injected into
@@ -77,4 +82,36 @@ default void publishEntry(String value) {
7782
this.publishEntry("value", value);
7883
}
7984

85+
/**
86+
* Publish the supplied file and attach it to the current test or container.
87+
* <p>
88+
* The file will be copied to the report output directory replacing any
89+
* potentially existing file with the same name.
90+
*
91+
* @param file the file to be attached; never {@code null} or blank
92+
* @since 5.12
93+
*/
94+
@API(status = EXPERIMENTAL, since = "5.12")
95+
default void publishFile(Path file) {
96+
publishFile(file.getFileName().toString(), path -> Files.copy(file, path, REPLACE_EXISTING));
97+
}
98+
99+
/**
100+
* Publish a file with the supplied name written by the supplied action and
101+
* attach it to the current test or container.
102+
* <p>
103+
* The {@link Path} passed to the supplied action will be relative to the
104+
* report output directory, but it's up to the action to write the file or
105+
* directory.
106+
*
107+
* @param fileName the name of the file to be attached; never {@code null} or blank
108+
* and must not contain any path separators
109+
* @param action the action to be executed to write the file; never {@code null}
110+
* @since 5.12
111+
*/
112+
@API(status = EXPERIMENTAL, since = "5.12")
113+
default void publishFile(String fileName, ThrowingConsumer<Path> action) {
114+
throw new UnsupportedOperationException();
115+
}
116+
80117
}

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
package org.junit.jupiter.api.extension;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.STABLE;
1415

1516
import java.lang.reflect.AnnotatedElement;
1617
import java.lang.reflect.Method;
18+
import java.nio.file.Path;
1719
import java.util.ArrayList;
1820
import java.util.Arrays;
1921
import java.util.Collections;
@@ -25,6 +27,7 @@
2527

2628
import org.apiguardian.api.API;
2729
import org.junit.jupiter.api.TestInstance.Lifecycle;
30+
import org.junit.jupiter.api.function.ThrowingConsumer;
2831
import org.junit.jupiter.api.parallel.ExecutionMode;
2932
import org.junit.platform.commons.PreconditionViolationException;
3033
import org.junit.platform.commons.support.ReflectionSupport;
@@ -364,6 +367,22 @@ default void publishReportEntry(String value) {
364367
this.publishReportEntry("value", value);
365368
}
366369

370+
/**
371+
* Publish a file with the supplied name written by the supplied action and
372+
* attach it to the current test or container.
373+
* <p>
374+
* The file will be resolved in the report output directory prior to
375+
* invoking the supplied action.
376+
*
377+
* @param fileName the name of the file to be attached; never {@code null} or blank
378+
* and must not contain any path separators
379+
* @param action the action to be executed to write the file; never {@code null}
380+
* @since 5.12
381+
* @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished
382+
*/
383+
@API(status = EXPERIMENTAL, since = "5.12")
384+
void publishFile(String fileName, ThrowingConsumer<Path> action);
385+
367386
/**
368387
* Get the {@link Store} for the supplied {@link Namespace}.
369388
*

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public Optional<String> getArtifactId() {
6363

6464
@Override
6565
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
66-
JupiterConfiguration configuration = new CachingJupiterConfiguration(
67-
new DefaultJupiterConfiguration(discoveryRequest.getConfigurationParameters()));
66+
JupiterConfiguration configuration = new CachingJupiterConfiguration(new DefaultJupiterConfiguration(
67+
discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider()));
6868
JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration);
6969
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor);
7070
return engineDescriptor;

0 commit comments

Comments
 (0)