package builder import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) // ownCheckOf runs a repository's own check whose script prints a captured output and fails, as the build // seat runs it, and answers the layer and every line it said in the build's log. func ownCheckOf(t *testing.T, printed string) (*Layer, []string) { t.Helper() tree := t.TempDir() if err := os.WriteFile(filepath.Join(tree, "printed.txt"), []byte(printed), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(tree, CheckScript), []byte("cat printed.txt\nexit 1\n"), 0o755); err != nil { t.Fatal(err) } var out tail var logged []string layer := ownCheck(t.Context(), CheckSpec{Toolchain: "go-image"}, []ScriptPart{{"go", CheckScript}}, tree, &out, func() bool { return false }, func(_, script string) *exec.Cmd { cmd := exec.CommandContext(t.Context(), "sh", script) cmd.Dir = tree return cmd }, func(step, format string, args ...any) { if step == "output" { logged = append(logged, fmt.Sprintf(format, args...)) } }) if layer == nil || layer.Verdict != "fail" { t.Fatalf("a failing check answered %+v", layer) } return layer, logged } func captured(t *testing.T, name string) string { t.Helper() body, err := os.ReadFile(filepath.Join("testdata", "issue460", name)) if err != nil { t.Fatal(err) } return string(body) } // **novox/hq issue 460**: mesh-controller #218's check failed with "FAIL 268.072s" and no test // name: the package's own logs, printed after its `--- FAIL:` lines, pushed them out of the last 200 lines, // which was all the summary was named from. The first failing test is named, and the others after it, // however much came after them. func TestAFailureEarlyInALongRunIsStillNamed(t *testing.T) { layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt")) if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") { t.Errorf("a failure 600 lines from the end is said as %q", layer.Summary) } if !strings.Contains(layer.Summary, "TestSubtests/the_hub") { t.Errorf("the second failing test is not named: %q", layer.Summary) } } // One -race run of every package: the first failure is the earliest, not the last within the tail. func TestARaceRunOfEveryPackageNamesItsFirstFailure(t *testing.T) { layer, _ := ownCheckOf(t, captured(t, "a-race-run-of-every-package.txt")) if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") || !strings.Contains(layer.Summary, "TestAMapIsNil") || !strings.Contains(layer.Summary, "TestACounterIsShared") { t.Errorf("a run failing in four packages is said as %q", layer.Summary) } } // A package that does not build is named by its error, not by its bare "[build failed]". func TestABuildErrorIsNamedByTheError(t *testing.T) { layer, _ := ownCheckOf(t, captured(t, "a-build-error.txt")) if !strings.HasSuffix(layer.Summary, "failed: broken/broken.go:3:28: undefined: undefinedThing") { t.Errorf("a build error is said as %q", layer.Summary) } } // The whole of what the check printed is in the build's log, line by line — where before none of it was. func TestTheWholeOutputIsInTheBuildsLog(t *testing.T) { printed := captured(t, "a-long-run-failing-early.txt") _, logged := ownCheckOf(t, printed) want := strings.Split(strings.TrimRight(printed, "\n"), "\n") if len(logged) != len(want) { t.Fatalf("the build's log holds %d line(s) of the %d printed", len(logged), len(want)) } for i := range want { if logged[i] != want[i] { t.Fatalf("line %d is logged as %q, printed as %q", i+1, logged[i], want[i]) } } } // What failed is kept whole in the verdict: each --- FAIL with its messages, a panic with its first frames, // a data race report from rule to rule — and none of the log lines around them. func TestTheVerdictKeepsWhatFailedWhole(t *testing.T) { layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt")) want := "--- FAIL: TestTheEnvelopeIsPinned (0.00s)\n" + " early_test.go:9: the envelope's subject is \"mesh.a\", not \"mesh.b\"\n" + " early_test.go:10: a second line of the same failure\n" + "--- FAIL: TestSubtests (0.00s)\n" + " --- FAIL: TestSubtests/the_hub (0.00s)\n" + " early_test.go:14: the hub does not compose\n" + "FAIL\texample.com/cap/early\t" if !strings.HasPrefix(layer.Failed, want) || strings.Contains(layer.Failed, "kept what home-server says") { t.Errorf("the early failure is kept as:\n%s", layer.Failed) } layer, _ = ownCheckOf(t, captured(t, "a-panic.txt")) for _, line := range []string{"--- FAIL: TestAMapIsNil (0.00s)", "panic: assignment to entry in nil map", "[running]:", "example.com/cap/panics.TestAMapIsNil(", "/src/cap/panics/panics_test.go:7", "FAIL\texample.com/cap/panics\t"} { if !strings.Contains(layer.Failed, line) { t.Errorf("a panic is kept without %q:\n%s", line, layer.Failed) } } layer, _ = ownCheckOf(t, captured(t, "a-data-race.txt")) race := captured(t, "a-data-race.txt") report := race[:strings.Index(race, "--- FAIL:")] if !strings.HasPrefix(layer.Failed, report) || !strings.Contains(layer.Failed, "--- FAIL: TestACounterIsShared (0.00s)\n testing.go:1865: race detected") { t.Errorf("a data race is kept as:\n%s", layer.Failed) } layer, _ = ownCheckOf(t, captured(t, "a-build-error.txt")) if layer.Failed != "# example.com/cap/broken\nbroken/broken.go:3:28: undefined: undefinedThing\n"+ "FAIL\texample.com/cap/broken [build failed]" { t.Errorf("a build error is kept as:\n%s", layer.Failed) } } // A run that fails everywhere is bounded in what it keeps, and what it keeps survives the controller's cut // of a report to its last 60 KiB, said at the report's end. func TestWhatFailedIsBoundedAndSurvivesTheReportsCut(t *testing.T) { var b strings.Builder for i := range 3000 { fmt.Fprintf(&b, "--- FAIL: TestNumber%d (0.00s)\n x_test.go:1: %s\n", i, strings.Repeat("why ", 600)) fmt.Fprintf(&b, "2026/10/11 01:30:03 a log line %d\n", i) } b.WriteString("FAIL\tx/everything\t1s\nFAIL\n") layer, logged := ownCheckOf(t, b.String()) if len(layer.Failed) > failureBytes+2*failureLineBytes || !strings.Contains(layer.Failed, "more line(s) naming what failed") { t.Errorf("what failed in a run that fails everywhere is %d bytes, ending %q", len(layer.Failed), layer.Failed[max(0, len(layer.Failed)-200):]) } if !strings.Contains(layer.Summary, "--- FAIL: TestNumber0 (0.00s); and TestNumber1, ") || !strings.Contains(layer.Summary, "and 2994 more") { t.Errorf("a run failing 3000 tests is said as %q", layer.Summary) } var out tail out.Write([]byte(b.String())) report := withWhatFailed(out.String(), layer) const maxCheckReport = 60 << 10 // as the controller cuts it, from the front if len(report) > maxCheckReport { report = report[len(report)-maxCheckReport:] } if !strings.Contains(report, "--- what failed") || !strings.Contains(report, "--- FAIL: TestNumber0 (0.00s)") { t.Error("what failed did not survive the controller's cut of the report") } // The build's log keeps its first logLines lines, says how many it left out, then what failed. if len(logged) < logLines+2 || logged[logLines] != "… 1002 more line(s) of its output are not in this log, which keeps its first 8000" || logged[logLines+1] != "--- what failed, picked from the whole of its output" || logged[logLines+2] != "--- FAIL: TestNumber0 (0.00s)" { t.Errorf("the log past its bound says %q", logged[logLines:min(len(logged), logLines+3)]) } } // A passing check says nothing of what failed. func TestAPassingCheckReportsAsBefore(t *testing.T) { if got := withWhatFailed("ok\tx\t1s", &Layer{Verdict: "pass"}); got != "ok\tx\t1s" { t.Errorf("a passing report became %q", got) } if got := withWhatFailed("r", nil); got != "r" { t.Errorf("no repository layer became %q", got) } }