The manifests, compiled-programs and composition rules judge facts in the checker's rules (issue 1921)
The running checker records the manifest validator's cases, each fetch's programs and the engines' answers as facts, and the rules package judges them, with the notes it gives a reader, so the checking program judges every rule the running checker judged. The module graph's cycle stays with the asking: its facts are whole manifests, over a run's input bound; G's program reads them from the heads. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -50,7 +50,7 @@ func run(args []string, getenv func(string) string, stdout, stderr io.Writer) in
|
||||
if err != nil {
|
||||
return refuse("%v", err)
|
||||
}
|
||||
line, err := json.Marshal(rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(facts)})
|
||||
line, err := json.Marshal(rules.Judged(facts))
|
||||
if err != nil {
|
||||
return refuse("the verdict cannot be written: %v", err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package check
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"mesh-delivery-checker/rules"
|
||||
)
|
||||
|
||||
// A change that adds or modifies a compiled program fails (issue 1577): it is build output, `go build` in a module's folder
|
||||
@@ -13,30 +14,20 @@ import (
|
||||
// tell, so the check does not refuse a program it did not look for.
|
||||
const NoProgramsReported = "the build module did not report programs: a compiled program in the change is not refused"
|
||||
|
||||
// checkBinariesOf fails a pull request whose change adds or modifies a compiled program, naming each file. It judges the
|
||||
// first fetch answer of the pull request, whichever build module gave it; a build module that reports no programs leaves a
|
||||
// warning on the check and passes it.
|
||||
func (st *state) checkBinariesOf(t *target) verdictPtr {
|
||||
// checkBinariesOf records what a pull request's first fetch answer read of compiled programs, whichever build module
|
||||
// gave it, as a fact the rules judge (rules.JudgePrograms); a build module that reports no programs leaves a warning on
|
||||
// the check and passes it.
|
||||
func (st *state) checkBinariesOf(t *target) {
|
||||
key := fmt.Sprintf("%s#%d", t.pull.Repository, t.pull.Number)
|
||||
if t.fetched.Merge == "" {
|
||||
return nil // no merge was made: nothing was changed to judge
|
||||
return // no merge was made: nothing was changed to judge
|
||||
}
|
||||
if t.fetched.Programs == nil {
|
||||
st.c.note(fmt.Sprintf("%s: %s (%s)", key, NoProgramsReported, t.module))
|
||||
st.c.warn(key, fmt.Sprintf("a compiled program is not refused: %s reported no programs", t.module))
|
||||
st.j.logf("%s: %s (%s)", key, NoProgramsReported, t.module)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
var found []string
|
||||
for _, p := range *t.fetched.Programs {
|
||||
if p.Commit != "" {
|
||||
found = append(found, fmt.Sprintf("%s: a compiled program added in %s and removed later: it stays in history; squash the branch (issue 1577)", p.Path, short12(p.Commit)))
|
||||
continue
|
||||
}
|
||||
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 st.fail(t.node, fmt.Sprintf("%s: %s", key, strings.Join(found, "; ")))
|
||||
}
|
||||
return nil
|
||||
st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Node: t.node,
|
||||
Programs: &rules.Programs{Key: key, Files: t.fetched.Programs}})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mesh-delivery-checker/rules"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -124,19 +123,20 @@ func (j *Examiner) buildWait() time.Duration {
|
||||
// awaitBuilds waits for the candidate's builds before the check composes (ADR 0603 §10): the check builds what the builder
|
||||
// builds of the candidate and composes with it. A failed build fails the check, naming the module and the builder's words;
|
||||
// a build not ended within the bound is named, and the check composes without it. It answers the words of the composition's
|
||||
// warnings: what the check composes without waiting on its build, and why.
|
||||
func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string {
|
||||
// warnings (what the check composes without waiting on its build, and why) and the failures of the builds that failed, for
|
||||
// the rules to take in (rules.JudgeComposition); failed says the check has failed already.
|
||||
func (st *state) awaitBuilds(ctx context.Context, failed bool) ([]string, []delivery.FailedFirst) {
|
||||
wait, words := st.buildsOfTheCheck(ctx)
|
||||
if len(wait) == 0 {
|
||||
return words
|
||||
return words, nil
|
||||
}
|
||||
modules := slices.Sorted(maps.Keys(wait))
|
||||
if v.State == delivery.VerdictFail {
|
||||
if failed {
|
||||
// The check has failed already: its red status is never held back by a build (the lead, 2026-11-03).
|
||||
return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", ")))
|
||||
return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", "))), nil
|
||||
}
|
||||
if st.j.BuildEnds == nil {
|
||||
return append(words, fmt.Sprintf("the builds of %s cannot be heard, so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", ")))
|
||||
return append(words, fmt.Sprintf("the builds of %s cannot be heard, so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "))), nil
|
||||
}
|
||||
bound := st.j.buildWait()
|
||||
waits := fmt.Sprintf("waits on the builder's builds of %s before it composes, at most %s (ADR 0603 §10)", strings.Join(modules, ", "), bound)
|
||||
@@ -147,25 +147,21 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string
|
||||
cancel()
|
||||
if err != nil {
|
||||
st.j.logf("%s: the builds of %s cannot be heard: %v", st.c.Key, strings.Join(modules, ", "), err)
|
||||
return append(words, fmt.Sprintf("the builds of %s cannot be heard (%v), so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "), err))
|
||||
return append(words, fmt.Sprintf("the builds of %s cannot be heard (%v), so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "), err)), nil
|
||||
}
|
||||
var late []string
|
||||
var builds []delivery.FailedFirst
|
||||
for _, module := range modules {
|
||||
b, done := ended[module]
|
||||
switch {
|
||||
case !done:
|
||||
late = append(late, module)
|
||||
case b.State == delivery.BuildFailed:
|
||||
f := delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)}
|
||||
if v.State != delivery.VerdictFail {
|
||||
v.State, v.Failed = delivery.VerdictFail, f
|
||||
continue
|
||||
}
|
||||
rules.AddFailure(v, f)
|
||||
builds = append(builds, delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)})
|
||||
}
|
||||
}
|
||||
if len(late) > 0 && ctx.Err() == nil {
|
||||
words = append(words, fmt.Sprintf("the builds of %s did not end within the build-wait-bound of %s, so the check composes without them (ADR 0603 §10)", strings.Join(late, ", "), bound))
|
||||
}
|
||||
return words
|
||||
return words, builds
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"mesh-delivery-checker/rules"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine.
|
||||
func withComposition(v *delivery.Verdict, comp delivery.CompositionVerdict) {
|
||||
v.Composition = comp.Composition()
|
||||
v.Took.Composition = comp.Took
|
||||
if comp.State == delivery.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil {
|
||||
v.Composition.Words = strings.Join(comp.Warnings, "; ") // passed with a warning (ADR 0593 §9)
|
||||
}
|
||||
if comp.State == delivery.CompositionFail {
|
||||
machine := comp.Machine
|
||||
if machine == "" {
|
||||
machine = "the composition"
|
||||
}
|
||||
f := delivery.FailedFirst{Machine: machine, Words: comp.Words}
|
||||
if v.State == delivery.VerdictFail {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"mesh-delivery-checker/rules"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -14,28 +15,43 @@ import (
|
||||
|
||||
// fakeCompositions answers a check's composition from memory, recording the keys asked.
|
||||
type fakeCompositions struct {
|
||||
v delivery.CompositionVerdict
|
||||
v rules.Composed
|
||||
err error
|
||||
asked []string
|
||||
// waits answers only when its context ends.
|
||||
waits bool
|
||||
}
|
||||
|
||||
func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) {
|
||||
func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (rules.Composed, error) {
|
||||
f.asked = append(f.asked, c.Key)
|
||||
if f.waits {
|
||||
<-ctx.Done()
|
||||
return delivery.CompositionVerdict{}, ctx.Err()
|
||||
return rules.Composed{}, ctx.Err()
|
||||
}
|
||||
return f.v, f.err
|
||||
}
|
||||
|
||||
// composedOn is the engines' answers of a composition by composer on the nodes, each composing it, taking took ms.
|
||||
func composedOn(composer string, took int64, nodes ...string) rules.Composed {
|
||||
c := rules.Composed{Group: "feat/x", Heads: "0123456789ab", Composer: composer, Nodes: nodes, Took: took, Answers: map[string]delivery.ComposeCandidateAnswer{}}
|
||||
for _, n := range nodes {
|
||||
c.Answers[n] = delivery.ComposeCandidateAnswer{Node: n, Verdict: delivery.ComposeCandidateComposed}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// refusedOn is the engines' answers of a composition the node refuses with the words.
|
||||
func refusedOn(composer, node, words string) rules.Composed {
|
||||
return rules.Composed{Group: "feat/x", Heads: "0123456789ab", Composer: composer, Nodes: []string{node},
|
||||
Answers: map[string]delivery.ComposeCandidateAnswer{node: {Node: node, Verdict: delivery.ComposeCandidateRefused, Refused: words}}}
|
||||
}
|
||||
|
||||
// 1297/AC3 (its unit half): a check whose tests pass takes the assigner's composition of it, by the check's own key,
|
||||
// into its verdict: a pass says it passed with the mesh-sdk and machines it composed, and is no longer "not run".
|
||||
func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) {
|
||||
t.Run("a change to a build module or base names the modules built on it", aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt)
|
||||
b := newBench(t)
|
||||
f := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}}
|
||||
f := &fakeCompositions{v: composedOn("v0.1.14-x", 40, "g14")}
|
||||
b.ex.Compositions = f
|
||||
cs := caseOf(t, pullOf(repoA, 7, head1))
|
||||
cs.FPSet = "fps-of-the-candidate"
|
||||
@@ -67,8 +83,7 @@ func compositionMentions(v delivery.Verdict, word string) bool {
|
||||
// 1297/AC2 at the checker: a failed composition fails the check, naming the machine it failed at and its words.
|
||||
func TestAFailedCompositionFailsTheCheck(t *testing.T) {
|
||||
b := newBench(t)
|
||||
b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x",
|
||||
Machine: "g14", Words: "g14 runs web, which the change removes from its source"}}
|
||||
b.ex.Compositions = &fakeCompositions{v: refusedOn("x", "g14", "g14 runs web, which the change removes from its source")}
|
||||
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
||||
if v.State != delivery.VerdictFail || v.Failed.Machine != "g14" || v.Composition == nil || v.Composition.State != delivery.CompositionFail {
|
||||
t.Fatalf("verdict = %+v", v)
|
||||
@@ -89,7 +104,7 @@ func TestACompositionNotHadIsNotRun(t *testing.T) {
|
||||
t.Fatalf("verdict = %+v", v)
|
||||
}
|
||||
b2 := newBench(t)
|
||||
f2 := &fakeCompositions{v: delivery.CompositionVerdict{State: delivery.CompositionFail, Words: "w", SDK: "x"}}
|
||||
f2 := &fakeCompositions{v: refusedOn("x", "g14", "w")}
|
||||
b2.ex.Compositions = f2
|
||||
cs := caseOf(t, pullOf(repoA, 7, head1))
|
||||
cs.Try = &TryOptions{}
|
||||
@@ -212,7 +227,7 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}}
|
||||
comps := &fakeCompositions{v: composedOn("x", 0, "g14")}
|
||||
b.ex.Compositions = comps
|
||||
var ends *fakeEnds
|
||||
if row.builds != nil {
|
||||
@@ -265,3 +280,5 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mesh-delivery-checker/rules"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -68,17 +69,18 @@ func composerHead(c *Case) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Compose asks every placed node and judges their answers (JudgeEngines). An error is only a case it cannot ask at all.
|
||||
func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) {
|
||||
// Compose asks every placed node and answers their answers, for the rules to judge (rules.JudgeEngines). An error is only a
|
||||
// case it cannot ask at all.
|
||||
func (e EngineCompositions) Compose(ctx context.Context, c *Case) (rules.Composed, error) {
|
||||
began := time.Now()
|
||||
key, err := delivery.CompositionKey(c.Group, c.HeadsHash)
|
||||
if err != nil {
|
||||
return delivery.CompositionVerdict{}, err
|
||||
return rules.Composed{}, err
|
||||
}
|
||||
var nodes []string
|
||||
if e.Placed != nil {
|
||||
if nodes, err = e.Placed(); err != nil {
|
||||
return delivery.CompositionVerdict{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err)
|
||||
return rules.Composed{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err)
|
||||
}
|
||||
}
|
||||
bound := e.Bound
|
||||
@@ -129,7 +131,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp
|
||||
} else if e.Composer != nil && e.Composer() != "" {
|
||||
composerName = e.Composer()
|
||||
}
|
||||
v := JudgeEngines(c.Group, c.HeadsHash, composerName, nodes, answers, silent)
|
||||
v := rules.Composed{Group: c.Group, Heads: c.HeadsHash, Composer: composerName, Nodes: nodes, Answers: answers, Silent: silent}
|
||||
if head != "" && e.Sandbox == nil {
|
||||
v.Warnings = append(v.Warnings, "the group changes the node-engine's composition code and was composed by the running engines")
|
||||
}
|
||||
@@ -137,57 +139,6 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// JudgeEngines is the check's composition verdict from the engines' answers (ADR 0593 §7, §9): failed when any node's
|
||||
// answer is refused, naming each node and each module refused with its words (the first refusing node is the machine);
|
||||
// passed otherwise. Every node in nodes without an answer (silent, by node with why, for the journal only) is named
|
||||
// with delivery.ComposeNoAnswer as a warning. composer names what composed, for the line. Pure.
|
||||
func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]delivery.ComposeCandidateAnswer, silent map[string]string) delivery.CompositionVerdict {
|
||||
v := delivery.CompositionVerdict{Group: group, Heads: heads, State: delivery.CompositionPass, SDK: composerName}
|
||||
sorted := append([]string(nil), nodes...)
|
||||
sort.Strings(sorted)
|
||||
var fails []string
|
||||
for _, node := range sorted {
|
||||
a, answered := answers[node]
|
||||
if !answered {
|
||||
v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer)
|
||||
continue
|
||||
}
|
||||
if a.Verdict == delivery.ComposeCandidateNotJudged {
|
||||
// Composed in the sandbox and not judged (ADR 0593 §9): named as no answer, with the composer's words; decided by the
|
||||
// verdict, never by the words (ADR 0601 §4's kind).
|
||||
v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer+" ("+a.Refused+")")
|
||||
continue
|
||||
}
|
||||
v.Machines = append(v.Machines, node)
|
||||
if a.Verdict != delivery.ComposeCandidateRefused {
|
||||
continue
|
||||
}
|
||||
if v.Machine == "" {
|
||||
v.Machine = node
|
||||
}
|
||||
var words []string
|
||||
if a.Refused != "" {
|
||||
words = append(words, a.Refused)
|
||||
}
|
||||
for _, m := range append(append([]delivery.ModuleComposed(nil), a.Modules...), a.Own) {
|
||||
if m.State == delivery.ModuleRefusedState {
|
||||
words = append(words, m.Module+": "+m.Words)
|
||||
}
|
||||
}
|
||||
if len(words) == 0 {
|
||||
words = append(words, "refused, naming no module")
|
||||
}
|
||||
fails = append(fails, "not composed on "+node+": "+strings.Join(words, "; "))
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ")
|
||||
}
|
||||
if len(sorted) == 0 {
|
||||
v.Warnings = append(v.Warnings, "no node is placed, so the change was composed on none")
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// inSandbox composes a node's answer again with the composer built from the node-engine member's head, over the input the
|
||||
// node's engine composed from: the node's answer becomes the sandbox's, refused with the composer's words where the
|
||||
// composer refuses the node.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"mesh-delivery-checker/rules"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -80,15 +81,17 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
}
|
||||
cs := caseOf(t, pullOf(repoA, 7, head1))
|
||||
key, _ := delivery.CompositionKey(cs.Group, cs.HeadsHash)
|
||||
var last rules.Composed // the engines' answers of the last compose, for the check's verdict to take in
|
||||
compose := func(e *fakeEngines, nodes ...string) delivery.CompositionVerdict {
|
||||
t.Helper()
|
||||
ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return nodes, nil }, Bound: 50 * time.Millisecond,
|
||||
Composer: func() string { return strings.Repeat("7", 40) }}
|
||||
v, err := ec.Compose(context.Background(), cs)
|
||||
raw, err := ec.Compose(context.Background(), cs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
last = raw
|
||||
return rules.JudgeEngines(raw)
|
||||
}
|
||||
|
||||
t.Run("one node refusing fails the composition, naming the node and the module", func(t *testing.T) {
|
||||
@@ -106,7 +109,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
t.Errorf("asked %+v, want the check's candidate %s and no input", e.asked, key)
|
||||
}
|
||||
var ver delivery.Verdict
|
||||
withComposition(&ver, v)
|
||||
rules.JudgeComposition(&ver, rules.Composition{Composed: &last})
|
||||
if ver.State != delivery.VerdictFail || ver.Failed.Machine != "laptop" {
|
||||
t.Errorf("the check's verdict = %+v", ver)
|
||||
}
|
||||
@@ -131,7 +134,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
}
|
||||
var ver delivery.Verdict
|
||||
ver.State = delivery.VerdictPass
|
||||
withComposition(&ver, v)
|
||||
rules.JudgeComposition(&ver, rules.Composition{Composed: &last})
|
||||
if ver.State != delivery.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+delivery.ComposeNoAnswer) {
|
||||
t.Errorf("the line = %q", composition(ver.Composition, 0, false))
|
||||
}
|
||||
@@ -152,7 +155,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
}}
|
||||
ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return []string{"anchor"}, nil }, Bound: time.Second,
|
||||
Sandbox: sb, Manifests: digests{"web": "sha256:" + strings.Repeat("c", 64)}, Published: published{}}
|
||||
v, err := ec.Compose(context.Background(), host)
|
||||
raw, err := ec.Compose(context.Background(), host)
|
||||
v := rules.JudgeEngines(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -186,7 +190,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
sb.v = func(node string) composer.Verdict {
|
||||
return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", Machines: map[string]composer.Machine{node: row.m}}}}
|
||||
}
|
||||
v, err = ec.Compose(context.Background(), host)
|
||||
raw, err = ec.Compose(context.Background(), host)
|
||||
v = rules.JudgeEngines(raw)
|
||||
named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+delivery.ComposeNoAnswer+" (its node-engine sends no running state for web)")
|
||||
if err != nil || (v.State == delivery.CompositionPass) != row.pass || named != row.pass {
|
||||
t.Errorf("%+v: verdict %+v, %v", row.m, v, err)
|
||||
@@ -195,7 +200,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
// Review finding 3 (ADR 0593 §9 lets only a node that does not answer pass): a composer at the head that cannot
|
||||
// run fails the composition, naming the head, never passing as a silent node.
|
||||
sb.err = errors.New("the build of mesh-compose failed")
|
||||
v, err = ec.Compose(context.Background(), host)
|
||||
raw, err = ec.Compose(context.Background(), host)
|
||||
v = rules.JudgeEngines(raw)
|
||||
if err != nil || v.State != delivery.CompositionFail || !strings.Contains(v.Words, head2) ||
|
||||
!strings.Contains(v.Words, "the build of mesh-compose failed") || len(v.Warnings) != 0 {
|
||||
t.Errorf("a composer that cannot run: %+v, %v", v, err)
|
||||
|
||||
@@ -91,7 +91,8 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) {
|
||||
t.Errorf("line %q", line)
|
||||
}
|
||||
// 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"})
|
||||
refused := refusedOn("x", "g14", "route-proxy is defined twice")
|
||||
rules.JudgeComposition(v, rules.Composition{Composed: &refused})
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -445,8 +445,7 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1
|
||||
func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
|
||||
for _, conflicts := range []bool{false, true} {
|
||||
b := newBench(t)
|
||||
comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x",
|
||||
Machine: "g14", Words: "route-proxy is defined twice"}, waits: conflicts}
|
||||
comps := &fakeCompositions{v: refusedOn("x", "g14", "route-proxy is defined twice"), waits: conflicts}
|
||||
b.ex.Compositions = comps
|
||||
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
|
||||
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
|
||||
@@ -480,7 +479,7 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
|
||||
got = append(got, firstFailed(delivery.Verdict{Failed: f}))
|
||||
}
|
||||
tests := []string{"unit: fixture/failing/bad.TestThreeIsFour", "integration: fixture/failing/bad.TestThreeIsFour"}
|
||||
want := append(append(slices.Clone(tests), tests...), "g14: route-proxy is defined twice") // both members fail their tests
|
||||
want := append(append(slices.Clone(tests), tests...), "g14: not composed on g14: route-proxy is defined twice") // both members fail their tests
|
||||
if conflicts {
|
||||
want = append([]string{"shanks: novox/mesh-sdk#12 does not merge into the trunk: files in conflict: a.go"}, tests...)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
@@ -29,14 +28,14 @@ type ManifestReader interface {
|
||||
|
||||
// refuseCycle fails the check for a cycle of the change's module graph, naming it; nil when the case touches no manifest,
|
||||
// when nothing reads manifests, or when there is none. A manifest that cannot be read fails the check, said, never passes it.
|
||||
func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
|
||||
func (st *state) refuseCycle(ctx context.Context) {
|
||||
if st.j.Manifests == nil || st.j.Read == nil || st.narrowTry() {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
trunk := st.j.Manifests.ManifestDigests()
|
||||
req, touched := judgeRequest(trunk, st.c.Pulls)
|
||||
if len(touched) == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
read := func(digests map[string]composer.Manifest) (map[string]catalogue.Manifest, error) {
|
||||
out := map[string]catalogue.Manifest{}
|
||||
@@ -51,12 +50,13 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
|
||||
}
|
||||
change, err := read(req.Cases[0].Manifests)
|
||||
if err != nil {
|
||||
return st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
|
||||
st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error())
|
||||
return
|
||||
}
|
||||
cycle := graph.OfManifests(change).Cycle()
|
||||
if cycle == nil {
|
||||
st.c.note("the change's module graph has no cycle")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
words := strings.Join(cycle, " -> ")
|
||||
if before, err := read(req.Cases[1].Manifests); err == nil {
|
||||
@@ -68,9 +68,9 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
|
||||
}
|
||||
if among.Cycle() != nil {
|
||||
st.c.note("the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
}
|
||||
return st.fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -151,9 +151,10 @@ type TryCandidates interface {
|
||||
TryCandidate(ctx context.Context, group, heads string, bound time.Duration) (delivery.Candidate, error)
|
||||
}
|
||||
|
||||
// CompositionReader makes a check's composition verdict; an error is only a case that cannot be composed at all.
|
||||
// CompositionReader composes a check: the engines' answers, which the rules judge (rules.JudgeEngines); an error is only
|
||||
// a case that cannot be composed at all.
|
||||
type CompositionReader interface {
|
||||
Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error)
|
||||
Compose(ctx context.Context, c *Case) (rules.Composed, error)
|
||||
}
|
||||
|
||||
func (j *Examiner) now() time.Time {
|
||||
@@ -234,8 +235,10 @@ 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
|
||||
clone time.Duration
|
||||
slow []string
|
||||
// composition is how the check composed, recorded after every step (compose).
|
||||
composition *rules.Composition
|
||||
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
|
||||
@@ -304,26 +307,24 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
|
||||
st.takeTryCandidate(ctx)
|
||||
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
|
||||
// composition of it, read also when the tests failed, so a failed composition is named beside them.
|
||||
func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome {
|
||||
out := st.outcome(v)
|
||||
st.composed(ctx, &out.Verdict)
|
||||
st.compose(ctx)
|
||||
answer := rules.Judged(st.factsOf())
|
||||
for _, n := range answer.Notes {
|
||||
st.c.note(n)
|
||||
}
|
||||
out := st.outcome(answer.Verdict)
|
||||
out.Builds = st.j.buildNotes(ctx, st.c)
|
||||
return out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// composed makes the check's composition from the engines' answers into its verdict (ADR 0593 §7, §9): a failed
|
||||
// composition fails the check, naming the machine it failed at; a node that did not answer is a warning. A try is not
|
||||
// composed (it has no candidate); a composition that cannot be made leaves it not run, said.
|
||||
func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
|
||||
// compose records how the check composed as the facts' composition, after every step (ADR 0593 §7, §9): kept by a carry,
|
||||
// not run (said why), or the engines' answers with the candidate's builds awaited first (ADR 0603 §10). A narrowed try
|
||||
// and an examiner that composes nothing record none.
|
||||
func (st *state) compose(ctx context.Context) {
|
||||
if st.c.Composition != nil && st.c.Try == nil {
|
||||
// A carry whose candidate's fingerprints did not change keeps the composition it passed (ADR 0548 §5).
|
||||
kept := *st.c.Composition
|
||||
v.Composition = &kept
|
||||
st.composition = &rules.Composition{Kept: &kept}
|
||||
st.c.note("the composition is kept: no fingerprint of a module it composed changed")
|
||||
return
|
||||
}
|
||||
@@ -331,7 +332,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
|
||||
return
|
||||
}
|
||||
if st.noCandidate != "" {
|
||||
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: st.noCandidate}
|
||||
st.composition = &rules.Composition{NotRun: st.noCandidate}
|
||||
return
|
||||
}
|
||||
if st.j.Compositions == nil {
|
||||
@@ -339,23 +340,24 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
|
||||
}
|
||||
if len(st.unpreviewed) > 0 {
|
||||
// An engine would only say it cannot read a manifest the indexer will not have: the check does not ask it.
|
||||
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "no preview of a merge of " +
|
||||
strings.Join(st.unpreviewed, ", ") + " (its head does not merge or its fetch did not answer), so the change cannot be composed"}
|
||||
st.composition = &rules.Composition{NotRun: "no preview of a merge of " + strings.Join(st.unpreviewed, ", ") +
|
||||
" (its head does not merge or its fetch did not answer), so the change cannot be composed"}
|
||||
return
|
||||
}
|
||||
words := st.awaitBuilds(ctx, v) // ADR 0603 §10: the check composes with the candidate's builds
|
||||
// ADR 0603 §10: the check composes with the candidate's builds; whether it failed already is the running rules' word
|
||||
// on the facts so far, an asking choice only (the verdict is the rules' of every fact).
|
||||
failed := rules.Judge(st.factsOf()).State == delivery.VerdictFail
|
||||
words, builds := st.awaitBuilds(ctx, failed)
|
||||
c := &rules.Composition{Waits: words, BuildsFailed: builds, FPSet: st.c.FPSet}
|
||||
st.composition = c
|
||||
comp, err := st.j.Compositions.Compose(ctx, st.c)
|
||||
if err != nil {
|
||||
// Said in the verdict, never left as a composition not yet run (review finding 4).
|
||||
st.j.logf("%s: the composition could not be made, so it is said as not run: %v", st.c.Key, err)
|
||||
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()}
|
||||
c.NotRun = "not made: " + err.Error()
|
||||
return
|
||||
}
|
||||
comp.Warnings = append(comp.Warnings, words...) // ADR 0603 §10
|
||||
withComposition(v, comp)
|
||||
if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun {
|
||||
v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
|
||||
}
|
||||
c.Composed = &comp
|
||||
}
|
||||
|
||||
// outcome is the verdict judged from the facts with the slowest tests, the siblings read and what judged a build module.
|
||||
@@ -386,7 +388,7 @@ func (st *state) fail(machine, words string) *delivery.Verdict {
|
||||
|
||||
// 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()}
|
||||
return rules.Facts{V: rules.FactsVersion, Steps: st.facts, CloneMS: st.clone.Milliseconds(), Composition: st.composition, Notes: st.c.Notes()}
|
||||
}
|
||||
|
||||
// fail is a verdict of failure at a machine.
|
||||
@@ -605,9 +607,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
|
||||
}
|
||||
if !placed { // the placed ask fetched the same merge: its preview was read from the first
|
||||
*merged = append(*merged, t.fetched)
|
||||
if v := st.checkBinariesOf(t); v != nil {
|
||||
failures = append(failures, v) // the levels still run: the verdict names every failure
|
||||
}
|
||||
st.checkBinariesOf(t) // the levels still run: the verdict names every failure
|
||||
}
|
||||
ran := 0.0
|
||||
for _, la := range answer.Levels {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mesh-delivery-checker/rules"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -31,7 +32,7 @@ const (
|
||||
JudgeCase = "change"
|
||||
JudgeTrunk = "trunk"
|
||||
// judgeBetween is how the judge names the problems between manifests that no one module's removal ends.
|
||||
judgeBetween = "(between manifests)"
|
||||
judgeBetween = rules.BetweenManifests
|
||||
)
|
||||
|
||||
// judgePaths are the paths of mesh-delivery whose change changes the judge: a group whose mesh-delivery member changes
|
||||
@@ -93,16 +94,17 @@ func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request,
|
||||
{Name: JudgeTrunk, Manifests: before}}}, slices.Sorted(maps.Keys(seen))
|
||||
}
|
||||
|
||||
// judgeManifests runs the indexer's judge over the case's manifests and fails the check for each touched manifest it
|
||||
// refuses. A case that touches no manifest, or an examiner that reads no trunk manifests, judges nothing; a judge that
|
||||
// cannot be asked or does not answer fails the check, said, never passes it.
|
||||
func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
|
||||
// judgeManifests runs the indexer's judge over the case's manifests and records its answers as a fact the rules judge
|
||||
// (rules.JudgeManifests), with the trunk validator's answer on a new indexer (ADR 0633 clause 6). A case that touches no
|
||||
// manifest, or an examiner that reads no trunk manifests, judges nothing; a judge that cannot be asked or does not answer
|
||||
// is a failure, said, never a pass.
|
||||
func (st *state) judgeManifests(ctx context.Context) {
|
||||
if st.j.Manifests == nil || st.narrowTry() {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.c.Pulls)
|
||||
if len(touched) == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
commit, at := st.judgeHead, "the group's mesh-delivery head "+st.judgeHead
|
||||
if commit == "" {
|
||||
@@ -112,7 +114,8 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
|
||||
at = "mesh-delivery's trunk " + commit
|
||||
}
|
||||
if commit == "" {
|
||||
return st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
|
||||
st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known")
|
||||
return
|
||||
}
|
||||
// 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.
|
||||
@@ -120,67 +123,24 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
|
||||
req.Predefined = list
|
||||
st.c.note("the manifests are judged against the predefined keys of " + from)
|
||||
}
|
||||
cv, trunkCase, v := st.runJudge(ctx, req, touched, commit, at, st.packagesOf(DeliveryRepository))
|
||||
if v != nil {
|
||||
return v
|
||||
cv, trunkCase, ok := st.runJudge(ctx, req, touched, commit, at, st.packagesOf(DeliveryRepository))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v := st.refuseUnmovableIndexer(ctx, req, touched, cv, at); v != nil {
|
||||
return v
|
||||
m := rules.Manifests{Touched: touched, At: at, Change: cv, Trunk: trunkCase}
|
||||
if !st.indexerOf(ctx, req, touched, cv, at, &m) {
|
||||
return
|
||||
}
|
||||
// A new or changed manifest is judged in full; of one the change leaves as it is, a problem the trunk already had is a
|
||||
// warning (it is not refused for a rule it already broke), and a problem the change causes in it, which the trunk case
|
||||
// lacks, fails the check as the change's (ADR 0579 §2).
|
||||
var refused, warned []string
|
||||
for _, module := range slices.Sorted(maps.Keys(cv.Manifests)) {
|
||||
r := cv.Manifests[module]
|
||||
if r.Accepted {
|
||||
continue
|
||||
}
|
||||
if r.Refused == "" { // held: a verdict not given is not one given (ADR 0633 clause 6)
|
||||
if slices.Contains(touched, module) {
|
||||
refused = append(refused, fmt.Sprintf("the indexer's judge at %s holds the manifest of %s: it gave no verdict", at, module))
|
||||
}
|
||||
continue
|
||||
}
|
||||
had := map[string]bool{}
|
||||
for _, p := range strings.Split(trunkCase.Manifests[module].Refused, "\n") {
|
||||
had[p] = true
|
||||
}
|
||||
var caused, old []string
|
||||
for _, p := range strings.Split(r.Refused, "\n") {
|
||||
if slices.Contains(touched, module) || !had[p] {
|
||||
caused = append(caused, p)
|
||||
} else {
|
||||
old = append(old, p)
|
||||
}
|
||||
}
|
||||
if len(caused) > 0 {
|
||||
what := "the manifest of " + module
|
||||
if module == judgeBetween {
|
||||
what = "the catalogue"
|
||||
}
|
||||
refused = append(refused, fmt.Sprintf("the indexer's judge at %s refuses %s: %s", at, what, strings.Join(caused, "; ")))
|
||||
}
|
||||
if len(old) > 0 {
|
||||
warned = append(warned, module+": "+strings.Join(old, "; "))
|
||||
}
|
||||
}
|
||||
if len(warned) > 0 {
|
||||
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 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
|
||||
st.facts = append(st.facts, rules.Step{Module: goModule, Manifests: &m})
|
||||
}
|
||||
|
||||
// runJudge asks the Go build seat to build the judge at the commit and run it over the request, and reads its change and
|
||||
// trunk cases; a failed verdict when it cannot be asked or does not answer.
|
||||
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) {
|
||||
// trunk cases; false, the failure recorded, when it cannot be asked or does not answer.
|
||||
func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string, versions delivery.PackageVersions) (change, trunk composer.CaseVerdict, ok bool) {
|
||||
raw, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return change, trunk, st.fail(goModule, err.Error())
|
||||
st.fail(goModule, err.Error())
|
||||
return change, trunk, false
|
||||
}
|
||||
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,48 +149,48 @@ 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, st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error())
|
||||
st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error())
|
||||
return change, trunk, false
|
||||
}
|
||||
step, err := st.j.Asks.Wait(ctx, a)
|
||||
if err != nil {
|
||||
return change, trunk, st.fail(goModule, "the judge of the change's manifests: "+err.Error())
|
||||
st.fail(goModule, "the judge of the change's manifests: "+err.Error())
|
||||
return change, trunk, false
|
||||
}
|
||||
change, trunk, why := judgeAnswer(step)
|
||||
if 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))
|
||||
st.fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why))
|
||||
return change, trunk, false
|
||||
}
|
||||
return change, trunk, nil
|
||||
return change, trunk, true
|
||||
}
|
||||
|
||||
// CatalogSeat is the seat the indexer holds: the module whose manifest claims it is the indexer, found by the claim and
|
||||
// never by its name (novox/hq ADR 0633 clause 6, ADR 0601 §4).
|
||||
const CatalogSeat = "mesh-catalog"
|
||||
// CatalogSeat is the seat the indexer holds (rules.CatalogSeat).
|
||||
const CatalogSeat = rules.CatalogSeat
|
||||
|
||||
// refuseUnmovableIndexer fails the check when the trunk's manifest validator, the one the running parts hold, would
|
||||
// refuse or hold the change's new indexer manifest: such an indexer would never run, and every revision behind it would
|
||||
// wait for ever (ADR 0633 clause 6). The indexer is the touched module whose manifest claims the seat mesh-catalog.
|
||||
// When the change judges with its own head's manifest validator (it changes it), the manifest is judged by the trunk's
|
||||
// as well. The verdicts read are the trunk's manifest validator's: its change case, where refused, held (not accepted
|
||||
// with no reason) or with a field set aside (a field the running manifest validator does not know waits, ADR 0601 §3)
|
||||
// each fail it, naming the module and the problems.
|
||||
func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *delivery.Verdict {
|
||||
// indexerOf finds the touched module whose manifest claims the seat mesh-catalog, the indexer, and puts in m the trunk's
|
||||
// manifest validator's change case on it, the one the running parts hold (ADR 0633 clause 6): when the change judges
|
||||
// with its own head's validator, or at other package versions than the latest published, the trunk's is asked as well.
|
||||
// False, the failure recorded, when a manifest cannot be read or the trunk's validator cannot be asked.
|
||||
func (st *state) indexerOf(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string, m *rules.Manifests) bool {
|
||||
if st.j.Read == nil {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
var indexer string
|
||||
for _, module := range touched {
|
||||
m, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest)
|
||||
man, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest)
|
||||
if err != nil {
|
||||
return st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
|
||||
st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err))
|
||||
return false
|
||||
}
|
||||
for _, c := range m.Claims {
|
||||
for _, c := range man.Claims {
|
||||
if c.Name == CatalogSeat {
|
||||
indexer = module
|
||||
}
|
||||
}
|
||||
}
|
||||
if indexer == "" {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
// The validator the running parts hold is the trunk's, built at the latest published versions alone, never at the group's
|
||||
// heads (ADR 0633 clause 6): a group's mesh-sdk or mesh-contracts head must not change the validator that judges the new
|
||||
@@ -241,32 +201,17 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
|
||||
trunk = st.j.Trunks.Heads()[DeliveryRepository]
|
||||
}
|
||||
if trunk == "" {
|
||||
return st.fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
|
||||
st.fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
|
||||
return false
|
||||
}
|
||||
var v *delivery.Verdict
|
||||
judgedAt = "mesh-delivery's trunk " + trunk
|
||||
if cv, _, v = st.runJudge(ctx, req, touched, trunk, judgedAt, trunkVersions); v != nil {
|
||||
return v
|
||||
var ok bool
|
||||
if cv, _, ok = st.runJudge(ctx, req, touched, trunk, judgedAt, trunkVersions); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
r, ok := cv.Manifests[indexer]
|
||||
var problems []string
|
||||
switch {
|
||||
case !ok:
|
||||
problems = append(problems, "the manifest validator gave no verdict")
|
||||
case r.Refused != "":
|
||||
problems = append(problems, strings.Split(r.Refused, "\n")...)
|
||||
case !r.Accepted:
|
||||
problems = append(problems, "the manifest validator holds it, with no verdict yet")
|
||||
case r.SetAside != "":
|
||||
problems = append(problems, "the manifest validator does not know its fields "+r.SetAside+", so it waits")
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
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))
|
||||
return nil
|
||||
m.Indexer, m.IndexerAt, m.OfIndexer = indexer, judgedAt, &cv
|
||||
return true
|
||||
}
|
||||
|
||||
// judgeAnswer reads the judge's change and trunk cases from its step, or why it cannot.
|
||||
|
||||
@@ -52,13 +52,8 @@ type Fetched struct {
|
||||
Programs *[]ProgramFile `json:"programs,omitempty"`
|
||||
}
|
||||
|
||||
// ProgramFile is a changed file that is a compiled program, and its format (ELF, Mach-O or PE).
|
||||
type ProgramFile struct {
|
||||
Path string `json:"path"`
|
||||
Kind string `json:"kind"`
|
||||
// Commit is set for a program a commit of the branch added and a later one removed: it stays in history once merged.
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
// ProgramFile is a changed file that is a compiled program (rules.ProgramFile).
|
||||
type ProgramFile = rules.ProgramFile
|
||||
|
||||
// The test step's answer is read by the judging rules (milestone 47/B0); these name their shapes here.
|
||||
type (
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mesh-delivery-checker/rules"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -167,7 +168,7 @@ func (f *fakeTryCandidates) TryCandidate(_ context.Context, group, heads string,
|
||||
// wholeBench makes the bench judge a whole try as the checker does: the composition on every placed node and the
|
||||
// indexer's candidate of the try's group.
|
||||
func wholeBench(b *bench, cand delivery.Candidate) (*fakeCompositions, *fakeTryCandidates) {
|
||||
comps := &fakeCompositions{v: delivery.CompositionVerdict{State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14", "shanks"}, Took: 40}}
|
||||
comps := &fakeCompositions{v: composedOn("v0.1.14-x", 40, "g14", "shanks")}
|
||||
cands := &fakeTryCandidates{cand: cand}
|
||||
b.ex.Compositions, b.ex.Candidates = comps, cands
|
||||
return comps, cands
|
||||
@@ -366,21 +367,21 @@ func TestAFailingTryNamesWhatFailedAndAStepThatCannotRunIsAnError(t *testing.T)
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
cand *fakeTryCandidates
|
||||
comp delivery.CompositionVerdict
|
||||
comp *rules.Composed
|
||||
state, said string
|
||||
}{
|
||||
{name: "the indexer wrote no candidate", cand: &fakeTryCandidates{err: errors.New("the indexer wrote no candidate within 5m0s")}, state: TryError, said: "the indexer wrote no candidate within 5m0s"},
|
||||
{name: "a node that gave no composition answer", cand: &fakeTryCandidates{}, comp: delivery.CompositionVerdict{State: delivery.CompositionPass, SDK: "x", Warnings: []string{"g14: " + delivery.ComposeNoAnswer}},
|
||||
{name: "a node that gave no composition answer", cand: &fakeTryCandidates{}, comp: &rules.Composed{Composer: "x", Nodes: []string{"g14"}},
|
||||
state: TryError, said: "g14: " + delivery.ComposeNoAnswer},
|
||||
{name: "a composition refused on a node", cand: &fakeTryCandidates{}, comp: delivery.CompositionVerdict{State: delivery.CompositionFail, Machine: "g14", Words: "not composed on g14: web: needs a state mail does not keep"},
|
||||
{name: "a composition refused on a node", cand: &fakeTryCandidates{}, comp: ptr(refusedOn("x", "g14", "web: needs a state mail does not keep")),
|
||||
state: TryFail, said: "not composed on g14: web: needs a state mail does not keep"},
|
||||
} {
|
||||
kv := newMemKV()
|
||||
b := newBench(t)
|
||||
comps, _ := wholeBench(b, delivery.Candidate{})
|
||||
b.ex.Candidates, comps.v = row.cand, row.comp
|
||||
if row.comp.State == "" {
|
||||
comps.v.State = delivery.CompositionPass
|
||||
b.ex.Candidates = row.cand
|
||||
if row.comp != nil {
|
||||
comps.v = *row.comp
|
||||
}
|
||||
id, _ := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, time.Now())
|
||||
var ends []string
|
||||
|
||||
@@ -34,6 +34,9 @@ type Facts struct {
|
||||
Steps []Step `json:"steps,omitempty"`
|
||||
// CloneMS is how much of the steps' time their levels' running did not take (delivery.Took.Clone).
|
||||
CloneMS int64 `json:"clone-ms,omitempty"`
|
||||
// Composition is how the check composed, after every step; nil when it was not composed (a narrowed try, a checker
|
||||
// that composes nothing): its composition is then not run, unsaid.
|
||||
Composition *Composition `json:"composition,omitempty"`
|
||||
// Notes are the running checker's notes of the check, for a reader.
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
}
|
||||
@@ -46,9 +49,10 @@ type Member struct {
|
||||
Trunk string `json:"trunk,omitempty"`
|
||||
}
|
||||
|
||||
// Step is one fact of a member's check step: the node that ran it, and either one level its build module answered
|
||||
// (Level, as the build module gave it) or a failure the asking met (Failed: a step not asked, refused or not answered,
|
||||
// a conflict, a binary, worded by the running checker). A step with neither only says which node ran it.
|
||||
// Step is one fact of the check, in the order met: the node that ran it, and one of a level its build module answered
|
||||
// (Level, as the build module gave it), what its fetch read of compiled programs (Programs), the manifest validator's
|
||||
// answers (Manifests) or a failure the asking met (Failed: a step not asked, refused or not answered, a conflict, worded
|
||||
// by the running checker). A step with none only says which node ran it.
|
||||
type Step struct {
|
||||
Repository string `json:"repository"`
|
||||
Number int `json:"number"`
|
||||
@@ -56,6 +60,8 @@ type Step struct {
|
||||
Node string `json:"node,omitempty"`
|
||||
Level *delivery.LevelAnswer `json:"level,omitempty"`
|
||||
Failed *delivery.FailedFirst `json:"failed,omitempty"`
|
||||
Programs *Programs `json:"programs,omitempty"`
|
||||
Manifests *Manifests `json:"manifests,omitempty"`
|
||||
}
|
||||
|
||||
// DecodeFacts reads facts, refusing a version this program does not read, naming it.
|
||||
@@ -131,9 +137,14 @@ func (t Tally) Level(repository string, number int, module, node string, la deli
|
||||
return nil, elapsed, results
|
||||
}
|
||||
|
||||
// Judge is the verdict of the facts: every failure in the order met, joined and settled, or a pass; the levels'
|
||||
// counts, times, machines and the levels over their bound; the composition not yet run (the running checker composes).
|
||||
func Judge(f Facts) delivery.Verdict {
|
||||
// Judge is the verdict of the facts (Judged without its notes).
|
||||
func Judge(f Facts) delivery.Verdict { return Judged(f).Verdict }
|
||||
|
||||
// Judged is the answer of the facts: every failure in the order met, joined and settled, or a pass; the levels'
|
||||
// counts, times, machines and the levels over their bound; then the composition taken in; and the notes the rules give
|
||||
// a reader (the manifest validator's warnings and acceptances).
|
||||
func Judged(f Facts) Answer {
|
||||
var notes []string
|
||||
tally := NewTally()
|
||||
var failures []*delivery.Verdict
|
||||
nodes := map[string]bool{}
|
||||
@@ -148,6 +159,16 @@ func Judge(f Facts) delivery.Verdict {
|
||||
if v, _, _ := tally.Level(s.Repository, s.Number, s.Module, s.Node, *s.Level); v != nil {
|
||||
failures = append(failures, v)
|
||||
}
|
||||
case s.Programs != nil:
|
||||
if ff := JudgePrograms(s.Node, *s.Programs); ff != nil {
|
||||
failures = append(failures, &delivery.Verdict{State: delivery.VerdictFail, Failed: *ff})
|
||||
}
|
||||
case s.Manifests != nil:
|
||||
ff, said := JudgeManifests(*s.Manifests)
|
||||
notes = append(notes, said...)
|
||||
if ff != nil {
|
||||
failures = append(failures, &delivery.Verdict{State: delivery.VerdictFail, Failed: *ff})
|
||||
}
|
||||
}
|
||||
}
|
||||
v := delivery.Verdict{State: delivery.VerdictPass}
|
||||
@@ -156,7 +177,10 @@ func Judge(f Facts) delivery.Verdict {
|
||||
}
|
||||
v.Group, v.Heads, v.Trunk = f.Group, f.Heads, f.Trunk
|
||||
Outcome(&v, tally, f.CloneMS, nodes)
|
||||
return v
|
||||
if f.Composition != nil {
|
||||
JudgeComposition(&v, *f.Composition)
|
||||
}
|
||||
return Answer{V: AnswerVersion, Verdict: v, Notes: notes}
|
||||
}
|
||||
|
||||
// Outcome fills a verdict's levels (on a pass, or when it names none), times, machines and levels over their bound from
|
||||
@@ -211,8 +235,10 @@ func failAt(machine, words string) *delivery.Verdict {
|
||||
// AnswerVersion is the version of Answer this program writes.
|
||||
const AnswerVersion = 1
|
||||
|
||||
// Answer is what the checking program answers on the last line of its standard output: its version and the verdict.
|
||||
// Answer is what the checking program answers on the last line of its standard output: its version, the verdict and the
|
||||
// notes its rules give a reader of the check.
|
||||
type Answer struct {
|
||||
V int `json:"v"`
|
||||
Verdict delivery.Verdict `json:"verdict"`
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The facts the manifest validator, the fetch's programs and the composer answered, and the rules that judge them
|
||||
// (milestone 47/B0, D1b): the running checker asks and records; these rules give the verdict.
|
||||
|
||||
// manifestsMachine is the machine a manifest's failure names: the build seat that ran the manifest validator.
|
||||
const manifestsMachine = buildrun.GoBuildModule
|
||||
|
||||
// CatalogSeat is the seat the indexer holds: the module whose manifest claims it is the indexer, found by the claim and
|
||||
// never by its name (novox/hq ADR 0633 clause 6, ADR 0601 §4).
|
||||
const CatalogSeat = "mesh-catalog"
|
||||
|
||||
// BetweenManifests is the manifest validator's name for what it judges between manifests (the catalogue as a whole).
|
||||
const BetweenManifests = "(between manifests)"
|
||||
|
||||
// Manifests are the manifest validator's answers on the manifests a check touches (ADR 0578 §2, ADR 0579 §2, ADR 0633
|
||||
// clause 6): its change and trunk cases where it judged (At), and, when a touched module claims the seat mesh-catalog,
|
||||
// that indexer and the trunk validator's change case on it (IndexerAt names where it judged).
|
||||
type Manifests struct {
|
||||
Touched []string `json:"touched"`
|
||||
At string `json:"at"`
|
||||
Change composer.CaseVerdict `json:"change"`
|
||||
Trunk composer.CaseVerdict `json:"trunk"`
|
||||
Indexer string `json:"indexer,omitempty"`
|
||||
IndexerAt string `json:"indexer-at,omitempty"`
|
||||
OfIndexer *composer.CaseVerdict `json:"of-indexer,omitempty"`
|
||||
}
|
||||
|
||||
// Programs are what one member's fetch read of the change's compiled programs (issue 1577): Files nil when the build
|
||||
// module reported none (an older module: not refused, the running checker warns), else each changed file that is one.
|
||||
type Programs struct {
|
||||
Key string `json:"key"`
|
||||
Files *[]ProgramFile `json:"files"`
|
||||
}
|
||||
|
||||
// ProgramFile is a changed file that is a compiled program, its format (ELF, Mach-O or PE) and, when it was added in an
|
||||
// earlier commit of the branch and removed later, that commit.
|
||||
type ProgramFile struct {
|
||||
Path string `json:"path"`
|
||||
Kind string `json:"kind"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
// Composition is how the check's composition went (ADR 0593 §7, §9; ADR 0603 §10; ADR 0548 §5): kept from the check it
|
||||
// carries, not run (NotRun says why), or composed by the engines; BuildsFailed are the candidate's builds that failed
|
||||
// before it composed, worded by the running checker; Waits are what it composed without waiting on, as warnings.
|
||||
type Composition struct {
|
||||
Kept *delivery.Composition `json:"kept,omitempty"`
|
||||
NotRun string `json:"not-run,omitempty"`
|
||||
BuildsFailed []delivery.FailedFirst `json:"builds-failed,omitempty"`
|
||||
Waits []string `json:"waits,omitempty"`
|
||||
Composed *Composed `json:"composed,omitempty"`
|
||||
FPSet string `json:"fp-set,omitempty"`
|
||||
}
|
||||
|
||||
// Composed are the engines' answers to one composition: the nodes placed, each answering node's answer, each silent
|
||||
// node with why (for the journal only), what composed (the node-engine's commit), the composer's own warnings and how
|
||||
// long it took in milliseconds.
|
||||
type Composed struct {
|
||||
Group string `json:"group"`
|
||||
Heads string `json:"heads"`
|
||||
Composer string `json:"composer"`
|
||||
Nodes []string `json:"nodes"`
|
||||
Answers map[string]delivery.ComposeCandidateAnswer `json:"answers,omitempty"`
|
||||
Silent map[string]string `json:"silent,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Took int64 `json:"took,omitempty"`
|
||||
}
|
||||
|
||||
// JudgeManifests is the failure of the manifests' facts (nil when they pass) and the notes a reader gets: the trunk
|
||||
// validator refusing, holding or setting aside the new indexer's manifest fails first (ADR 0633 clause 6); else a new or
|
||||
// changed manifest is judged in full, a held one is refused, and of one the change leaves as it is a problem the trunk
|
||||
// already had is a warning while one the change causes fails the check (ADR 0579 §2).
|
||||
func JudgeManifests(m Manifests) (*delivery.FailedFirst, []string) {
|
||||
var notes []string
|
||||
if m.Indexer != "" {
|
||||
var problems []string
|
||||
ok := false
|
||||
var iv composer.ManifestRead
|
||||
if m.OfIndexer != nil {
|
||||
iv, ok = m.OfIndexer.Manifests[m.Indexer]
|
||||
}
|
||||
switch {
|
||||
case !ok:
|
||||
problems = append(problems, "the manifest validator gave no verdict")
|
||||
case iv.Refused != "":
|
||||
problems = append(problems, strings.Split(iv.Refused, "\n")...)
|
||||
case !iv.Accepted:
|
||||
problems = append(problems, "the manifest validator holds it, with no verdict yet")
|
||||
case iv.SetAside != "":
|
||||
problems = append(problems, "the manifest validator does not know its fields "+iv.SetAside+", so it waits")
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return &delivery.FailedFirst{Machine: manifestsMachine, Words: 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",
|
||||
m.Indexer, CatalogSeat, m.IndexerAt, strings.Join(problems, "; "))}, nil
|
||||
}
|
||||
notes = append(notes, fmt.Sprintf("the new indexer's manifest of %s is accepted by the trunk's manifest validator at %s", m.Indexer, m.IndexerAt))
|
||||
}
|
||||
var refused, warned []string
|
||||
for _, module := range slices.Sorted(maps.Keys(m.Change.Manifests)) {
|
||||
r := m.Change.Manifests[module]
|
||||
if r.Accepted {
|
||||
continue
|
||||
}
|
||||
if r.Refused == "" { // held: a verdict not given is not one given (ADR 0633 clause 6)
|
||||
if slices.Contains(m.Touched, module) {
|
||||
refused = append(refused, fmt.Sprintf("the indexer's judge at %s holds the manifest of %s: it gave no verdict", m.At, module))
|
||||
}
|
||||
continue
|
||||
}
|
||||
had := map[string]bool{}
|
||||
for _, p := range strings.Split(m.Trunk.Manifests[module].Refused, "\n") {
|
||||
had[p] = true
|
||||
}
|
||||
var caused, old []string
|
||||
for _, p := range strings.Split(r.Refused, "\n") {
|
||||
if slices.Contains(m.Touched, module) || !had[p] {
|
||||
caused = append(caused, p)
|
||||
} else {
|
||||
old = append(old, p)
|
||||
}
|
||||
}
|
||||
if len(caused) > 0 {
|
||||
what := "the manifest of " + module
|
||||
if module == BetweenManifests {
|
||||
what = "the catalogue"
|
||||
}
|
||||
refused = append(refused, fmt.Sprintf("the indexer's judge at %s refuses %s: %s", m.At, what, strings.Join(caused, "; ")))
|
||||
}
|
||||
if len(old) > 0 {
|
||||
warned = append(warned, module+": "+strings.Join(old, "; "))
|
||||
}
|
||||
}
|
||||
if len(warned) > 0 {
|
||||
notes = append(notes, "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 &delivery.FailedFirst{Machine: manifestsMachine, Words: strings.Join(refused, "\n")}, notes
|
||||
}
|
||||
return nil, append(notes, fmt.Sprintf("the manifests of %s are accepted by the indexer's judge at %s", strings.Join(m.Touched, ", "), m.At))
|
||||
}
|
||||
|
||||
// JudgePrograms fails a member whose change adds or modifies a compiled program, naming each file (issue 1577); a fetch
|
||||
// that reported none, or found none, passes.
|
||||
func JudgePrograms(machine string, p Programs) *delivery.FailedFirst {
|
||||
if p.Files == nil {
|
||||
return nil
|
||||
}
|
||||
var found []string
|
||||
for _, f := range *p.Files {
|
||||
if f.Commit != "" {
|
||||
found = append(found, fmt.Sprintf("%s: a compiled program added in %s and removed later: it stays in history; squash the branch (issue 1577)", f.Path, short12(f.Commit)))
|
||||
continue
|
||||
}
|
||||
found = append(found, f.Path+": a compiled program is build output: remove it and add it to .gitignore (issue 1577)")
|
||||
}
|
||||
if len(found) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &delivery.FailedFirst{Machine: machine, Words: fmt.Sprintf("%s: %s", p.Key, strings.Join(found, "; "))}
|
||||
}
|
||||
|
||||
func short12(s string) string {
|
||||
if len(s) > 12 {
|
||||
return s[:12]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// JudgeEngines is the check's composition verdict from the engines' answers (ADR 0593 §7, §9): failed when any node's
|
||||
// answer is refused, naming each node and each module refused with its words (the first refusing node is the machine);
|
||||
// passed otherwise. Every placed node without an answer is named with delivery.ComposeNoAnswer as a warning.
|
||||
func JudgeEngines(c Composed) delivery.CompositionVerdict {
|
||||
v := delivery.CompositionVerdict{Group: c.Group, Heads: c.Heads, State: delivery.CompositionPass, SDK: c.Composer, Took: c.Took}
|
||||
sorted := append([]string(nil), c.Nodes...)
|
||||
sort.Strings(sorted)
|
||||
var fails []string
|
||||
for _, node := range sorted {
|
||||
a, answered := c.Answers[node]
|
||||
if !answered {
|
||||
v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer)
|
||||
continue
|
||||
}
|
||||
if a.Verdict == delivery.ComposeCandidateNotJudged {
|
||||
// Composed in the sandbox and not judged (ADR 0593 §9): named as no answer, with the composer's words; decided
|
||||
// by the verdict, never by the words (ADR 0601 §4's kind).
|
||||
v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer+" ("+a.Refused+")")
|
||||
continue
|
||||
}
|
||||
v.Machines = append(v.Machines, node)
|
||||
if a.Verdict != delivery.ComposeCandidateRefused {
|
||||
continue
|
||||
}
|
||||
if v.Machine == "" {
|
||||
v.Machine = node
|
||||
}
|
||||
var words []string
|
||||
if a.Refused != "" {
|
||||
words = append(words, a.Refused)
|
||||
}
|
||||
for _, m := range append(append([]delivery.ModuleComposed(nil), a.Modules...), a.Own) {
|
||||
if m.State == delivery.ModuleRefusedState {
|
||||
words = append(words, m.Module+": "+m.Words)
|
||||
}
|
||||
}
|
||||
if len(words) == 0 {
|
||||
words = append(words, "refused, naming no module")
|
||||
}
|
||||
fails = append(fails, "not composed on "+node+": "+strings.Join(words, "; "))
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ")
|
||||
}
|
||||
if len(sorted) == 0 {
|
||||
v.Warnings = append(v.Warnings, "no node is placed, so the change was composed on none")
|
||||
}
|
||||
v.Warnings = append(v.Warnings, c.Warnings...)
|
||||
return v
|
||||
}
|
||||
|
||||
// JudgeComposition takes the composition's facts into the verdict: a failed build of the candidate fails it, named after
|
||||
// the tests' failures; a failed composition fails it, naming its machine; a passed one with warnings says them.
|
||||
func JudgeComposition(v *delivery.Verdict, c Composition) {
|
||||
if c.Kept != nil {
|
||||
kept := *c.Kept
|
||||
v.Composition = &kept
|
||||
return
|
||||
}
|
||||
for _, f := range c.BuildsFailed {
|
||||
if v.State != delivery.VerdictFail {
|
||||
v.State, v.Failed = delivery.VerdictFail, f
|
||||
continue
|
||||
}
|
||||
AddFailure(v, f)
|
||||
}
|
||||
if c.NotRun != "" {
|
||||
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: c.NotRun}
|
||||
return
|
||||
}
|
||||
if c.Composed == nil {
|
||||
return
|
||||
}
|
||||
comp := JudgeEngines(*c.Composed)
|
||||
comp.Warnings = append(comp.Warnings, c.Waits...) // ADR 0603 §10
|
||||
v.Composition = comp.Composition()
|
||||
v.Took.Composition = comp.Took
|
||||
if comp.State == delivery.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil {
|
||||
v.Composition.Words = strings.Join(comp.Warnings, "; ") // passed with a warning (ADR 0593 §9)
|
||||
}
|
||||
if comp.State == delivery.CompositionFail {
|
||||
machine := comp.Machine
|
||||
if machine == "" {
|
||||
machine = "the composition"
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
v.State, v.Failed = delivery.VerdictFail, f
|
||||
}
|
||||
}
|
||||
if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun {
|
||||
v.Composition.FPSet = c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user