github.com/tiagovtristao/plz@v13.4.0+incompatible/src/test/xml_results.go (about) 1 // Parser for JUnit XML output. 2 3 package test 4 5 import ( 6 "bytes" 7 "encoding/xml" 8 "io/ioutil" 9 "os" 10 "path" 11 "time" 12 13 "github.com/thought-machine/please/src/core" 14 "io" 15 ) 16 17 func looksLikeJUnitXMLTestResults(b []byte) bool { 18 return bytes.HasPrefix(b, []byte{'<', '?', 'x', 'm', 'l'}) || bytes.HasPrefix(b, []byte{'<', 't', 'e', 's', 't'}) 19 } 20 21 func parseJUnitXMLTestResults(data []byte) (core.TestSuites, error) { 22 results := core.TestSuites{} 23 decoder := xml.NewDecoder(bytes.NewReader(data)) 24 for { 25 token, err := decoder.Token() 26 switch err { 27 case nil: 28 case io.EOF: 29 return results, nil 30 default: 31 return results, err 32 } 33 34 switch tok := token.(type) { 35 case xml.StartElement: 36 switch tok.Name.Local { 37 case "test": 38 // UnitTest.cpp Test 39 uxmlTest := unitTestXMLTest{} 40 decoder.DecodeElement(&uxmlTest, &tok) 41 xmlTest := uxmlTest.toJUnitXMLTest() 42 testSuite := core.TestSuite{ 43 Name: uxmlTest.Suite, 44 } 45 testCase := core.TestCase{ 46 Name: uxmlTest.Name, 47 } 48 appendResult(xmlTest, &testCase) 49 testSuite.TestCases = append(testSuite.TestCases, testCase) 50 testSuite.Duration += xmlTest.Duration() 51 results.TestSuites = append(results.TestSuites, testSuite) 52 case "testcase": 53 // One or more bare tests, put each one in a synthetic test suite 54 testSuite := core.TestSuite{} 55 xmlTest := jUnitXMLTest{} 56 testCase := core.TestCase{} 57 decoder.DecodeElement(&xmlTest, &tok) 58 appendResult(xmlTest, &testCase) 59 testSuite.TestCases = append(testSuite.TestCases, testCase) 60 testSuite.Duration += xmlTest.Duration() 61 results.TestSuites = append(results.TestSuites, testSuite) 62 case "testsuite": // Just a single test suite (this is the usual output from junit, for example) 63 xmlTestSuite := jUnitXMLTestSuite{} 64 decoder.DecodeElement(&xmlTestSuite, &tok) 65 results.TestSuites = append(results.TestSuites, toCoreTestSuite(xmlTestSuite)) 66 case "testsuites": // We might have a collection of existing test suites, if we're parsing our own output. 67 xmlTestSuites := jUnitXMLTestSuites{} 68 decoder.DecodeElement(&xmlTestSuites, &tok) 69 70 var duration time.Duration 71 for _, xmlTestSuite := range xmlTestSuites.TestSuites { 72 results.TestSuites = append(results.TestSuites, toCoreTestSuite(xmlTestSuite)) 73 duration += xmlTestSuite.Duration() 74 } 75 } 76 } 77 } 78 } 79 80 func toCoreTestSuite(xmlTestSuite jUnitXMLTestSuite) core.TestSuite { 81 testSuite := core.TestSuite{ 82 Package: xmlTestSuite.Package, 83 Name: xmlTestSuite.Name, 84 Timestamp: xmlTestSuite.Timestamp, 85 Duration: xmlTestSuite.Duration(), 86 Properties: toCoreProperties(xmlTestSuite.Properties), 87 } 88 for _, test := range xmlTestSuite.TestCases { 89 result := core.TestCase{ 90 ClassName: test.ClassName, 91 Name: test.Name, 92 } 93 appendResult(test, &result) 94 testSuite.TestCases = append(testSuite.TestCases, result) 95 } 96 return testSuite 97 } 98 99 func toCoreProperties(properties jUnitXMLProperties) map[string]string { 100 props := make(map[string]string) 101 for _, prop := range properties.Property { 102 props[prop.Name] = prop.Value 103 } 104 return props 105 } 106 107 func appendResult(test jUnitXMLTest, results *core.TestCase) { 108 // There can be only one of these 109 if test.Failure != nil { 110 appendFailure(test, results, *test.Failure) 111 } else if test.Error != nil { 112 appendError(test, results, *test.Error) 113 } else if test.Skipped != nil { 114 appendSkipped(test, results, *test.Skipped) 115 } else { 116 appendSuccess(test, results) 117 } 118 119 if len(test.FlakyFailure) > 0 { 120 for _, flake := range test.FlakyFailure { 121 appendFlakyFailure(test, results, flake) 122 } 123 } 124 if len(test.FlakyError) > 0 { 125 // The test ultimately succeeded but errored possibly several times. 126 // We have added the success above. 127 for _, flake := range test.FlakyError { 128 appendFlakyError(test, results, flake) 129 } 130 } 131 if len(test.RerunFailure) > 0 { 132 // The test never succeeded and flaked possibly several times. 133 // We have already added the first failure above. 134 for _, flake := range test.RerunFailure { 135 appendRerunFailure(test, results, flake) 136 } 137 } 138 if len(test.RerunError) > 0 { 139 // The test never succeeded and errored possibly several times. 140 // We have already added the first error above. 141 for _, flake := range test.RerunError { 142 appendRerunError(test, results, flake) 143 } 144 } 145 } 146 147 func appendFailure(test jUnitXMLTest, results *core.TestCase, failure jUnitXMLFailure) { 148 d := time.Duration(test.Time) 149 results.Executions = append(results.Executions, core.TestExecution{ 150 Failure: &core.TestResultFailure{ 151 Message: failure.Message, 152 Type: failure.Type, 153 Traceback: failure.Traceback, 154 }, 155 Duration: &d, 156 Stdout: test.Stdout, 157 Stderr: test.Stderr, 158 }) 159 } 160 161 func appendFlakyFailure(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLFlaky) { 162 d := time.Duration(test.Time) 163 results.Executions = append(results.Executions, core.TestExecution{ 164 Failure: &core.TestResultFailure{ 165 Message: flake.Message, 166 Type: flake.Type, 167 Traceback: flake.Traceback, 168 }, 169 Duration: &d, 170 Stdout: test.Stdout, 171 Stderr: test.Stderr, 172 }) 173 } 174 175 func appendFlakyError(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLFlaky) { 176 results.Executions = append(results.Executions, core.TestExecution{ 177 Error: &core.TestResultFailure{ 178 Message: flake.Message, 179 Type: flake.Type, 180 Traceback: flake.Traceback, 181 }, 182 Stdout: test.Stdout, 183 Stderr: test.Stderr, 184 }) 185 } 186 187 func appendRerunFailure(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLRerunFailure) { 188 d := time.Duration(test.Time) 189 results.Executions = append(results.Executions, core.TestExecution{ 190 Failure: &core.TestResultFailure{ 191 Message: flake.Message, 192 Type: flake.Type, 193 Traceback: flake.Traceback, 194 }, 195 Duration: &d, 196 Stdout: test.Stdout, 197 Stderr: test.Stderr, 198 }) 199 } 200 201 func appendRerunError(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLRerunError) { 202 results.Executions = append(results.Executions, core.TestExecution{ 203 Error: &core.TestResultFailure{ 204 Message: flake.Message, 205 Type: flake.Type, 206 Traceback: flake.Traceback, 207 }, 208 Stdout: test.Stdout, 209 Stderr: test.Stderr, 210 }) 211 } 212 213 func appendError(test jUnitXMLTest, results *core.TestCase, error jUnitXMLError) { 214 results.Executions = append(results.Executions, core.TestExecution{ 215 Error: &core.TestResultFailure{ 216 Message: error.Message, 217 Type: error.Type, 218 Traceback: error.Traceback, 219 }, 220 Stdout: test.Stdout, 221 Stderr: test.Stderr, 222 }) 223 } 224 225 func appendSkipped(test jUnitXMLTest, results *core.TestCase, skipped jUnitXMLSkipped) { 226 results.Executions = append(results.Executions, core.TestExecution{ 227 Skip: &core.TestResultSkip{ 228 Message: skipped.Message, 229 }, 230 Stdout: test.Stdout, 231 Stderr: test.Stderr, 232 }) 233 } 234 235 func appendSuccess(test jUnitXMLTest, results *core.TestCase) { 236 duration := test.Duration() 237 results.Executions = append(results.Executions, core.TestExecution{ 238 Duration: &duration, 239 Stdout: test.Stdout, 240 Stderr: test.Stderr, 241 }) 242 } 243 244 type jUnitXMLTestSuites struct { 245 Errors uint `xml:"errors,attr,omitempty"` 246 Failures uint `xml:"failures,attr,omitempty"` 247 Name string `xml:"name,attr,omitempty"` 248 Skipped uint `xml:"skipped,attr,omitempty"` 249 Tests uint `xml:"tests,attr,omitempty"` 250 timed `xml:"time,attr,omitempty"` 251 252 TestSuites []jUnitXMLTestSuite `xml:"testsuite,omitempty"` 253 254 XMLName xml.Name `xml:"testsuites"` 255 } 256 257 type jUnitXMLTestSuite struct { 258 Name string `xml:"name,attr"` 259 Tests int `xml:"tests,attr"` 260 261 Errors int `xml:"errors,attr,omitempty"` 262 Failures int `xml:"failures,attr,omitempty"` 263 HostName string `xml:"hostname,attr,omitempty"` 264 Skipped int `xml:"skipped,attr,omitempty"` 265 Package string `xml:"package,attr,omitempty"` 266 timed `xml:"time,attr,omitempty"` 267 Timestamp string `xml:"timestamp,attr,omitempty"` 268 269 Properties jUnitXMLProperties `xml:"properties,omitempty"` 270 TestCases []jUnitXMLTest `xml:"testcase"` 271 Stdout string `xml:"system-out,omitempty"` 272 Stderr string `xml:"system-err,omitempty"` 273 274 XMLName xml.Name `xml:"testsuite"` 275 } 276 277 type jUnitXMLTest struct { 278 Name string `xml:"name,attr"` 279 280 Assertions uint `xml:"assertions,attr,omitempty"` 281 ClassName string `xml:"classname,attr,omitempty"` 282 Status string `xml:"status,attr,omitempty"` 283 timed `xml:"time,attr,omitempty"` 284 285 Error *jUnitXMLError `xml:"error,omitempty"` 286 FlakyError []jUnitXMLFlaky `xml:"flakyError,omitempty"` 287 RerunError []jUnitXMLRerunError `xml:"rerunError,omitempty"` 288 Failure *jUnitXMLFailure `xml:"failure,omitempty"` 289 FlakyFailure []jUnitXMLFlaky `xml:"flakyFailure,omitempty"` 290 RerunFailure []jUnitXMLRerunFailure `xml:"rerunFailure,omitempty"` 291 Skipped *jUnitXMLSkipped `xml:"skipped,omitempty"` 292 Stdout string `xml:"system-out,omitempty"` 293 Stderr string `xml:"system-err,omitempty"` 294 } 295 296 type jUnitXMLProperties struct { 297 Property []jUnitXMLProperty `xml:"property"` 298 } 299 300 type jUnitXMLProperty struct { 301 Name string `xml:"name,attr"` 302 Value string `xml:"value,attr"` 303 } 304 305 type jUnitXMLError struct { 306 Message string `xml:"message,attr,omitempty"` 307 Type string `xml:"type,attr"` 308 309 Traceback string `xml:",chardata"` 310 } 311 312 type jUnitXMLFailure struct { 313 Message string `xml:"message,attr,omitempty"` 314 Type string `xml:"type,attr"` 315 316 Traceback string `xml:",chardata"` 317 } 318 319 type jUnitXMLFlaky struct { 320 Message string `xml:"message,attr,omitempty"` 321 Type string `xml:"type,attr"` 322 323 Traceback string `xml:",chardata"` 324 Stdout string `xml:"system-out,omitempty"` 325 Stderr string `xml:"system-err,omitempty"` 326 } 327 328 type jUnitXMLRerunError struct { 329 Message string `xml:"message,attr,omitempty"` 330 Type string `xml:"type,attr"` 331 332 Traceback string `xml:",chardata"` 333 Stdout string `xml:"system-out,omitempty"` 334 Stderr string `xml:"system-err,omitempty"` 335 } 336 337 type jUnitXMLRerunFailure struct { 338 Message string `xml:"message,attr,omitempty"` 339 timed `xml:"time,attr"` 340 Type string `xml:"type,attr"` 341 342 Traceback string `xml:",chardata"` 343 Stdout string `xml:"system-out,omitempty"` 344 Stderr string `xml:"system-err,omitempty"` 345 } 346 347 type jUnitXMLSkipped struct { 348 Message string `xml:"message,attr,omitempty"` 349 } 350 351 type timed struct { 352 Time float64 `xml:"time,attr"` 353 } 354 355 func (t timed) Duration() time.Duration { 356 return time.Duration(t.Time * float64(time.Second)) 357 } 358 359 func (j jUnitXMLTest) WasSuccessful() bool { 360 return j.Skipped == nil && 361 j.Error == nil && 362 j.Failure == nil 363 } 364 365 type unitTestXMLTest struct { 366 Suite string `xml:"suite,attr"` 367 Name string `xml:"name,attr"` 368 Elapsed float64 `xml:"elapsed,attr"` 369 370 Failure *unitTestXMLFailure `xml:"failure,omitempty"` 371 } 372 373 func (uxmlTest *unitTestXMLTest) toJUnitXMLTest() jUnitXMLTest { 374 var failure *jUnitXMLFailure 375 if uxmlTest.Failure != nil { 376 failure = &jUnitXMLFailure{ 377 Message: uxmlTest.Failure.Message, 378 } 379 } 380 return jUnitXMLTest{ 381 Name: uxmlTest.Name, 382 ClassName: uxmlTest.Suite, 383 timed: timed{uxmlTest.Elapsed}, 384 Failure: failure, 385 } 386 } 387 388 type unitTestXMLFailure struct { 389 Message string `xml:"message,attr"` 390 } 391 392 // WriteResultsToFileOrDie writes test results out to a file in xUnit format. Dies on any errors. 393 func WriteResultsToFileOrDie(graph *core.BuildGraph, filename string) { 394 if err := os.MkdirAll(path.Dir(filename), core.DirPermissions); err != nil { 395 log.Fatalf("Failed to create directory for test output") 396 } 397 xmlTestResults := jUnitXMLTestSuites{} 398 xmlTestResults.XMLName.Local = "testsuites" 399 400 // Collapse any testsuite with the same name 401 xmlSuites := make(map[string]jUnitXMLTestSuite) 402 for _, target := range graph.AllTargets() { 403 if target.IsTest { 404 testSuite := target.Results 405 if len(testSuite.TestCases) > 0 { 406 var xmlTestSuite jUnitXMLTestSuite 407 if _, ok := xmlSuites[testSuite.JavaStyleName()]; ok { 408 xmlTestSuite = xmlSuites[testSuite.Name] 409 xmlTestSuite.Tests += testSuite.Tests() 410 xmlTestSuite.Errors += testSuite.Errors() 411 xmlTestSuite.Failures += testSuite.Failures() 412 xmlTestSuite.Skipped += testSuite.Skips() 413 xmlTestSuite.timed.Time += testSuite.Duration.Seconds() 414 } else { 415 xmlTestSuite = jUnitXMLTestSuite{ 416 Name: testSuite.Name, 417 Package: testSuite.Package, 418 Timestamp: testSuite.Timestamp, 419 Tests: testSuite.Tests(), 420 Errors: testSuite.Errors(), 421 Failures: testSuite.Failures(), 422 Skipped: testSuite.Skips(), 423 timed: timed{testSuite.Duration.Seconds()}, 424 Properties: toXmlProperties(testSuite.Properties), 425 } 426 } 427 for _, testCase := range testSuite.TestCases { 428 xmlTest := toXmlTestCase(testCase) 429 if xmlTest.ClassName == "" { 430 xmlTest.ClassName = testSuite.JavaStyleName() 431 } 432 xmlTestSuite.TestCases = append(xmlTestSuite.TestCases, xmlTest) 433 } 434 xmlSuites[testSuite.JavaStyleName()] = xmlTestSuite 435 for _, testCase := range testSuite.TestCases { 436 xmlTest := toXmlTestCase(testCase) 437 xmlTestSuite.TestCases = append(xmlTestSuite.TestCases, xmlTest) 438 } 439 } 440 xmlTestResults.Time += testSuite.Duration.Seconds() 441 } 442 } 443 for _, xmlTestSuite := range xmlSuites { 444 xmlTestResults.TestSuites = append(xmlTestResults.TestSuites, xmlTestSuite) 445 } 446 if b, err := xml.MarshalIndent(xmlTestResults, "", " "); err != nil { 447 log.Fatalf("Failed to serialise XML: %s", err) 448 } else if err = ioutil.WriteFile(filename, b, 0644); err != nil { 449 log.Fatalf("Failed to write XML to %s: %s", filename, err) 450 } 451 } 452 453 func toXmlProperties(props map[string]string) jUnitXMLProperties { 454 out := jUnitXMLProperties{} 455 for k, v := range props { 456 out.Property = append(out.Property, jUnitXMLProperty{ 457 Name: k, 458 Value: v, 459 }) 460 } 461 return out 462 } 463 464 func toXmlTestCase(result core.TestCase) jUnitXMLTest { 465 testcase := jUnitXMLTest{ 466 ClassName: result.ClassName, 467 Name: result.Name, 468 } 469 success := result.Success() 470 failures := result.Failures() 471 errors := result.Errors() 472 skip := result.Skip() 473 if success != nil { 474 // We passed but we might have had flakes 475 testcase.Stderr = success.Stderr 476 testcase.Stdout = success.Stdout 477 testcase.Time = success.Duration.Seconds() 478 for _, execution := range failures { 479 testcase.FlakyFailure = append(testcase.FlakyFailure, jUnitXMLFlaky{ 480 Message: execution.Failure.Message, 481 Stderr: execution.Stderr, 482 Stdout: execution.Stdout, 483 Traceback: execution.Failure.Traceback, 484 Type: execution.Failure.Type, 485 }) 486 } 487 for _, execution := range errors { 488 testcase.FlakyError = append(testcase.FlakyError, jUnitXMLFlaky{ 489 Message: execution.Error.Message, 490 Stderr: execution.Stderr, 491 Stdout: execution.Stdout, 492 Traceback: execution.Error.Traceback, 493 Type: execution.Error.Type, 494 }) 495 } 496 } else if skip != nil { 497 testcase.Skipped = &jUnitXMLSkipped{ 498 Message: skip.Skip.Message, 499 } 500 } else { 501 // We didn't have a single pass, everything is darkness 502 // See if we 'failed' or 'errored' first. 503 doneFirst := false 504 setDuration := false 505 for _, execution := range result.Executions { 506 if execution.Error != nil { 507 if !doneFirst { 508 testcase.Error = &jUnitXMLError{ 509 Message: execution.Error.Message, 510 Traceback: execution.Error.Traceback, 511 Type: execution.Error.Type, 512 } 513 testcase.Stderr = execution.Stderr 514 testcase.Stdout = execution.Stdout 515 doneFirst = true 516 } else { 517 testcase.RerunError = append(testcase.RerunError, jUnitXMLRerunError{ 518 Message: execution.Error.Message, 519 Stderr: execution.Stderr, 520 Stdout: execution.Stdout, 521 Traceback: execution.Error.Traceback, 522 Type: execution.Error.Type, 523 }) 524 } 525 } else if execution.Failure != nil { 526 if !doneFirst { 527 testcase.Failure = &jUnitXMLFailure{ 528 Message: execution.Failure.Message, 529 Traceback: execution.Failure.Traceback, 530 Type: execution.Failure.Type, 531 } 532 testcase.Stderr = execution.Stderr 533 testcase.Stdout = execution.Stdout 534 doneFirst = true 535 } else { 536 testcase.RerunFailure = append(testcase.RerunFailure, jUnitXMLRerunFailure{ 537 Message: execution.Failure.Message, 538 Stderr: execution.Stderr, 539 Stdout: execution.Stdout, 540 timed: timed{execution.Duration.Seconds()}, 541 Traceback: execution.Failure.Traceback, 542 Type: execution.Failure.Type, 543 }) 544 } 545 if !setDuration && execution.Duration != nil { 546 testcase.Time = execution.Duration.Seconds() 547 setDuration = true 548 } 549 } 550 } 551 } 552 return testcase 553 }