Skip to content

Commit 54dbe6e

Browse files
mhdatieclaude
andcommitted
Add RetryMarkerListener to write retry marker files during test execution
Tracks tests executed more than once via TestIdentifier.getUniqueId() and writes TEST-retried-{classname}.xml alongside the standard JUnit XML output. Accumulated across all retry rounds so the final write reflects the complete set of retried tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 689d430 commit 54dbe6e

4 files changed

Lines changed: 93 additions & 0 deletions

File tree

buildSrc/src/main/kotlin/dd-trace-java.configure-tests.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ tasks.withType<Test>().configureEach {
4949
// Trick to avoid on CI: "Couldn't flush user prefs: java.util.prefs.BackingStoreException: Couldn't get file lock."
5050
// Use a task-specific user prefs directory
5151
systemProperty("java.util.prefs.userRoot", layout.buildDirectory.dir("tmp/userPrefs/$name").get().asFile.absolutePath)
52+
systemProperty("dd.test.results.dir", reports.junitXml.outputLocation.get().asFile.absolutePath)
5253

5354
// Enable JUnit 5 auto-detection so ConfigInversionExtension (STRICT mode) is loaded automatically
5455
systemProperty("junit.jupiter.extensions.autodetection.enabled", "true")

utils/junit-utils/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ dependencies {
1313

1414
compileOnly(libs.junit.jupiter)
1515
compileOnly(libs.tabletest)
16+
compileOnly(libs.junit.platform.launcher)
1617
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package datadog.trace.junit.utils.retry;
2+
3+
import java.io.BufferedWriter;
4+
import java.nio.charset.StandardCharsets;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.util.LinkedHashMap;
9+
import java.util.LinkedHashSet;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import javax.xml.stream.XMLOutputFactory;
14+
import javax.xml.stream.XMLStreamWriter;
15+
import org.junit.platform.engine.TestExecutionResult;
16+
import org.junit.platform.engine.TestSource;
17+
import org.junit.platform.engine.support.descriptor.ClassSource;
18+
import org.junit.platform.engine.support.descriptor.MethodSource;
19+
import org.junit.platform.launcher.TestExecutionListener;
20+
import org.junit.platform.launcher.TestIdentifier;
21+
import org.junit.platform.launcher.TestPlan;
22+
23+
public class RetryMarkerListener implements TestExecutionListener {
24+
25+
static final String OUTPUT_DIR_PROP = "dd.test.results.dir";
26+
27+
private final Map<String, Integer> executionCounts = new ConcurrentHashMap<>();
28+
private final Map<String, TestIdentifier> identifiers = new ConcurrentHashMap<>();
29+
30+
@Override
31+
public void executionFinished(TestIdentifier id, TestExecutionResult result) {
32+
if (!id.isTest()) return;
33+
executionCounts.merge(id.getUniqueId(), 1, Integer::sum);
34+
identifiers.put(id.getUniqueId(), id);
35+
}
36+
37+
// Called once per retry round; overwrites marker files so the last round wins.
38+
@Override
39+
public void testPlanExecutionFinished(TestPlan plan) {
40+
String outputDirProp = System.getProperty(OUTPUT_DIR_PROP);
41+
if (outputDirProp == null) return;
42+
Map<String, Set<String>> retriedByClass = retriedTestsByClass();
43+
if (retriedByClass.isEmpty()) return;
44+
Path outputDir = Paths.get(outputDirProp);
45+
try {
46+
Files.createDirectories(outputDir);
47+
for (Map.Entry<String, Set<String>> entry : retriedByClass.entrySet()) {
48+
writeMarkerFile(outputDir, entry.getKey(), entry.getValue());
49+
}
50+
} catch (Exception ex) {
51+
System.err.println("[RetryMarkerListener] Failed to write retry markers: " + ex.getMessage());
52+
}
53+
}
54+
55+
private Map<String, Set<String>> retriedTestsByClass() {
56+
Map<String, Set<String>> byClass = new LinkedHashMap<>();
57+
for (Map.Entry<String, Integer> entry : executionCounts.entrySet()) {
58+
if (entry.getValue() <= 1) continue;
59+
TestIdentifier id = identifiers.get(entry.getKey());
60+
byClass.computeIfAbsent(classNameOf(id), k -> new LinkedHashSet<>()).add(id.getDisplayName());
61+
}
62+
return byClass;
63+
}
64+
65+
private static void writeMarkerFile(Path outputDir, String className, Set<String> testNames)
66+
throws Exception {
67+
Path file = outputDir.resolve("TEST-retried-" + className + ".xml");
68+
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
69+
XMLStreamWriter xml = XMLOutputFactory.newInstance().createXMLStreamWriter(writer);
70+
xml.writeStartDocument("UTF-8", "1.0");
71+
xml.writeStartElement("testsuite");
72+
xml.writeAttribute("name", className);
73+
for (String testName : testNames) {
74+
xml.writeEmptyElement("testcase");
75+
xml.writeAttribute("name", testName);
76+
xml.writeAttribute("classname", className);
77+
}
78+
xml.writeEndElement();
79+
xml.writeEndDocument();
80+
xml.flush();
81+
}
82+
}
83+
84+
private static String classNameOf(TestIdentifier id) {
85+
TestSource src = id.getSource().orElse(null);
86+
if (src instanceof MethodSource) return ((MethodSource) src).getClassName();
87+
if (src instanceof ClassSource) return ((ClassSource) src).getClassName();
88+
return id.getDisplayName();
89+
}
90+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
datadog.trace.junit.utils.retry.RetryMarkerListener

0 commit comments

Comments
 (0)