Files
mesh-delivery/modules/mesh-delivery-checker/rules/words.go
T
jschoubben 2159375873 Merge the trunk: B0's facts and checking program carry the registry's steps (issue 1921, milestone 47/G D3-D11)
B0's shape wins: the running checker asks and records facts, the checking program
decides. The registry's types win: the rules and the program read the generated
checker contract and the domain SDK, and no mesh-sdk delivery import is left.
The registry's series, renames and unparsed seat definitions become facts with
their refusals as rules; the tagged versions ride on the case. The program reads
the heads' seat definitions through sdk/seatdefs and the running checker lays them
over its reader's list by seat, reading no file of the heads: the reader's list
is larger than a run input's bound.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 02:57:21 +01:00

283 lines
12 KiB
Go

package rules
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The words rules: how a failed test, a failed package and a list of failures are worded in a verdict (issue 1901, issue
// 1921). Moved whole from the running checker's package (milestone 47/B0): they are the checking program's.
// FailedName names a failure: its package and test.
func FailedName(f Failure) string {
if f.Test == "" {
return f.Package
}
return f.Package + "." + f.Test
}
// MaxWords is the most a failed test's words in a verdict hold (issue 1901). **The words keep the failure's last lines**,
// where a test says why it failed: the first line is often something it logged before (the group r4g verdict kept a
// "judged against" line and lost the t.Fatalf after it), and the 120 characters kept before cut a docker command's echo
// before docker's own error. Four kilobytes holds a long message and is small beside a verdict's other parts.
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. **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 {
return FailureWordsAfter(f, "")
}
// FailureWordsAfter is FailureWords with the line loud put first, within the same MaxWords: what the checker itself says of
// the failure (that the build module hid it), which the status line then names.
func FailureWordsAfter(f Failure, loud string) string {
out, passed := DropSummary(strings.TrimSpace(f.Output))
cause := strings.TrimSpace(f.Cause)
if cause == "" {
cause = CauseIn(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 loud line, the cause and their newlines, the marker and the kept lines.
budget := MaxWords
if cause != "" {
budget -= len(cause) + 1
}
if loud != "" {
budget -= len(loud) + 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")
}
if loud != "" {
out = strings.TrimRight(loud+"\n"+out, "\n")
}
return out
}
// HidWords is the checker's loud line for a failure the build module sent hiding its cause (issue 1901, its thorough
// fix): with no cause, or a passing line as its cause, under the failure contract (buildstep.Hidden). It names the build
// module and the version it says it is (Tested.By, "<module> with mesh-sdk <version>"); one from before the contract
// says nothing of itself. "" when the failure keeps the contract. The check fails either way: only its words change.
func HidWords(module, by string, f Failure) string {
why := buildstep.Hidden(f)
if why == "" {
return ""
}
version := "a version before the failure contract"
if module == "" {
module = "an unnamed build module"
}
if m, v, ok := strings.Cut(by, " with "); ok && m != "" {
module, version = m, v
}
return fmt.Sprintf("the build module hid the failure (%s at %s): %s", module, version, why)
}
// 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 <pkg>", "FAIL <pkg>"), 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<pkg>\t<time>", "FAIL\t<pkg>\t<time>".
switch t := strings.TrimRight(l, "\r"); {
case t == "PASS":
passed = true
case t == "FAIL", strings.HasPrefix(t, "ok \t"), strings.HasPrefix(t, "FAIL\t"):
default:
kept = append(kept, l)
}
}
return strings.TrimSpace(strings.Join(kept, "\n")), passed
}
// CauseIn is a race the race detector reported, as its heading, its access and the frame that made it ("WARNING: DATA
// RACE: Write at 0x... by goroutine 9 in pkg.F.func1()"); else the first "panic:", "fatal error:" or "signal:" line of a
// failure's output, in that order; or "". **Only lines at column 0**, where go prints the runtime's: a test's indented log
// line holding the same words is not the cause (review round 1).
func CauseIn(out string) string {
lines := strings.Split(out, "\n")
for i, l := range lines {
if strings.TrimRight(l, "\r") != "WARNING: DATA RACE" {
continue
}
words := "WARNING: DATA RACE"
if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) != "" {
words += ": " + strings.TrimSuffix(strings.TrimSpace(lines[i+1]), ":")
if i+2 < len(lines) && strings.HasPrefix(lines[i+2], " ") {
words += " in " + strings.TrimSpace(lines[i+2])
}
}
return words
}
for _, prefix := range []string{"panic: ", "fatal error: ", "signal: "} {
for _, l := range lines {
if l = strings.TrimRight(l, "\r"); strings.HasPrefix(l, prefix) {
return l
}
}
}
return ""
}
// FirstLine is the first non-empty line of a failure's words.
func FirstLine(s string) string {
for _, l := range strings.Split(s, "\n") {
if l = strings.TrimSpace(l); l != "" {
return l
}
}
return ""
}
// MaxFailureLine is the most bytes of a failure's line in a verdict's Failures: the first line of its words, which is
// its cause where the build module told one apart (issue 1901). Failed keeps the first failure's words whole.
const MaxFailureLine = 300
// LevelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole,
// and Failures every failed test of the level's results with its first line, at most meshdeliverycheckerclient.MaxFailures and an entry
// saying how many more (issue 1921).
func LevelFailure(repository, module, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue {
v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}
var all []FailedFirst
for _, res := range results {
if len(res.Failed) == 0 {
why := res.Why
if why == "" {
why = "the " + level + " level did not pass"
}
all = append(all, FailedFirst{Test: level + ": " + repository, Words: why})
continue
}
for _, f := range res.Failed {
name := FailedName(f)
if module == buildrun.NodeBuildModule && f.Package == "" {
name = res.Module
}
all = append(all, FailedFirst{Test: level + ": " + name, Words: FailureWordsAfter(f, HidWords(module, res.By, f))})
}
}
if len(all) == 0 { // no result: a level said failed with nothing to name
all = append(all, FailedFirst{Test: level + ": " + repository, Words: "the " + level + " level did not pass"})
}
first := all[0]
v.Failed = &first
v.Failures = failureItems(all) // whole: Settled cuts them to their first lines once every failure of the check is joined
return &v
}
// JoinFailures is one failed verdict of every failure of vs, in the order given (a nil one passed), unsettled: Failed is
// the first whole, and Failures every failure whole, or none when the one failure is no level's (a conflict, a refused
// step: one line, as before issue 1921). Nil when none failed.
func JoinFailures(vs ...*meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue {
var out *meshdeliverychecker.VerdictsByGroupHeadsValue
var all []FailedFirst
listed := false
for _, v := range vs {
if v == nil {
continue
}
if out == nil {
out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: v.Failed}
}
if len(v.Failures) > 0 {
all, listed = append(all, failedList(v.Failures)...), true
continue
}
all = append(all, failedOf(*v))
}
if out != nil && (listed || len(all) > 1) {
out.Failures = failureItems(all)
}
return out
}
// Settled is a joined verdict as it is written: each failure of its list cut to its first line, at most
// meshdeliverycheckerclient.MaxFailures and then how many more (FailureLines).
func Settled(v *meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if v != nil && len(v.Failures) > 0 {
v.Failures = failureItems(FailureLines(failedList(v.Failures)))
}
return v
}
// AddFailure is a Settled list of failures with one more at its end: the first failure is put first when the list is
// empty (a verdict of one failure keeps none), and past meshdeliverycheckerclient.MaxFailures the last entry counts one more.
func AddFailure(v *meshdeliverychecker.VerdictsByGroupHeadsValue, f FailedFirst) {
if len(v.Failures) == 0 {
v.Failures = failureItems(FailureLines([]FailedFirst{failedOf(*v)}))
}
if n := len(v.Failures); n > meshdeliverycheckerclient.MaxFailures {
v.Failures[n-1].Test = fmt.Sprintf("and %d more", FailedCount(*v)-meshdeliverycheckerclient.MaxFailures+1)
return
}
v.Failures = append(v.Failures, failureItems(FailureLines([]FailedFirst{f}))...)
}
// FailureLines is every failure with the first line of its words, cut to MaxFailureLine, at most meshdeliverycheckerclient.MaxFailures
// and then one entry saying how many more failed.
func FailureLines(all []FailedFirst) []FailedFirst {
n := min(len(all), meshdeliverycheckerclient.MaxFailures)
out := make([]FailedFirst, 0, n+1)
for _, f := range all[:n] {
out = append(out, FailedFirst{Test: f.Test, Machine: f.Machine, Words: ClipLine(FirstLine(f.Words), MaxFailureLine)})
}
if more := len(all) - n; more > 0 {
out = append(out, FailedFirst{Test: fmt.Sprintf("and %d more", more)})
}
return out
}
// ClipLine cuts a line to at most n bytes on a whole character, marking the cut.
func ClipLine(s string, n int) string {
if len(s) <= n {
return s
}
s = strings.ToValidUTF8(s[:n-3], "")
return s + "..."
}
// FailedCount is how many failures a verdict names, the ones after meshdeliverycheckerclient.MaxFailures counted too.
func FailedCount(v meshdeliverychecker.VerdictsByGroupHeadsValue) int {
n := len(v.Failures)
if n > meshdeliverycheckerclient.MaxFailures {
var more int
if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil {
return meshdeliverycheckerclient.MaxFailures + more
}
}
return n
}