diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 0567a4c..1697068 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -531,23 +531,98 @@ const MaxWords = 4096 // failureWords is a failure's words: its output's last lines up to MaxWords, with the line the build module told apart as // the test's error first when the output does not start with it, so the first line of the words is the cause and the -// status line, which is cut short, names it. +// status line, which is cut short, names it. **A failure's words are never "PASS" or "ok"** (issue 1901, its third +// meeting: a package that failed after its tests passed was worded "PASS"): go test's summary lines are dropped, and with +// no cause told apart (an older build module) the cause is the output's first "panic:", "fatal error:" or "signal:" +// line, read before the output is cut, or the words say the tests passed and the program failed after them. func failureWords(f Failure) string { - out := strings.TrimSpace(f.Output) - if len(out) > MaxWords { - out = out[len(out)-MaxWords:] - // Start on a whole line, and a whole character. - if i := strings.IndexByte(out, '\n'); i >= 0 { - out = "... " + strings.TrimLeft(out[i:], "\n") - } - out = strings.ToValidUTF8(out, "") + out, passed := dropSummary(strings.TrimSpace(f.Output)) + cause := strings.TrimSpace(f.Cause) + if cause == "" { + cause = causeIn(out) } - if c := strings.TrimSpace(f.Cause); c != "" && !strings.HasPrefix(firstLine(out), c) { - out = c + "\n" + out + switch { + case cause == "" && out == "" && passed: + cause = "the tests passed, then the test program exited with a failure: an os.Exit or a panic in TestMain or after the tests" + case cause == "" && out == "": + // The build module on main sends a package that stopped (a timeout, an exit, a kill) as its "FAIL" line alone. + cause = "the package failed with no test to name and no panic, signal or build error in its output (an older build module sent no cause)" + } + if len(cause) > MaxCause { + cause = strings.ToValidUTF8(cause[:MaxCause], "") + "..." + } + // The words keep MaxWords exactly: the cause and its newline, the marker and the kept lines (review round 1). + budget := MaxWords + if cause != "" { + budget -= len(cause) + 1 + } + if len(out) > budget { + const marker = "... " + tail := out[len(out)-(budget-len(marker)):] + // Start on a whole line, and a whole character. + if i := strings.IndexByte(tail, '\n'); i >= 0 { + tail = strings.TrimLeft(tail[i:], "\n") + } + out = marker + strings.ToValidUTF8(tail, "") + } + if cause != "" && !strings.HasPrefix(firstLine(out), cause) { + out = strings.TrimRight(cause+"\n"+out, "\n") } return out } +// MaxCause is the most a cause in a failure's words holds: one long line (the 1901 review: a cause of a kilobyte could +// push the words past MaxWords). +const MaxCause = 1024 + +// dropSummary drops go test's summary lines from a failure's output ("PASS", "FAIL", "ok ", "FAIL "), and says +// whether a "PASS" was among them: the package's tests passed before it failed. +func dropSummary(out string) (string, bool) { + passed := false + lines := strings.Split(out, "\n") + kept := lines[:0] + for _, l := range lines { + // go's exact forms only (review round 1): "PASS" or "FAIL" alone, "ok \t\t