Skip to content

Commit f99de33

Browse files
committed
Implement JUnitContributor to add JUnit-specific metadata to HTML report
1 parent 460b40b commit f99de33

File tree

8 files changed

+188
-3
lines changed

8 files changed

+188
-3
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ 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" }
6061
openTestReporting-tooling-core = { module = "org.opentest4j.reporting:open-test-reporting-tooling-core", version.ref = "openTestReporting" }
6162
openTestReporting-tooling-spi = { module = "org.opentest4j.reporting:open-test-reporting-tooling-spi", version.ref = "openTestReporting" }

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
import com.gradle.develocity.agent.gradle.internal.test.PredictiveTestSelectionConfigurationInternal
22
import com.gradle.develocity.agent.gradle.test.PredictiveTestSelectionMode
3+
import org.gradle.api.tasks.PathSensitivity.NONE
34
import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
45
import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED
56
import org.gradle.internal.os.OperatingSystem
7+
import java.nio.file.Files
68

79
plugins {
810
`java-library`
911
id("junitbuild.build-parameters")
1012
}
1113

14+
var openTestReportingCli = configurations.dependencyScope("openTestReportingCli")
15+
var openTestReportingCliClasspath = configurations.resolvable("openTestReportingCliClasspath") {
16+
extendsFrom(openTestReportingCli.get())
17+
}
18+
19+
val htmlReportFile = layout.buildDirectory.file("reports/open-test-report.html")
20+
val eventXmlFiles =
21+
files(tasks.withType<Test>().map { it.reports.junitXml.outputLocation.get().asFileTree.matching { include("junit-platform-events-*.xml") } })
22+
23+
val generateOpenTestHtmlReport by tasks.registering(JavaExec::class) {
24+
mustRunAfter(tasks.withType<Test>())
25+
mainClass.set("org.opentest4j.reporting.cli.ReportingCli")
26+
args("html-report")
27+
classpath(openTestReportingCliClasspath)
28+
outputs.file(htmlReportFile)
29+
inputs.files(eventXmlFiles).withPathSensitivity(NONE).skipWhenEmpty()
30+
argumentProviders += CommandLineArgumentProvider {
31+
listOf(
32+
"--output",
33+
htmlReportFile.get().asFile.absolutePath
34+
) + eventXmlFiles.files.map { it.absolutePath }.toList()
35+
}
36+
}
37+
1238
tasks.withType<Test>().configureEach {
1339
useJUnitPlatform {
1440
includeEngines("junit-jupiter")
@@ -79,6 +105,16 @@ tasks.withType<Test>().configureEach {
79105
"-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}"
80106
)
81107
}
108+
109+
doFirst {
110+
files(reports.junitXml.outputLocation.get().asFileTree.matching {
111+
include("junit-platform-events-*.xml")
112+
}).files.forEach {
113+
Files.delete(it.toPath())
114+
}
115+
}
116+
117+
finalizedBy(generateOpenTestHtmlReport)
82118
}
83119

