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  }