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
283 lines
12 KiB
Go
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
|
|
}
|