From d4716b733afe4fe85a37fcac74289319d1771e6c Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 19 May 2026 19:40:56 +0200 Subject: [PATCH 1/2] Rewrite collect results script in Java --- .gitlab/TagSyntheticFailures.java | 140 ------------- .gitlab/add_final_status.xsl | 58 ------ .gitlab/collect-result/CollectResults.java | 14 ++ .gitlab/collect-result/JUnitReport.java | 197 ++++++++++++++++++ .gitlab/collect-result/ResultCollector.java | 114 ++++++++++ .../collect-result/SourceFileResolver.java | 177 ++++++++++++++++ .gitlab/collect_results.sh | 101 +-------- 7 files changed, 509 insertions(+), 292 deletions(-) delete mode 100644 .gitlab/TagSyntheticFailures.java delete mode 100644 .gitlab/add_final_status.xsl create mode 100644 .gitlab/collect-result/CollectResults.java create mode 100644 .gitlab/collect-result/JUnitReport.java create mode 100644 .gitlab/collect-result/ResultCollector.java create mode 100644 .gitlab/collect-result/SourceFileResolver.java diff --git a/.gitlab/TagSyntheticFailures.java b/.gitlab/TagSyntheticFailures.java deleted file mode 100644 index a52b8d1e5a9..00000000000 --- a/.gitlab/TagSyntheticFailures.java +++ /dev/null @@ -1,140 +0,0 @@ -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 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. -/// -/// **`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): -/// -/// ``` -/// -/// -/// ``` -/// -/// After (only the intermediate attempt is tagged; the last entry is left untouched): -/// -/// ``` -/// -/// -/// -/// -/// -/// -/// ``` -/// -/// Usage (Java 25): `java TagSyntheticFailures.java junit-report.xml` - -class TagSyntheticFailures { - public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.err.println("Usage: java TagSyntheticFailures.java "); - 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"); - Map> byClassname = new LinkedHashMap<>(); - boolean modified = false; - 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; - } - } - for (var group : byClassname.values()) { - for (int i = 0; i < group.size() - 1; i++) { - if (tagSkip(doc, group.get(i))) { - 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; - } - } - - 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; - } -} diff --git a/.gitlab/add_final_status.xsl b/.gitlab/add_final_status.xsl deleted file mode 100644 index 4b4c0da17fd..00000000000 --- a/.gitlab/add_final_status.xsl +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - fail - skip - pass - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.gitlab/collect-result/CollectResults.java b/.gitlab/collect-result/CollectResults.java new file mode 100644 index 00000000000..ad7c6429cda --- /dev/null +++ b/.gitlab/collect-result/CollectResults.java @@ -0,0 +1,14 @@ +import java.nio.file.Path; +import java.util.List; + +class CollectResults { + public static void main(String[] args) throws Exception { + var collector = + new ResultCollector( + Path.of("results"), + Path.of("workspace"), + List.of(Path.of("workspace"), Path.of("buildSrc"))); + + collector.collect(); + } +} diff --git a/.gitlab/collect-result/JUnitReport.java b/.gitlab/collect-result/JUnitReport.java new file mode 100644 index 00000000000..7b03b64b6d9 --- /dev/null +++ b/.gitlab/collect-result/JUnitReport.java @@ -0,0 +1,197 @@ +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +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; + +final class JUnitReport { + private static final String FINAL_STATUS_PROPERTY = "dd_tags[test.final_status]"; + private static final Pattern HASH_CODE = Pattern.compile("@[0-9a-f]{5,}"); + private static final Pattern LOCALHOST_PORT = Pattern.compile("localhost:[0-9]{2,5}"); + + private final Document document; + + private JUnitReport(Document document) { + this.document = document; + } + + static JUnitReport parse(Path xmlFile) throws Exception { + 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.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + return new JUnitReport(dbf.newDocumentBuilder().parse(xmlFile.toFile())); + } + + boolean addFileAttribute(String sourceFile) { + var changed = false; + for (var testcase : testcases()) { + if (testcase.hasAttribute("time")) { + changed |= !sourceFile.equals(testcase.getAttribute("file")); + testcase.setAttribute("file", sourceFile); + } + } + return changed; + } + + boolean normalizeStableTestNames() { + var changed = false; + for (var testcase : testcases()) { + var attributes = testcase.getAttributes(); + for (var i = 0; i < attributes.getLength(); i++) { + var attribute = attributes.item(i); + var value = attribute.getNodeValue(); + var normalized = + LOCALHOST_PORT + .matcher(HASH_CODE.matcher(value).replaceAll("@HASHCODE")) + .replaceAll("localhost:PORT"); + if (!value.equals(normalized)) { + attribute.setNodeValue(normalized); + changed = true; + } + } + } + return changed; + } + + void tagSyntheticFailures() { + Map> initializationErrorsByClassname = new LinkedHashMap<>(); + for (var testcase : testcases()) { + var name = testcase.getAttribute("name"); + if ("initializationError".equals(name)) { + initializationErrorsByClassname + .computeIfAbsent(testcase.getAttribute("classname"), ignored -> new ArrayList<>()) + .add(testcase); + } else if ("executionError".equals(name) || "test exception".equals(name)) { + addFinalStatusProperty(testcase, "skip", MissingPropertiesPlacement.APPEND_TO_TESTCASE); + } + } + + for (var group : initializationErrorsByClassname.values()) { + for (var i = 0; i < group.size() - 1; i++) { + addFinalStatusProperty( + group.get(i), "skip", MissingPropertiesPlacement.APPEND_TO_TESTCASE); + } + } + } + + void tagFinalStatuses() { + for (var testcase : testcases()) { + if (hasFinalStatusProperty(testcase)) { + continue; + } + addFinalStatusProperty( + testcase, finalStatus(testcase), MissingPropertiesPlacement.FIRST_CHILD); + } + } + + void write(Path xmlFile) throws Exception { + Files.createDirectories(xmlFile.getParent()); + var tmpFile = Files.createTempFile(xmlFile.getParent(), "collect-results-", ".xml"); + try (OutputStream output = Files.newOutputStream(tmpFile)) { + var transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + var transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.transform(new DOMSource(document), new StreamResult(output)); + } catch (Exception e) { + Files.deleteIfExists(tmpFile); + throw e; + } + Files.move(tmpFile, xmlFile, StandardCopyOption.REPLACE_EXISTING); + } + + private List testcases() { + var testcases = document.getElementsByTagName("testcase"); + var elements = new ArrayList(testcases.getLength()); + for (var i = 0; i < testcases.getLength(); i++) { + elements.add((Element) testcases.item(i)); + } + return elements; + } + + private boolean addFinalStatusProperty( + Element testcase, String status, MissingPropertiesPlacement missingPropertiesPlacement) { + var properties = firstChildElement(testcase, "properties"); + if (properties != null) { + if (propertiesHasFinalStatusProperty(properties)) { + return false; + } + } else { + properties = document.createElement("properties"); + if (missingPropertiesPlacement == MissingPropertiesPlacement.FIRST_CHILD) { + testcase.insertBefore(properties, testcase.getFirstChild()); + } else { + testcase.appendChild(properties); + } + } + + var property = document.createElement("property"); + property.setAttribute("name", FINAL_STATUS_PROPERTY); + property.setAttribute("value", status); + properties.appendChild(property); + return true; + } + + private static boolean hasFinalStatusProperty(Element testcase) { + var properties = firstChildElement(testcase, "properties"); + return properties != null && propertiesHasFinalStatusProperty(properties); + } + + private static boolean propertiesHasFinalStatusProperty(Element properties) { + var children = properties.getChildNodes(); + for (var i = 0; i < children.getLength(); i++) { + if (children.item(i) instanceof Element element + && "property".equals(element.getTagName()) + && FINAL_STATUS_PROPERTY.equals(element.getAttribute("name"))) { + return true; + } + } + return false; + } + + private static String finalStatus(Element testcase) { + if (hasChildElement(testcase, "failure") || hasChildElement(testcase, "error")) { + return "fail"; + } + if (hasChildElement(testcase, "skipped")) { + return "skip"; + } + return "pass"; + } + + private static Element firstChildElement(Element parent, String tagName) { + var children = parent.getChildNodes(); + for (var i = 0; i < children.getLength(); i++) { + if (children.item(i) instanceof Element element && tagName.equals(element.getTagName())) { + return element; + } + } + return null; + } + + private static boolean hasChildElement(Element parent, String tagName) { + return firstChildElement(parent, tagName) != null; + } + + private enum MissingPropertiesPlacement { + APPEND_TO_TESTCASE, + FIRST_CHILD + } +} diff --git a/.gitlab/collect-result/ResultCollector.java b/.gitlab/collect-result/ResultCollector.java new file mode 100644 index 00000000000..d993cd41d32 --- /dev/null +++ b/.gitlab/collect-result/ResultCollector.java @@ -0,0 +1,114 @@ +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +final class ResultCollector { + private static final int[] AGGREGATED_NAME_FIELDS_FROM_END = {5, 2, 1}; + + private final Path resultsDir; + private final Path workspaceDir; + private final List searchDirs; + private final SourceFileResolver sourceFileResolver; + + ResultCollector(Path resultsDir, Path workspaceDir, List searchDirs) { + this.resultsDir = resultsDir; + this.workspaceDir = workspaceDir; + this.searchDirs = searchDirs; + this.sourceFileResolver = new SourceFileResolver(workspaceDir); + } + + void collect() throws Exception { + Files.createDirectories(resultsDir); + Files.createDirectories(workspaceDir); + + var testResultDirs = findTestResultDirs(); + if (testResultDirs.isEmpty()) { + System.out.println("No test results found"); + return; + } + + System.out.println("Saving test results:"); + for (var sourceXml : findXmlFiles(testResultDirs)) { + collect(sourceXml); + } + } + + private void collect(Path sourceXml) throws Exception { + var aggregatedName = aggregatedFileName(sourceXml); + var targetXml = resultsDir.resolve(aggregatedName); + System.out.print("- " + toUnixString(sourceXml) + " as " + aggregatedName); + + var sourceFile = sourceFileResolver.resolve(sourceXml); + var report = JUnitReport.parse(sourceXml); + var reportChangedBeforeFinalStatus = report.addFileAttribute(sourceFile); + reportChangedBeforeFinalStatus |= report.normalizeStableTestNames(); + report.tagSyntheticFailures(); + report.tagFinalStatuses(); + report.write(targetXml); + + if (reportChangedBeforeFinalStatus) { + System.out.print(" (non-stable test names detected)"); + } + System.out.println(); + } + + private List findTestResultDirs() throws IOException { + var found = new ArrayList(); + for (var searchDir : searchDirs) { + if (!Files.isDirectory(searchDir)) { + continue; + } + try (var paths = Files.walk(searchDir)) { + paths + .filter(Files::isDirectory) + .filter(path -> "test-results".equals(fileName(path))) + .forEach(found::add); + } + } + found.sort(Comparator.comparing(ResultCollector::toUnixString)); + return found; + } + + private static List findXmlFiles(List testResultDirs) throws IOException { + var found = new ArrayList(); + for (var testResultDir : testResultDirs) { + try (Stream paths = Files.walk(testResultDir)) { + paths + .filter(Files::isRegularFile) + .filter(path -> fileName(path).endsWith(".xml")) + .forEach(found::add); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + found.sort(Comparator.comparing(ResultCollector::toUnixString)); + return found; + } + + private static String aggregatedFileName(Path sourceXml) { + var normalized = sourceXml.normalize(); + var parts = new ArrayList(AGGREGATED_NAME_FIELDS_FROM_END.length); + var nameCount = normalized.getNameCount(); + for (var fieldFromEnd : AGGREGATED_NAME_FIELDS_FROM_END) { + var index = nameCount - fieldFromEnd; + if (index >= 0) { + parts.add(normalized.getName(index).toString()); + } + } + return String.join("_", parts); + } + + private static String fileName(Path path) { + var fileName = path.getFileName(); + return fileName == null ? "" : fileName.toString(); + } + + static String toUnixString(Path path) { + return path.toString().replace(path.getFileSystem().getSeparator(), "/"); + } +} diff --git a/.gitlab/collect-result/SourceFileResolver.java b/.gitlab/collect-result/SourceFileResolver.java new file mode 100644 index 00000000000..bf35fc842be --- /dev/null +++ b/.gitlab/collect-result/SourceFileResolver.java @@ -0,0 +1,177 @@ +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +final class SourceFileResolver { + private static final String UNKNOWN = "UNKNOWN"; + private static final Pattern CLASS_DECLARATION = + Pattern.compile("\\b(?:static\\s+)?class\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\b"); + + private final String workspacePrefix; + private final Map indexes = new HashMap<>(); + + SourceFileResolver(Path workspaceDir) { + this.workspacePrefix = ResultCollector.toUnixString(workspaceDir) + "/"; + } + + String resolve(Path resultXml) throws IOException { + var resultXmlPath = ResultCollector.toUnixString(resultXml); + var sourceRoot = sourceRoot(resultXmlPath); + if (resultXmlPath.contains("#")) { + return sourceRoot; + } + + var className = className(resultXml); + if (className.isEmpty()) { + return UNKNOWN; + } + + return indexes.computeIfAbsent(sourceRoot, SourceIndex::new).resolve(className); + } + + private String sourceRoot(String resultXmlPath) { + var buildIndex = resultXmlPath.indexOf("/build"); + var projectPath = buildIndex >= 0 ? resultXmlPath.substring(0, buildIndex) : resultXmlPath; + if (projectPath.startsWith(workspacePrefix)) { + projectPath = projectPath.substring(workspacePrefix.length()); + } + return projectPath + "/src"; + } + + private static String className(Path resultXml) { + var fileName = resultXml.getFileName(); + if (fileName == null) { + return ""; + } + + var name = fileName.toString(); + if (name.endsWith(".xml")) { + name = name.substring(0, name.length() - ".xml".length()); + } + + var testPrefix = name.lastIndexOf("TEST-"); + if (testPrefix >= 0) { + name = name.substring(testPrefix + "TEST-".length()); + } + + var packageSeparator = name.lastIndexOf('.'); + if (packageSeparator >= 0) { + name = name.substring(packageSeparator + 1); + } + + var innerClassSeparator = name.lastIndexOf('$'); + if (innerClassSeparator >= 0) { + name = name.substring(innerClassSeparator + 1); + } + return name; + } + + private static final class SourceIndex { + private final String sourceRoot; + private final Map> classLocations = new HashMap<>(); + private boolean indexed; + + private SourceIndex(String sourceRoot) { + this.sourceRoot = sourceRoot; + } + + private String resolve(String className) { + try { + indexIfNecessary(); + } catch (IOException e) { + return UNKNOWN; + } + + var locations = locations(className); + if (locations.isEmpty()) { + return UNKNOWN; + } + + var commonRoot = commonRoot(locations); + if (commonRoot == null) { + return UNKNOWN; + } + return "/" + ResultCollector.toUnixString(commonRoot); + } + + private List locations(String className) { + var locations = new ArrayList(); + for (var entry : classLocations.entrySet()) { + if (entry.getKey().startsWith(className)) { + locations.addAll(entry.getValue()); + } + } + return locations; + } + + private void indexIfNecessary() throws IOException { + if (indexed) { + return; + } + indexed = true; + + var root = Path.of(sourceRoot); + if (!Files.isDirectory(root)) { + return; + } + + try (var paths = Files.walk(root)) { + var iterator = + paths.filter(Files::isRegularFile).filter(SourceIndex::isSourceFile).iterator(); + while (iterator.hasNext()) { + try { + index(iterator.next()); + } catch (IOException ignored) { + // Match grep's best-effort behavior from the old shell implementation. + } + } + } + } + + private static boolean isSourceFile(Path path) { + var fileName = path.getFileName(); + if (fileName == null) { + return false; + } + var name = fileName.toString(); + return name.endsWith(".java") + || name.endsWith(".groovy") + || name.endsWith(".kt") + || name.endsWith(".scala"); + } + + private void index(Path sourceFile) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(sourceFile, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + var matcher = CLASS_DECLARATION.matcher(line); + while (matcher.find()) { + classLocations + .computeIfAbsent(matcher.group(1), ignored -> new ArrayList<>()) + .add(sourceFile); + } + } + } + } + + private static Path commonRoot(List locations) { + var commonRoot = locations.get(0); + for (var location : locations) { + while (commonRoot != null && !location.startsWith(commonRoot)) { + commonRoot = commonRoot.getParent(); + } + if (commonRoot == null) { + return null; + } + } + return commonRoot.getNameCount() == 0 ? null : commonRoot; + } + } +} diff --git a/.gitlab/collect_results.sh b/.gitlab/collect_results.sh index 36614632481..de404f48513 100755 --- a/.gitlab/collect_results.sh +++ b/.gitlab/collect_results.sh @@ -3,100 +3,13 @@ # Save all important reports and artifacts into (project-root)/results # This folder will be saved by gitlab and available after test runs. -set -e -# Enable '**' support -shopt -s globstar +set -euo pipefail -TEST_RESULTS_DIR=results -WORKSPACE_DIR=workspace -mkdir -p $TEST_RESULTS_DIR -mkdir -p $WORKSPACE_DIR - -# Main project modules redirect their build directory to workspace//build/ in CI -# (see build.gradle.kts layout.buildDirectory override). buildSrc is a separate Gradle build -# that runs before the main build is configured, so this redirect never applies to it; -# its test results always land in buildSrc/**/build/test-results/, not under workspace/. -SEARCH_DIRS=($WORKSPACE_DIR buildSrc) - -mapfile -t TEST_RESULT_DIRS < <(find "${SEARCH_DIRS[@]}" -name test-results -type d) - -if [[ ${#TEST_RESULT_DIRS[@]} -eq 0 ]]; then - echo "No test results found" - exit 0 +java_bin="${JAVA_25_HOME:-}" +if [[ -n "$java_bin" ]]; then + java_bin="$java_bin/bin/java" +else + java_bin="java" fi -function get_source_file () { - file_path="${RESULT_XML_FILE%%"/build"*}" - file_path="${file_path/#"$WORKSPACE_DIR"\//}/src" - if ! [[ $RESULT_XML_FILE == *"#"* ]]; then - class="${RESULT_XML_FILE%.xml}" - class="${class##*"TEST-"}" - class="${class##*"."}" - class="${class##*"$"}" # remove inner class name if it exists - set +e # allow grep to fail - common_root=$(grep -rl "class $class\|static class $class" "$file_path" 2>/dev/null | head -n 1) - set -e - - if [[ -n "$common_root" ]]; then - while IFS= read -r line; do - while [[ $line != "$common_root"* ]]; do - common_root=$(dirname "$common_root") - if [[ "$common_root" == "$common_root/.." ]] || [[ "$common_root" == "/" ]]; then - common_root="" - break - fi - done - done < <(grep -rl "class $class\|static class $class" "$file_path" 2>/dev/null) - - if [[ -n "$common_root" && "$common_root" != "/" ]]; then - file_path="/$common_root" - else - file_path="UNKNOWN" - fi - else - file_path="UNKNOWN" - fi - fi -} - -echo "Saving test results:" -while IFS= read -r -d '' RESULT_XML_FILE -do - echo -n "- $RESULT_XML_FILE" - # Assuming the path looks like that: dd-java-agent/instrumentation/tomcat/tomcat-5.5/build/test-results/forkedTest/TEST-TomcatServletV1ForkedTest.xml - # it will extracts 3 components from the path (counting from the end), to form the new name AGGREGATED_FILE_NAME: - # - # 1. Field 1 (from end): The XML filename itself - # 2. Field 2 (from end): The test suite type (test, forkedTest, etc.) - # 3. Field 5 (from end): The module/subproject name - # - # E.g. for the example path: tomcat-5.5_forkedTest_TEST-TomcatServletV1ForkedTest.xml - AGGREGATED_FILE_NAME=$(echo "$RESULT_XML_FILE" | rev | cut -d "/" -f 1,2,5 | rev | tr "/" "_") - echo -n " as $AGGREGATED_FILE_NAME" - TARGET_DIR="$TEST_RESULTS_DIR" - mkdir -p "$TARGET_DIR" - cp "$RESULT_XML_FILE" "$TARGET_DIR/$AGGREGATED_FILE_NAME" - # Insert file attribute to testcase XML nodes - get_source_file - sed -i "/ Date: Tue, 19 May 2026 21:40:01 +0200 Subject: [PATCH 2/2] Reuse XML factories in collect results --- .gitlab/collect-result/JUnitReport.java | 40 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/.gitlab/collect-result/JUnitReport.java b/.gitlab/collect-result/JUnitReport.java index 7b03b64b6d9..0c74f4c934b 100644 --- a/.gitlab/collect-result/JUnitReport.java +++ b/.gitlab/collect-result/JUnitReport.java @@ -20,6 +20,9 @@ final class JUnitReport { private static final String FINAL_STATUS_PROPERTY = "dd_tags[test.final_status]"; private static final Pattern HASH_CODE = Pattern.compile("@[0-9a-f]{5,}"); private static final Pattern LOCALHOST_PORT = Pattern.compile("localhost:[0-9]{2,5}"); + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = + newDocumentBuilderFactory(); + private static final TransformerFactory TRANSFORMER_FACTORY = newTransformerFactory(); private final Document document; @@ -28,14 +31,8 @@ private JUnitReport(Document document) { } static JUnitReport parse(Path xmlFile) throws Exception { - 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.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - return new JUnitReport(dbf.newDocumentBuilder().parse(xmlFile.toFile())); + var document = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(xmlFile.toFile()); + return new JUnitReport(document); } boolean addFileAttribute(String sourceFile) { @@ -104,10 +101,7 @@ void write(Path xmlFile) throws Exception { Files.createDirectories(xmlFile.getParent()); var tmpFile = Files.createTempFile(xmlFile.getParent(), "collect-results-", ".xml"); try (OutputStream output = Files.newOutputStream(tmpFile)) { - var transformerFactory = TransformerFactory.newInstance(); - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - var transformer = transformerFactory.newTransformer(); + var transformer = TRANSFORMER_FACTORY.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.transform(new DOMSource(document), new StreamResult(output)); } catch (Exception e) { @@ -117,6 +111,28 @@ void write(Path xmlFile) throws Exception { Files.move(tmpFile, xmlFile, StandardCopyOption.REPLACE_EXISTING); } + private static DocumentBuilderFactory newDocumentBuilderFactory() { + try { + var factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory; + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private static TransformerFactory newTransformerFactory() { + var factory = TransformerFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + return factory; + } + private List testcases() { var testcases = document.getElementsByTagName("testcase"); var elements = new ArrayList(testcases.getLength());