Skip to content

Commit 5e2b694

Browse files
authored
Add JUnit-specific metadata to open-test-reporting's HTML report (#4116)
When creating the event-based XML report, the `OpenTestReportGeneratingListener` includes JUnit-specific metadata: type, unique ID, and legacy reporting name. This PR implements open-test-reporting's `Contributor` SPI to include that data in the new HTML report of open-test-reporting. Moreover, it configures the build to create a single report for all test tasks per subproject and uploads it as part of GitHub Action runs.
1 parent be4edac commit 5e2b694

File tree

28 files changed

+343
-18
lines changed

28 files changed

+343
-18
lines changed

.github/actions/main-build/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ runs:
1616
with:
1717
arguments: ${{ inputs.arguments }}
1818
encryptionKey: ${{ inputs.encryptionKey }}
19+
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
20+
if: ${{ always() }}
21+
with:
22+
name: Open Test Reports (${{ github.job }})
23+
path: '**/build/reports/open-test-report.html'

.github/workflows/cross-version.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ jobs:
6666
-Dscan.tag.JDK_${{ matrix.jdk.version }} \
6767
build \
6868
--configuration-cache
69+
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
70+
if: ${{ always() }}
71+
with:
72+
name: Open Test Reports (${{ github.job }} ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }}))
73+
path: '**/build/reports/open-test-report.html'
6974
openj9:
7075
strategy:
7176
fail-fast: false
@@ -102,3 +107,8 @@ jobs:
102107
-Dscan.tag.OpenJ9 \
103108
build \
104109
--configuration-cache
110+
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
111+
if: ${{ always() }}
112+
with:
113+
name: Open Test Reports (${{ github.job }})
114+
path: '**/build/reports/open-test-report.html'

documentation/documentation.gradle.kts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import junitbuild.javadoc.ModuleSpecificJavadocFileOption
66
import org.asciidoctor.gradle.base.AsciidoctorAttributeProvider
77
import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
88
import org.gradle.api.tasks.PathSensitivity.RELATIVE
9+
import java.nio.file.Files
910

1011
plugins {
1112
alias(libs.plugins.asciidoctorConvert)
@@ -144,23 +145,40 @@ require(externalModulesWithoutModularJavadoc.values.all { it.endsWith("/") }) {
144145

145146
tasks {
146147

148+
val consoleLauncherTestReportsDir = project.layout.buildDirectory.dir("console-launcher-test-results")
149+
val consoleLauncherTestEventXmlFiles =
150+
files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("junit-platform-events-*.xml") } })
151+
147152
val consoleLauncherTest by registering(RunConsoleLauncher::class) {
148153
args.addAll("execute")
149154
args.addAll("--scan-classpath")
150155
args.addAll("--config=junit.platform.reporting.open.xml.enabled=true")
151-
val reportsDir = project.layout.buildDirectory.dir("console-launcher-test-results")
152-
outputs.dir(reportsDir)
156+
outputs.dir(consoleLauncherTestReportsDir)
153157
argumentProviders.add(CommandLineArgumentProvider {
154158
listOf(
155-
"--reports-dir=${reportsDir.get()}",
156-
"--config=junit.platform.reporting.output.dir=${reportsDir.get()}"
157-
159+
"--reports-dir=${consoleLauncherTestReportsDir.get()}",
160+
"--config=junit.platform.reporting.output.dir=${consoleLauncherTestReportsDir.get()}",
158161
)
159162
})
160163
args.addAll("--include-classname", ".*Tests")
161164
args.addAll("--include-classname", ".*Demo")
162165
args.addAll("--exclude-tag", "exclude")
163166
args.addAll("--exclude-tag", "timeout")
167+
168+
doFirst {
169+
consoleLauncherTestEventXmlFiles.files.forEach {
170+
Files.delete(it.toPath())
171+
}
172+
}
173+
174+
finalizedBy(generateOpenTestHtmlReport)
175+
}
176+
177+
generateOpenTestHtmlReport {
178+
mustRunAfter(consoleLauncherTest)
179+
argumentProviders += CommandLineArgumentProvider {
180+
consoleLauncherTestEventXmlFiles.files.map { it.absolutePath }.toList()
181+
}
164182
}
165183

