Name what failed in a repository check from its whole output, not its tail (issue 460)
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

A long run's logs pushed the --- FAIL lines out of the last 200 lines the
summary was named from, so a failed check could not say which test failed.
Pick the lines that name a failure as the output streams, keep them whole at
the end of the verdict's report, and say the whole output in the build's log.
This commit is contained in:
2026-10-11 04:24:34 +02:00
parent 45b4ae92bc
commit 1747e33923
9 changed files with 1905 additions and 7 deletions
+182
View File
@@ -0,0 +1,182 @@
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 <package> 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)
}
}