package check import ( "strings" "testing" ) // What the build module before this fix sent for two packages that failed after their tests passed (issue 1901, its third // meeting: a try of mesh-controller kept "PASS" as its failure's words). Each is that build module's Output for the real // `go test -json` of a throwaway package (mesh-catalog modules/mesh-build-go testdata/captured/go-test-json-pkg-*.jsonl): // TestMain panicking in a goroutine after m.Run, TestMain exiting 2 after m.Run, and TestMain racing after m.Run. const ( oldPassThenPanic = "PASS\npanic: the bus closed after the tests: nats: connection closed\ngoroutine 20 [running]:\n" + "example.com/passthenfail.TestMain.func1()\n\t/tmp/fix-1901b/passthenfail/p_test.go:13 +0x25\n" + "created by example.com/passthenfail.TestMain in goroutine 1\n\t/tmp/fix-1901b/passthenfail/p_test.go:13 +0x25\n" + "FAIL\texample.com/passthenfail\t0.005s" oldPassThenExit = "PASS\nFAIL\texample.com/mainexit\t0.004s" // TestMain racing after m.Run passed, under go test -race (go-test-json-pkg-racemain.jsonl). oldPassThenRace = "PASS\n==================\nWARNING: DATA RACE\nWrite at 0x000000836428 by goroutine 9:\n example.com/racemain.TestMain.func1()\n /tmp/fix-1901b/racemain/p_test.go:15 +0x24\nPrevious write at 0x000000836428 by main goroutine:\n example.com/racemain.TestMain()\n /tmp/fix-1901b/racemain/p_test.go:16 +0x44\n main.main()\n _testmain.go:48 +0x171\nGoroutine 9 (running) created at:\n example.com/racemain.TestMain()\n /tmp/fix-1901b/racemain/p_test.go:15 +0x37\n main.main()\n _testmain.go:48 +0x171\n==================\nFound 1 data race(s)\nFAIL\texample.com/racemain\t1.031s" ) // A failure with no cause told apart never has "PASS" or "ok" as its words: the checker takes the panic line from the // output, or says that the tests passed and the program failed after them (1901/AC5). func TestAPackageFailureIsNeverWordedPass(t *testing.T) { // 1901/AC5 cases := []struct { name, output, want string }{ {"panic after the tests", oldPassThenPanic, "panic: the bus closed after the tests: nats: connection closed"}, {"exit after the tests", oldPassThenExit, "the tests passed, then the test program exited with a failure"}, {"race after the tests", oldPassThenRace, "WARNING: DATA RACE: Write at 0x000000836428 by goroutine 9 in example.com/racemain.TestMain.func1()"}, {"a panic deep in a long output", "PASS\npanic: the seat row vanished\n" + strings.Repeat("goroutine 7 [chan receive]:\n\tsome/frame.go:1 +0x1\n", 300) + "FAIL\tp\t2.0s", "panic: the seat row vanished"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { v := levelFailure(&target{}, "integration", Tested{Failed: []Failure{{Package: "github.com/novox/mesh-controller/cmd/mesh-controller", Output: c.output}}}) for _, l := range strings.Split(v.Failed.Words, "\n") { if l = strings.TrimSpace(l); l == "PASS" || strings.HasPrefix(l, "ok ") || strings.HasPrefix(l, "ok\t") { t.Errorf("1901/AC5: the words keep %q: %q", l, v.Failed.Words) } } if !strings.HasPrefix(v.Failed.Words, c.want) { t.Errorf("1901/AC5: words start %q, want %q", firstLine(v.Failed.Words), c.want) } if got := firstFailed(*v); !strings.Contains(got, c.want) || strings.HasSuffix(got, ": PASS") { t.Errorf("1901/AC5: status = %q", got) } }) } } // A cause the build module told apart is cut to a line's bound, so the words keep their bound (the 1901 review's note). func TestALongCauseIsCutToALinesBound(t *testing.T) { // 1901/AC5 f := Failure{Package: "p", Test: "TestX", Cause: strings.Repeat("c", 5000), Output: "x_test.go:1: something"} v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{f}}) if len(v.Failed.Words) > MaxWords+200 { t.Errorf("1901/AC5: words are %d bytes, bound %d", len(v.Failed.Words), MaxWords) } } // The build module on main sends a package that stopped (a timeout, an exit, a kill) as only go test's last line, with no // cause: the words then say so, never nothing (review round 1). Each output is what that build module sends for the // capture of the same name (go-test-json-pkg-timeout, -exiting, -killed): the package's own output, its "FAIL" line. func TestAPackageStoppedWithOnlyItsFailLineIsStillWorded(t *testing.T) { // 1901/AC5 for _, out := range []string{"FAIL\texample.com/timeout\t2.017s", "FAIL\texample.com/exiting\t0.006s", "FAIL\texample.com/killed\t0.006s"} { v := levelFailure(&target{}, "integration", Tested{Failed: []Failure{{Package: "example.com/x", Output: out}}}) if !strings.HasPrefix(v.Failed.Words, "the package failed with no test to name and no panic, signal or build error in its output") { t.Errorf("1901/AC5: %q is worded %q", out, v.Failed.Words) } } } // A line of the test's own that starts as go's summary does, or that holds a runtime's words indented, is not taken for // either (review round 1): go prints its summary in exact forms and the runtime's lines at column 0. func TestOnlyGosExactFormsAreSummaryOrCause(t *testing.T) { // 1901/AC5 out := " x_test.go:3: ok so far\nok then: the seat is free\n panic: child retried and recovered\npanic: the real one\nFAIL\tp\t1.0s" v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{{Package: "p", Output: out}}}) if !strings.HasPrefix(v.Failed.Words, "panic: the real one") || !strings.Contains(v.Failed.Words, "ok then: the seat is free") { t.Errorf("1901/AC5: words = %q", v.Failed.Words) } } // The words keep MaxWords exactly, counting the cause, the marker and the newlines (review round 1). func TestTheWordsKeepTheirBoundExactly(t *testing.T) { // 1901/AC5 for _, cause := range []string{"", "a cause", strings.Repeat("c", 3000)} { for _, n := range []int{100, 170, 171, 172, 500} { f := Failure{Package: "p", Test: "TestX", Cause: cause, Output: strings.Repeat("a log line of twenty\n", n) + "the end"} if w := failureWords(f); len(w) > MaxWords { t.Errorf("1901/AC5: cause %d bytes, %d lines: words are %d bytes, bound %d", len(cause), n, len(w), MaxWords) } } } }