166184
register<RunConsoleLauncher>("consoleLauncher") {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ JUnit repository on GitHub.
3838
`--select-file` and `--select-resource`.
3939
* `ConsoleLauncher` now accepts multiple values for all `--select` options.
4040
* Add `--select-unique-id` support to ConsoleLauncher.
41+
* The `junit-platform-reporting` module now contributes a section containing
42+
JUnit-specific metadata about each test/container to the HTML report written by
43+
open-test-reporting when added to the classpath/module path.
4144

4245

4346
[[release-notes-5.12.0-M1-junit-jupiter]]

gradle/libs.versions.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ junit4Min = "4.12"
1515
ktlint = "1.4.1"
1616
log4j = "2.24.1"
1717
opentest4j = "1.3.0"
18-
openTestReporting = "0.1.0-M2"
18+
openTestReporting = "0.1.0-SNAPSHOT"
1919
surefire = "3.5.2"
2020
xmlunit = "2.10.0"
2121

@@ -56,8 +56,10 @@ memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version =
5656
mockito = { module = "org.mockito:mockito-junit-jupiter", version = "5.14.2" }
5757
nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version = "0.0.11" }
5858
opentest4j = { module = "org.opentest4j:opentest4j", version.ref = "opentest4j" }
59+
openTestReporting-cli = { module = "org.opentest4j.reporting:open-test-reporting-cli", version.ref = "openTestReporting" }
5960
openTestReporting-events = { module = "org.opentest4j.reporting:open-test-reporting-events", version.ref = "openTestReporting" }
60-
openTestReporting-tooling = { module = "org.opentest4j.reporting:open-test-reporting-tooling", version.ref = "openTestReporting" }
61+
openTestReporting-tooling-core = { module = "org.opentest4j.reporting:open-test-reporting-tooling-core", version.ref = "openTestReporting" }
62+
openTestReporting-tooling-spi = { module = "org.opentest4j.reporting:open-test-reporting-tooling-spi", version.ref = "openTestReporting" }
6163
picocli = { module = "info.picocli:picocli", version = "4.7.6" }
6264
slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.16" }
6365
spock1 = { module = "org.spockframework:spock-core", version = "1.3-groovy-2.5" }

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,50 @@
1+
12
import com.gradle.develocity.agent.gradle.internal.test.PredictiveTestSelectionConfigurationInternal
23
import com.gradle.develocity.agent.gradle.test.PredictiveTestSelectionMode
4+
import org.gradle.api.tasks.PathSensitivity.NONE
35
import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
46
import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED
57
import org.gradle.internal.os.OperatingSystem
8+
import java.nio.file.Files
69

710
plugins {
811
`java-library`
912
id("junitbuild.build-parameters")
1013
}
1114

15+
var openTestReportingCli = configurations.dependencyScope("openTestReportingCli")
16+
var openTestReportingCliClasspath = configurations.resolvable("openTestReportingCliClasspath") {
17+
extendsFrom(openTestReportingCli.get())
18+
}
19+
20+
val generateOpenTestHtmlReport by tasks.registering(JavaExec::class) {
21+
mustRunAfter(tasks.withType<Test>())
22+
mainClass.set("org.opentest4j.reporting.cli.ReportingCli")
23+
args("html-report")
24+
classpath(openTestReportingCliClasspath)
25+
argumentProviders += objects.newInstance(HtmlReportParameters::class).apply {
26+
eventXmlFiles.from(tasks.withType<Test>().map {
27+
objects.fileTree()
28+
.from(it.reports.junitXml.outputLocation)
29+
.include("junit-platform-events-*.xml")
30+
})
31+
outputLocation = layout.buildDirectory.file("reports/open-test-report.html")
32+
}
33+
}
34+
35+
abstract class HtmlReportParameters : CommandLineArgumentProvider {
36+
37+
@get:InputFiles
38+
@get:PathSensitive(NONE)
39+
abstract val eventXmlFiles: ConfigurableFileCollection
40+
41+
@get:OutputFile
42+
abstract val outputLocation: RegularFileProperty
43+
44+
override fun asArguments() = listOf("--output", outputLocation.get().asFile.absolutePath) +
45+
eventXmlFiles.map { it.absolutePath }.toList()
46+
}
47+
1248
tasks.withType<Test>().configureEach {
1349
useJUnitPlatform {
1450
includeEngines("junit-jupiter")
@@ -79,6 +115,15 @@ tasks.withType<Test>().configureEach {
79115
"-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}"
80116
)
81117
}
118+
119+
val reportFiles = objects.fileTree().from(reports.junitXml.outputLocation).matching { include("junit-platform-events-*.xml") }
120+
doFirst {
121+
reportFiles.files.forEach {
122+
Files.delete(it.toPath())
123+
}
124+
}
125+
126+
finalizedBy(generateOpenTestHtmlReport)
82127
}
83128

84129
dependencies {
@@ -98,4 +143,7 @@ dependencies {
98143
testRuntimeOnly(dependencyFromLibs("openTestReporting-events")) {
99144
because("it's required to run tests via IntelliJ which does not consumed the shadowed jar of junit-platform-reporting")
100145
}
146+
147+
openTestReportingCli(dependencyFromLibs("openTestReporting-cli"))
148+
openTestReportingCli(project(":junit-platform-reporting"))
101149
}

junit-platform-console-standalone/junit-platform-console-standalone.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ dependencies {
1717
shadowed(libs.apiguardian) {
1818
because("downstream projects need it to avoid compiler warnings")
1919
}
20+
21+
osgiVerification(libs.openTestReporting.tooling.spi)
2022
}
2123

2224
val jupiterVersion = rootProject.version

junit-platform-console/junit-platform-console.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020

2121
osgiVerification(projects.junitJupiterEngine)
2222
osgiVerification(projects.junitPlatformLauncher)
23+
osgiVerification(libs.openTestReporting.tooling.spi)
2324
}
2425

