Name what failed in a repository check from its whole output, not its tail (issue 460)
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user