84120
dependencies {
@@ -98,4 +134,7 @@ dependencies {
98134
testRuntimeOnly(dependencyFromLibs("openTestReporting-events")) {
99135
because("it's required to run tests via IntelliJ which does not consumed the shadowed jar of junit-platform-reporting")
100136
}
137+
138+
openTestReportingCli(dependencyFromLibs("openTestReporting-cli"))
139+
openTestReportingCli(project(":junit-platform-reporting"))
101140
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
api(projects.junitPlatformLauncher)
1212

1313
compileOnlyApi(libs.apiguardian)
14+
compileOnlyApi(libs.openTestReporting.tooling.spi)
1415

1516
shadowed(libs.openTestReporting.events)
1617

@@ -22,7 +23,7 @@ dependencies {
2223

2324
tasks {
2425
shadowJar {
25-
relocate("org.opentest4j.reporting", "org.junit.platform.reporting.shadow.org.opentest4j.reporting")
26+
relocate("org.opentest4j.reporting.events", "org.junit.platform.reporting.shadow.org.opentest4j.reporting.events")
2627
from(projectDir) {
2728
include("LICENSE-open-test-reporting.md")
2829
into("META-INF")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
@API(status = INTERNAL, since = "1.12")
32+
public class JUnitContributor implements Contributor {
33+
34+
public JUnitContributor() {
35+
}
36+
37+
@Override
38+
public List<Section> contributeSectionsForTestNode(Element testNodeElement) {
39+
return findChild(testNodeElement, Namespace.REPORTING_CORE, "metadata") //
40+
.map(metadata -> {
41+
Map<String, String> table = new LinkedHashMap<>();
42+
findChild(metadata, JUnitFactory.NAMESPACE, "type") //
43+
.map(Node::getTextContent) //
44+
.ifPresent(value -> table.put("Type", value));
45+
findChild(metadata, JUnitFactory.NAMESPACE, "uniqueId") //
46+
.map(Node::getTextContent) //
47+
.ifPresent(value -> table.put("Unique ID", value));
48+
findChild(metadata, JUnitFactory.NAMESPACE, "legacyReportingName") //
49+
.map(Node::getTextContent) //
50+
.ifPresent(value -> table.put("Legacy reporting name", value));
51+
return table;
52+
}) //
53+
.filter(table -> !table.isEmpty()) //
54+
.map(table -> singletonList(Section.builder() //
55+
.title("JUnit metadata") //
56+
.order(15) //
57+
.addBlock(KeyValuePairs.builder().content(table).build()) //
58+
.build())) //
59+
.orElse(emptyList());
60+
}
61+
62+
private static Optional<Node> findChild(Node parent, Namespace namespace, String localName) {
63+
NodeList children = parent.getChildNodes();
64+
for (int i = 0; i < children.getLength(); i++) {
65+
Node child = children.item(i);
66+
if (localName.equals(child.getLocalName()) && namespace.getUri().equals(child.getNamespaceURI())) {
67+
return Optional.of(child);
68+
}
69+
}
70+
return Optional.empty();
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.junit.platform.reporting.open.xml.JUnitContributor

junit-platform-reporting/src/module/org.junit.platform.reporting/module-info.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
* @since 1.4
1515
*/
1616
module org.junit.platform.reporting {
17-
requires java.xml;
17+
requires transitive java.xml;
1818
requires static transitive org.apiguardian.api;
1919
requires org.junit.platform.commons;
2020
requires transitive org.junit.platform.engine;
2121
requires transitive org.junit.platform.launcher;
22+
requires static transitive org.opentest4j.reporting.tooling.spi;
2223

2324
// exports org.junit.platform.reporting; empty package
2425
exports org.junit.platform.reporting.legacy;
@@ -27,4 +28,7 @@
2728

2829
provides org.junit.platform.launcher.TestExecutionListener
2930
with org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener;
31+
32+
provides org.opentest4j.reporting.tooling.spi.htmlreport.Contributor
33+
with org.junit.platform.reporting.open.xml.JUnitContributor;
3034
}

platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ public void write(char[] buffer, int off, int len) {
248248
writeXmlReport(testPlan, reportData, assertingWriter);
249249
}
250250

251-
@ParameterizedTest
251+
@ParameterizedTest(name = "{index}")
252252
@MethodSource("stringPairs")
253253
void escapesIllegalChars(String input, String output) {
254254
assertEquals(output, XmlReportWriter.escapeIllegalChars(input));
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 org.assertj.core.api.Assertions.assertThat;
14+
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.util.List;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.io.TempDir;
21+
import org.opentest4j.reporting.tooling.core.htmlreport.DefaultHtmlReportWriter;
22+
23+
public class JUnitContributorTests {
24+
25+
@Test
26+
void contributesJUnitSpecificMetadata(@TempDir Path tempDir) throws Exception {
27+
var xmlFile = Files.writeString(tempDir.resolve("report.xml"),
28+
"""
29+
<e:events xmlns="https://schemas.opentest4j.org/reporting/core/0.1.0" xmlns:e="https://schemas.opentest4j.org/reporting/events/0.1.0" xmlns:java="https://schemas.opentest4j.org/reporting/java/0.1.0"
30+
xmlns:junit="https://schemas.junit.org/open-test-reporting" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
31+
xsi:schemaLocation="https://schemas.junit.org/open-test-reporting https://junit.org/junit5/schemas/open-test-reporting/junit-1.9.xsd">
32+
<e:started id="1" name="dummy" time="2024-11-10T16:31:35.000Z">
33+
<metadata>
34+
<junit:uniqueId>[engine:dummy]</junit:uniqueId>
35+
<junit:legacyReportingName>dummy</junit:legacyReportingName>
36+
<junit:type>CONTAINER</junit:type>
37+
</metadata>
38+
</e:started>
39+
<e:started id="2" name="method" parentId="1" time="2024-11-10T16:31:35.001Z">
40+
<metadata>
41+
<junit:uniqueId>[engine:dummy]/[test:method]</junit:uniqueId>
42+
<junit:legacyReportingName>method()</junit:legacyReportingName>
43+
<junit:type>TEST</junit:type>
44+
</metadata>
45+
</e:started>
46+
<e:finished id="2" time="2024-11-10T16:31:35.002Z">
47+
<result status="SUCCESSFUL"/>
48+
</e:finished>
49+
<e:finished id="1" time="2024-11-10T16:31:35.003Z">
50+
<result status="SUCCESSFUL"/>
51+
</e:finished>
52+
</e:events>
53+
""");
54+
var htmlReport = tempDir.resolve("report.html");
55+
56+
new DefaultHtmlReportWriter().writeHtmlReport(List.of(xmlFile), htmlReport);
57+
58+
assertThat(htmlReport).content() //
59+
.contains("JUnit metadata") //
60+
.contains("Type").contains("CONTAINER") //
61+
.contains("Unique ID").contains("[engine:dummy]") //
62+
.contains("Legacy reporting name").contains("dummy") //
63+
.contains("Type").contains("TEST") //
64+
.contains("Unique ID").contains("[engine:dummy]/[test:method]") //
65+
.contains("Legacy reporting name").contains("method()");
66+
}
67+
}

0 commit comments

Comments
 (0)