Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 64 additions & 36 deletions .gitlab/TagSyntheticFailures.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
Expand All @@ -27,6 +28,7 @@
/// **`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.

///
/// Before (two retries of the same class — first is intermediate, second is the final outcome):
///
Expand All @@ -46,59 +48,70 @@
/// <testcase name="initializationError" classname="com.example.MyTest" />
/// ```
///
/// Usage (Java 25): `java TagSyntheticFailures.java junit-report.xml`
/// Usage (Java 25): `java TagSyntheticFailures.java batch.list`
///
/// The argument is a list file with one XML path per line.
/// Each referenced XML is processed in turn.

class TagSyntheticFailures {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.err.println("Usage: java TagSyntheticFailures.java <xml-file>");
System.exit(1);
}
var xmlFile = new File(args[0]);
if (!xmlFile.exists()) {
System.err.println("File not found: " + xmlFile);
System.exit(1);
}
var dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
var doc = dbf.newDocumentBuilder().parse(xmlFile);
var testcases = doc.getElementsByTagName("testcase");
var docBuilder = dbf.newDocumentBuilder();
var transformerFactory = TransformerFactory.newInstance();

var listFile = new File(args[0]);
for (String line : Files.readAllLines(listFile.toPath())) {
processFile(new File(line), docBuilder, transformerFactory);
}
}

static void processFile(File xmlFile, DocumentBuilder docBuilder, TransformerFactory transformerFactory) throws Exception {
if (!xmlFile.exists()) {
System.err.println("xml file not found, skipping: " + xmlFile);
return;
}

var doc = docBuilder.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);
for (int i = 0; i < testCases.getLength(); i++) {
var e = (Element) testCases.item(i);
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)) {
if (tagSkip(doc, e)) modified = true;
if (tagFinalStatus(doc, e, "skip")) modified = true;
}
}

for (var group : byClassname.values()) {
for (int i = 0; i < group.size() - 1; i++) {
if (tagSkip(doc, group.get(i))) {
if (tagFinalStatus(doc, group.get(i), "skip")) {
modified = true;
}
}
}

// Tag remaining testcases with their pass/skip/fail status.
// tagFinalStatus is idempotent — already-tagged synthetics from the passes above are skipped.
for (int i = 0; i < testCases.getLength(); i++) {
var e = (Element) testCases.item(i);
if (tagFinalStatus(doc, e, computeStatus(e))) modified = true;
}

if (!modified) {
return;
}
var tmpFile = File.createTempFile("TagSyntheticFailures", ".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);
} catch (Exception e) {
tmpFile.delete();
throw e;
}

var transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(new DOMSource(doc), new StreamResult(xmlFile));
}

static Element firstChildElement(Element parent, String tagName) {
Expand All @@ -112,7 +125,7 @@ static Element firstChildElement(Element parent, String tagName) {
return null;
}

static boolean tagSkip(Document doc, Element testcase) {
static boolean tagFinalStatus(Document doc, Element testcase, String status) {
var props = firstChildElement(testcase, "properties");
if (props != null) {
var children = props.getChildNodes();
Expand All @@ -123,18 +136,33 @@ static boolean tagSkip(Document doc, Element testcase) {
return false;
}
}
var property = doc.createElement("property");
property.setAttribute("name", "dd_tags[test.final_status]");
property.setAttribute("value", "skip");
props.appendChild(property);
props.appendChild(newStatusProperty(doc, status));
} 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);
properties.appendChild(newStatusProperty(doc, status));
testcase.appendChild(properties);
}
return true;
}

static Element newStatusProperty(Document doc, String status) {
var p = doc.createElement("property");
p.setAttribute("name", "dd_tags[test.final_status]");
p.setAttribute("value", status);
return p;
}

/// Derives a testcase status: failure/error -> fail, skipped -> skip, else pass.
static String computeStatus(Element testcase) {
var children = testcase.getChildNodes();
boolean hasSkipped = false;
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element e) {
var tag = e.getTagName();
if ("failure".equals(tag) || "error".equals(tag)) return "fail";
if ("skipped".equals(tag)) hasSkipped = true;
}
}
return hasSkipped ? "skip" : "pass";
}
}
58 changes: 0 additions & 58 deletions .gitlab/add_final_status.xsl

This file was deleted.

25 changes: 16 additions & 9 deletions .gitlab/collect_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ function get_source_file () {
}

echo "Saving test results:"

# Collect normalized XML paths so the Java tagger can run once for the whole batch
# instead of paying JVM startup per file.
BATCH_FILE="./synthetic-tag-batch.list"
: > "$BATCH_FILE"

while IFS= read -r -d '' RESULT_XML_FILE
do
echo -n "- $RESULT_XML_FILE"
Expand Down Expand Up @@ -90,13 +96,14 @@ do
echo " (non-stable test names detected)"
fi

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")/TagSyntheticFailures.java" "$TARGET_DIR/$AGGREGATED_FILE_NAME"

echo "Add dd_tags[test.final_status] property to each testcase on $TARGET_DIR/$AGGREGATED_FILE_NAME"
xsl_file="$(dirname "$0")/add_final_status.xsl"
tmp_file="$(mktemp)"
xsltproc --huge --output "$tmp_file" "$xsl_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"
mv "$tmp_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"

echo "$TARGET_DIR/$AGGREGATED_FILE_NAME" >> "$BATCH_FILE"
done < <(find "${TEST_RESULT_DIRS[@]}" -name \*.xml -print0)

# Tag every testcase with dd_tags[test.final_status]:
# - synthetic testcases (intermediate initializationError, executionError, test exception) -> skip
# - everything else -> pass/skip/fail derived from <failure>/<error>/<skipped> children
if [ -s "$BATCH_FILE" ]; then
echo "Add dd_tags[test.final_status] property to every testcase (batched, $(wc -l < "$BATCH_FILE") files)"
$JAVA_25_HOME/bin/java "$(dirname "$0")/TagSyntheticFailures.java" "$BATCH_FILE"
fi
rm -f "$BATCH_FILE"
Loading