diff --git a/cmd/testwrapper/testwrapper.go b/cmd/testwrapper/testwrapper.go index 91aea904e..1e6eb3c7e 100644 --- a/cmd/testwrapper/testwrapper.go +++ b/cmd/testwrapper/testwrapper.go @@ -10,6 +10,7 @@ package main import ( "bufio" "bytes" + "cmp" "context" "encoding/json" "errors" @@ -65,11 +66,12 @@ type packageTests struct { } type goTestOutput struct { - Time time.Time - Action string - Package string - Test string - Output string + Time time.Time + Action string + ImportPath string + Package string + Test string + Output string } var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != "" @@ -117,42 +119,43 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te for s.Scan() { var goOutput goTestOutput if err := json.Unmarshal(s.Bytes(), &goOutput); err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) { - break - } - - // `go test -json` outputs invalid JSON when a build fails. - // In that case, discard the the output and start reading again. - // The build error will be printed to stderr. - // See: https://github.com/golang/go/issues/35169 - if _, ok := err.(*json.SyntaxError); ok { - fmt.Println(s.Text()) - continue - } - panic(err) + return fmt.Errorf("failed to parse go test output %q: %w", s.Bytes(), err) } - pkg := goOutput.Package + pkg := cmp.Or( + goOutput.Package, + "build:"+goOutput.ImportPath, // can be "./cmd" while Package is "tailscale.com/cmd" so use separate namespace + ) pkgTests := resultMap[pkg] if pkgTests == nil { - pkgTests = make(map[string]*testAttempt) + pkgTests = map[string]*testAttempt{ + "": {}, // Used for start time and build logs. + } resultMap[pkg] = pkgTests } if goOutput.Test == "" { switch goOutput.Action { case "start": - pkgTests[""] = &testAttempt{start: goOutput.Time} - case "fail", "pass", "skip": + pkgTests[""].start = goOutput.Time + case "build-output": + pkgTests[""].logs.WriteString(goOutput.Output) + case "build-fail", "fail", "pass", "skip": for _, test := range pkgTests { if test.testName != "" && test.outcome == "" { test.outcome = "fail" ch <- test } } + outcome := goOutput.Action + if outcome == "build-fail" { + outcome = "FAIL" + } + pkgTests[""].logs.WriteString(goOutput.Output) ch <- &testAttempt{ pkg: goOutput.Package, - outcome: goOutput.Action, + outcome: outcome, start: pkgTests[""].start, end: goOutput.Time, + logs: pkgTests[""].logs, pkgFinished: true, } } @@ -221,6 +224,9 @@ func main() { } toRun := []*nextRun{firstRun} printPkgOutcome := func(pkg, outcome string, attempt int, runtime time.Duration) { + if pkg == "" { + return // We reach this path on a build error. + } if outcome == "skip" { fmt.Printf("?\t%s [skipped/no tests] \n", pkg) return @@ -314,6 +320,7 @@ func main() { // when a package times out. failed = true } + os.Stdout.ReadFrom(&tr.logs) printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start)) continue } diff --git a/cmd/testwrapper/testwrapper_test.go b/cmd/testwrapper/testwrapper_test.go index fb2ed2c52..ace53ccd0 100644 --- a/cmd/testwrapper/testwrapper_test.go +++ b/cmd/testwrapper/testwrapper_test.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strings" "sync" "testing" ) @@ -154,24 +155,24 @@ func TestBuildError(t *testing.T) { t.Fatalf("writing package: %s", err) } - buildErr := []byte("builderror_test.go:3:1: expected declaration, found derp\nFAIL command-line-arguments [setup failed]") + wantErr := "builderror_test.go:3:1: expected declaration, found derp\nFAIL" // Confirm `go test` exits with code 1. goOut, err := exec.Command("go", "test", testfile).CombinedOutput() if code, ok := errExitCode(err); !ok || code != 1 { - t.Fatalf("go test %s: expected error with exit code 0 but got: %v", testfile, err) + t.Fatalf("go test %s: got exit code %d, want 1 (err: %v)", testfile, code, err) } - if !bytes.Contains(goOut, buildErr) { - t.Fatalf("go test %s: expected build error containing %q but got:\n%s", testfile, buildErr, goOut) + if !strings.Contains(string(goOut), wantErr) { + t.Fatalf("go test %s: got output %q, want output containing %q", testfile, goOut, wantErr) } // Confirm `testwrapper` exits with code 1. twOut, err := cmdTestwrapper(t, testfile).CombinedOutput() if code, ok := errExitCode(err); !ok || code != 1 { - t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v", testfile, err) + t.Fatalf("testwrapper %s: got exit code %d, want 1 (err: %v)", testfile, code, err) } - if !bytes.Contains(twOut, buildErr) { - t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, twOut) + if !strings.Contains(string(twOut), wantErr) { + t.Fatalf("testwrapper %s: got output %q, want output containing %q", testfile, twOut, wantErr) } if testing.Verbose() {