//go:build integration package builder import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) 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 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) } } 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) } } 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) } } 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]) } } } 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) } } 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)]) } }