@@ -13,6 +13,18 @@ import (
1313 "github.com/jstemmer/go-junit-report/v2/junit"
1414)
1515
16+ // alertsSuiteName is the JUnit suite name used for structural alerts (data
17+ // races, panics, fatal errors).
18+ const alertsSuiteName = "ALERTS"
19+
20+ const junitAlertDetailsMaxBytes = 64 * 1024
21+
22+ const (
23+ failureKindFailed = "Failed"
24+ failureKindTimeout = "TIMEOUT"
25+ failureKindCrash = "CRASH"
26+ )
27+
1628type junitReport struct {
1729 junit.Testsuites
1830 path string
@@ -33,6 +45,9 @@ func (j *junitReport) read() error {
3345 return nil
3446}
3547
48+ // generateStatic creates synthetic failures for non-JUnit failure modes such as
49+ // timeouts and crashes. Failure.Message is the canonical failure kind for these
50+ // synthetic rows (for example TIMEOUT or CRASH), and Failure.Data is left empty.
3651func generateStatic (names []string , suffix string , message string ) * junitReport {
3752 var testcases []junit.Testcase
3853 for _ , name := range names {
@@ -77,25 +92,32 @@ func (j *junitReport) appendAlertsSuite(alerts []alert) {
7792 if len (alerts ) == 0 {
7893 return
7994 }
95+
96+ // Convert alerts to JUnit test cases.
8097 var cases []junit.Testcase
8198 for _ , a := range alerts {
8299 name := fmt .Sprintf ("%s: %s" , a .Kind , a .Summary )
83100 if p := primaryTestName (a .Tests ); p != "" {
84101 name = fmt .Sprintf ("%s — in %s" , name , p )
85102 }
86- // Include only test names for context, not the full log details to avoid XML malformation
87- var details string
103+ var sb strings.Builder
104+ if a .Details != "" {
105+ sb .WriteString (truncateAlertDetails (sanitizeXML (a .Details )))
106+ sb .WriteByte ('\n' )
107+ }
88108 if len (a .Tests ) > 0 {
89- details = fmt .Sprintf ( "Detected in tests:\n \t %s" , strings .Join (a .Tests , "\n \t " ))
109+ fmt .Fprintf ( & sb , "Detected in tests:\n \t %s" , strings .Join (a .Tests , "\n \t " ))
90110 }
91- r := & junit.Result {Message : string (a .Kind ), Data : details }
111+ r := & junit.Result {Message : string (a .Kind ), Data : strings . TrimRight ( sb . String (), " \n " ) }
92112 cases = append (cases , junit.Testcase {
93113 Name : name ,
94114 Failure : r ,
95115 })
96116 }
117+
118+ // Append the alerts suite to the report.
97119 suite := junit.Testsuite {
98- Name : "ALERTS" ,
120+ Name : alertsSuiteName ,
99121 Failures : len (cases ),
100122 Tests : len (cases ),
101123 Testcases : cases ,
@@ -105,6 +127,30 @@ func (j *junitReport) appendAlertsSuite(alerts []alert) {
105127 j .Tests += suite .Tests
106128}
107129
130+ // sanitizeXML removes characters that are invalid in XML 1.0. Go's xml.Encoder
131+ // escapes <, >, & etc., but control characters other than \t, \n, \r are not
132+ // legal XML and cause parsers to reject the document.
133+ func sanitizeXML (s string ) string {
134+ return strings .Map (func (r rune ) rune {
135+ if r == '\t' || r == '\n' || r == '\r' {
136+ return r
137+ }
138+ if r < 0x20 || r == 0xFFFE || r == 0xFFFF {
139+ return - 1
140+ }
141+ return r
142+ }, s )
143+ }
144+
145+ // truncateAlertDetails keeps alert payloads from bloating the JUnit artifact.
146+ func truncateAlertDetails (s string ) string {
147+ if len (s ) <= junitAlertDetailsMaxBytes {
148+ return s
149+ }
150+ const marker = "\n ... (truncated) ...\n "
151+ return s [:junitAlertDetailsMaxBytes - len (marker )] + marker
152+ }
153+
108154// dedupeAlerts removes duplicate alerts (e.g., repeated across retries) based
109155// on kind and details while preserving the first-seen order.
110156func dedupeAlerts (alerts []alert ) []alert {
@@ -224,6 +270,19 @@ func mergeReports(reports []*junitReport) (*junitReport, error) {
224270 // Discard test case parents since they provide no value.
225271 continue
226272 }
273+
274+ // Parse failure details from Failure.Data, if present.
275+ if testCase .Failure != nil && testCase .Failure .Data != "" {
276+ if details := parseFailureDetails (testCase .Failure .Data ); details != noFailureDetails {
277+ testCase .Failure .Data = details
278+ }
279+ }
280+
281+ // Normalize ordinary failures to failureKindFailed;
282+ // alert messages keep their specific kind.
283+ if testCase .Failure != nil && suite .Name != alertsSuiteName {
284+ testCase .Failure .Message = failureKindFailed
285+ }
227286 testCase .Name += suffix
228287 newSuite .Testcases = append (newSuite .Testcases , testCase )
229288 }
0 commit comments