gotest.tools/gotestsum@v1.11.0/testjson/execution.go (about) 1 package testjson 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "sort" 11 "strings" 12 "sync" 13 "time" 14 15 "golang.org/x/sync/errgroup" 16 "gotest.tools/gotestsum/internal/log" 17 ) 18 19 // Action of TestEvent 20 type Action string 21 22 // nolint: unused 23 const ( 24 ActionRun Action = "run" 25 ActionPause Action = "pause" 26 ActionCont Action = "cont" 27 ActionPass Action = "pass" 28 ActionBench Action = "bench" 29 ActionFail Action = "fail" 30 ActionOutput Action = "output" 31 ActionSkip Action = "skip" 32 ) 33 34 // IsTerminal returns true if the Action is one of: pass, fail, skip. 35 func (a Action) IsTerminal() bool { 36 switch a { 37 case ActionPass, ActionFail, ActionSkip: 38 return true 39 default: 40 return false 41 } 42 } 43 44 // TestEvent is a structure output by go tool test2json and go test -json. 45 type TestEvent struct { 46 // Time encoded as an RFC3339-format string 47 Time time.Time 48 Action Action 49 Package string 50 Test string 51 // Elapsed time in seconds 52 Elapsed float64 53 // Output of test or benchmark 54 Output string 55 // raw is the raw JSON bytes of the event 56 raw []byte 57 // RunID from the ScanConfig which produced this test event. 58 RunID int 59 } 60 61 // PackageEvent returns true if the event is a package start or end event 62 func (e TestEvent) PackageEvent() bool { 63 return e.Test == "" 64 } 65 66 // Bytes returns the serialized JSON bytes that were parsed to create the event. 67 func (e TestEvent) Bytes() []byte { 68 return e.raw 69 } 70 71 // Package is the set of TestEvents for a single go package 72 type Package struct { 73 Total int 74 running map[string]TestCase 75 Failed []TestCase 76 Skipped []TestCase 77 Passed []TestCase 78 79 // elapsed time reported by the pass or fail event for the package. 80 elapsed time.Duration 81 82 // mapping of root TestCase ID to all sub test IDs. Used to mitigate 83 // github.com/golang/go/issues/29755, and github.com/golang/go/issues/40771. 84 // In the future when those bug are fixed this mapping can likely be removed. 85 subTests map[int][]int 86 87 // output printed by test cases, indexed by TestCase.ID. Package output is 88 // saved with key 0. 89 output map[int][]string 90 // coverage stores the code coverage output for the package without the 91 // trailing newline (ex: coverage: 91.1% of statements). 92 coverage string 93 // action identifies if the package passed or failed. A package may fail 94 // with no test failures if an init() or TestMain exits non-zero. 95 // skip indicates there were no tests. 96 action Action 97 // cached is true if the package was marked as (cached) 98 cached bool 99 // panicked is true if the package, or one of the tests in the package, 100 // contained output that looked like a panic. This is used to mitigate 101 // github.com/golang/go/issues/45508. This field may be removed in the future 102 // if the issue is fixed in Go. 103 panicked bool 104 // shuffleSeed is the seed used to shuffle the tests. The value is set when 105 // tests are run with -shuffle 106 shuffleSeed string 107 108 // testTimeoutPanicInTest stores the name of a test that received the panic 109 // output caused by a test timeout. This is necessary to work around a race 110 // condition in test2json. See https://github.com/golang/go/issues/57305. 111 testTimeoutPanicInTest string 112 } 113 114 // Result returns if the package passed, failed, or was skipped because there 115 // were no tests. 116 func (p *Package) Result() Action { 117 return p.action 118 } 119 120 // Elapsed returns the elapsed time of the package, as reported by the 121 // pass or fail event for the package. 122 func (p *Package) Elapsed() time.Duration { 123 return p.elapsed 124 } 125 126 // TestCases returns all the test cases. 127 func (p *Package) TestCases() []TestCase { 128 tc := append([]TestCase{}, p.Passed...) 129 tc = append(tc, p.Failed...) 130 tc = append(tc, p.Skipped...) 131 return tc 132 } 133 134 // LastFailedByName returns the most recent test with name in the list of Failed 135 // tests. If no TestCase is found with that name, an empty TestCase is returned. 136 // 137 // LastFailedByName may be used by formatters to find the TestCase.ID for the current 138 // failing TestEvent. It is very likely the last TestCase in Failed, but this method 139 // provides a little more safety if that ever changes. 140 func (p *Package) LastFailedByName(name string) TestCase { 141 for i := len(p.Failed) - 1; i >= 0; i-- { 142 if p.Failed[i].Test.Name() == name { 143 return p.Failed[i] 144 } 145 } 146 return TestCase{} 147 } 148 149 // Output returns the full test output for a test. Unlike OutputLines() it does 150 // not return lines from subtests in some cases. 151 // 152 // Deprecated: use WriteOutputTo to avoid lots of allocation 153 func (p *Package) Output(id int) string { 154 return strings.Join(p.output[id], "") 155 } 156 157 // WriteOutputTo writes the output for TestCase with id to out. 158 func (p *Package) WriteOutputTo(out io.StringWriter, id int) error { 159 for _, v := range p.output[id] { 160 if _, err := out.WriteString(v); err != nil { 161 return err 162 } 163 } 164 return nil 165 } 166 167 // OutputLines returns the full test output for a test as a slice of strings. 168 // 169 // As a workaround for test output being attributed to the wrong subtest, if: 170 // - the TestCase is a root TestCase (not a subtest), and 171 // - the TestCase has no subtest failures; 172 // 173 // then all output for every subtest under the root test is returned. 174 // See https://github.com/golang/go/issues/29755. 175 func (p *Package) OutputLines(tc TestCase) []string { 176 lines := p.output[tc.ID] 177 178 // If this is a subtest, or a root test case with subtest failures the 179 // subtest failure output should contain the relevant lines, so we don't need 180 // extra lines. 181 if tc.Test.IsSubTest() || tc.hasSubTestFailed { 182 return lines 183 } 184 185 result := make([]string, 0, len(lines)+1) 186 result = append(result, lines...) 187 for _, sub := range p.subTests[tc.ID] { 188 result = append(result, p.output[sub]...) 189 } 190 return result 191 } 192 193 func (p *Package) addOutput(id int, output string) { 194 if strings.HasPrefix(output, "panic: ") { 195 p.panicked = true 196 } 197 p.output[id] = append(p.output[id], output) 198 } 199 200 type TestName string 201 202 func (n TestName) Split() (root string, sub string) { 203 parts := strings.SplitN(string(n), "/", 2) 204 if len(parts) < 2 { 205 return string(n), "" 206 } 207 return parts[0], parts[1] 208 } 209 210 // IsSubTest returns true if the name indicates the test is a subtest run using 211 // t.Run(). 212 func (n TestName) IsSubTest() bool { 213 return strings.Contains(string(n), "/") 214 } 215 216 func (n TestName) Name() string { 217 return string(n) 218 } 219 220 func (n TestName) Parent() string { 221 idx := strings.LastIndex(string(n), "/") 222 if idx < 0 { 223 return "" 224 } 225 return string(n)[:idx] 226 } 227 228 func (p *Package) removeOutput(id int) { 229 delete(p.output, id) 230 231 skipped := tcIDSet(p.Skipped) 232 for _, sub := range p.subTests[id] { 233 if _, isSkipped := skipped[sub]; !isSkipped { 234 delete(p.output, sub) 235 } 236 } 237 } 238 239 func tcIDSet(skipped []TestCase) map[int]struct{} { 240 result := make(map[int]struct{}) 241 for _, tc := range skipped { 242 result[tc.ID] = struct{}{} 243 } 244 return result 245 } 246 247 // TestMainFailed returns true if the package has output related to a failure. This 248 // may happen if a TestMain or init function panic, or if test timeout 249 // is reached and output is associated with the package instead of the running 250 // test. 251 func (p *Package) TestMainFailed() bool { 252 if p.testTimeoutPanicInTest != "" { 253 return true 254 } 255 return p.action == ActionFail && len(p.Failed) == 0 256 } 257 258 // IsEmpty returns true if this package contains no tests. 259 func (p *Package) IsEmpty() bool { 260 return p.Total == 0 && !p.TestMainFailed() 261 } 262 263 const neverFinished time.Duration = -1 264 265 // end adds any tests that were missing an ActionFail TestEvent to the list of 266 // Failed, and returns a slice of artificial TestEvent for the missing ones. 267 // 268 // This is done to work around 'go test' not sending the ActionFail TestEvents 269 // in some cases, when a test panics. 270 func (p *Package) end() []TestEvent { 271 result := make([]TestEvent, 0, len(p.running)) 272 for k, tc := range p.running { 273 if tc.Test.IsSubTest() && rootTestPassed(p, tc) { 274 // mitigate github.com/golang/go/issues/40771 (gotestsum/issues/141) 275 // by skipping missing subtest end events when the root test passed. 276 continue 277 } 278 279 tc.Elapsed = neverFinished 280 p.Failed = append(p.Failed, tc) 281 282 result = append(result, TestEvent{ 283 Action: ActionFail, 284 Package: tc.Package, 285 Test: tc.Test.Name(), 286 Elapsed: float64(neverFinished), 287 }) 288 delete(p.running, k) 289 } 290 return result 291 } 292 293 // rootTestPassed looks for the root test associated with subtest and returns 294 // true if the root test passed. This is used to mitigate 295 // github.com/golang/go/issues/40771 (gotestsum/issues/141) and may be removed 296 // in the future since that issue was patched in go1.16. 297 // 298 // This function is slightly expensive because it has to scan every test in the 299 // package, but it should only run in the rare case where a subtest was missing 300 // an end event. Spending a little more time in that rare case is probably better 301 // than keeping extra mapping of tests in all cases. 302 func rootTestPassed(p *Package, subtest TestCase) bool { 303 root, _ := subtest.Test.Split() 304 305 for _, tc := range p.Passed { 306 if tc.Test.Name() != root { 307 continue 308 } 309 310 for _, subID := range p.subTests[tc.ID] { 311 if subID == subtest.ID { 312 return true 313 } 314 } 315 } 316 return false 317 } 318 319 // TestCase stores the name and elapsed time for a test case. 320 type TestCase struct { 321 // ID is unique ID for each test case. A test run may include multiple instances 322 // of the same Package and Name if -count is used, or if the input comes from 323 // multiple runs. The ID can be used to uniquely reference an instance of a 324 // test case. 325 ID int 326 Package string 327 Test TestName 328 Elapsed time.Duration 329 // RunID from the ScanConfig which produced this test case. 330 RunID int 331 // hasSubTestFailed is true when a subtest of this TestCase has failed. It is 332 // used to find root TestCases which have no failing subtests. 333 hasSubTestFailed bool 334 // Time when the test was run. 335 Time time.Time 336 } 337 338 func newPackage() *Package { 339 return &Package{ 340 output: make(map[int][]string), 341 running: make(map[string]TestCase), 342 subTests: make(map[int][]int), 343 } 344 } 345 346 // Execution of one or more test packages 347 type Execution struct { 348 started time.Time 349 packages map[string]*Package 350 errorsLock sync.RWMutex 351 errors []string 352 done bool 353 lastRunID int 354 } 355 356 func (e *Execution) add(event TestEvent) { 357 pkg, ok := e.packages[event.Package] 358 if !ok { 359 pkg = newPackage() 360 e.packages[event.Package] = pkg 361 } 362 if event.PackageEvent() { 363 pkg.addEvent(event) 364 return 365 } 366 pkg.addTestEvent(event) 367 } 368 369 func (p *Package) addEvent(event TestEvent) { 370 switch event.Action { 371 case ActionPass, ActionFail: 372 p.action = event.Action 373 p.elapsed = elapsedDuration(event.Elapsed) 374 case ActionOutput: 375 if coverage, ok := isCoverageOutput(event.Output); ok { 376 p.coverage = coverage 377 } 378 if strings.Contains(event.Output, "\t(cached)") { 379 p.cached = true 380 } 381 if isShuffleSeedOutput(event.Output) { 382 p.shuffleSeed = strings.TrimRight(event.Output, "\n") 383 } 384 p.addOutput(0, event.Output) 385 } 386 } 387 388 func (p *Package) newTestCaseFromEvent(event TestEvent) TestCase { 389 // Incremental total before using it as the ID, because ID 0 is used for 390 // the package output 391 p.Total++ 392 return TestCase{ 393 Package: event.Package, 394 Test: TestName(event.Test), 395 ID: p.Total, 396 RunID: event.RunID, 397 Time: event.Time, 398 } 399 } 400 401 func (p *Package) addTestEvent(event TestEvent) { 402 if event.Action == ActionRun { 403 tc := p.newTestCaseFromEvent(event) 404 p.running[event.Test] = tc 405 406 if tc.Test.IsSubTest() { 407 root, _ := TestName(event.Test).Split() 408 rootID := p.running[root].ID 409 p.subTests[rootID] = append(p.subTests[rootID], tc.ID) 410 } 411 return 412 } 413 414 tc := p.running[event.Test] 415 // This appears to be a bug in 'go test' or test2json. This test is missing 416 // an Action=run event. Create one on the first event received from the test. 417 if tc.ID == 0 { 418 tc = p.newTestCaseFromEvent(event) 419 p.running[event.Test] = tc 420 } 421 422 switch event.Action { 423 case ActionOutput, ActionBench: 424 if strings.HasPrefix(event.Output, "panic: test timed out") { 425 p.testTimeoutPanicInTest = event.Test 426 } 427 if p.testTimeoutPanicInTest == event.Test { 428 p.addOutput(0, event.Output) 429 return 430 } 431 432 tc := p.running[event.Test] 433 p.addOutput(tc.ID, event.Output) 434 return 435 case ActionPause, ActionCont: 436 return 437 } 438 439 // the event.Action must be one of the three "test end" events 440 delete(p.running, event.Test) 441 tc.Elapsed = elapsedDuration(event.Elapsed) 442 443 switch event.Action { 444 case ActionFail: 445 p.Failed = append(p.Failed, tc) 446 447 // If this is a subtest, mark the root test as having a failed subtest 448 if tc.Test.IsSubTest() { 449 root, _ := TestName(event.Test).Split() 450 rootTestCase := p.running[root] 451 rootTestCase.hasSubTestFailed = true 452 p.running[root] = rootTestCase 453 } 454 case ActionSkip: 455 p.Skipped = append(p.Skipped, tc) 456 457 case ActionPass: 458 p.Passed = append(p.Passed, tc) 459 460 // Do not immediately remove output for subtests, to work around a bug 461 // in 'go test' where output is attributed to the wrong sub test. 462 // github.com/golang/go/issues/29755. 463 if tc.Test.IsSubTest() { 464 return 465 } 466 467 // Remove test output once a test passes, it wont be used. 468 p.removeOutput(tc.ID) 469 } 470 } 471 472 func elapsedDuration(elapsed float64) time.Duration { 473 return time.Duration(elapsed*1000) * time.Millisecond 474 } 475 476 func isCoverageOutput(output string) (string, bool) { 477 start := strings.Index(output, "coverage:") 478 if start < 0 { 479 return "", false 480 } 481 482 if !strings.Contains(output[start:], "% of statements") { 483 return "", false 484 } 485 486 return strings.TrimRight(output[start:], "\n"), true 487 } 488 489 func isCoverageOutputPreGo119(output string) bool { 490 return strings.HasPrefix(output, "coverage:") && 491 strings.HasSuffix(output, "% of statements\n") 492 } 493 494 func isShuffleSeedOutput(output string) bool { 495 return strings.HasPrefix(output, "-test.shuffle ") 496 } 497 498 func isWarningNoTestsToRunOutput(output string) bool { 499 return output == "testing: warning: no tests to run\n" 500 } 501 502 // OutputLines returns the full test output for a test as an slice of lines. 503 // This function is a convenient wrapper around Package.OutputLines() to 504 // support the hiding of output in the summary. 505 // 506 // See Package.OutLines() for more details. 507 func (e *Execution) OutputLines(tc TestCase) []string { 508 return e.packages[tc.Package].OutputLines(tc) 509 } 510 511 // Package returns the Package by name. 512 func (e *Execution) Package(name string) *Package { 513 return e.packages[name] 514 } 515 516 // Packages returns a sorted list of all package names. 517 func (e *Execution) Packages() []string { 518 return sortedKeys(e.packages) 519 } 520 521 var timeNow = time.Now 522 523 // Elapsed returns the time elapsed since the execution started. 524 func (e *Execution) Elapsed() time.Duration { 525 return timeNow().Sub(e.started) 526 } 527 528 // Failed returns a list of all the failed test cases. 529 func (e *Execution) Failed() []TestCase { 530 if e == nil { 531 return nil 532 } 533 var failed []TestCase //nolint:prealloc 534 for _, name := range sortedKeys(e.packages) { 535 pkg := e.packages[name] 536 537 // Add package-level failure output if there were no failed tests, or 538 // if the test timeout was reached (because we now have to store that 539 // output on the package). 540 if pkg.TestMainFailed() { 541 failed = append(failed, TestCase{Package: name}) 542 } 543 failed = append(failed, pkg.Failed...) 544 } 545 return failed 546 } 547 548 // FilterFailedUnique filters a slice of failed TestCases to remove any parent 549 // tests that have failed subtests. The parent test will always be run when 550 // running any of its subtests. 551 func FilterFailedUnique(tcs []TestCase) []TestCase { 552 sort.Slice(tcs, func(i, j int) bool { 553 a, b := tcs[i], tcs[j] 554 if a.Package != b.Package { 555 return a.Package < b.Package 556 } 557 return len(a.Test.Name()) > len(b.Test.Name()) 558 }) 559 560 var result []TestCase //nolint:prealloc 561 var parents = make(map[string]map[string]bool) 562 for _, tc := range tcs { 563 if _, exists := parents[tc.Package]; !exists { 564 parents[tc.Package] = make(map[string]bool) 565 } 566 if parent := tc.Test.Parent(); parent != "" { 567 parents[tc.Package][parent] = true 568 } 569 if _, exists := parents[tc.Package][tc.Test.Name()]; exists { 570 continue // tc is a parent of a failing subtest 571 } 572 result = append(result, tc) 573 } 574 575 // Restore the original order of test cases 576 sort.Slice(result, func(i, j int) bool { 577 a, b := result[i], result[j] 578 if a.Package != b.Package { 579 return a.Package < b.Package 580 } 581 return a.ID < b.ID 582 }) 583 return result 584 } 585 586 func sortedKeys(pkgs map[string]*Package) []string { 587 keys := make([]string, 0, len(pkgs)) 588 for key := range pkgs { 589 keys = append(keys, key) 590 } 591 sort.Strings(keys) 592 return keys 593 } 594 595 // Skipped returns a list of all the skipped test cases. 596 func (e *Execution) Skipped() []TestCase { 597 skipped := make([]TestCase, 0, len(e.packages)) 598 for _, pkg := range sortedKeys(e.packages) { 599 skipped = append(skipped, e.packages[pkg].Skipped...) 600 } 601 return skipped 602 } 603 604 // Total returns a count of all test cases. 605 func (e *Execution) Total() int { 606 total := 0 607 for _, pkg := range e.packages { 608 total += pkg.Total 609 } 610 return total 611 } 612 613 func (e *Execution) addError(err string) { 614 // Build errors start with a header 615 if strings.HasPrefix(err, "# ") { 616 return 617 } 618 e.errorsLock.Lock() 619 e.errors = append(e.errors, err) 620 e.errorsLock.Unlock() 621 } 622 623 // Errors returns a list of all the errors. 624 func (e *Execution) Errors() []string { 625 e.errorsLock.RLock() 626 defer e.errorsLock.RUnlock() 627 return e.errors 628 } 629 630 // HasPanic returns true if at least one package had output that looked like a 631 // panic. 632 func (e *Execution) HasPanic() bool { 633 for _, pkg := range e.packages { 634 if pkg.panicked { 635 return true 636 } 637 } 638 return false 639 } 640 641 func (e *Execution) end() []TestEvent { 642 e.done = true 643 var result []TestEvent // nolint: prealloc 644 for _, pkg := range e.packages { 645 result = append(result, pkg.end()...) 646 } 647 return result 648 } 649 650 func (e *Execution) Started() time.Time { 651 return e.started 652 } 653 654 // newExecution returns a new Execution and records the current time as the 655 // time the test execution started. 656 func newExecution() *Execution { 657 return &Execution{ 658 started: timeNow(), 659 packages: make(map[string]*Package), 660 } 661 } 662 663 // ScanConfig used by ScanTestOutput. 664 type ScanConfig struct { 665 // RunID is a unique identifier for the run. It may be set to the pid of the 666 // process, or some other identifier. It will stored as the TestCase.RunID. 667 RunID int 668 // Stdout is a reader that yields the test2json output stream. 669 Stdout io.Reader 670 // Stderr is a reader that yields stderr from the 'go test' process. Often 671 // it contains build errors, or panics. Stderr may be nil. 672 Stderr io.Reader 673 // Handler is a set of callbacks for receiving TestEvents and stderr text. 674 Handler EventHandler 675 // Execution to populate while scanning. If nil a new one will be created 676 // and returned from ScanTestOutput. 677 Execution *Execution 678 // Stop is called when ScanTestOutput fails during a scan. 679 Stop func() 680 // IgnoreNonJSONOutputLines causes ScanTestOutput to ignore non-JSON lines received from 681 // the Stdout reader. Instead of causing an error, the lines will be sent to Handler.Err. 682 IgnoreNonJSONOutputLines bool 683 } 684 685 // EventHandler is called by ScanTestOutput for each event and write to stderr. 686 type EventHandler interface { 687 // Event is called for every TestEvent, with the current value of Execution. 688 // It may return an error to stop scanning. 689 Event(event TestEvent, execution *Execution) error 690 // Err is called for every line from the Stderr reader and may return an 691 // error to stop scanning. 692 Err(text string) error 693 } 694 695 // ScanTestOutput reads lines from config.Stdout and config.Stderr, populates an 696 // Execution, calls the Handler for each event, and returns the Execution. 697 // 698 // If config.Handler is nil, a default no-op handler will be used. 699 func ScanTestOutput(config ScanConfig) (*Execution, error) { 700 if config.Stdout == nil { 701 return nil, fmt.Errorf("stdout reader must be non-nil") 702 } 703 if config.Handler == nil { 704 config.Handler = noopHandler{} 705 } 706 if config.Stderr == nil { 707 config.Stderr = new(bytes.Reader) 708 } 709 if config.Stop == nil { 710 config.Stop = func() {} 711 } 712 execution := config.Execution 713 if execution == nil { 714 execution = newExecution() 715 } 716 execution.done = false 717 execution.lastRunID = config.RunID 718 719 var group errgroup.Group 720 group.Go(func() error { 721 return stopOnError(config.Stop, readStdout(config, execution)) 722 }) 723 group.Go(func() error { 724 return stopOnError(config.Stop, readStderr(config, execution)) 725 }) 726 727 err := group.Wait() 728 for _, event := range execution.end() { 729 if err := config.Handler.Event(event, execution); err != nil { 730 return execution, err 731 } 732 } 733 return execution, err 734 } 735 736 func stopOnError(stop func(), err error) error { 737 if err != nil { 738 stop() 739 return err 740 } 741 return nil 742 } 743 744 func readStdout(config ScanConfig, execution *Execution) error { 745 scanner := bufio.NewScanner(config.Stdout) 746 for scanner.Scan() { 747 raw := scanner.Bytes() 748 event, err := parseEvent(raw) 749 switch { 750 case err == errBadEvent: 751 // nolint: errcheck 752 config.Handler.Err(errBadEvent.Error() + ": " + scanner.Text()) 753 continue 754 case err != nil: 755 if config.IgnoreNonJSONOutputLines { 756 // nolint: errcheck 757 config.Handler.Err(string(raw)) 758 continue 759 } 760 return fmt.Errorf("failed to parse test output: %s: %w", string(raw), err) 761 } 762 763 event.RunID = config.RunID 764 execution.add(event) 765 if err := config.Handler.Event(event, execution); err != nil { 766 return err 767 } 768 } 769 if err := scanner.Err(); err != nil { 770 return fmt.Errorf("failed to scan test output: %w", err) 771 } 772 return nil 773 } 774 775 func readStderr(config ScanConfig, execution *Execution) error { 776 scanner := bufio.NewScanner(config.Stderr) 777 for scanner.Scan() { 778 line := scanner.Text() 779 if err := config.Handler.Err(line); err != nil { 780 return fmt.Errorf("failed to handle stderr: %v", err) 781 } 782 if isGoModuleOutput(line) || isGoDebugOutput(line) { 783 continue 784 } 785 if strings.HasPrefix(line, "warning:") { 786 continue 787 } 788 execution.addError(line) 789 } 790 791 if err := scanner.Err(); err != nil { 792 return fmt.Errorf("failed to scan stderr: %v", err) 793 } 794 return nil 795 } 796 797 func isGoModuleOutput(scannerText string) bool { 798 prefixes := []string{ 799 "go: copying", 800 "go: creating", 801 "go: downloading", 802 "go: extracting", 803 "go: finding", 804 } 805 806 for _, prefix := range prefixes { 807 if strings.HasPrefix(scannerText, prefix) { 808 return true 809 } 810 } 811 return false 812 } 813 814 func isGoDebugOutput(scannerText string) bool { 815 prefixes := []string{ 816 "HASH", // Printed when tests are running with `GODEBUG=gocachehash=1`. 817 "testcache:", // Printed when tests are running with `GODEBUG=gocachetest=1`. 818 } 819 820 for _, prefix := range prefixes { 821 if strings.HasPrefix(scannerText, prefix) { 822 return true 823 } 824 } 825 return false 826 } 827 828 func parseEvent(raw []byte) (TestEvent, error) { 829 // TODO: this seems to be a bug in the `go test -json` output 830 if bytes.HasPrefix(raw, []byte("FAIL")) { 831 log.Warnf("invalid TestEvent: %v", string(raw)) 832 return TestEvent{}, errBadEvent 833 } 834 835 event := TestEvent{} 836 err := json.Unmarshal(raw, &event) 837 event.raw = raw 838 return event, err 839 } 840 841 var errBadEvent = errors.New("bad output from test2json") 842 843 type noopHandler struct{} 844 845 func (s noopHandler) Event(TestEvent, *Execution) error { 846 return nil 847 } 848 849 func (s noopHandler) Err(string) error { 850 return nil 851 }