1- import org .w3c .dom .Element ;
2- import javax .xml .XMLConstants ;
3- import javax .xml .parsers .DocumentBuilderFactory ;
4- import javax .xml .transform .OutputKeys ;
5- import javax .xml .transform .TransformerFactory ;
6- import javax .xml .transform .dom .DOMSource ;
7- import javax .xml .transform .stream .StreamResult ;
81import java .io .File ;
92import java .nio .file .Files ;
103import java .util .ArrayList ;
114import java .util .LinkedHashMap ;
125import java .util .List ;
136import java .util .Map ;
7+ import javax .xml .XMLConstants ;
8+ import javax .xml .parsers .DocumentBuilderFactory ;
9+ import javax .xml .transform .OutputKeys ;
10+ import javax .xml .transform .TransformerFactory ;
11+ import javax .xml .transform .dom .DOMSource ;
12+ import javax .xml .transform .stream .StreamResult ;
13+ import org .w3c .dom .Document ;
14+ import org .w3c .dom .Element ;
1415
15- /// Tags intermediate `initializationError` retries with `dd_tags[test.final_status]=skip`.
16+ /// Tags synthetic testcases (`initializationError`, `executionError`, `test exception`) with
17+ /// `dd_tags[test.final_status]=skip` so Test Optimization does not treat them as real failures.
18+ /// The script is idempotent — testcases that already carry a `dd_tags[test.final_status]` property
19+ /// are left unchanged.
20+ ///
21+ /// **`initializationError`** — Gradle generates these for setup methods. When retried and
22+ // eventually
23+ /// successful, multiple testcases appear; only the last one passes. All intermediate attempts are
24+ /// tagged skip. Groups with only one (or zero) `initializationError` entries per classname are left
25+ // unmodified.
1626///
17- /// Gradle generates synthetic "initializationError" testcases in JUnit reports for setup methods.
18- /// When a setup is retried and eventually succeeds, multiple testcases are created, with only the
19- /// last one passing. All intermediate attempts are marked skip so Test Optimization is not misled .
27+ /// **`executionError`** and **`test exception`** — Framework-level synthetic failures that do not
28+ /// represent real test results. Tagged skip unconditionally so Test Optimization treats them as
29+ // non-failures .
2030///
21- /// For any suite with multiple `initializationError` test cases (when retries occurred), all entries
22- /// but the last one are tagged by this script with `dd_tags[test.final_status]=skip`. The last
23- /// entry is left unmodified, allowing **Test Optimization** to apply its default status inference based
24- /// on the actual outcome. Files with only one (or zero) `initializationError` test cases are left unmodified.
31+ /// Before (two retries of the same class — first is intermediate, second is the final outcome):
2532///
26- /// Before:
27- ///
2833/// ```
29- /// <testcase name="initializationError" />
34+ /// <testcase name="initializationError" classname="com.example.MyTest" />
35+ /// <testcase name="initializationError" classname="com.example.MyTest" />
3036/// ```
31- ///
32- /// After:
33- ///
37+ ///
38+ /// After (only the intermediate attempt is tagged; the last entry is left untouched) :
39+ ///
3440/// ```
35- /// <testcase name="initializationError">
41+ /// <testcase name="initializationError" classname="com.example.MyTest" >
3642/// <properties>
37- /// <property name="dd_tags[test.final_status]" value="skip" />
43+ /// <property name="dd_tags[test.final_status]" value="skip" />
3844/// </properties>
3945/// </testcase>
46+ /// <testcase name="initializationError" classname="com.example.MyTest" />
4047/// ```
41- ///
48+ ///
4249/// Usage (Java 25): `java TagInitializationErrors.java junit-report.xml`
4350
4451class TagInitializationErrors {
@@ -61,54 +68,73 @@ public static void main(String[] args) throws Exception {
6168 var doc = dbf .newDocumentBuilder ().parse (xmlFile );
6269 var testcases = doc .getElementsByTagName ("testcase" );
6370 Map <String , List <Element >> byClassname = new LinkedHashMap <>();
71+ boolean modified = false ;
6472 for (int i = 0 ; i < testcases .getLength (); i ++) {
6573 var e = (Element ) testcases .item (i );
66- if ("initializationError" .equals (e .getAttribute ("name" ))) {
74+ var name = e .getAttribute ("name" );
75+ if ("initializationError" .equals (name )) {
6776 byClassname .computeIfAbsent (e .getAttribute ("classname" ), k -> new ArrayList <>()).add (e );
77+ } else if ("executionError" .equals (name ) || "test exception" .equals (name )) {
78+ if (tagSkip (doc , e )) modified = true ;
6879 }
6980 }
70- boolean modified = false ;
7181 for (var group : byClassname .values ()) {
72- if (group .size () <= 1 ) continue ;
7382 for (int i = 0 ; i < group .size () - 1 ; i ++) {
74- var testcase = group .get (i );
75- var existingProperties = testcase .getElementsByTagName ("properties" );
76- if (existingProperties .getLength () > 0 ) {
77- var props = (Element ) existingProperties .item (0 );
78- var existingProps = props .getElementsByTagName ("property" );
79- boolean alreadyTagged = false ;
80- for (int j = 0 ; j < existingProps .getLength (); j ++) {
81- if ("dd_tags[test.final_status]" .equals (((Element ) existingProps .item (j )).getAttribute ("name" ))) {
82- alreadyTagged = true ;
83- break ;
84- }
85- }
86- if (alreadyTagged ) continue ;
87- var property = doc .createElement ("property" );
88- property .setAttribute ("name" , "dd_tags[test.final_status]" );
89- property .setAttribute ("value" , "skip" );
90- props .appendChild (property );
91- } else {
92- var properties = doc .createElement ("properties" );
93- var property = doc .createElement ("property" );
94- property .setAttribute ("name" , "dd_tags[test.final_status]" );
95- property .setAttribute ("value" , "skip" );
96- properties .appendChild (property );
97- testcase .appendChild (properties );
83+ if (tagSkip (doc , group .get (i ))) {
84+ modified = true ;
9885 }
99- modified = true ;
10086 }
10187 }
102- if (!modified ) return ;
88+ if (!modified ) {
89+ return ;
90+ }
10391 var tmpFile = File .createTempFile ("TagInitializationErrors" , ".xml" , xmlFile .getParentFile ());
10492 try {
10593 var transformer = TransformerFactory .newInstance ().newTransformer ();
10694 transformer .setOutputProperty (OutputKeys .ENCODING , "UTF-8" );
10795 transformer .transform (new DOMSource (doc ), new StreamResult (tmpFile ));
108- Files .move (tmpFile .toPath (), xmlFile .toPath (), java .nio .file .StandardCopyOption .REPLACE_EXISTING );
96+ Files .move (
97+ tmpFile .toPath (), xmlFile .toPath (), java .nio .file .StandardCopyOption .REPLACE_EXISTING );
10998 } catch (Exception e ) {
11099 tmpFile .delete ();
111100 throw e ;
112101 }
113102 }
103+
104+ static Element firstChildElement (Element parent , String tagName ) {
105+ var children = parent .getChildNodes ();
106+ for (int i = 0 ; i < children .getLength (); i ++) {
107+ var child = children .item (i );
108+ if (child instanceof Element e && tagName .equals (e .getTagName ())) {
109+ return e ;
110+ }
111+ }
112+ return null ;
113+ }
114+
115+ static boolean tagSkip (Document doc , Element testcase ) {
116+ var props = firstChildElement (testcase , "properties" );
117+ if (props != null ) {
118+ var children = props .getChildNodes ();
119+ for (int j = 0 ; j < children .getLength (); j ++) {
120+ if (children .item (j ) instanceof Element e
121+ && "property" .equals (e .getTagName ())
122+ && "dd_tags[test.final_status]" .equals (e .getAttribute ("name" ))) {
123+ return false ;
124+ }
125+ }
126+ var property = doc .createElement ("property" );
127+ property .setAttribute ("name" , "dd_tags[test.final_status]" );
128+ property .setAttribute ("value" , "skip" );
129+ props .appendChild (property );
130+ } else {
131+ var properties = doc .createElement ("properties" );
132+ var property = doc .createElement ("property" );
133+ property .setAttribute ("name" , "dd_tags[test.final_status]" );
134+ property .setAttribute ("value" , "skip" );
135+ properties .appendChild (property );
136+ testcase .appendChild (properties );
137+ }
138+ return true ;
139+ }
114140}
0 commit comments