From 03bb56481aff9debb7ddf8b11fc028ec67252e16 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 22:12:47 +0100 Subject: [PATCH 01/17] Move the level rules and the failure words into the checker's rules package (issue 1921) The checking program judges a check from its facts (milestone 47/B0, ADR 0644, ADR 0656): the rules package holds Facts v1, the level rule, the failure words and the verdict's outcome, and the running checker records each level answer as a fact and calls the rules. A row of the every-failure tests judges the gathered facts and finds the running checker's verdict. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../internal/check/builds.go | 3 +- .../internal/check/checker.go | 3 +- .../internal/check/composition.go | 3 +- .../internal/check/every_failure_test.go | 25 +- .../internal/check/failure_words_test.go | 19 +- .../internal/check/judge.go | 374 ++---------------- .../internal/check/line.go | 17 +- .../internal/check/package_cause_test.go | 21 +- .../internal/check/results.go | 60 +-- modules/mesh-delivery-checker/rules/facts.go | 209 ++++++++++ .../mesh-delivery-checker/rules/results.go | 59 +++ modules/mesh-delivery-checker/rules/words.go | 280 +++++++++++++ 12 files changed, 628 insertions(+), 445 deletions(-) create mode 100644 modules/mesh-delivery-checker/rules/facts.go create mode 100644 modules/mesh-delivery-checker/rules/results.go create mode 100644 modules/mesh-delivery-checker/rules/words.go diff --git a/modules/mesh-delivery-checker/internal/check/builds.go b/modules/mesh-delivery-checker/internal/check/builds.go index 2afa2d42..82e79c2c 100644 --- a/modules/mesh-delivery-checker/internal/check/builds.go +++ b/modules/mesh-delivery-checker/internal/check/builds.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "maps" + "mesh-delivery-checker/rules" "slices" "sort" "strings" @@ -160,7 +161,7 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string v.State, v.Failed = delivery.VerdictFail, f continue } - addFailure(v, f) + rules.AddFailure(v, f) } } if len(late) > 0 && ctx.Err() == nil { diff --git a/modules/mesh-delivery-checker/internal/check/checker.go b/modules/mesh-delivery-checker/internal/check/checker.go index 4e8e91bc..193b7147 100644 --- a/modules/mesh-delivery-checker/internal/check/checker.go +++ b/modules/mesh-delivery-checker/internal/check/checker.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "maps" + "mesh-delivery-checker/rules" "slices" "sort" "strings" @@ -449,7 +450,7 @@ func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []P continue } o := delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "error", - Target: strings.TrimSuffix(ch.Link, "/") + "/" + key, Text: clipLine(words, MaxLine)} + Target: strings.TrimSuffix(ch.Link, "/") + "/" + key, Text: rules.ClipLine(words, MaxLine)} data, err := delivery.EncodeCheckerOwed(o) if err != nil { ch.logf("%s: the status of %s cannot be written: %v", key, okey, err) diff --git a/modules/mesh-delivery-checker/internal/check/composition.go b/modules/mesh-delivery-checker/internal/check/composition.go index 0501844b..18944c34 100644 --- a/modules/mesh-delivery-checker/internal/check/composition.go +++ b/modules/mesh-delivery-checker/internal/check/composition.go @@ -1,6 +1,7 @@ package check import ( + "mesh-delivery-checker/rules" "strings" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -20,7 +21,7 @@ func withComposition(v *delivery.Verdict, comp delivery.CompositionVerdict) { } f := delivery.FailedFirst{Machine: machine, Words: comp.Words} if v.State == delivery.VerdictFail { - addFailure(v, f) // the tests failed too: the composition is named after them (the operator, 2026-11-02) + rules.AddFailure(v, f) // the tests failed too: the composition is named after them (the operator, 2026-11-02) return } v.State, v.Failed = delivery.VerdictFail, f diff --git a/modules/mesh-delivery-checker/internal/check/every_failure_test.go b/modules/mesh-delivery-checker/internal/check/every_failure_test.go index 444e9743..d4ba7709 100644 --- a/modules/mesh-delivery-checker/internal/check/every_failure_test.go +++ b/modules/mesh-delivery-checker/internal/check/every_failure_test.go @@ -3,6 +3,8 @@ package check import ( "encoding/json" "fmt" + "mesh-delivery-checker/rules" + "reflect" "strings" "testing" @@ -21,9 +23,18 @@ func failingResult(t *testing.T, tests int, failed ...Failure) json.RawMessage { func judgeLevel(t *testing.T, tests ...json.RawMessage) (*delivery.Verdict, *state) { t.Helper() - st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}} + st := &state{j: &Examiner{}, c: &Case{}, levels: rules.NewTally(), nodes: map[string]bool{}} v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests}) - return settled(joinFailures(v)), st + v = rules.Settled(rules.JoinFailures(v)) + // 1921 (B0, D1a): the facts the running checker gathered, judged by the rules, give the same failures and counts. + judged := rules.Judge(rules.Facts{V: rules.FactsVersion, Steps: st.facts}) + switch { + case v == nil && judged.State != delivery.VerdictPass, v != nil && (judged.Failed != v.Failed || !reflect.DeepEqual(judged.Failures, v.Failures)): + t.Errorf("the rules judge the facts %+v, the running checker %+v", judged, v) + case judged.Levels[delivery.LevelIntegration] != (delivery.LevelResult{Run: st.levels[delivery.LevelIntegration].Run}): + t.Errorf("the rules count %+v, the running checker %+v", judged.Levels, *st.levels[delivery.LevelIntegration]) + } + return v, st } // Issue 1921: two failing tests in two packages are two entries of the verdict's Failures and two lines of a try's @@ -48,7 +59,7 @@ func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) { if len(v.Failures) != 2 || v.Failed.Test != "integration: x/a.TestA" || !strings.Contains(v.Failed.Words, "a_test.go:3: logged") { t.Errorf("%s: verdict %+v", name, v) } - if run := st.levels[delivery.LevelIntegration].run; run != 10 { + if run := st.levels[delivery.LevelIntegration].Run; run != 10 { t.Errorf("%s: %d tests counted, want all 10", name, run) } if _, err := delivery.EncodeVerdict(delivery.Verdict{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil { @@ -58,7 +69,7 @@ func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) { } // Issue 1921: past delivery.MaxFailures the list names the first fifty and how many more, the status line says how many -// failed and names the first, cut so "and N more" stays whole, and each entry's line is cut to MaxFailureLine. +// failed and names the first, cut so "and N more" stays whole, and each entry's line is cut to rules.MaxFailureLine. func TestFailuresAreCappedWithHowManyMore(t *testing.T) { var fs []Failure for i := range 60 { @@ -68,10 +79,10 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) { if len(v.Failures) != delivery.MaxFailures+1 || v.Failures[delivery.MaxFailures].Test != "and 10 more" { t.Fatalf("%d entries, last %+v", len(v.Failures), v.Failures[len(v.Failures)-1]) } - if len(v.Failures[0].Words) > MaxFailureLine { + if len(v.Failures[0].Words) > rules.MaxFailureLine { t.Errorf("an entry's line is %d bytes", len(v.Failures[0].Words)) } - if n := failedCount(*v); n != 60 { + if n := rules.FailedCount(*v); n != 60 { t.Errorf("failedCount = %d, want 60", n) } cs := &Case{Pulls: []Pull{{Repository: "novox/x", Head: "0123456789abcdef", Trunk: "fedcba9876543210"}}} @@ -81,7 +92,7 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) { } // A composition failing after them is counted in the last entry (the operator, 2026-11-02). withComposition(v, delivery.CompositionVerdict{State: delivery.CompositionFail, Machine: "g14", Words: "route-proxy is defined twice"}) - if n := failedCount(*v); n != 61 || v.Failed.Test != "integration: x/p0.TestT" { + if n := rules.FailedCount(*v); n != 61 || v.Failed.Test != "integration: x/p0.TestT" { t.Errorf("after the composition failedCount = %d, failed %+v", n, v.Failed) } diff --git a/modules/mesh-delivery-checker/internal/check/failure_words_test.go b/modules/mesh-delivery-checker/internal/check/failure_words_test.go index 9c01774d..0d9edc4b 100644 --- a/modules/mesh-delivery-checker/internal/check/failure_words_test.go +++ b/modules/mesh-delivery-checker/internal/check/failure_words_test.go @@ -2,6 +2,7 @@ package check import ( "encoding/json" + "mesh-delivery-checker/rules" "strings" "testing" @@ -21,19 +22,19 @@ func fatalAfterLog() Failure { } func TestAFailedTestsVerdictKeepsItsWholeFailureMessage(t *testing.T) { // 1901/AC1 - v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{fatalAfterLog()}}) + v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}}) for _, want := range []string{"3 subscription(s) derive to a subject no emitter owns:", "a.b.c", "d.e.f", "g.h.i"} { if !strings.Contains(v.Failed.Words, want) { t.Errorf("words lack %q: %q", want, v.Failed.Words) } } - if len(v.Failed.Words) > MaxWords+200 { - t.Errorf("words are %d bytes, bound %d", len(v.Failed.Words), MaxWords) + if len(v.Failed.Words) > rules.MaxWords+200 { + t.Errorf("words are %d bytes, bound %d", len(v.Failed.Words), rules.MaxWords) } } func TestTheStatusLineNamesTheFailureNotTheFirstLoggedLine(t *testing.T) { // 1901/AC2 - v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{fatalAfterLog()}}) + v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{fatalAfterLog()}}) got := firstFailed(*v) if !strings.Contains(got, "3 subscription(s) derive to a subject no emitter owns:") || strings.Contains(got, "judged against") || strings.Contains(got, "\n") { t.Errorf("firstFailed = %q", got) @@ -41,7 +42,7 @@ func TestTheStatusLineNamesTheFailureNotTheFirstLoggedLine(t *testing.T) { // 19 // With no cause told apart (an older build module), the words still start at the first line, as before. f := fatalAfterLog() f.Cause = "" - v = levelFailure(&target{}, "unit", Tested{Failed: []Failure{f}}) + v = rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}}) if !strings.HasPrefix(afterHid(v.Failed.Words), "agreement_catalogue_test.go:130") { t.Errorf("words = %q", v.Failed.Words) } @@ -49,8 +50,8 @@ func TestTheStatusLineNamesTheFailureNotTheFirstLoggedLine(t *testing.T) { // 19 func TestWordsLongerThanTheBoundKeepTheirLastLines(t *testing.T) { // 1901/AC1 f := Failure{Output: strings.Repeat("log line of some length\n", 500) + "the cause is here"} - v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{f}}) - if !strings.HasSuffix(v.Failed.Words, "the cause is here") || len(v.Failed.Words) > MaxWords+200 { + v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}}) + if !strings.HasSuffix(v.Failed.Words, "the cause is here") || len(v.Failed.Words) > rules.MaxWords+200 { t.Errorf("%d bytes, ends %q", len(v.Failed.Words), v.Failed.Words[len(v.Failed.Words)-30:]) } _ = delivery.VerdictFail @@ -66,7 +67,7 @@ func TestAnOlderBuildModulesFailureIsAcceptedWithItsWholeOutput(t *testing.T) { if err := json.Unmarshal([]byte(old), &f); err != nil || f.Cause != "" { t.Fatalf("1901/AC3: err = %v, cause = %q", err, f.Cause) } - v := levelFailure(&target{}, "unit", Tested{Failed: []Failure{f}}) + v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}}) if afterHid(v.Failed.Words) != "x_test.go:1: logged\nx_test.go:2: the old kept lines" { t.Errorf("1901/AC3: words = %q", v.Failed.Words) } @@ -84,7 +85,7 @@ func TestAnOlderBuildModulesFailureIsAcceptedWithItsWholeOutput(t *testing.T) { {"a real cause", "mesh-build-node", by, "one plus one is not three", "not ok 3 - fails", ""}, } { t.Run(c.name, func(t *testing.T) { - v := levelFailure(&target{module: c.module}, "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}}) + v := rules.LevelFailure("", c.module, "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}}) if v.State != delivery.VerdictFail { t.Errorf("1901/AC8: the verdict is %q, want it failed", v.State) } diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 391d9f27..e9981b10 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "path" + "mesh-delivery-checker/rules" "slices" "sort" "strings" @@ -228,24 +228,17 @@ func (t *target) stepArgs(args map[string]any) map[string]any { return args } -// acc collects a level's counts across targets. -type acc struct { - run, kept, skipped int - elapsed, setup float64 - lint float64 - over bool - slowest []TestTime -} - // state is one judging. type state struct { j *Examiner c *Case targets []*target - levels map[string]*acc - nodes map[string]bool - clone time.Duration - slow []string + levels rules.Tally + // facts are the facts of the check met so far, in order: what the checking program judges (milestone 47/B0). + facts []rules.Step + nodes map[string]bool + clone time.Duration + slow []string // beside are the siblings read at their trunk's head, by repository (ADR 0548 §6). beside map[string]string // judgeHead is the group's mesh-delivery member head when its merge changes the judge (ADR 0578 §2); empty judges @@ -305,13 +298,13 @@ func (st *state) takeTryCandidate(ctx context.Context) { // Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot // be judged at all. func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { - st := &state{j: j, c: c, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}, + st := &state{j: j, c: c, levels: rules.NewTally(), nodes: map[string]bool{}, beside: map[string]string{}} // Every member and level runs to its end, and every manifest the change touches is judged by the indexer's judge (ADR // 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02). tests := st.judgeAll(ctx) st.takeTryCandidate(ctx) - if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx))); v != nil { + if v := rules.Settled(rules.JoinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx))); v != nil { return st.finish(ctx, *v), nil } return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil @@ -370,40 +363,8 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { // outcome fills the verdict's levels, times, machines and composition from what ran. func (st *state) outcome(v delivery.Verdict) Outcome { - if v.State == delivery.VerdictPass || len(v.Levels) == 0 { - v.Levels = map[string]delivery.LevelResult{} - for level, a := range st.levels { - v.Levels[level] = delivery.LevelResult{Run: a.run, Kept: a.kept, Skipped: a.skipped} - } - } - ms := func(s float64) int64 { return int64(s * 1000) } - u, i := st.levels[delivery.LevelUnit], st.levels[delivery.LevelIntegration] - v.Took = delivery.Took{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), Lint: ms(u.lint), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)} - for node := range st.nodes { - v.Machines = append(v.Machines, node) - } - sort.Strings(v.Machines) - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { - if st.levels[level].over { - v.OverBound = append(v.OverBound, level) - } - } - // The composition is made from the engines' answers (ADR 0593 §7): until it is, a verdict says it was not run (ADR 0484 clause 6). - v.Composition = &delivery.Composition{State: delivery.CompositionNotRun} - var slow []string - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { - a := st.levels[level] - if !a.over { - continue - } - sort.SliceStable(a.slowest, func(x, y int) bool { return a.slowest[x].Elapsed > a.slowest[y].Elapsed }) - for n, s := range a.slowest { - if n == 3 { - break - } - slow = append(slow, fmt.Sprintf("%s %s %.1fs", level, path.Base(s.Package)+"."+s.Test, s.Elapsed)) - } - } + rules.Outcome(&v, st.levels, st.clone.Milliseconds(), st.nodes) + slow := rules.Slowest(st.levels) out := Outcome{Verdict: v, Slowest: slow} if len(st.beside) > 0 { out.Beside = st.beside @@ -506,7 +467,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)} } } - return joinFailures(failures...) + return rules.JoinFailures(failures...) } // DefaultDeclineBound is an Examiner's DeclineBound when it names none: longer than a check runs, so a node with the @@ -675,7 +636,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l st.j.logf("%s#%d: %s on %s left untested; asked of a node with %s", p.Repository, p.Number, unmetOf(answer.Levels), step.Node, strings.Join(unmet, ", ")) failures = append(failures, st.askNeeding(ctx, t, merged, again, unmet, packages)) } - return joinFailures(failures...) + return rules.JoinFailures(failures...) } // unmetNames names the unmet packages and their needs. @@ -703,62 +664,30 @@ func unmetOf(levels []delivery.LevelAnswer) string { // one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's // running time (its tests' and setup's). func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer) (*delivery.Verdict, float64) { - level := la.Level - a := st.levels[level] - if a == nil { - return fail(t.node, fmt.Sprintf("the check of %s#%d answered the level %q, which is no level", t.pull.Repository, t.pull.Number, level)), 0 - } - var digests map[string]string - if t.module == buildrun.GoBuildModule && la.Digest != nil { - // The packages come as a list, never keyed by a package path (issue 1930); an answer stored before reads too. - digests = la.Digest.Map() - if t.runner != "" { - t.judgedBy(la.Digest.JudgedBy) // the installed module judged it: the runner is not used again - } + if t.runner != "" && t.module == buildrun.GoBuildModule && la.Digest != nil { + t.judgedBy(la.Digest.JudgedBy) // the installed module judged it: the runner is not used again } if t.runner != "" && la.Judged != "" { t.judgedBy(la.Judged) // the candidate judged the level: nothing is kept (ADR 0535 §4) } - for _, k := range la.Kept { - a.kept += k.Tests + v, elapsed, results := st.levels.Level(t.pull.Repository, t.pull.Number, t.module, t.node, la) + st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Node: t.node, Level: &la}) + var digests map[string]string + if t.module == buildrun.GoBuildModule && la.Digest != nil { + // The packages come as a list, never keyed by a package path (issue 1930); an answer stored before reads too. + digests = la.Digest.Map() } - elapsed := 0.0 - var failedResults []Tested - for _, raw := range la.Tests { - var res Tested - if err := json.Unmarshal(raw, &res); err != nil { - return fail(t.node, fmt.Sprintf("the %s level of %s#%d answered what is not a result: %v", level, t.pull.Repository, t.pull.Number, err)), elapsed - } + for _, res := range results { if t.runner != "" { t.judgedBy(res.JudgedBy) } - elapsed += res.Elapsed + res.Setup + res.Lint - if t.module == buildrun.NodeBuildModule { - if !res.NoTests { - a.run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10) - } - } else { - a.run += res.Tests - a.skipped += res.Skipped - a.slowest = append(a.slowest, res.Slowest...) - } - a.elapsed += res.Elapsed - a.setup += res.Setup - a.lint += res.Lint - a.over = a.over || res.OverBound if len(digests) > 0 { - if _, err := st.j.Kept.KeepPasses(ctx, t.pull.Repository, level, t.toolchain, res, digests, st.j.now()); err != nil { + if _, err := st.j.Kept.KeepPasses(ctx, t.pull.Repository, la.Level, t.toolchain, res, digests, st.j.now()); err != nil { st.j.logf("%s#%d: a passing package could not be kept: %v", t.pull.Repository, t.pull.Number, err) } } - if !res.Passed { - failedResults = append(failedResults, res) - } } - if len(failedResults) > 0 { - return levelFailure(t, level, failedResults...), elapsed - } - return nil, elapsed + return v, elapsed } // changesPackage says whether the merge of a pull request changes a file inside the named paths of one of its repository's @@ -780,258 +709,3 @@ func (j *Examiner) changesPackage(p Pull, merged []Fetched) bool { } return false } - -// 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, " with mesh-sdk "); 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 ", "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