diff --git a/modules/mesh-delivery-checker/internal/check/binaries.go b/modules/mesh-delivery-checker/internal/check/binaries.go index 5329c61d..1835be61 100644 --- a/modules/mesh-delivery-checker/internal/check/binaries.go +++ b/modules/mesh-delivery-checker/internal/check/binaries.go @@ -36,7 +36,7 @@ func (st *state) checkBinariesOf(t *target) verdictPtr { found = append(found, p.Path+": a compiled program is build output: remove it and add it to .gitignore (issue 1577)") } if len(found) > 0 { - return failed(t.node, fmt.Sprintf("%s: %s", key, strings.Join(found, "; "))) + return st.fail(t.node, fmt.Sprintf("%s: %s", key, strings.Join(found, "; "))) } return nil } 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 d4ba7709..162f19b5 100644 --- a/modules/mesh-delivery-checker/internal/check/every_failure_test.go +++ b/modules/mesh-delivery-checker/internal/check/every_failure_test.go @@ -23,7 +23,7 @@ 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: rules.NewTally(), nodes: map[string]bool{}} + st := &state{j: &Examiner{}, c: &Case{}, levels: rules.NewTally()} v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests}) 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. diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index e0c03e66..4965b20f 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -550,6 +550,10 @@ func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 { t.Errorf("%d digest and test steps ran after a conflict", n) } + // 1921/D1c: the node that ran the step is a fact, so the verdict judged from the facts names its machine. + if m := out.Verdict.Machines; len(m) != 1 || m[0] != "shanks" { + t.Errorf("machines = %v, want the node that ran the step", m) + } } func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // 1287/AC4 diff --git a/modules/mesh-delivery-checker/internal/check/graph_cycle.go b/modules/mesh-delivery-checker/internal/check/graph_cycle.go index b3209485..1aa6804a 100644 --- a/modules/mesh-delivery-checker/internal/check/graph_cycle.go +++ b/modules/mesh-delivery-checker/internal/check/graph_cycle.go @@ -51,7 +51,7 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { } change, err := read(req.Cases[0].Manifests) if err != nil { - return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) + return st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) } cycle := graph.OfManifests(change).Cycle() if cycle == nil { @@ -71,6 +71,6 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { return nil } } - return fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+ + return st.fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+ "good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words)) } diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index e9981b10..2826afd7 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -50,8 +50,6 @@ const ( // verdictPtr is a verdict of failure a step of the check ends with. type verdictPtr = *delivery.Verdict -func failed(machine, words string) verdictPtr { return fail(machine, words) } - // Asks puts a check's ask on its build seat's stream and waits for its step (buildrun.Asker; ADR 0525 §1, §6): the // checker chooses no node, starts no step and holds none; whichever build node pulls the ask runs the check. type Asks interface { @@ -236,7 +234,6 @@ type state struct { 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). @@ -298,16 +295,16 @@ 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: rules.NewTally(), nodes: map[string]bool{}, + st := &state{j: j, c: c, levels: rules.NewTally(), 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) + // Each failure is a fact recorded where it is met; the verdict is the judging rules' of the facts. + st.judgeAll(ctx) st.takeTryCandidate(ctx) - 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 + st.judgeManifests(ctx) + st.refuseCycle(ctx) + return st.finish(ctx, rules.Judge(st.factsOf())), nil } // finish is the outcome of the verdict with the state of the builds the candidate's modules carry and the assigner's @@ -361,9 +358,8 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { } } -// outcome fills the verdict's levels, times, machines and composition from what ran. +// outcome is the verdict judged from the facts with the slowest tests, the siblings read and what judged a build module. func (st *state) outcome(v delivery.Verdict) Outcome { - 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 { @@ -380,6 +376,19 @@ func (st *state) outcome(v delivery.Verdict) Outcome { return out } +// fail records a failure the asking met as a fact of the check, where it is met, and answers it as a verdict of failure +// at a machine: the verdict is the judging rules' of the facts (rules.Judge), so a failure only returned is no failure +// (milestone 47/B0). +func (st *state) fail(machine, words string) *delivery.Verdict { + st.facts = append(st.facts, rules.Step{Failed: &delivery.FailedFirst{Machine: machine, Words: words}}) + return fail(machine, words) +} + +// factsOf are the check's facts as met: what the checking program judges. +func (st *state) factsOf() rules.Facts { + return rules.Facts{V: rules.FactsVersion, Steps: st.facts, CloneMS: st.clone.Milliseconds(), Notes: st.c.Notes()} +} + // fail is a verdict of failure at a machine. func fail(machine, words string) *delivery.Verdict { v := failure(machine, words) @@ -416,7 +425,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { for _, p := range pulls { if ctx.Err() != nil { if len(failures) == 0 { // an ask that ended with the context said it already - failures = append(failures, fail(delivery.CheckerModule, ctx.Err().Error())) + failures = append(failures, st.fail(delivery.CheckerModule, ctx.Err().Error())) } break } @@ -435,7 +444,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { if bm == buildrun.GoBuildModule { beside, err := st.j.besideFor(ctx, st.c, p) if err != nil { - failures = append(failures, fail(forgeMachine(err, bm), err.Error()+unheardWords(err))) + failures = append(failures, st.fail(forgeMachine(err, bm), err.Error()+unheardWords(err))) continue } if len(beside) > 0 { @@ -495,7 +504,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l err = fmt.Errorf("no node record is on the bus") } if err != nil { - return fail(t.module, fmt.Sprintf("the check of %s#%d is not asked: the nodes' names the names check needs cannot be read: %v", p.Repository, p.Number, err)) + return st.fail(t.module, fmt.Sprintf("the check of %s#%d is not asked: the nodes' names the names check needs cannot be read: %v", p.Repository, p.Number, err)) } job.NodeNames = names } @@ -524,7 +533,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l st.c.Progress.Set(what, "") st.c.Asked(a.ID) if err := st.j.Asks.Put(ctx, a); err != nil { - return fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err)) + return st.fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err)) } waiting := ctx if watch := st.c.Watch; watch != nil { @@ -536,9 +545,9 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l step, err = st.j.Asks.Wait(waiting, a) if err != nil { if ctx.Err() != nil { - return fail(t.module, ctx.Err().Error()) + return st.fail(t.module, ctx.Err().Error()) } - return fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err)) + return st.fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err)) } d, declined := delivery.DeclinedOf(step) if !declined { @@ -559,7 +568,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if lacks == "" { lacks = strings.Join(d.Lacks, ", ") // a node off the agreed toolchain declined it (ADR 0584) } - return fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)", + return st.fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)", p.Repository, p.Number, lacks, bound, step.Node)) } st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", ")) @@ -572,23 +581,23 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l took = step.Ended.Sub(step.Started) } t.node = step.Node - st.nodes[step.Node] = true + st.facts = append(st.facts, rules.Step{Repository: p.Repository, Number: p.Number, Module: t.module, Node: step.Node}) st.c.Progress.Set(fmt.Sprintf("judge the check of %s#%d", p.Repository, p.Number), step.Node) if step.State != delivery.StepPassed { - return fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason)) + return st.fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason)) } var answer delivery.CheckAnswer if err := json.Unmarshal(step.Result, &answer); err != nil { - return fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result)) + return st.fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result)) } if err := json.Unmarshal(answer.Fetch, &t.fetched); err != nil || t.fetched.Workspace == "" { - return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch)) + return st.fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch)) } if len(t.fetched.Conflicts) > 0 { if !st.narrowTry() { st.j.writePreview(ctx, p, append(*merged, t.fetched)) } - return fail(step.Node, fmt.Sprintf("%s#%d %s%s", p.Repository, p.Number, ConflictWords, strings.Join(t.fetched.Conflicts, ", "))) + return st.fail(step.Node, fmt.Sprintf("%s#%d %s%s", p.Repository, p.Number, ConflictWords, strings.Join(t.fetched.Conflicts, ", "))) } t.changed = t.fetched.Changed if !changesBuildGo(p.Repository, t.changed) { @@ -605,7 +614,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if placed { if len(la.Unmet) > 0 { st.clone += max(0, took-time.Duration(ran*float64(time.Second))) - return fail(step.Node, fmt.Sprintf("the %s level of %s#%d left %s untested on %s, which was asked for a node with %s", + return st.fail(step.Node, fmt.Sprintf("the %s level of %s#%d left %s untested on %s, which was asked for a node with %s", la.Level, p.Repository, p.Number, unmetNames(la.Unmet), step.Node, strings.Join(needs, ", "))) } la.Kept = nil // it names its packages and takes no other's kept verdict; this is only a guard against an older module diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index 3b192271..fc63f5fb 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -112,7 +112,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { at = "mesh-delivery's trunk " + commit } if commit == "" { - return fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known") + return st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known") } // The list of predefined keys the manifests are judged against (ADR 0578 §2): the catalogue member's predefined.json // when the change alters it, else the one the indexer keeps. @@ -169,7 +169,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { st.c.note("warnings of the indexer's judge on manifests the change leaves as they are (ADR 0579 §2): " + strings.Join(warned, " | ")) } if len(refused) > 0 { - return fail(goModule, strings.Join(refused, "\n")) + return st.fail(goModule, strings.Join(refused, "\n")) } st.c.note(fmt.Sprintf("the manifests of %s are accepted by the indexer's judge at %s", strings.Join(touched, ", "), at)) return nil @@ -180,7 +180,7 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string, versions delivery.PackageVersions) (change, trunk composer.CaseVerdict, v *delivery.Verdict) { raw, err := json.Marshal(req) if err != nil { - return change, trunk, fail(goModule, err.Error()) + return change, trunk, st.fail(goModule, err.Error()) } job := delivery.ComposeJob{Fetch: withPackages(map[string]any{"repository": DeliveryRepository, "commit": commit}, versions), Dir: JudgeDir, Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at} @@ -189,15 +189,15 @@ func (st *state) runJudge(ctx context.Context, req composer.Request, touched []s st.c.Progress.Set(fmt.Sprintf("the judge ask %s of the manifests of %s", a.ID, strings.Join(touched, ", ")), "") st.c.Asked(a.ID) if err := st.j.Asks.Put(ctx, a); err != nil { - return change, trunk, fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error()) + return change, trunk, st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error()) } step, err := st.j.Asks.Wait(ctx, a) if err != nil { - return change, trunk, fail(goModule, "the judge of the change's manifests: "+err.Error()) + return change, trunk, st.fail(goModule, "the judge of the change's manifests: "+err.Error()) } change, trunk, why := judgeAnswer(step) if why != "" { - return change, trunk, fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why)) + return change, trunk, st.fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why)) } return change, trunk, nil } @@ -221,7 +221,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques for _, module := range touched { m, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest) if err != nil { - return fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err)) + return st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err)) } for _, c := range m.Claims { if c.Name == CatalogSeat { @@ -241,7 +241,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques trunk = st.j.Trunks.Heads()[DeliveryRepository] } if trunk == "" { - return fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known") + return st.fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known") } var v *delivery.Verdict judgedAt = "mesh-delivery's trunk " + trunk @@ -262,7 +262,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques problems = append(problems, "the manifest validator does not know its fields "+r.SetAside+", so it waits") } if len(problems) > 0 { - return fail(goModule, fmt.Sprintf("the indexer %s, the holder of the seat %s, would never run: the trunk's manifest validator at %s does not accept its manifest: %s", + return st.fail(goModule, fmt.Sprintf("the indexer %s, the holder of the seat %s, would never run: the trunk's manifest validator at %s does not accept its manifest: %s", indexer, CatalogSeat, judgedAt, strings.Join(problems, "; "))) } st.c.note(fmt.Sprintf("the new indexer's manifest of %s is accepted by the trunk's manifest validator at %s", indexer, judgedAt))