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.
183 lines
7.7 KiB
Go
183 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|