Skip to content
134 changes: 80 additions & 54 deletions .gitlab/TagInitializationErrors.java
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
import org.w3c.dom.Element;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/// Tags intermediate `initializationError` retries with `dd_tags[test.final_status]=skip`.
/// Tags synthetic testcases (`initializationError`, `executionError`, `test exception`) with
/// `dd_tags[test.final_status]=skip` so Test Optimization does not treat them as real failures.
/// The script is idempotent — testcases that already carry a `dd_tags[test.final_status]` property
/// are left unchanged.
///
/// **`initializationError`** — Gradle generates these for setup methods. When retried and
// eventually
/// successful, multiple testcases appear; only the last one passes. All intermediate attempts are
/// tagged skip. Groups with only one (or zero) `initializationError` entries per classname are left
// unmodified.
///
/// Gradle generates synthetic "initializationError" testcases in JUnit reports for setup methods.
/// When a setup is retried and eventually succeeds, multiple testcases are created, with only the
/// last one passing. All intermediate attempts are marked skip so Test Optimization is not misled.
/// **`executionError`** and **`test exception`** — Framework-level synthetic failures that do not
/// represent real test results. Tagged skip unconditionally so Test Optimization treats them as
// non-failures.
///
/// For any suite with multiple `initializationError` test cases (when retries occurred), all entries
/// but the last one are tagged by this script with `dd_tags[test.final_status]=skip`. The last
/// entry is left unmodified, allowing **Test Optimization** to apply its default status inference based
/// on the actual outcome. Files with only one (or zero) `initializationError` test cases are left unmodified.
/// Before (two retries of the same class — first is intermediate, second is the final outcome):
///
/// Before:
///
/// ```
/// <testcase name="initializationError" />
/// <testcase name="initializationError" classname="com.example.MyTest" />
/// <testcase name="initializationError" classname="com.example.MyTest" />
/// ```
///
/// After:
///
///
/// After (only the intermediate attempt is tagged; the last entry is left untouched):
///
/// ```
/// <testcase name="initializationError">
/// <testcase name="initializationError" classname="com.example.MyTest">
/// <properties>
/// <property name="dd_tags[test.final_status]" value="skip" />
/// <property name="dd_tags[test.final_status]" value="skip" />
/// </properties>
/// </testcase>
/// <testcase name="initializationError" classname="com.example.MyTest" />
/// ```
///
///
/// Usage (Java 25): `java TagInitializationErrors.java junit-report.xml`

class TagInitializationErrors {
Expand All @@ -61,54 +68,73 @@ public static void main(String[] args) throws Exception {
var doc = dbf.newDocumentBuilder().parse(xmlFile);
var testcases = doc.getElementsByTagName("testcase");
Map<String, List<Element>> byClassname = new LinkedHashMap<>();
boolean modified = false;
for (int i = 0; i < testcases.getLength(); i++) {
var e = (Element) testcases.item(i);
if ("initializationError".equals(e.getAttribute("name"))) {
var name = e.getAttribute("name");
if ("initializationError".equals(name)) {
byClassname.computeIfAbsent(e.getAttribute("classname"), k -> new ArrayList<>()).add(e);
} else if ("executionError".equals(name) || "test exception".equals(name)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had codex take a look, and it pointed out that we have a real test case with the name of "test exception": https://github.com/DataDog/dd-trace-java/blob/master/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy#L1251. The test is marked as Flaky, so the output is skipped anyway IIUC, but worth noting... We could consider changing the test case name to keep this logic simpler?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, will make an update here

Copy link
Copy Markdown
Author

@mhdatie mhdatie May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (tagSkip(doc, e)) modified = true;
}
}
boolean modified = false;
for (var group : byClassname.values()) {
if (group.size() <= 1) continue;
for (int i = 0; i < group.size() - 1; i++) {
var testcase = group.get(i);
var existingProperties = testcase.getElementsByTagName("properties");
if (existingProperties.getLength() > 0) {
var props = (Element) existingProperties.item(0);
var existingProps = props.getElementsByTagName("property");
boolean alreadyTagged = false;
for (int j = 0; j < existingProps.getLength(); j++) {
if ("dd_tags[test.final_status]".equals(((Element) existingProps.item(j)).getAttribute("name"))) {
alreadyTagged = true;
break;
}
}
if (alreadyTagged) continue;
var property = doc.createElement("property");
property.setAttribute("name", "dd_tags[test.final_status]");
property.setAttribute("value", "skip");
props.appendChild(property);
} else {
var properties = doc.createElement("properties");
var property = doc.createElement("property");
property.setAttribute("name", "dd_tags[test.final_status]");
property.setAttribute("value", "skip");
properties.appendChild(property);
testcase.appendChild(properties);
if (tagSkip(doc, group.get(i))) {
modified = true;
}
modified = true;
}
}
if (!modified) return;
if (!modified) {
return;
}
var tmpFile = File.createTempFile("TagInitializationErrors", ".xml", xmlFile.getParentFile());
try {
var transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(new DOMSource(doc), new StreamResult(tmpFile));
Files.move(tmpFile.toPath(), xmlFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
Files.move(
tmpFile.toPath(), xmlFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
tmpFile.delete();
throw e;
}
}

static Element firstChildElement(Element parent, String tagName) {
var children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
var child = children.item(i);
if (child instanceof Element e && tagName.equals(e.getTagName())) {
return e;
}
}
return null;
}

static boolean tagSkip(Document doc, Element testcase) {
var props = firstChildElement(testcase, "properties");
if (props != null) {
var children = props.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
if (children.item(j) instanceof Element e
&& "property".equals(e.getTagName())
&& "dd_tags[test.final_status]".equals(e.getAttribute("name"))) {
return false;
}
}
var property = doc.createElement("property");
property.setAttribute("name", "dd_tags[test.final_status]");
property.setAttribute("value", "skip");
props.appendChild(property);
} else {
var properties = doc.createElement("properties");
var property = doc.createElement("property");
property.setAttribute("name", "dd_tags[test.final_status]");
property.setAttribute("value", "skip");
properties.appendChild(property);
testcase.appendChild(properties);
}
return true;
}
}
2 changes: 1 addition & 1 deletion .gitlab/collect_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ do
echo " (non-stable test names detected)"
fi

echo "Add dd_tags[test.final_status] property on retried synthetics testcase initializationErrors"
echo "Add dd_tags[test.final_status] property on retried synthetics testcase initializationErrors, and all executionError and test exception synthetic testcases"
$JAVA_25_HOME/bin/java "$(dirname "$0")/TagInitializationErrors.java" "$TARGET_DIR/$AGGREGATED_FILE_NAME"

echo "Add dd_tags[test.final_status] property to each testcase on $TARGET_DIR/$AGGREGATED_FILE_NAME"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
}

@Flaky(value = "https://github.com/DataDog/dd-trace-java/issues/9396", suites = ["PekkoHttpServerInstrumentationAsyncHttp2Test"])
def "test exception"() {
def "Instrumentation test exception"() {
setup:
def method = "GET"
def body = null
Expand Down