diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index ec4c4213..06dc3040 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -19,6 +19,8 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -418,11 +420,42 @@ func stepBound() time.Duration { return check.DefaultStepBound } +// seatListAgainCallers are who may ask the seat list again by hand (design 47 G §204). +var seatListAgainCallers = []string{stdio.AnyPerson, "node-tools", "mesh-healers"} + +// holderOf is a seat's holder as the seat that places modules answers it (seatlist.HolderOf, design 47 G A-126): false while the +// seat has none or the answer cannot be read. +func holderOf(seat string) (string, bool) { + module, err := seatlist.HolderOf(stdio.Ask, seat) + return module, err == nil +} + func main() { + // The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat + // that places modules names for it; the seat list and its two tools are wired once by mesh-sdk (§204). + gate := seatlist.NewPart("checker", seatListAgainCallers) + go func() { + <-stdio.Served() + gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] "+f+"\n", a...) }) + }() + seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition { + if rd := gate.Reader(); rd != nil { + return rd.Definitions() + } + return nil + }, holderOf) r := check.Reader{VerdictState: read(stdio.State("verdicts")), KeptState: read(stdio.State("kept")), CandidateState: ownerRead(stdio.Ask, indexerCandidatesTool), PreviewState: read(stdio.State("previews")), InputState: read(stdio.State(check.InputsStateName)), - StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))}, - HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), + StepStates: func() map[string]check.StateRead { + out := map[string]check.StateRead{} + for _, m := range seats.All() { + if m.Module != "" && m.Toolchain != "" { + out[m.Module] = read(stdio.State(m.Module + ".steps")) + } + } + return out + }, + HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), CouldNotRunState: read(stdio.State(check.CouldNotRunState)), HearingState: read(stdio.State(buildrun.HearingState)), AskState: read(stdio.State(check.AsksState)), BuildState: read(stdio.State("mesh-delivery-builder.builds"))} r.Runs = runsOf{ownerRead(stdio.Ask, indexerModulesTool)} // the node-engine at the commit the mesh runs, as the checker reads it @@ -466,6 +499,7 @@ func main() { // checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's // states stand, and asking one again by hand (ADR 0615, DLV-M2). tools = append(tools, views.StatusTools(check.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) + tools = append(tools, gate.Tools()...) tools = append(tools, SeatInputTools(r)...) tools = append(tools, RequestTools(read(stdio.State(check.RequestsStateName)))...) if err := stdio.Serve("", logged(tools)); err != nil { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index c435dfad..a0f3bd8d 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -5,9 +5,11 @@ package main import ( "bytes" "context" + "crypto/sha256" "encoding/json" "errors" "fmt" + "maps" "os" "path/filepath" "slices" @@ -47,6 +49,9 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "git.novox.be/novox/mesh-sdk/go/seatlist" + "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" "git.novox.be/novox/mesh-sdk/go/views/viewstest" "mesh-delivery-checker/rules" @@ -294,7 +299,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key if err := gobuild.DeclareAskWorkers(f.ctx, f.js, language, 30*time.Second); err != nil { t.Fatal(err) } - prefix := map[string]string{"mesh-build-go": "build_go_", "mesh-build-node": "build_node_"}[module] + prefix := map[string]string{goBuildModule: "build_go_", nodeBuildModule: "build_node_"}[module] call := func(name string) gobuild.StepFunc { return func(ctx context.Context, args map[string]any) (any, error) { tool := prefix + name @@ -320,7 +325,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key v, err := meshdeliverycheckerclient.DecodeKept(e.Value()) return int(v.Tests), err == nil, err }} - if module == "mesh-build-go" { + if module == goBuildModule { steps.Digest = call("digest") } put := func(key string, value any) (uint64, error) { @@ -527,8 +532,10 @@ func startBed(t *testing.T) *bed { } // The seat mesh-catalog's holder answers the seat list with no trunk seat named: the checker's reader has then heard the seat // and holds the bootstrapped definitions, which the judge is handed (ADR 0636 §1). + // It lists the build seats (design 47 G A-82): the seat whose acting rule builds Go and the one that builds TypeScript, from + // which the checker finds its build modules. for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} { - if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond([]byte(`{"result":{"revision":1,"seats":[]},"node":"ctl"}`)) }); err != nil { + if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond(seatDefinitionsAnswer(m.Data)) }); err != nil { t.Fatal(err) } } @@ -550,13 +557,7 @@ func startBed(t *testing.T) *bed { ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) t.Cleanup(cancel) b := &bed{trunkNow: trunkView, ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()} - for module, stream := range toolchainStreams { - if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: stream, Subjects: []string{"mesh.stream." + module + ".event.toolchain.reported"}}); err != nil { - t.Fatal(err) - } - if _, err := js.CreateOrUpdateConsumer(ctx, stream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil { - t.Fatal(err) - } + for _, module := range []string{goBuildModule, nodeBuildModule} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buildrun.ToolchainsBucket(module), History: 1}) if err != nil { t.Fatal(err) @@ -626,16 +627,19 @@ func startBed(t *testing.T) *bed { } b.setOnline(t, "n1", true) b.setNode(t, "n1") - for _, module := range []string{"mesh-build-go", "mesh-build-node"} { + // The indexer's modules name the holders of the build seats, by the seats their manifests claim. + b.putBuildModule(t, goBuildModule, "node-build-go") + b.putBuildModule(t, nodeBuildModule, "node-build-node") + for _, module := range []string{goBuildModule, nodeBuildModule} { b.setHealth(t, "n1", module) } b.serveForge(t, ctl) b.serveInputVerb(t, ctl) b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}} - b.fake.serve(t, "mesh-build-go", b.buckets[check.CheckerKeptBucket]) - b.fake.serve(t, "mesh-build-node", b.buckets[check.CheckerKeptBucket]) - b.report(t, "mesh-build-go", "n1", "go", "go1.27.1", "1") - b.report(t, "mesh-build-node", "n1", "typescript", "tsc5", "1") + b.fake.serve(t, goBuildModule, b.buckets[check.CheckerKeptBucket]) + b.fake.serve(t, nodeBuildModule, b.buckets[check.CheckerKeptBucket]) + b.report(t, goBuildModule, "n1", "go", "go1.27.1", "1") + b.report(t, nodeBuildModule, "n1", "typescript", "tsc5", "1") b.setTrunk(t, map[string]string{repo: trunk}) return b } @@ -693,7 +697,11 @@ func (b *bed) report(t *testing.T, module, node, language, version, recipe strin if _, err := b.tools[module].Put(b.ctx, key, data); err != nil { t.Fatal(err) } - if _, err := b.js.Publish(b.ctx, "mesh.stream."+module+".event.toolchain.reported", []byte(`{}`)); err != nil { + seat := "node-build-go" + if module == nodeBuildModule { + seat = "node-build-node" + } + if _, err := b.js.Publish(b.ctx, "mesh.seat."+seat+".event.toolchain-reported."+node, []byte(`{}`)); err != nil { t.Fatal(err) } } @@ -884,7 +892,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 t.Errorf("owed = %+v", o) } // Two checks of the same heads: the one failed on m's build and the one it asked itself again; the second runs no test. - for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 2, "mesh-build-go.build_go_digest": 4, "mesh-build-go.build_go_test": 2} { + for tool, want := range map[string]int{goBuildModule + ".build_go_fetch": 2, goBuildModule + ".build_go_digest": 4, goBuildModule + ".build_go_test": 2} { if got := b.fake.count(tool); got != want { t.Errorf("%s called %d times, want %d", tool, got, want) } @@ -917,13 +925,13 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 first := b.candidate(t, "feat/x", pull(repo, 7, head1)) b.start(t, nil) b.verdict(t, first, ended) - tests, kept := b.fake.count("mesh-build-go.build_go_test"), keptCount(t, b) + tests, kept := b.fake.count(goBuildModule+".build_go_test"), keptCount(t, b) second := b.candidate(t, "feat/y", pull(repo, 8, head2)) v := b.verdict(t, second, ended) if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Run != 0 { t.Fatalf("verdict = %+v, want everything taken from the kept verdicts", v) } - if got := b.fake.count("mesh-build-go.build_go_test"); got != tests { + if got := b.fake.count(goBuildModule + ".build_go_test"); got != tests { t.Errorf("build_go_test called %d times, was %d: a test ran for equal inputs", got, tests) } if got := keptCount(t, b); got != kept { @@ -935,14 +943,14 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 b.verdict(t, third, ended) // §5: the trunk moves; the first verdict is carried by its digests, nothing runs. - tests = b.fake.count("mesh-build-go.build_go_test") + tests = b.fake.count(goBuildModule + ".build_go_test") const moved = "abababababababababababababababababababab" b.setTrunk(t, map[string]string{repo: moved}) c := b.verdict(t, first, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return ended(v) && v.Trunk[repo] == moved }) if c.State != meshdeliverycheckerclient.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[buildstep.LevelUnit].Run != 0 || c.Levels[buildstep.LevelIntegration].Run != 0 { t.Fatalf("the carried verdict = %+v, want a pass carried from %s with no test run", c, trunk[:12]) } - if got := b.fake.count("mesh-build-go.build_go_test"); got != tests { + if got := b.fake.count(goBuildModule + ".build_go_test"); got != tests { t.Errorf("build_go_test called %d times on the carry, was %d: a carry of unchanged digests runs nothing", got, tests) } if o := b.owed(t, 7, head1, "success"); !strings.HasPrefix(o.Text, "pass 111111111111 on abababababab | carried from aaaaaaaaaaaa") { @@ -992,7 +1000,7 @@ func TestACheckThatCannotBeJudgedFailsNamingWhy(t *testing.T) { // 1289/AC4, 192 !strings.Contains(o.Text, map[bool]string{true: row.words, false: "the checking program"}[row.program]) { t.Errorf("owed = %+v", o) } - if n := b.fake.count("mesh-build-go.build_go_digest") + b.fake.count("mesh-build-go.build_go_test"); (n != 0) != row.tests { + if n := b.fake.count(goBuildModule+".build_go_digest") + b.fake.count(goBuildModule+".build_go_test"); (n != 0) != row.tests { t.Errorf("%d digest and test steps ran, want them run %v", n, row.tests) } if n := b.fake.dropped.Load(); n != row.dropped { @@ -1170,7 +1178,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) { if got := asked(); len(got) != 1 || !strings.HasPrefix(got[0].Candidate, id+".") { t.Errorf("the placed node's engine was asked %+v, want the try's candidate %s.", got, id) } - if b.fake.count("mesh-build-go.build_go_fetch") == 0 { + if b.fake.count(goBuildModule+".build_go_fetch") == 0 { t.Errorf("the try fetched nothing") } // A whole try's start and end are said on the stream checks, which the indexer hears to write and delete the @@ -1201,7 +1209,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) { if err != nil { t.Fatal(err) } - steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket("mesh-build-go")) + steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket(goBuildModule)) if err != nil { t.Fatal(err) } @@ -1265,3 +1273,72 @@ func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1 t.Fatalf("the checker said %+v, want its twelve states, asks, hearing, first-views, inputs, compositions and could-not-run its own (DLV-M2; W6.9), with kept, owed, previews, tries and verdicts readable, tries by the delivery view (1929/AC4; 1887 A5b; build_usage)", said) } } + +// buildSeatDefinitions are the seat definitions the bed's seat mesh-catalog lists: the build seats, as the mesh's own definitions of +// them, whose acting rules say which language each builds. +var buildSeatDefinitions = func() map[string]seatdef.Definition { + out := map[string]seatdef.Definition{} + for _, d := range seatlisttest.Definitions() { + if d.Seat == "node-build-go" || d.Seat == "node-build-node" { + out[d.Seat] = d + } + } + return out +}() + +// seatDefinitionsAnswer is the seat mesh-catalog's answer to the seat list's asks: every trunk seat with its digest when no +// seat is named, one seat's definition when it is. +func seatDefinitionsAnswer(body []byte) []byte { + var ask struct { + Seat string `json:"seat"` + } + _ = json.Unmarshal(body, &ask) + entry := func(seat string) seatlist.Entry { + raw, _ := json.Marshal(buildSeatDefinitions[seat]) + return seatlist.Entry{Seat: seat, Source: seatlist.SourceTrunk, Digest: fmt.Sprintf("%x", sha256.Sum256(raw)), Definition: raw} + } + var out any + if ask.Seat == "" { + l := seatlist.Listing{Revision: 1} + for _, seat := range slices.Sorted(maps.Keys(buildSeatDefinitions)) { + e := entry(seat) + e.Definition = nil + l.Seats = append(l.Seats, e) + } + out = l + } else if _, ok := buildSeatDefinitions[ask.Seat]; ok { + e := entry(ask.Seat) + out = seatlist.Answer{Seat: ask.Seat, Found: true, Entry: &e, Revision: 1} + } else { + out = seatlist.Answer{Seat: ask.Seat, Revision: 1} + } + raw, _ := json.Marshal(map[string]any{"result": out, "node": "ctl"}) + return raw +} + +// putBuildModule writes the indexer's module and manifest of a module that claims a build seat, so the checker finds the seat's +// holder (design 47 G A-82). Its repository is its own, so no repository the tests judge changes the build modules it names. +func (b *bed) putBuildModule(t *testing.T, module, seat string) { + t.Helper() + manifest := fmt.Sprintf(`{"module":%q,"version":"0.1","slug":"b","claims":[{"name":%q,"scope":"node"}]}`, module, seat) + digest := fmt.Sprintf("%x", sha256.Sum256([]byte(manifest))) + mkey, err := meshcatalogclient.ManifestsKey(digest) + if err != nil { + t.Fatal(err) + } + stored, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Manifest: manifest, Module: module}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[check.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil { + t.Fatal(err) + } + value, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/build-modules", Commit: strings.Repeat("b", 40), Path: "modules/" + module, Manifest: digest, + None2: "the fingerprint is computed with its chain"}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[check.ModulesBucket].Put(b.ctx, module, value); err != nil { + t.Fatal(err) + } +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 509aab7c..755b3cef 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strconv" "strings" @@ -38,11 +39,13 @@ type judgeDeps struct { renames check.Renames seatDefs check.SeatDefinitions // seats are the seat list the checker's reader holds, the base the checking program lays the heads over (seatlist.go). - seats check.SeatList - tools *buildrun.ToolchainSet - trunks *check.Trunks - kept check.KV - previews check.KV + seats check.SeatList + // buildSeats finds the build modules from the seat list and the holders of seats (design 47 G A-82). + buildSeats *buildrun.BuildSeats + tools *buildrun.ToolchainSet + trunks *check.Trunks + kept check.KV + previews check.KV // candidates answers the indexer's candidate of a try's group (try_candidates.go); nil leaves a whole try with no // manifest or composition judged, said. candidates check.TryCandidates @@ -232,7 +235,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Candidates: d.candidates, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Inputs: d.inputs, BeginBound: d.beginBound, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Candidates: d.candidates, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Seats: d.buildSeats, Inputs: d.inputs, BeginBound: d.beginBound, Logf: d.logf} if d.nodes != nil && d.builds != nil && d.repos != nil { ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf} } @@ -252,8 +255,11 @@ type repoModules struct { logf func(format string, args ...any) // verdicts keeps the indexer's verdict on each module's manifest, for the sandbox's composer (engines.go); nil keeps none. verdicts *manifestVerdicts + // seats finds the build modules (design 47 G A-82); set before the first check. + seats *buildrun.BuildSeats mu sync.Mutex + claims map[string][]string // by manifest digest: the seats its module claims langs map[string]map[string]bool // by manifest digest: the languages of its artifacts tsPkgs map[string][]string // by manifest digest: the names of its TypeScript package artifacts path map[string]string // by module: its directory in its repository @@ -338,10 +344,15 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted return } set := map[string]bool{} - var pkgs []string + var pkgs, claimed []string if mk, err := meshcatalogclient.ManifestsKey(m.Manifest); err == nil { if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found { if kept, err := meshcatalogclient.DecodeManifest(raw); err == nil { + if man, err := parseManifest(kept.Manifest); err == nil { + for _, c := range man.Claims { + claimed = append(claimed, c.Name) + } + } if man, err := parseManifest(kept.Manifest); err == nil && man.Build != nil { for _, a := range man.Build.Artifacts { if a.Language != "" { @@ -360,6 +371,10 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted r.mu.Lock() r.langs[m.Manifest] = set r.tsPkgs[m.Manifest] = pkgs + if r.claims == nil { + r.claims = map[string][]string{} + } + r.claims[m.Manifest] = claimed r.mu.Unlock() } @@ -399,14 +414,35 @@ func (r *repoModules) PackagePaths(repository string) []string { return out } -// BuildModules are the build modules that judge a repository, sorted: the Go build module always, and the Node build -// module when a module of the repository builds TypeScript. A repository of which the mesh knows no module (mesh-lab, hq) -// is judged by both, each finding its own directories in the tree (go.mod files; package.json files), so a repository -// with tests in either language and no manifest is judged whole (issue 1887 A12). -func (r *repoModules) BuildModules(repository string) []string { - out := []string{goBuildModule} +// HolderOf is the module whose manifest claims the seat, as the indexer's modules hold them: none while no module of the mesh +// claims it, and the first by name where several do (design 47 G A-82, as the catalog half's holder is read by the builder). +func (r *repoModules) HolderOf(seat string) (string, bool) { r.mu.Lock() defer r.mu.Unlock() + found := "" + for module, digest := range r.typed { + if slices.Contains(r.claims[digest], seat) && (found == "" || module < found) { + found = module + } + } + return found, found != "" +} + +// BuildModules are the build modules that judge a repository, sorted: the build module of Go always, and that of TypeScript +// when a module of the repository builds TypeScript. A repository of which the mesh knows no module (mesh-lab, hq) +// is judged by both, each finding its own directories in the tree (go.mod files; package.json files), so a repository +// with tests in either language and no manifest is judged whole (issue 1887 A12). A language no build seat on the seat list +// builds has none. +func (r *repoModules) BuildModules(repository string) []string { + var out []string + add := func(language string) { + if m, err := r.seats.For(language, ""); err == nil { + out = append(out, m.Module) + } + } + add("go") + r.mu.Lock() + typescript := false known := false for module, repo := range r.repoOf { if repo != repository { @@ -414,12 +450,13 @@ func (r *repoModules) BuildModules(repository string) []string { } known = true if r.langs[r.typed[module]]["typescript"] { - out = append(out, nodeBuildModule) + typescript = true break } } - if !known { - out = append(out, nodeBuildModule) + r.mu.Unlock() + if typescript || !known { + add("typescript") } sort.Strings(out) return out diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go index a42ad2ab..840c1c5a 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go @@ -66,7 +66,7 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test } store[meshcatalogclient.ManifestDigest([]byte(m))] = data } - r := &repoModules{manifests: store} + r := &repoModules{manifests: store, seats: testBuildSeats()} putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest) putModule(t, r, "mesh-ui", "novox/mesh-catalog", tsManifest) if got := r.BuildModules("novox/mesh-delivery"); strings.Join(got, ",") != goBuildModule { @@ -221,3 +221,28 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { t.Errorf("a waiting verdict is handed to the sandbox as %+v", v) } } + +// The build modules are no names (design 47 G A-82): the holder of a seat is the module whose manifest, as the indexer's modules +// hold it, claims the seat. +func TestTheHolderOfASeatIsTheModuleWhoseManifestClaimsIt(t *testing.T) { + manifest := `{"module":"go-builds","version":"1","slug":"g","claims":[{"name":"node-build-go","scope":"node"}]}` + data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "go-builds", Manifest: manifest}) + if err != nil { + t.Fatal(err) + } + r := &repoModules{manifests: memManifests{meshcatalogclient.ManifestDigest([]byte(manifest)): data}} + putModule(t, r, "go-builds", "novox/builds", manifest) + if got, ok := r.HolderOf("node-build-go"); !ok || got != "go-builds" { + t.Errorf("the holder of node-build-go = %q, %v, want go-builds", got, ok) + } + if got, ok := r.HolderOf("node-build-node"); ok { + t.Errorf("the holder of a seat no manifest claims = %q, want none", got) + } + var found []string + var changed buildsWatch + changed.Set(func() { found = append(found, "changed") }) + changed.Changed() + if len(found) != 1 { + t.Errorf("the build modules' change was not called through its watch: %v", found) + } +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 7c37d5a1..16c37f2f 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -34,6 +34,7 @@ import ( "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -160,7 +161,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // published, started once both are registered. hub := process.NewEventHub(logger.Printf) reader := &views.ProcessReader{Module: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub} - seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)} + var buildsChanged buildsWatch + seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf, buildsChanged.Changed)} // The finding writer is made once the checker is (its observe reads the checker's kept checks); until then a first view // unanswered is said in the log alone. var findings atomic.Pointer[checkerFindings] @@ -188,6 +190,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if followed != nil { return followed } + // The build modules are no names (design 47 G A-82): the holders of the build seats, the seat list's definitions and the + // holders the indexer's modules name by their manifests' claims, found at each use. + verdicts := &manifestVerdicts{} + repos := &repoModules{manifests: kvStore{manifests}, logf: logger.Printf, verdicts: verdicts} + buildSeats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return definitionsOf(seats.reader) }, repos.HolderOf) + repos.seats = buildSeats stepBound, err := durationEnv(getenv, "MESH_CHECKER_STEP_BOUND", check.DefaultStepBound) if err != nil { @@ -210,7 +218,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if link == "" { link = defaultLink } - tools := &buildrun.ToolchainSet{Seats: checkerBuildSeats} + tools := &buildrun.ToolchainSet{Seats: buildSeats} presence := &buildrun.PresenceSet{} trunks := &check.Trunks{} // A check is one ask per pull request and build module on the build seats' check stream, pulled by a build node with a @@ -228,7 +236,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro defer hearing.Stop() // Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read // (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28). - heard := buildrun.HearEnds(ctx, asks, asks, []string{goBuildModule, nodeBuildModule}, logger.Printf) + heard := buildrun.HearEnds(ctx, asks, asks, buildSeats.Modules(), logger.Printf) conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") }) waiter := buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing} asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf) @@ -279,13 +287,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } answerBound := set.answer holdCallsFor(answerBound) - verdicts := &manifestVerdicts{} compositions := check.EngineCompositions{ Requests: check.RequestKeeper{Store: requestsKV}, Asker: engineAsks{call: caller.CallOn}, Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return assignments, nil }}.Placed, Bound: answerBound, - Sandbox: seatSandbox{asker: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}}, + Sandbox: seatSandbox{asker: buildrun.Asker{Seats: buildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}}, Verdicts: verdicts.all, // The node-engine's trunk head: a group with no node-engine member composes with the composer built there (ADR 0656 §5). Composer: func() string { return trunks.Heads()[check.NodeEngineRepository] }, @@ -304,7 +311,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro logger.Printf("%s of %s was written and the part that reads it could not be told: %v", event, key, err) } } - repos := &repoModules{manifests: kvStore{manifests}, logf: logger.Printf, verdicts: verdicts} compositions.Composer = func() string { return repos.CommitOf(check.NodeEngineModule) } compositions.Manifests = repos // The indexer's packages state (ADR 0593 §2), so a Go check builds every mesh package at its published version: read @@ -314,7 +320,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro ch.Runs = repos // the node-engine a check reads beside at its running commit (ADR 0548 §6) ch.Says = verdictSays{js: js, node: cred.Node, now: time.Now} // a pull request's verdict event, on the stream asks (design 26 *Out of step*) ch.Judge = newJudge(judgeDeps{ - asks: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks, + asks: buildrun.Asker{Seats: buildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks, kept: kvStore{buckets[check.CheckerKeptBucket]}, previews: kvStore{buckets[check.PreviewsBucket]}, candidates: tryCandidates{candidates}, repos: repos, published: published, builds: buildStates, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf, announce: announce, compositions: compositions, inputs: runInputs{awaitedKV{inputsKV}}, beginBound: beginBound, @@ -323,15 +329,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro renames: seatRenames{call: caller.Call}, seatDefs: seatDefinitionsOf(seatDefs), seats: seats, + buildSeats: buildSeats, }) ch.Announce = announce tryer.Judge = ch.Judge // The toolchains first, so a check that starts from the first candidate finds the nodes. - watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger) + watch := newToolchainWatch(ctx, js, buildSeats, tools, ch.NodesChanged, logger) // Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the // assignments is settled first, so the toolchains settle through the gate already knowing it. - gate := buildrun.NewToolchainGate([]string{goBuildModule, nodeBuildModule}, tools.Put, logger.Printf) + gate := buildrun.NewToolchainGate(buildSeats.Modules(), tools.Put, logger.Printf) watch.put = gate.Toolchain // Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled. assignmentsErrs := make(chan error, 1) @@ -351,22 +358,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := watch.start(); err != nil { return err } - // A build module says each time it wrote its toolchains key: its stream, and the checker's durable consumer on it, are - // made by the controller. Where the stream is not there yet the checker says so and is started again with the build - // module. - for _, module := range []string{goBuildModule, nodeBuildModule} { - stream := toolchainStreams[module] - cons, err := cred.EventConsumer(ctx, js, stream) - if err != nil { - logger.Printf("the consumer %s_%s on %s does not exist yet, so %s's reports are not heard until the checker starts again: %v", cred.Node, cred.Module, stream, module, err) - continue - } - cc, err := process.HearEvents(cons, func(m jetstream.Msg) { watch.Reported(m) }, logger.Printf) + // A build seat says each time a node reported its toolchain (its event toolchain-reported, design 47 G §492): the bucket of + // the seat's holder is watched then, if it is not (Reported), and again when the seat list or the holders change (below). + for _, lang := range []string{buildstep.AskGo, buildstep.AskNode} { + seat, err := buildstep.BuildSeat(lang) if err != nil { return err } - defer cc.Stop() + if err := hub.Handle("mesh.seat."+seat+".event.toolchain-reported.>", func(m jetstream.Msg) { watch.Reported(m) }); err != nil { + return err + } } + // The build modules are named by the seat list and the indexer's modules: a change of either watches the steps and toolchains of + // a build module not watched yet and tells the gate which modules' reports it passes on. + buildsChanged.Set(func() { + heard.Watch(buildSeats.Modules()) + gate.UseModules(buildSeats.Modules()) + if err := watch.start(); err != nil { + logger.Printf("the toolchains of a build module cannot be watched: %v", err) + } + }) // The node-engine seat's running.., kept last per subject (design 47): a part a check could not hear runs, // and the check is asked again (issue 1373). Its stream and the checker's durable consumer on it are made from the // manifest's consumes; where they are not there yet the checker says so, and a check that could not run waits for its @@ -419,10 +430,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err != nil { return err } + buildsChanged.Changed() // the holders of the build seats are known from here go func() { errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) { before := repos.CommitOf(check.NodeEngineModule) repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) + buildsChanged.Changed() // a holder of a build seat may have moved if after := repos.CommitOf(check.NodeEngineModule); after != before { ch.TrunkMoved(ctx) // the node-engine the mesh runs moved: a verdict that read it beside is carried (ADR 0548 §6) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/refused_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/refused_test.go index 304cbd30..9370f34f 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/refused_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/refused_test.go @@ -20,11 +20,11 @@ import ( // its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate, // issue 774). func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) { - set := &buildrun.ToolchainSet{Seats: checkerBuildSeats} + set := &buildrun.ToolchainSet{Seats: testBuildSeats()} var logs bytes.Buffer logger := log.New(&logs, "", 0) refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-build-node_toolchains")} - if err := newToolchainWatch(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, set, func(context.Context) {}, logger).start(); err != nil { + if err := newToolchainWatch(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, testBuildSeats(), set, func(context.Context) {}, logger).start(); err != nil { t.Errorf("a refused toolchains bucket stopped the start: %v", err) } if !strings.Contains(logs.String(), "cannot be read") { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go index 73daac8f..697e256a 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go @@ -5,8 +5,10 @@ import ( "fmt" "maps" "slices" + "sync/atomic" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/seatlist" ) @@ -16,9 +18,9 @@ import ( // indexer's judge as a request's Seats, which the manifests of a change are judged against. // followSeatList starts the checker's reader of the seat list over the bus; the hub is started by the caller once every handler is registered. -func followSeatList(bus seatlist.Bus, logf func(string, ...any)) *seatlist.Reader { +func followSeatList(bus seatlist.Bus, logf func(string, ...any), changed func()) *seatlist.Reader { reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(), - OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }}) + OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats); changed() }}) if err != nil { logf("the checker cannot follow its seat list, so no manifest is judged until it starts again: %v", err) } @@ -58,3 +60,26 @@ func readIfUnread(reader *seatlist.Reader) { reader.ReloadIfUnread() } } + +// buildsWatch is what is done when the build modules may have changed (design 47 G A-82): the seat list names the build seats +// and the indexer's modules the holders of the seats, so a change of either may name a build module not watched yet. It is set once +// the parts that watch the build modules are made, and called from then on. +type buildsWatch struct{ fn atomic.Pointer[func()] } + +// Set takes the function in. +func (b *buildsWatch) Set(f func()) { b.fn.Store(&f) } + +// Changed calls it, none before it is set. +func (b *buildsWatch) Changed() { + if f := b.fn.Load(); f != nil { + (*f)() + } +} + +// definitionsOf are the seat definitions the reader holds, none before it has answered. +func definitionsOf(reader *seatlist.Reader) map[string]seatdef.Definition { + if reader == nil { + return nil + } + return reader.Definitions() +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist_test.go index 6090e162..b4d23411 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist_test.go @@ -27,7 +27,7 @@ func (b *silentThenAnswering) Subscribe(string, func(string, json.RawMessage) er // A seat that spoke before the checker's consumer existed is read when the hub is heard (ADR 0636 §1). func TestTheSeatListIsReadWhenTheHubIsHeard(t *testing.T) { bus := &silentThenAnswering{} - reader := followSeatList(bus, t.Logf) + reader := followSeatList(bus, t.Logf, func() {}) <-reader.Ready() if reader.Status().Heard { t.Fatal("a silent seat was heard") diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/buildmodules.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seats_test.go similarity index 65% rename from modules/mesh-delivery-checker/cmd/mesh-delivery-checker/buildmodules.go rename to modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seats_test.go index f07a698c..856e6f6e 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/buildmodules.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seats_test.go @@ -5,19 +5,19 @@ import ( "git.novox.be/novox/mesh-sdk/go/seatdef" ) -// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G -// A-82 is the builder's change; the builder's package holds no name of them any more): the holders of the three build seats. +// The holders of the build seats in the tests' seat list: names the checker's code does not know, found by the seats' acting +// rules (design 47 G A-82). const ( goBuildModule = "mesh-build-go" nodeBuildModule = "mesh-build-node" ) -// checkerBuildSeats is the finder over the build seats those names hold. -var checkerBuildSeats = func() *buildrun.BuildSeats { +// testBuildSeats is the finder over the build seats of the tests' seat list. +func testBuildSeats() *buildrun.BuildSeats { rule := func(on string) seatdef.Definition { return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}} } defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript")} holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule} return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }) -}() +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go index eb75a3c2..65f4643c 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go @@ -15,18 +15,15 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" ) -// toolchainStreams are the stream each build module carries its toolchain.reported event on, as the bus names it -// (declared by the build modules, heard by this module through consumes). -var toolchainStreams = map[string]string{goBuildModule: "TOOLCHAINS_GO", nodeBuildModule: "TOOLCHAINS_NODE"} - // toolchainWatch keeps the build modules' toolchains in a buildrun.ToolchainSet and tells the builder when one changed. A // build module's bucket is made when it is assigned, after the builder started, and the builder holds no timer (ADR 0437 // §8): it watches the buckets that exist at start, and a build module says each time it wrote its toolchains key // (toolchain.reported, handled by Reported); a bucket not watched yet is then watched. type toolchainWatch struct { - ctx context.Context - js bucketOpener - set *buildrun.ToolchainSet + ctx context.Context + js bucketOpener + seats *buildrun.BuildSeats // the build modules: the holders of the build seats (design 47 G A-82) + set *buildrun.ToolchainSet // put takes a change of a toolchains bucket in: set.Put, or the gate of the assigned build nodes before it (issue 1940). put func(module, key string, value []byte, deleted bool) bool changed func(ctx context.Context) // a language's entries changed: the failed builds are tried again @@ -36,13 +33,13 @@ type toolchainWatch struct { watched map[string]bool } -func newToolchainWatch(ctx context.Context, js bucketOpener, set *buildrun.ToolchainSet, changed func(context.Context), logger *log.Logger) *toolchainWatch { - return &toolchainWatch{ctx: ctx, js: js, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, len(checkerBuildSeats.Modules())), watched: map[string]bool{}} +func newToolchainWatch(ctx context.Context, js bucketOpener, seats *buildrun.BuildSeats, set *buildrun.ToolchainSet, changed func(context.Context), logger *log.Logger) *toolchainWatch { + return &toolchainWatch{ctx: ctx, js: js, seats: seats, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, 16), watched: map[string]bool{}} } // start watches the buckets that exist now; a missing one is said and waits for its build module's event. func (w *toolchainWatch) start() error { - for _, module := range checkerBuildSeats.Modules() { + for _, module := range w.seats.Modules() { if err := w.ensure(module); err != nil { return err } @@ -91,13 +88,15 @@ type delivered interface { Term() error } -// Reported takes one toolchain.reported event of a build module in: the module is read off the subject, -// mesh.stream..event.toolchain.reported, and its bucket is watched if it is not. A subject of no build module is -// terminated; a failure stays unacknowledged, and the server delivers it again. +// Reported takes one toolchain-reported event of a build seat in: the seat is read off the subject, +// mesh.seat..event.toolchain-reported., and the bucket of its holder is watched if it is not. A subject of no build +// seat with a holder is terminated; a failure stays unacknowledged, and the server delivers it again. func (w *toolchainWatch) Reported(m delivered) { - module, _, _ := strings.Cut(strings.TrimPrefix(m.Subject(), "mesh.stream."), ".event.") - if _, known := checkerBuildSeats.LanguageOf(module); !known || !strings.HasPrefix(m.Subject(), "mesh.stream.") { - w.logger.Printf("%s: not a build module's event, dropped", m.Subject()) + seat, _, _ := strings.Cut(strings.TrimPrefix(m.Subject(), "mesh.seat."), ".event.") + bm, known := w.seats.OfSeat(seat) + module := bm.Module + if !known || module == "" || bm.Toolchain == "" || !strings.HasPrefix(m.Subject(), "mesh.seat.") { + w.logger.Printf("%s: not the event of a build seat with a holder, dropped", m.Subject()) _ = m.Term() return } diff --git a/modules/mesh-delivery-checker/internal/check/build_change.go b/modules/mesh-delivery-checker/internal/check/build_change.go index 17f7cbee..bdd3dd46 100644 --- a/modules/mesh-delivery-checker/internal/check/build_change.go +++ b/modules/mesh-delivery-checker/internal/check/build_change.go @@ -74,13 +74,16 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str if !readable { return wait, words } - for _, bm := range slices.Sorted(maps.Keys(languageOf)) { + for _, bm := range st.j.Seats.Modules() { + if st.j.languageOf(bm) == "" { + continue + } if _, alters := changed[bm]; !alters { continue } var built []string for module, m := range changed { - if _, isBuildModule := languageOf[module]; !isBuildModule && buildsWith(m, languageOf[bm]) { + if isBuildModule := st.j.languageOf(module) != ""; !isBuildModule && buildsWith(m, st.j.languageOf(bm)) { built = append(built, module) delete(wait, module) } diff --git a/modules/mesh-delivery-checker/internal/check/buildmodules.go b/modules/mesh-delivery-checker/internal/check/buildmodules.go deleted file mode 100644 index 9ce5ca9c..00000000 --- a/modules/mesh-delivery-checker/internal/check/buildmodules.go +++ /dev/null @@ -1,11 +0,0 @@ -package check - -// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G -// A-82 is the builder's change; the builder's package holds no name of them any more). -const ( - goBuildModule = "mesh-build-go" - nodeBuildModule = "mesh-build-node" -) - -// languageOf is the language each build module's toolchain reports hold. -var languageOf = map[string]string{goBuildModule: "go", nodeBuildModule: "typescript"} diff --git a/modules/mesh-delivery-checker/internal/check/buildseats.go b/modules/mesh-delivery-checker/internal/check/buildseats.go new file mode 100644 index 00000000..dfa8f7f8 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/buildseats.go @@ -0,0 +1,56 @@ +package check + +import ( + "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "mesh-delivery-checker/rules" +) + +// **The checker names no build module** (design 47 G A-82): a build module is the holder of a build seat found on the seat list +// (buildrun.BuildSeats), by the language of the toolchain its seat's acting rule `build` names. The checker's own logic keys on +// that language: the Go repositories and the manifests are judged by the build module of Go, a TypeScript module's by that of +// TypeScript. + +// Languages of the toolchains the checker's logic is keyed on (the `on` of the build seats' acting rules). +const ( + languageGo = rules.LanguageGo + languageTypeScript = rules.LanguageTypeScript +) + +// buildModuleOf is the module that holds the build seat building a language's artifacts; the seat's name while no module is +// known to hold it or the seat list is not read, so a failure always names where it is. +func (j *Examiner) buildModuleOf(language string) string { + if m, err := j.Seats.For(language, ""); err == nil { + return m.Module + } + for _, m := range j.Seats.All() { + if m.Toolchain == language { + return m.Seat + } + } + ask := buildstep.AskGo + if language == languageTypeScript { + ask = buildstep.AskNode + } + seat, _ := buildstep.BuildSeat(ask) + return seat +} + +// goModule is the build module the Go repositories are judged by. +func (j *Examiner) goModule() string { return j.buildModuleOf(languageGo) } + +// goModule is the build module the manifests, the module graph and the checking program are judged by. +func (st *state) goModule() string { return st.j.goModule() } + +// languageOf is the language of the toolchain a build module builds with, empty for a module no build seat names. +func (j *Examiner) languageOf(module string) string { + l, _ := j.Seats.LanguageOf(module) + return l +} + +// askLanguageOf is the language of the asks the module's build seat takes (buildstep.AskGo, buildstep.AskNode, ...). +func (j *Examiner) askLanguageOf(module string) string { + if m, ok := j.Seats.OfModule(module); ok { + return m.AskLanguage() + } + return "" +} diff --git a/modules/mesh-delivery-checker/internal/check/buildseats_test.go b/modules/mesh-delivery-checker/internal/check/buildseats_test.go index e2eafa80..7dc80331 100644 --- a/modules/mesh-delivery-checker/internal/check/buildseats_test.go +++ b/modules/mesh-delivery-checker/internal/check/buildseats_test.go @@ -1,11 +1,20 @@ package check import ( + "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-sdk/go/seatdef" ) -// testSeats is the finder over the build seats the checker's names hold (buildmodules.go). +// The holders of the build seats in the tests' seat list: names the checker's code does not know, found by the seats' +// acting rules (design 47 G A-82). +const ( + goBuildModule = "mesh-build-go" + nodeBuildModule = "mesh-build-node" +) + +// testSeats is the finder over the build seats of the tests' seat list. func testSeats() *buildrun.BuildSeats { rule := func(on string) seatdef.Definition { return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}} @@ -14,3 +23,36 @@ func testSeats() *buildrun.BuildSeats { holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule} return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }) } + +// stepStatesOf is a Reader's StepStates over fixed states. +func stepStatesOf(m map[string]StateRead) func() map[string]StateRead { + return func() map[string]StateRead { return m } +} + +// The checker names no build module (design 47 G A-82): it finds each by the language its seat's acting rule builds, whatever the +// holder is called, and says that holder's name in what it judges. +func TestTheBuildModulesAreFoundOnTheSeatListWhateverTheyAreCalled(t *testing.T) { + rule := func(on string) seatdef.Definition { + return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}} + } + defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule("kind:image")} + holders := map[string]string{"node-build-go": "compilers", "node-build-node": "bundlers"} + j := &Examiner{Seats: buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })} + if got := j.goModule(); got != "compilers" { + t.Errorf("the module that judges Go = %q, want its seat's holder", got) + } + if got, lang, ask := j.buildModuleOf(languageTypeScript), j.languageOf("bundlers"), j.askLanguageOf("bundlers"); got != "bundlers" || lang != languageTypeScript || ask != "node" { + t.Errorf("TypeScript: module %q, language %q, ask language %q", got, lang, ask) + } + if got := j.languageOf("mesh-build-go"); got != "" { + t.Errorf("a module that holds no build seat builds with %q", got) + } + if !changesBuildGo(catalogueRepository, "compilers", []string{"modules/compilers/main.go"}) || changesBuildGo(catalogueRepository, "compilers", []string{"modules/mesh-build-go/main.go"}) { + t.Error("a change under the Go build module's own directory is the one that changes it") + } + // With no seat on the list the failure still names where it is: the seat. + none := &Examiner{Seats: buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, nil)} + if got := none.goModule(); got != "node-build-go" { + t.Errorf("a build seat no module holds is named by the seat, got %q", got) + } +} diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index 755d7339..7b006a3a 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -279,7 +279,7 @@ func newBench(t testing.TB) *bench { now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC) b.asks = &buildrun.FakeAsks{Answer: b.answer} b.ex = &Examiner{Asks: buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev, - Modules: langs{}, Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }} + Modules: langs{}, Seats: testSeats(), Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }} return b } 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 d43c905d..0ec67c82 100644 --- a/modules/mesh-delivery-checker/internal/check/failure_words_test.go +++ b/modules/mesh-delivery-checker/internal/check/failure_words_test.go @@ -28,7 +28,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) { test func(t *testing.T) }{ {"a failed tests verdict keeps its whole failure message", func(t *testing.T) { // 1901/AC1 - v := rules.LevelFailure("", "", "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) @@ -39,7 +39,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) { } }}, {"the status line names the failure not the first logged line", func(t *testing.T) { // 1901/AC2 - v := rules.LevelFailure("", "", "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) @@ -47,14 +47,14 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(t *testing.T) { // With no cause told apart (an older build module), the words still start at the first line, as before. f := fatalAfterLog() f.Cause = "" - v = rules.LevelFailure("", "", "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) } }}, {"words longer than the bound keep their last lines", func(t *testing.T) { // 1901/AC1 f := Failure{Output: strings.Repeat("log line of some length\n", 500) + "the cause is here"} - v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}}) + 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:]) } @@ -70,7 +70,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(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 := rules.LevelFailure("", "", "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) } @@ -88,7 +88,7 @@ func TestAFailuresWordsAreItsWholeMessageWithinTheirBound(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 := rules.LevelFailure("", 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 != meshdeliverycheckerclient.VerdictFail { t.Errorf("1901/AC8: the verdict is %q, want it failed", v.State) } diff --git a/modules/mesh-delivery-checker/internal/check/graph_edges.go b/modules/mesh-delivery-checker/internal/check/graph_edges.go index 766ad6c0..bb51c3b6 100644 --- a/modules/mesh-delivery-checker/internal/check/graph_edges.go +++ b/modules/mesh-delivery-checker/internal/check/graph_edges.go @@ -51,7 +51,7 @@ func (st *state) gatherGraph(ctx context.Context) { } change, err := read(req.Cases[0].Manifests) if err != nil { - st.fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) + st.fail(st.goModule(), "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) return } g := rules.Graph{} @@ -73,7 +73,7 @@ func (st *state) gatherGraph(ctx context.Context) { g.Edges = append(g.Edges, rules.Edge{From: pair[0], To: pair[1], Kind: trunk[pair], Only: rules.EdgeTrunk}) } } - st.facts = append(st.facts, rules.Step{Graph: &g}) + st.facts = append(st.facts, rules.Step{Module: st.goModule(), Graph: &g}) } func cmpPair(a, b [2]string) int { diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 099ba764..05172788 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -18,25 +18,22 @@ import ( "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) -// goModule is the build module the Go repositories are judged by. -const goModule = goBuildModule - // RunnerWorkspace is the runner the Go build module's digest and test steps are asked with when the merge changes the build // module itself: the installed module compiles the candidate's program from the merged tree and runs the step as it, shut // off (ADR 0535 §1). const RunnerWorkspace = "workspace" // buildGoDir is the Go build module's directory in the catalogue: a merge changing a path under it is judged by its -// candidate (ADR 0535 §1). mesh-build-node's own directory is judged by the installed module until its part of issue 1898. -const buildGoDir = "modules/mesh-build-go/" +// candidate (ADR 0535 §1). The other build modules' own directories are judged by the installed module until its part of issue 1898. +func buildGoDir(goModule string) string { return "modules/" + goModule + "/" } // changesBuildGo says whether a merge of the repository with these changed paths changes the Go build module. -func changesBuildGo(repository string, changed []string) bool { - if repository != catalogueRepository { +func changesBuildGo(repository, goModule string, changed []string) bool { + if repository != catalogueRepository || goModule == "" { return false } for _, p := range changed { - if strings.HasPrefix(p, buildGoDir) { + if strings.HasPrefix(p, buildGoDir(goModule)) { return true } } @@ -44,11 +41,11 @@ func changesBuildGo(repository string, changed []string) bool { } // The words of the line on the program that judged a change to the Go build module (ADR 0535 §2, §5). -const ( - judgedByOldInstalled = "judged by the installed mesh-build-go: it does not run a candidate yet" - judgedByInstalled = "judged by the installed mesh-build-go" - judgedByCandidate = "judged by the candidate's mesh-build-go at " -) +func judgedByOldInstalled(module string) string { + return "judged by the installed " + module + ": it does not run a candidate yet" +} +func judgedByInstalled(module string) string { return "judged by the installed " + module } +func judgedByCandidate(module string) string { return "judged by the candidate's " + module + " at " } // 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. @@ -68,9 +65,6 @@ type BuildModules interface { BuildModules(repository string) []string } -// askLanguage is the build seat language of each build module the checker asks. -var askLanguage = map[string]string{goBuildModule: buildstep.AskGo, nodeBuildModule: buildstep.AskNode} - // Examiner judges a case through the build seats (ADR 0484 clauses 3, 4, 10, 13; ADR 0525 §1, §3): for each pull request // and build module it puts one check ask whose job fetches the head merged onto its trunk head, then runs the unit level // and the integration level, taking the kept verdicts of unchanged packages and running the change's own tests first; @@ -87,6 +81,8 @@ type Examiner struct { // Previews is the previews bucket; nil writes none. Previews KV Modules BuildModules + // Seats finds the build modules from the seat list and the holders of seats (design 47 G A-82). + Seats *buildrun.BuildSeats // Runs says which commit the mesh runs of a module (the indexer's modules); nil knows none. Runs interface{ CommitOf(module string) string } // Forge reads the forge's files for the siblings a repository's tests read; nil reads none. @@ -217,8 +213,10 @@ func besideLines(refs []BesideRef) []string { // target is a pull request on one build module: the node that pulled its ask, the workspace its fetch made and what it // said. type target struct { - pull Pull - module string + pull Pull + module string + // language is the language of the toolchain the build module builds with (the `on` of its seat's acting rule). + language string node string toolchain string // agreed is the toolchain the ask names, so a node off it declines the check (ADR 0584); nil when none is agreed. @@ -243,12 +241,12 @@ type target struct { func (t *target) judgedBy(answer string) bool { switch { case strings.HasPrefix(answer, "candidate "): - t.judged = judgedByCandidate + short12(strings.TrimPrefix(answer, "candidate ")) + t.judged = judgedByCandidate(t.module) + short12(strings.TrimPrefix(answer, "candidate ")) return true case answer == "": - t.judged, t.runner = judgedByOldInstalled, "" + t.judged, t.runner = judgedByOldInstalled(t.module), "" default: - t.judged, t.runner = judgedByInstalled, "" + t.judged, t.runner = judgedByInstalled(t.module), "" } return false } @@ -544,18 +542,18 @@ func (st *state) judgeAll(ctx context.Context) { var merged []Fetched modules := st.j.Modules.BuildModules(p.Repository) for _, bm := range modules { - t := &target{pull: p, module: bm} - if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(languageOf[bm])); none == "" { + t := &target{pull: p, module: bm, language: st.j.languageOf(bm)} + if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(t.language)); none == "" { t.toolchain = tc.Version - t.agreed = &buildstep.ToolchainUsed{Language: languageOf[bm], Version: tc.Version, Recipe: tc.Recipe} + t.agreed = &buildstep.ToolchainUsed{Language: t.language, Version: tc.Version, Recipe: tc.Recipe} } args := buildstep.Fetch{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number), Base: p.Trunk} - if bm == nodeBuildModule { + if t.language == languageTypeScript { if groupPackage != nil && p.Repository != SDKRepository { args.Package = groupPackage } } - if bm == goBuildModule { + if t.language == languageGo { beside, err := st.j.besideFor(ctx, st.c, p) if err != nil { st.fail(forgeMachine(err, bm), err.Error()+unheardWords(err)) @@ -632,7 +630,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l job.NodeNames = names } if t.runner != "" { - job.Runner, job.RunnerWhen = t.runner, buildGoDir + job.Runner, job.RunnerWhen = t.runner, buildGoDir(st.goModule()) } if st.c.Try != nil && st.c.Try.Run != "" { job.Run, job.Kept = st.c.Try.Run, false // a try of some tests runs them alone: a partial run proves no package @@ -641,7 +639,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if st.c.Kind == KindTrunk { kind = buildstep.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4) } - a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: CheckerModule, + a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: st.j.askLanguageOf(t.module), Asker: CheckerModule, At: st.j.now().UTC(), Check: job, Needs: needs} if a.Language == buildstep.AskGo { a.Packages = st.packagesOf(p.Repository) @@ -730,7 +728,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l return } t.changed = t.fetched.Changed - if !changesBuildGo(p.Repository, t.changed) { + if !changesBuildGo(p.Repository, st.goModule(), t.changed) { t.runner = "" // the build module used no runner: the merge does not change it } if !placed { // the placed ask fetched the same merge: its preview was read from the first @@ -797,16 +795,16 @@ func unmetOf(levels []buildstep.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 buildstep.LevelAnswer) float64 { - if t.runner != "" && t.module == goBuildModule && la.Digest != nil { + if t.runner != "" && t.language == languageGo && 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) } - _, 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: rules.LevelFact(la)}) + _, elapsed, results := st.levels.Level(t.pull.Repository, t.pull.Number, t.module, t.language, t.node, la) + st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Language: t.language, Node: t.node, Level: rules.LevelFact(la)}) var digests map[string]string - if t.module == goBuildModule && la.Digest != nil { + if t.language == languageGo && 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() } diff --git a/modules/mesh-delivery-checker/internal/check/judged_test.go b/modules/mesh-delivery-checker/internal/check/judged_test.go index 40a8ebaf..d35a0321 100644 --- a/modules/mesh-delivery-checker/internal/check/judged_test.go +++ b/modules/mesh-delivery-checker/internal/check/judged_test.go @@ -189,7 +189,7 @@ func (e errorString) Error() string { return string(e) } func TestTheStatusTheForgeShowsNamesTheProgramThatJudged(t *testing.T) { // 1898/AC6, the forge's line ctx := context.Background() - words := judgedByCandidate + "fedcba987654" + words := judgedByCandidate(goBuildModule) + "fedcba987654" j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) { return Outcome{Verdict: passing(), Judged: map[string]string{repoA + "#7": words}}, nil }} diff --git a/modules/mesh-delivery-checker/internal/check/lostask_test.go b/modules/mesh-delivery-checker/internal/check/lostask_test.go index d9464d6e..43e49384 100644 --- a/modules/mesh-delivery-checker/internal/check/lostask_test.go +++ b/modules/mesh-delivery-checker/internal/check/lostask_test.go @@ -314,16 +314,17 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { ckey, _ := meshcatalogclient.CandidatesKey("feat/x") _, _ = cands.Create(ctx, ckey, cand) none := StateRead{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, error) { return nil, nil }} + steps := map[string]StateRead{goBuildModule: none} rd := Reader{CandidateState: kvRead(cands), VerdictState: kvRead(r.verdicts), AskState: kvRead(asksKept), - StepStates: map[string]StateRead{goBuildModule: none}} // no node took its ask yet + StepStates: func() map[string]StateRead { return steps }} // no node took its ask yet if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "waits in its build seat's stream") { t.Errorf("asked again while its ask waits in the stream (issue 1791's kind): %v", err) } - rd.StepStates[goBuildModule] = tr.Steps(true) + steps[goBuildModule] = tr.Steps(true) if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "a step of it runs") { t.Errorf("asked again while its step runs: %v", err) } - rd.StepStates[goBuildModule] = tr.Steps(false) + steps[goBuildModule] = tr.Steps(false) if st, _ := rd.Status(); !strings.Contains(st.Lost["feat/x"], AskAgainTool) { t.Errorf("status lost = %v, want feat/x named with %s", st.Lost, AskAgainTool) } @@ -368,8 +369,8 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { id := fmt.Sprint("mvl0frv18d19", 80+i) vals["ace."+id], _ = buildstep.EncodeStep(buildstep.Step{ID: id, Node: "ace", Tool: "check ask", State: buildstep.StepFailed, Passing: p, Ended: time.Now(), What: "check " + repoA + "@" + short12(head1)}) } - rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: map[string]StateRead{goBuildModule: { - Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}}} + rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: stepStatesOf(map[string]StateRead{goBuildModule: { + Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}})} if got, err := rd.AskAgain(asVerdicts{failedKV}, "feat/x", "the network"); (err == nil) != c.ok || (c.ok && !strings.HasPrefix(got.Said, AskedAgainWords)) { t.Errorf("%s: asked again = %+v, %v, want asked again %v", c.name, got, err, c.ok) } diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index 10fd9fe4..b8855c41 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -125,7 +125,7 @@ func (st *state) judgeManifests(ctx context.Context) { at = "mesh-delivery's trunk " + commit } if commit == "" { - st.fail(goModule, "the change's manifests cannot be judged: the trunk head of "+DeliveryRepository+" is not known") + st.fail(st.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 @@ -146,7 +146,7 @@ func (st *state) judgeManifests(ctx context.Context) { if !st.indexerOf(ctx, req, touched, cv, at, &m) { return } - st.facts = append(st.facts, rules.Step{Module: goModule, Manifests: &m}) + st.facts = append(st.facts, rules.Step{Module: st.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 @@ -154,7 +154,7 @@ func (st *state) judgeManifests(ctx context.Context) { func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string, versions buildstep.PackageVersions) (change, trunk composer.CaseVerdict, ok bool) { raw, err := json.Marshal(req) if err != nil { - st.fail(goModule, err.Error()) + st.fail(st.goModule(), err.Error()) return change, trunk, false } job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: DeliveryRepository, Commit: commit, Packages: versions}, Dir: JudgeDir, @@ -164,17 +164,17 @@ 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 { - st.fail(goModule, "the judge of the change's manifests could not be asked: "+err.Error()) + st.fail(st.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 { - st.fail(goModule, "the judge of the change's manifests: "+err.Error()) + st.fail(st.goModule(), "the judge of the change's manifests: "+err.Error()) return change, trunk, false } change, trunk, why := judgeAnswer(step) if why != "" { - st.fail(goModule, fmt.Sprintf("the indexer's judge at %s did not judge the change's manifests: %s", at, why)) + st.fail(st.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, true @@ -195,7 +195,7 @@ func (st *state) indexerOf(ctx context.Context, req composer.Request, touched [] for _, module := range touched { man, err := st.j.Read.Manifest(ctx, req.Cases[0].Manifests[module].Digest) if err != nil { - st.fail(goModule, fmt.Sprintf("the manifest of %s cannot be read, so the indexer cannot be ruled out: %v", module, err)) + st.fail(st.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 man.Claims { @@ -216,7 +216,7 @@ func (st *state) indexerOf(ctx context.Context, req composer.Request, touched [] trunk = st.j.Trunks.Heads()[DeliveryRepository] } if trunk == "" { - 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(st.goModule(), "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known") return false } judgedAt = "mesh-delivery's trunk " + trunk diff --git a/modules/mesh-delivery-checker/internal/check/package_cause_test.go b/modules/mesh-delivery-checker/internal/check/package_cause_test.go index 5e1ada4e..7922e012 100644 --- a/modules/mesh-delivery-checker/internal/check/package_cause_test.go +++ b/modules/mesh-delivery-checker/internal/check/package_cause_test.go @@ -39,7 +39,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - v := rules.LevelFailure("", "", "integration", Tested{Failed: []Failure{{Package: "github.com/novox/mesh-controller/cmd/mesh-controller", Output: c.output}}}) + v := rules.LevelFailure("", "", "", "integration", Tested{Failed: []Failure{{Package: "github.com/novox/mesh-controller/cmd/mesh-controller", Output: c.output}}}) for _, l := range strings.Split(v.Failed.Words, "\n") { if l = strings.TrimSpace(l); l == "PASS" || strings.HasPrefix(l, "ok ") || strings.HasPrefix(l, "ok\t") { t.Errorf("1901/AC5: the words keep %q: %q", l, v.Failed.Words) @@ -57,7 +57,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) { // A cause the build module told apart is cut to a line's bound, so the words keep their bound (the 1901 review's note). {"a long cause is cut to a lines bound", func(t *testing.T) { // 1901/AC5 f := Failure{Package: "p", Test: "TestX", Cause: strings.Repeat("c", 5000), Output: "x_test.go:1: something"} - v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{f}}) + v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{f}}) if len(v.Failed.Words) > rules.MaxWords+200 { t.Errorf("1901/AC5: words are %d bytes, bound %d", len(v.Failed.Words), rules.MaxWords) } @@ -67,7 +67,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) { // capture of the same name (go-test-json-pkg-timeout, -exiting, -killed): the package's own output, its "FAIL" line. {"a package stopped with only its fail line is still worded", func(t *testing.T) { // 1901/AC5 for _, out := range []string{"FAIL\texample.com/timeout\t2.017s", "FAIL\texample.com/exiting\t0.006s", "FAIL\texample.com/killed\t0.006s"} { - v := rules.LevelFailure("", "", "integration", Tested{Failed: []Failure{{Package: "example.com/x", Output: out}}}) + v := rules.LevelFailure("", "", "", "integration", Tested{Failed: []Failure{{Package: "example.com/x", Output: out}}}) if !strings.HasPrefix(afterHid(v.Failed.Words), "the package failed with no test to name and no panic, signal or build error in its output") { t.Errorf("1901/AC5: %q is worded %q", out, v.Failed.Words) } @@ -77,7 +77,7 @@ func TestAPackageFailuresCauseIsWordedNeverPass(t *testing.T) { // either (review round 1): go prints its summary in exact forms and the runtime's lines at column 0. {"only gos exact forms are summary or cause", func(t *testing.T) { // 1901/AC5 out := " x_test.go:3: ok so far\nok then: the seat is free\n panic: child retried and recovered\npanic: the real one\nFAIL\tp\t1.0s" - v := rules.LevelFailure("", "", "unit", Tested{Failed: []Failure{{Package: "p", Output: out}}}) + v := rules.LevelFailure("", "", "", "unit", Tested{Failed: []Failure{{Package: "p", Output: out}}}) if !strings.HasPrefix(afterHid(v.Failed.Words), "panic: the real one") || !strings.Contains(v.Failed.Words, "ok then: the seat is free") { t.Errorf("1901/AC5: words = %q", v.Failed.Words) } diff --git a/modules/mesh-delivery-checker/internal/check/programs.go b/modules/mesh-delivery-checker/internal/check/programs.go index 34710fa1..e0ad2d5e 100644 --- a/modules/mesh-delivery-checker/internal/check/programs.go +++ b/modules/mesh-delivery-checker/internal/check/programs.go @@ -184,7 +184,7 @@ func (st *state) seatList(ctx context.Context) bool { if st.j.SeatList != nil { defs, err := st.j.SeatList.Definitions() if err != nil { - st.fail(goModule, "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")") + st.fail(st.goModule(), "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")") return false } base = defs @@ -223,7 +223,7 @@ func (st *state) seatList(ctx context.Context) bool { } } } - st.fail(goModule, "the heads' seat list could not be made by the checking program: "+err.Error()) + st.fail(st.goModule(), "the heads' seat list could not be made by the checking program: "+err.Error()) return false } @@ -275,7 +275,7 @@ func (st *state) judged(ctx context.Context) (rules.Answer, *meshdeliverychecker } } } - st.fail(goModule, "the check could not be judged by the checking program: "+err.Error()) + st.fail(st.goModule(), "the check could not be judged by the checking program: "+err.Error()) return rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(st.factsOf())}, by } diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index 2f08a084..1056369d 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -44,11 +44,11 @@ type StateRead struct { type Reader struct { VerdictState, KeptState, CandidateState StateRead - PreviewState StateRead // the checker's merge previews - InputState StateRead // the checking program's inputs (InputsStateName) - StepStates map[string]StateRead // the build modules' steps states, by module - HaveState StateRead // the node-engine's have: whether a build module is on a node and running - StepBound time.Duration // how long the checker waits for one step; DefaultStepBound when 0 + PreviewState StateRead // the checker's merge previews + InputState StateRead // the checking program's inputs (InputsStateName) + StepStates func() map[string]StateRead // the build modules' steps states, by module: the holders of the build seats (design 47 G A-82), found at each read + HaveState StateRead // the node-engine's have: whether a build module is on a node and running + StepBound time.Duration // how long the checker waits for one step; DefaultStepBound when 0 // CouldNotRunState is the checker's own state could-not-run (CouldNotRunState); without it none is named. CouldNotRunState StateRead // Trunks are the trunk heads of every repository (the gate's source); nil names no re-judge after a trunk move. @@ -605,8 +605,12 @@ type moduleStep struct { // readSteps reads the newest MaxReads keys of each build module's steps state. func (r Reader) readSteps() stepsRead { out := stepsRead{running: []RunningStep{}, left: []LeftStep{}} - mods := make([]string, 0, len(r.StepStates)) - for m := range r.StepStates { + var states map[string]StateRead + if r.StepStates != nil { + states = r.StepStates() + } + mods := make([]string, 0, len(states)) + for m := range states { mods = append(mods, m) } sort.Strings(mods) @@ -618,7 +622,7 @@ func (r Reader) readSteps() stepsRead { bound = DefaultStepBound } for _, m := range mods { - sr := r.StepStates[m] + sr := states[m] if sr.Keys == nil || sr.Get == nil { continue } diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go index 06859e4d..80c1c1e1 100644 --- a/modules/mesh-delivery-checker/internal/check/read_test.go +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -211,7 +211,7 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { builds[k], _ = json.Marshal(buildsrec.BuilderBuild{V: 1, State: state}) } rd := Reader{ - StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}, + StepStates: stepStatesOf(map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}), CandidateState: fakeState{ckey: cand, ykey: candY}.read(), VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(), BuildState: builds.read(), @@ -269,14 +269,14 @@ func stepOf(t *testing.T, id, node string, started time.Time) []byte { func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *testing.T) { // 1890/AC2, AC3 now := time.Date(2026, 10, 23, 7, 0, 0, 0, time.UTC) rd := Reader{ - StepStates: map[string]StateRead{ + StepStates: stepStatesOf(map[string]StateRead{ "mesh-build-go": fakeState{ "g14.fresh1": stepOf(t, "fresh1", "g14", now.Add(-5*time.Minute)), "g14.old1": stepOf(t, "old1", "g14", now.Add(-33*time.Hour)), "ace.gone1": stepOf(t, "gone1", "ace", now.Add(-2*time.Minute)), "shanks.down1": stepOf(t, "down1", "shanks", now.Add(-time.Minute)), }.read(), - }, + }), HaveState: fakeState{ "g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning), "shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseFailed), @@ -427,7 +427,7 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { // steps["g14."+id] = raw } sb := &slowBucket{vals: steps} - st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(), + st, err := Reader{StepStates: stepStatesOf(map[string]StateRead{"mesh-build-go": sb.read()}), HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(), Now: func() time.Time { return now }}.Status() if err != nil || sb.reads.Load() != MaxReads || len(st.Running) != 1 || sb.maxFlight.Load() < 2 { t.Errorf("status: %d reads (want %d), running %+v, in flight %d, %v", sb.reads.Load(), MaxReads, st.Running, sb.maxFlight.Load(), err) diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index da264e05..b3543658 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -112,8 +112,9 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "inputs", TTL: 86400}, {Name: "compositions", TTL: 259200}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}, {Name: "could-not-run", History: 1}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } - if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains", - "mesh-delivery-builder.builds", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { + // The build modules are no names (design 47 G A-82): the steps and toolchains of the build seats are read as the seat's holder's states. + if want := []string{"mesh-delivery-builder.builds", "mesh-delivery-watchman.presence", "node-engine.have", + "seat:node-build-go.steps", "seat:node-build-go.toolchains", "seat:node-build-node.steps", "seat:node-build-node.toolchains"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } // DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the @@ -130,16 +131,15 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_seat_definitions", - "seat:git.file", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + "seat:git.file", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want the seat git's file, the seats' drop and compose-candidate", m.Invokes) } if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { t.Errorf("uses = %v, want the Go and Node build seats", m.Uses) } - // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. - if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", - "mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "node-engine.running"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + // The checker is woken by a build seat's report of a toolchain, as the builder is: the bucket may not exist when it starts. + if want := []string{"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", + "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.running"}; !reflect.DeepEqual(sorted(m.Consumes), want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index cc5ebb2c..edc99cbe 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -18,8 +18,6 @@ "mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", - "mesh-build-go.toolchain.reported", - "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", @@ -28,6 +26,8 @@ "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "mesh-conditions.findings-asked", + "node-build-go.toolchain-reported", + "node-build-node.toolchain-reported", "node-engine.running" ], "streams": [ @@ -50,13 +50,13 @@ { "name": "could-not-run", "history": 1 } ], "reads": [ - "mesh-build-go.steps", - "mesh-build-go.toolchains", - "mesh-build-node.steps", - "mesh-build-node.toolchains", "mesh-delivery-builder.builds", "mesh-delivery-watchman.presence", - "node-engine.have" + "node-engine.have", + "seat:node-build-go.steps", + "seat:node-build-go.toolchains", + "seat:node-build-node.steps", + "seat:node-build-node.toolchains" ], "uses": [ "node-build-go", @@ -91,6 +91,7 @@ "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", + "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate" diff --git a/modules/mesh-delivery-checker/rules/buildmodules.go b/modules/mesh-delivery-checker/rules/buildmodules.go deleted file mode 100644 index 805158a0..00000000 --- a/modules/mesh-delivery-checker/rules/buildmodules.go +++ /dev/null @@ -1,8 +0,0 @@ -package rules - -// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G -// A-82 is the builder's change; the builder's package holds no name of them any more). -const ( - goBuildModule = "mesh-build-go" - nodeBuildModule = "mesh-build-node" -) diff --git a/modules/mesh-delivery-checker/rules/facts.go b/modules/mesh-delivery-checker/rules/facts.go index b9b559cb..caee5d12 100644 --- a/modules/mesh-delivery-checker/rules/facts.go +++ b/modules/mesh-delivery-checker/rules/facts.go @@ -62,14 +62,16 @@ type Member struct { // answers (Manifests), the module graph's ordering edges (Graph) 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"` - Module string `json:"module,omitempty"` - Node string `json:"node,omitempty"` - Level *buildstep.LevelAnswer `json:"level,omitempty"` - Failed *FailedFirst `json:"failed,omitempty"` - Programs *Programs `json:"programs,omitempty"` - Manifests *Manifests `json:"manifests,omitempty"` + Repository string `json:"repository"` + Number int `json:"number"` + Module string `json:"module,omitempty"` + // Language is the language of the toolchain the build module that ran the step builds with (a level's counts differ by it). + Language string `json:"language,omitempty"` + Node string `json:"node,omitempty"` + Level *buildstep.LevelAnswer `json:"level,omitempty"` + Failed *FailedFirst `json:"failed,omitempty"` + Programs *Programs `json:"programs,omitempty"` + Manifests *Manifests `json:"manifests,omitempty"` // Graph is the module graph's ordering edges, as the change leaves them and as the trunk has them (JudgeGraph). Graph *Graph `json:"graph,omitempty"` // Renames is what a module brings and drops of `renamed-from` and how the seat mesh-config answered (JudgeRenames). @@ -141,7 +143,7 @@ func NewTally() Tally { // result is read and counted, also after one that did not pass, so the verdict names every failed test of the level // (issue 1921). It answers the level's failure (nil when it passed), its running time (its tests' and setup's) and the // results read, for the running checker to keep the passes from. -func (t Tally) Level(repository string, number int, module, node string, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64, []Tested) { +func (t Tally) Level(repository string, number int, module, language, node string, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64, []Tested) { a := t[la.Level] if a == nil { return failAt(node, fmt.Sprintf("the check of %s#%d answered the level %q, which is no level", repository, number, la.Level)), 0, nil @@ -158,7 +160,7 @@ func (t Tally) Level(repository string, number int, module, node string, la buil } results = append(results, res) elapsed += res.Elapsed + res.Setup + res.Lint - if module == nodeBuildModule { + if language == LanguageTypeScript { if !res.NoTests { a.Run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10) } @@ -176,7 +178,7 @@ func (t Tally) Level(repository string, number int, module, node string, la buil } } if len(failed) > 0 { - return LevelFailure(repository, module, la.Level, failed...), elapsed, results + return LevelFailure(repository, module, language, la.Level, failed...), elapsed, results } return nil, elapsed, results } @@ -200,7 +202,7 @@ func Judged(f Facts) Answer { case s.Failed != nil: failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: s.Failed}) case s.Level != nil: - if v, _, _ := tally.Level(s.Repository, s.Number, s.Module, s.Node, *s.Level); v != nil { + if v, _, _ := tally.Level(s.Repository, s.Number, s.Module, s.Language, s.Node, *s.Level); v != nil { failures = append(failures, v) } case s.Programs != nil: @@ -208,7 +210,7 @@ func Judged(f Facts) Answer { failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff}) } case s.Graph != nil: - ff, said := JudgeGraph(*s.Graph) + ff, said := JudgeGraph(*s.Graph, s.Module) notes = append(notes, said...) if ff != nil { failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff}) @@ -224,7 +226,7 @@ func Judged(f Facts) Answer { failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &ff}) } case s.Manifests != nil: - ff, said := JudgeManifests(*s.Manifests) + ff, said := JudgeManifests(*s.Manifests, s.Module) notes = append(notes, said...) if ff != nil { failures = append(failures, &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: ff}) diff --git a/modules/mesh-delivery-checker/rules/graph.go b/modules/mesh-delivery-checker/rules/graph.go index 166ddcc1..9e370d4f 100644 --- a/modules/mesh-delivery-checker/rules/graph.go +++ b/modules/mesh-delivery-checker/rules/graph.go @@ -44,7 +44,7 @@ type Graph struct { // JudgeGraph is the cycle rule over the graph's facts: the failure of a cycle the change makes, or the note of no cycle // or of a cycle the trunk already has. -func JudgeGraph(g Graph) (*FailedFirst, []string) { +func JudgeGraph(g Graph, machine string) (*FailedFirst, []string) { cycle := firstCycle(g.Edges, func(e Edge) bool { return e.Only != EdgeTrunk || !g.Trunk }) if cycle == nil { return nil, []string{"the change's module graph has no cycle"} @@ -58,7 +58,7 @@ func JudgeGraph(g Graph) (*FailedFirst, []string) { return nil, []string{"the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)"} } } - return &FailedFirst{Machine: goBuildModule, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+ + return &FailedFirst{Machine: machine, Words: 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)}, nil } diff --git a/modules/mesh-delivery-checker/rules/judged.go b/modules/mesh-delivery-checker/rules/judged.go index a5d99b06..7fad5525 100644 --- a/modules/mesh-delivery-checker/rules/judged.go +++ b/modules/mesh-delivery-checker/rules/judged.go @@ -19,9 +19,6 @@ import ( // 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 = 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" @@ -83,7 +80,7 @@ type Composed struct { // 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) (*FailedFirst, []string) { +func JudgeManifests(m Manifests, machine string) (*FailedFirst, []string) { var notes []string if m.Indexer != "" { var problems []string @@ -103,7 +100,7 @@ func JudgeManifests(m Manifests) (*FailedFirst, []string) { problems = append(problems, "the manifest validator does not know its fields "+iv.SetAside+", so it waits") } if len(problems) > 0 { - return &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", + return &FailedFirst{Machine: machine, 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)) @@ -147,7 +144,7 @@ func JudgeManifests(m Manifests) (*FailedFirst, []string) { 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 &FailedFirst{Machine: manifestsMachine, Words: strings.Join(refused, "\n")}, notes + return &FailedFirst{Machine: machine, 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)) } diff --git a/modules/mesh-delivery-checker/rules/language.go b/modules/mesh-delivery-checker/rules/language.go new file mode 100644 index 00000000..ac7e4474 --- /dev/null +++ b/modules/mesh-delivery-checker/rules/language.go @@ -0,0 +1,8 @@ +package rules + +// LanguageTypeScript is the language of the toolchain whose build module counts a package whole, not its tests (ADR 0484 clause +// 10): the `on` of a build seat's acting rule, never the name of a build module (design 47 G A-82). +const LanguageTypeScript = "typescript" + +// LanguageGo is the language of the toolchain of the build module that judges the Go repositories and the manifests. +const LanguageGo = "go" diff --git a/modules/mesh-delivery-checker/rules/words.go b/modules/mesh-delivery-checker/rules/words.go index 9a1d06a6..8e84c982 100644 --- a/modules/mesh-delivery-checker/rules/words.go +++ b/modules/mesh-delivery-checker/rules/words.go @@ -168,7 +168,7 @@ const MaxFailureLine = 300 // LevelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole, // and Failures every failed test of the level's results with its first line, at most meshdeliverycheckerclient.MaxFailures and an entry // saying how many more (issue 1921). -func LevelFailure(repository, module, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue { +func LevelFailure(repository, module, language, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue { v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail} var all []FailedFirst for _, res := range results { @@ -182,7 +182,7 @@ func LevelFailure(repository, module, level string, results ...Tested) *meshdeli } for _, f := range res.Failed { name := FailedName(f) - if module == nodeBuildModule && f.Package == "" { + if language == LanguageTypeScript && f.Package == "" { name = res.Module } all = append(all, FailedFirst{Test: level + ": " + name, Words: FailureWordsAfter(f, HidWords(module, res.By, f))})