2526
tasks {

junit-platform-reporting/junit-platform-reporting.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id("junitbuild.java-library-conventions")
33
id("junitbuild.shadow-conventions")
4+
`java-test-fixtures`
45
}
56

67
description = "JUnit Platform Reporting"
@@ -10,16 +11,23 @@ dependencies {
1011
api(projects.junitPlatformLauncher)
1112

1213
compileOnlyApi(libs.apiguardian)
14+
compileOnlyApi(libs.openTestReporting.tooling.spi)
1315

1416
shadowed(libs.openTestReporting.events)
1517

1618
osgiVerification(projects.junitJupiterEngine)
1719
osgiVerification(projects.junitPlatformLauncher)
20+
osgiVerification(libs.openTestReporting.tooling.spi)
21+
22+
testFixturesApi(projects.junitJupiterApi)
1823
}
1924

2025
tasks {
2126
shadowJar {
22-
relocate("org.opentest4j.reporting", "org.junit.platform.reporting.shadow.org.opentest4j.reporting")
27+
listOf("events", "schema").forEach { name ->
28+
val packageName = "org.opentest4j.reporting.${name}"
29+
relocate(packageName, "org.junit.platform.reporting.shadow.${packageName}")
30+
}
2331
from(projectDir) {
2432
include("LICENSE-open-test-reporting.md")
2533
into("META-INF")
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.reporting.open.xml;
12+
13+
import static java.util.Collections.emptyList;
14+
import static java.util.Collections.singletonList;
15+
import static org.apiguardian.api.API.Status.INTERNAL;
16+
17+
import java.util.LinkedHashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import org.apiguardian.api.API;
23+
import org.opentest4j.reporting.schema.Namespace;
24+
import org.opentest4j.reporting.tooling.spi.htmlreport.Contributor;
25+
import org.opentest4j.reporting.tooling.spi.htmlreport.KeyValuePairs;
26+
import org.opentest4j.reporting.tooling.spi.htmlreport.Section;
27+
import org.w3c.dom.Element;
28+
import org.w3c.dom.Node;
29+
import org.w3c.dom.NodeList;
30+
31+
/**
32+
* Contributes a section containing JUnit-specific metadata for each test node
33+
* to the open-test-reporting HTML report.
34+
*
35+
* @since 1.12
36+
*/
37+
@SuppressWarnings("exports") // we don't want to export 'org.opentest4j.reporting.tooling.spi' transitively
38+
@API(status = INTERNAL, since = "1.12")
39+
public class JUnitContributor implements Contributor {
40+
41+
public JUnitContributor() {
42+
}
43+
44+
@Override
45+
public List<Section> contributeSectionsForTestNode(Element testNodeElement) {
46+
return findChild(testNodeElement, Namespace.REPORTING_CORE, "metadata") //
47+
.map(metadata -> {
48+
Map<String, String> table = new LinkedHashMap<>();
49+
findChild(metadata, JUnitFactory.NAMESPACE, "type") //
50+
.map(Node::getTextContent) //
51+
.ifPresent(value -> table.put("Type", value));
52+
findChild(metadata, JUnitFactory.NAMESPACE, "uniqueId") //
53+
.map(Node::getTextContent) //
54+
.ifPresent(value -> table.put("Unique ID", value));
55+
findChild(metadata, JUnitFactory.NAMESPACE, "legacyReportingName") //
56+
.map(Node::getTextContent) //
57+
.ifPresent(value -> table.put("Legacy reporting name", value));
58+
return table;
59+
}) //
60+
.filter(table -> !table.isEmpty()) //
61+
.map(table -> singletonList(Section.builder() //
62+
.title("JUnit metadata") //
63+
.order(15) //
64+
.addBlock(KeyValuePairs.builder().content(table).build()) //
65+
.build())) //
66+
.orElse(emptyList());
67+
}
68+
69+
private static Optional<Node> findChild(Node parent, Namespace namespace, String localName) {
70+
NodeList children = parent.getChildNodes();
71+
for (int i = 0; i < children.getLength(); i++) {
72+
Node child = children.item(i);
73+
if (localName.equals(child.getLocalName()) && namespace.getUri().equals(child.getNamespaceURI())) {
74+
return Optional.of(child);
75+
}
76+
}
77+
return Optional.empty();
78+
}
79+
}

0 commit comments

Comments
 (0)