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 4b4a008..b80e7b0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -2,9 +2,11 @@ package main import ( "context" - "errors" - "log" + "sort" + "sync" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" "mesh-delivery-checker/internal/check" @@ -12,7 +14,6 @@ import ( // judgeDeps are the parts the judge of a check is made of. type judgeDeps struct { - checker *check.Checker runner buildrun.Runner tools *buildrun.ToolchainSet presence *buildrun.PresenceSet @@ -21,15 +22,90 @@ type judgeDeps struct { trunks *check.Trunks kept check.KV previews check.KV + repos *repoModules logf func(format string, args ...any) } -var _ = log.Printf - -type unbuilt struct{} - -func (unbuilt) Judge(context.Context, *check.Case) (check.Outcome, error) { - return check.Outcome{}, errors.New("the judge of a check is not built yet") +func newJudge(d judgeDeps) check.Judge { + return &check.Examiner{Steps: d.runner, Toolchains: d.tools, Presence: d.presence, Health: d.have, Places: d.places, + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Trunks: d.trunks, Logf: d.logf} } -func newJudge(judgeDeps) check.Judge { return unbuilt{} } +// manifestKV reads a manifest by its digest. +type manifestKV interface { + Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error) +} + +// repoModules knows which build modules judge a repository, from the indexer's modules and their manifests: the Go build +// module always (a repository's go.mod files are its own), the Node one when a module of the repository has a TypeScript +// artifact (ADR 0484 clause 10). Safe for use from the watch and from a check at once. +type repoModules struct { + manifests manifestKV + logf func(format string, args ...any) + + mu sync.Mutex + langs map[string]map[string]bool // by manifest digest: the languages of its artifacts + repoOf map[string]string // by module: its repository + typed map[string]string // by module: its manifest digest +} + +// Put takes a change of the indexer's modules bucket in: a module's repository and the languages its manifest names. +func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted bool) { + r.mu.Lock() + if r.repoOf == nil { + r.repoOf, r.typed, r.langs = map[string]string{}, map[string]string{}, map[string]map[string]bool{} + } + if deleted { + delete(r.repoOf, key) + delete(r.typed, key) + r.mu.Unlock() + return + } + r.mu.Unlock() + m, err := delivery.DecodeModule(value) + if err != nil { + return + } + r.mu.Lock() + r.repoOf[key] = m.Repository + r.typed[key] = m.Manifest + _, known := r.langs[m.Manifest] + r.mu.Unlock() + if m.Manifest == "" || known { + return + } + set := map[string]bool{} + if mk, err := delivery.ManifestsKey(m.Manifest); err == nil { + if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found { + if kept, err := delivery.DecodeManifest(raw); err == nil { + if man, err := catalogue.ParseManifest([]byte(kept.JSON)); err == nil && man.Build != nil { + for _, a := range man.Build.Artifacts { + if a.Language != "" { + set[a.Language] = true + } + } + } else if err != nil && r.logf != nil { + r.logf("the manifest %s cannot be read, so its module counts as no TypeScript: %v", m.Manifest, err) + } + } + } + } + r.mu.Lock() + r.langs[m.Manifest] = set + r.mu.Unlock() +} + +// BuildModules are the build modules that judge a repository, sorted. +func (r *repoModules) BuildModules(repository string) []string { + out := []string{buildrun.GoBuildModule} + r.mu.Lock() + defer r.mu.Unlock() + for module, repo := range r.repoOf { + if repo == repository && r.langs[r.typed[module]]["typescript"] { + out = append(out, buildrun.NodeBuildModule) + break + } + } + 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 new file mode 100644 index 0000000..ba3c1c0 --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" +) + +type memManifests map[string][]byte + +func (m memManifests) Get(_ context.Context, key string) ([]byte, uint64, bool, error) { + v, ok := m[key] + return v, 1, ok, nil +} + +func putModule(t *testing.T, r *repoModules, name, repository, manifest string) { + t.Helper() + data, err := delivery.EncodeModule(delivery.Module{Repository: repository, Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(manifest))}) + if err != nil { + t.Fatal(err) + } + r.Put(context.Background(), name, data, false) +} + +func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *testing.T) { // 1292/AC2 + own, err := os.ReadFile("../../module.json") // a manifest with a Go bundle: this module's own + if err != nil { + t.Fatal(err) + } + goManifest := string(own) + // A TypeScript bundle names neither a system nor a directory it is built from (the catalogue's parser says so). + var doc map[string]any + if err := json.Unmarshal(own, &doc); err != nil { + t.Fatal(err) + } + artifacts := doc["build"].(map[string]any)["artifacts"].([]any) + program := artifacts[0].(map[string]any) + program["language"] = "typescript" + delete(program, "system") + delete(program, "from") + delete(program, "binary") + tsRaw, err := json.Marshal(doc) + if err != nil { + t.Fatal(err) + } + tsManifest := string(tsRaw) + store := memManifests{} + for _, m := range []string{goManifest, tsManifest} { + data, err := delivery.EncodeManifest(delivery.Manifest{Module: "demo", JSON: m}) + if err != nil { + t.Fatal(err) + } + store[delivery.ManifestDigest([]byte(m))] = data + } + r := &repoModules{manifests: store} + 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, ",") != buildrun.GoBuildModule { + t.Errorf("mesh-delivery: %v, want the Go build module alone", got) + } + if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule { + t.Errorf("mesh-catalog: %v, want both", got) + } + if got := r.BuildModules("novox/mesh-sdk"); strings.Join(got, ",") != buildrun.GoBuildModule { + t.Errorf("a repository with no module: %v, want the Go build module", got) + } +} 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 6faacfa..9fc4703 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -192,10 +192,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Link: link, Logf: logger.Printf, } + repos := &repoModules{manifests: kvStore{buckets[delivery.ManifestsBucket]}, logf: logger.Printf} ch.Judge = newJudge(judgeDeps{ - checker: ch, runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks, + runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks, kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, - logf: logger.Printf, + repos: repos, logf: logger.Printf, }) // The toolchains first, so a check that starts from the first candidate finds the nodes. @@ -220,7 +221,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } defer cc.Stop() } - errs := make(chan error, 6) + errs := make(chan error, 8) // The watchman's presence says which nodes are asleep. Where its bucket is not there yet no node is online, so nothing // is checked; the checker says so and is started again with the watchman. switch kv, err := js.KeyValue(ctx, delivery.PresenceBucket); { @@ -285,6 +286,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }) }() } + // The modules say which repository holds a TypeScript module, so the Node build module judges it too. + mw, err := settle(ctx, buckets[delivery.ModulesBucket], func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) }) + if err != nil { + return err + } + go func() { + errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) }) + }() each := func(e jetstream.KeyValueEntry) { if opOf(e) { ch.Candidate(ctx, e.Key(), e.Value()) diff --git a/modules/mesh-delivery-checker/internal/check/beside.go b/modules/mesh-delivery-checker/internal/check/beside.go new file mode 100644 index 0000000..f18ae1a --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/beside.go @@ -0,0 +1,62 @@ +package check + +import "strconv" + +// BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR +// 0484 clause 4). Ref names the pull request's head when the commit is only there. +type BesideRef struct { + Repository string `json:"repository"` + Commit string `json:"commit"` + Ref string `json:"ref,omitempty"` +} + +// besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside +// beside.go): the catalogue at its trunk, the node-engine, the SDK as the controller pins it, and the delivery's pinned +// forge dependencies (mesh-catalog checks/deps). The checker chooses which sibling at which commit; the build module only +// places what it is told. +var besideRules = map[string][]string{ + "novox/mesh-controller": {"novox/mesh-catalog", "novox/mesh-host", "novox/mesh-sdk"}, + "novox/mesh-host": {"novox/mesh-catalog", "novox/mesh-sdk"}, + "novox/mesh-sdk": {"novox/mesh-catalog", "novox/mesh-host"}, + "novox/mesh-catalog": {"novox/mesh-sdk", "novox/mesh-controller"}, + "novox/mesh-delivery": {"novox/mesh-sdk", "novox/mesh-catalog"}, +} + +// Beside are the siblings a pull request's tests read: each at its trunk head, and at the head of the group's pull request +// when the group holds one of that repository (a group's members are checked together, ADR 0482 clause 2) — except the +// catalogue, which the tests read at its trunk even when the group holds a catalogue pull request (beside.go). +func (j *Examiner) Beside(c *Case, p Pull) []BesideRef { + var heads map[string]string + if j.Trunks != nil { + heads = j.Trunks.Heads() + } + var out []BesideRef + for _, repo := range besideRules[p.Repository] { + if member := c.member(repo); member != nil && repo != "novox/mesh-catalog" { + out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)}) + continue + } + if head := heads[repo]; head != "" { + out = append(out, BesideRef{Repository: repo, Commit: head}) + } + } + return out +} + +func (c *Case) member(repository string) *Pull { + for i := range c.Pulls { + if c.Pulls[i].Repository == repository { + return &c.Pulls[i] + } + } + return nil +} + +func refOf(number int) string { + if number <= 0 { + return "" + } + return "refs/pull/" + itoa(number) + "/head" +} + +func itoa(n int) string { return strconv.Itoa(n) } diff --git a/modules/mesh-delivery-checker/internal/check/checker.go b/modules/mesh-delivery-checker/internal/check/checker.go index 5706ae4..9084403 100644 --- a/modules/mesh-delivery-checker/internal/check/checker.go +++ b/modules/mesh-delivery-checker/internal/check/checker.go @@ -23,6 +23,8 @@ const NodesWords = "build node qualifies" type Outcome struct { Verdict delivery.Verdict Builds []BuildNote + // Slowest names the slowest tests of a level that went over its bound ("unit pkg.Test 1.2s"). + Slowest []string } // BuildNote is the state of a candidate's module build, by module: built, failed or waiting (ADR 0484 clause 7). diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go new file mode 100644 index 0000000..446ee9c --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -0,0 +1,456 @@ +package check + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" +) + +// captured reads a result the build module's own code marshalled (testdata/captured/README). +func captured[T any](t testing.TB, name string) T { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "captured", name+".json")) + if err != nil { + t.Fatal(err) + } + var v T + if err := json.Unmarshal(raw, &v); err != nil { + t.Fatal(err) + } + return v +} + +type stepCall struct { + module, node, tool string + args map[string]any +} + +// fakeSteps stands for the build modules: a handler for each tool answers what the module's code answers. +type fakeSteps struct { + mu sync.Mutex + calls []stepCall + handlers map[string]func(c stepCall) (any, error) +} + +func (f *fakeSteps) Run(_ context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error) { + c := stepCall{module, node, tool, args} + f.mu.Lock() + f.calls = append(f.calls, c) + h := f.handlers[tool] + f.mu.Unlock() + if h == nil { + return nil, fmt.Errorf("the fake build module has no tool %s", tool) + } + v, err := h(c) + if err != nil { + return nil, err + } + return json.Marshal(v) +} + +func (f *fakeSteps) tool(name string) []stepCall { + f.mu.Lock() + defer f.mu.Unlock() + var out []stepCall + for _, c := range f.calls { + if strings.HasSuffix(c.tool, "_"+name) { + out = append(out, c) + } + } + return out +} + +type entries []delivery.NodeToolchain + +func (e entries) Entries(language string) []delivery.NodeToolchain { + var out []delivery.NodeToolchain + for _, x := range e { + if x.Language == language { + out = append(out, x) + } + } + return out +} + +type langs map[string][]string + +func (l langs) BuildModules(repository string) []string { + if m, ok := l[repository]; ok { + return m + } + return []string{buildrun.GoBuildModule} +} + +func goEntries(nodes ...string) entries { + var e entries + for _, n := range nodes { + e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}}) + e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}}) + } + return e +} + +const digC = "cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33" +const digD = "dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44" + +type bench struct { + ex *Examiner + steps *fakeSteps + kept *memKV + prev *memKV + fetch Fetched + digest map[string]Digested // by level + test func(c stepCall) (any, error) +} + +// newBench is an examiner over fake build modules that merge cleanly, digest the captured fixture's packages and pass +// the captured slow fixture's result at both levels, on shanks. +func newBench(t testing.TB) *bench { + b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV()} + b.fetch = Fetched{Workspace: "ws1", Commit: head1, Modules: []string{"."}, Base: trunkA, Merge: "9999999999999999999999999999999999999999", + Changed: []string{"README.md"}} + one := captured[Digested](t, "go-digest") + b.digest = map[string]Digested{ + delivery.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}}, + delivery.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}}, + } + _ = one + slow := captured[Tested](t, "go-test-slow") + b.test = func(c stepCall) (any, error) { + r := slow + r.Level, _ = c.args["level"].(string) + return r, nil + } + b.steps.handlers = map[string]func(stepCall) (any, error){ + "build_go_fetch": func(stepCall) (any, error) { return b.fetch, nil }, + "build_node_fetch": func(stepCall) (any, error) { return b.fetch, nil }, + "build_go_digest": func(c stepCall) (any, error) { return b.digest[c.args["level"].(string)], nil }, + "build_go_test": func(c stepCall) (any, error) { return b.test(c) }, + "build_node_test": func(c stepCall) (any, error) { return b.test(c) }, + } + now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC) + b.ex = &Examiner{Steps: b.steps, Toolchains: goEntries("shanks"), Places: &buildrun.Places{}, Kept: Kept{Store: b.kept}, Previews: b.prev, + Modules: langs{}, Trunks: &Trunks{}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }} + return b +} + +func caseOf(t testing.TB, pulls ...delivery.CandidatePull) *Case { + t.Helper() + raw := candidateOf(t, "feat/x", pulls...) + c, err := delivery.DecodeCandidate(raw) + if err != nil { + t.Fatal(err) + } + cs := CaseOf(c, map[string]string{repoA: trunkA, repoSDK: trunkS}) + if cs == nil { + t.Fatal("no case") + } + return cs +} + +func (b *bench) judge(t testing.TB, cs *Case) Outcome { + t.Helper() + out, err := b.ex.Judge(context.Background(), cs) + if err != nil { + t.Fatal(err) + } + return out +} + +func testedWith(t testing.TB, name string, edit func(*Tested)) Tested { + r := captured[Tested](t, name) + if edit != nil { + edit(&r) + } + return r +} + +func packagesOf(c stepCall) []string { + var out []string + if l, ok := c.args["packages"].([]string); ok { + out = l + } + return out +} + +func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679/AC1 + b := newBench(t) + cs := caseOf(t, pullOf(repoA, 7, head1)) + out := b.judge(t, cs) + v := out.Verdict + if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 { + t.Fatalf("verdict = %+v", v) + } + if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun { + t.Errorf("composition = %+v, want not run", v.Composition) + } + if len(v.Machines) != 1 || v.Machines[0] != "shanks" { + t.Errorf("machines = %v", v.Machines) + } + fetch := b.steps.tool("fetch") + if len(fetch) != 1 || fetch[0].args["repository"] != repoA || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "refs/pull/7/head" { + t.Fatalf("fetch calls = %+v", fetch) + } + if v.Took.Clone <= 0 || v.Took.Unit <= 0 { + t.Errorf("took = %+v, want the clone and the unit level timed", v.Took) + } + // What passed is kept under each level's digest. + for _, k := range []string{"unit." + digA, "integration." + digB} { + if _, _, found, _ := b.kept.Get(context.Background(), k); !found { + t.Errorf("%s was not kept", k) + } + } +} + +func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 + b := newBench(t) + b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}} + if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digC, keptValue("fixture/other", 40)); err != nil { + t.Fatal(err) + } + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + tests := b.steps.tool("test") + if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" { + t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests) + } + if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 { + t.Errorf("unit = %+v, want 2 run and 40 kept", u) + } +} + +func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3 + b := newBench(t) + failing := captured[Tested](t, "go-test-failing") + b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}} + b.test = func(c stepCall) (any, error) { + if c.args["level"] == "unit" { + return failing, nil + } + return captured[Tested](t, "go-test-slow"), nil + } + cs := caseOf(t, pullOf(repoA, 7, head1)) + first := b.judge(t, cs).Verdict + if first.State != delivery.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" { + t.Fatalf("first = %+v", first) + } + if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digB); !found { + t.Fatal("the package that passed in the failing run was not kept") + } + if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digA); found { + t.Fatal("the package that failed was kept") + } + // Asked again with nothing changed: only the failed package runs; the good one is taken. + b.steps.calls = nil + b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + tests := b.steps.tool("test") + if len(tests) == 0 || strings.Join(packagesOf(tests[0]), ",") != "fixture/failing/bad" { + t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests) + } + // The trunk moved and one digest changed: that package runs, the unchanged one is taken. + b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}} + b.steps.calls = nil + b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + tests = b.steps.tool("test") + if got := strings.Join(packagesOf(tests[0]), ","); got != "fixture/failing/bad,fixture/failing/good" && got != "fixture/failing/good,fixture/failing/bad" { + t.Errorf("after a digest changed the run was %q, want the failed package and the changed one", got) + } +} + +func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1292/AC1 + b := newBench(t) + b.ex.Modules = langs{repoA: {buildrun.NodeBuildModule}} + b.fetch.Modules = []string{"modules/ui"} + b.test = func(c stepCall) (any, error) { + return Tested{Level: c.args["level"].(string), Module: "modules/ui", Passed: true, Elapsed: 0.4, Bound: 5}, nil + } + cs := caseOf(t, pullOf(repoA, 7, head1)) + for i := 0; i < 2; i++ { + v := b.judge(t, cs).Verdict + if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 0 || v.Levels[delivery.LevelUnit].Run != 1 { + t.Fatalf("round %d: verdict = %+v", i, v) + } + } + if n := len(b.steps.tool("test")); n != 4 { + t.Errorf("%d node test calls in two checks, want 4 (both levels each time)", n) + } + if n := len(b.steps.tool("digest")); n != 0 { + t.Errorf("the node build module was asked for a digest %d times", n) + } + if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 { + t.Errorf("kept %v for a node package", keys) + } +} + +func TestTheChangesOwnFailingTestRunsFirstAndNothingElseRuns(t *testing.T) { // 1289/AC1, AC2 + b := newBench(t) + b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"} + b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} + failing := captured[Tested](t, "go-test-failing") + b.test = func(c stepCall) (any, error) { + if got := packagesOf(c); len(got) == 1 && got[0] == "m/internal/b" { + return failing, nil + } + return captured[Tested](t, "go-test-slow"), nil + } + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Test, "TestThreeIsFour") { + t.Fatalf("verdict = %+v", out.Verdict) + } + tests := b.steps.tool("test") + if len(tests) != 1 || strings.Join(packagesOf(tests[0]), ",") != "m/internal/b" { + t.Fatalf("test calls = %+v, want only m/internal/b, once", tests) + } + if n := len(b.steps.tool("digest")); n != 1 { + t.Errorf("%d digest calls, want the unit level's alone (the check stopped)", n) + } + if out.Verdict.Composition == nil || out.Verdict.Composition.State != delivery.CompositionNotRun { + t.Errorf("composition = %+v", out.Verdict.Composition) + } +} + +func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 1289/AC3 + b := newBench(t) + b.fetch.Changed = []string{"modules/m/internal/b/b_test.go", "modules/m/internal/a/a.go"} + b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + if out.Verdict.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", out.Verdict) + } + var unit []string + for _, c := range b.steps.tool("test") { + if c.args["level"] == "unit" { + unit = append(unit, strings.Join(packagesOf(c), ",")) + } + } + if strings.Join(unit, " | ") != "m/internal/b | m/internal/a" { + t.Errorf("the unit level ran %v, want the changed test's package first, then the rest", unit) + } +} + +func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T) { // 1289/AC4 + b := newBench(t) + b.fetch = Fetched{Workspace: "ws1", Commit: head1, Base: trunkA, Conflicts: []string{"go.mod", "internal/x.go"}} + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + f := out.Verdict.Failed + if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: go.mod, internal/x.go") { + t.Fatalf("verdict = %+v", out.Verdict) + } + if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 { + t.Errorf("%d digest and test steps ran after a conflict", n) + } +} + +func TestNoQualifyingNodeIsAFailureOfTheNodes(t *testing.T) { // N6/AC4 + b := newBench(t) + b.ex.Toolchains = entries{} + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + if !IsNodesFailure(out.Verdict) { + t.Fatalf("verdict = %+v, want a failure of the nodes", out.Verdict) + } + b.ex.Toolchains = goEntries("shanks") + b.ex.Presence = offline{} + if out = b.judge(t, caseOf(t, pullOf(repoA, 7, head1))); !IsNodesFailure(out.Verdict) { + t.Fatalf("an offline node: verdict = %+v", out.Verdict) + } +} + +type offline struct{} + +func (offline) Online(string) bool { return false } + +func TestAStepThatFailsIsAVerdictNamingTheNode(t *testing.T) { + b := newBench(t) + b.steps.handlers["build_go_fetch"] = func(stepCall) (any, error) { return nil, fmt.Errorf("the step failed: ssh refused") } + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "ssh refused") { + t.Fatalf("verdict = %+v", out.Verdict) + } +} + +func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // 1287/AC4 + b := newBench(t) + b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, fmt.Errorf("go list failed") } + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + if out.Verdict.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", out.Verdict) + } + tests := b.steps.tool("test") + if len(tests) != 2 || tests[0].args["packages"] != nil { + t.Errorf("test calls = %+v, want each level once, whole", tests) + } + if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 { + t.Errorf("kept %v with no digest", keys) + } +} + +func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndKeepsItsVerdict(t *testing.T) { // 1300/AC3 + b := newBench(t) + b.test = func(c stepCall) (any, error) { + return testedWith(t, "go-test-slow", func(r *Tested) { + r.Level = c.args["level"].(string) + r.OverBound = true + r.OverWhich = "run" + r.Elapsed = 7 + }), nil + } + out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) + v := out.Verdict + if v.State != delivery.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" { + t.Fatalf("verdict = %+v", v) + } + if len(out.Slowest) == 0 || !strings.Contains(out.Slowest[0], "TestTakesAWhile") { + t.Errorf("slowest = %v", out.Slowest) + } +} + +func TestSkippedTestsAreCountedPerLevel(t *testing.T) { // 1300/AC6, 1251 + b := newBench(t) + b.test = func(c stepCall) (any, error) { + return testedWith(t, "go-test-slow", func(r *Tested) { r.Level = c.args["level"].(string); r.Skipped = 3 }), nil + } + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict + if v.Levels[delivery.LevelUnit].Skipped != 3 || v.Levels[delivery.LevelIntegration].Skipped != 3 { + t.Errorf("levels = %+v", v.Levels) + } +} + +func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testing.T) { // 1624/AC1 + b := newBench(t) + cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)) + v := b.judge(t, cs).Verdict + if v.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", v) + } + fetch := b.steps.tool("fetch") + if len(fetch) != 2 { + t.Fatalf("%d fetches, want one for each member", len(fetch)) + } + var forA []BesideRef + for _, f := range fetch { + if f.args["repository"] == repoA { + forA, _ = f.args["beside"].([]BesideRef) + } + } + found := false + for _, r := range forA { + if r.Repository == repoSDK && r.Commit == head2 && r.Ref == "refs/pull/12/head" { + found = true + } + } + if !found { + t.Errorf("mesh-delivery's tests were not given the SDK member at its head: %+v", forA) + } +} + +func TestAnExaminerIsAJudgeOfTheChecker(t *testing.T) { + var _ Judge = (*Examiner)(nil) +} diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go new file mode 100644 index 0000000..a6ac48c --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -0,0 +1,455 @@ +package check + +import ( + "context" + "encoding/json" + "fmt" + "path" + "sort" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" +) + +// Steps runs a build module's long step on a node and answers its result (buildrun.Runner). +type Steps interface { + Run(ctx context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error) +} + +// ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet). +type ToolchainEntries interface { + Entries(language string) []delivery.NodeToolchain +} + +// BuildModules says which build modules judge a repository: the Go one always, the Node one when the repository holds a +// TypeScript module. +type BuildModules interface { + BuildModules(repository string) []string +} + +// toolPrefix is the start of each build module's tool names. +var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_"} + +// Examiner judges a case through the build modules (ADR 0484 clauses 3, 4, 10, 13): for each pull request it fetches the head +// merged onto its trunk head on a build node, then runs the unit level and the integration level, taking the kept verdicts +// of unchanged packages, running the change's own tests first and stopping at the first failure. +type Examiner struct { + Steps Steps + Toolchains ToolchainEntries + // Presence says which nodes are online; Health whether a node's build module is running and healthy. A node that is not + // is not chosen; nil means every node is. + Presence interface{ Online(node string) bool } + Health interface { + Healthy(node, module string) bool + } + // Places holds a step back while its node is at the checker's capacity (ADR 0484 clause 12). + Places *buildrun.Places + Kept Kept + // Previews is the previews bucket; nil writes none. + Previews KV + Modules BuildModules + // Trunks are the trunk heads of every repository, for the siblings a repository's tests read. + Trunks interface{ Heads() map[string]string } + Now func() time.Time + Logf func(format string, args ...any) +} + +func (j *Examiner) now() time.Time { + if j.Now != nil { + return j.Now() + } + return time.Now() +} + +func (j *Examiner) logf(format string, args ...any) { + if j.Logf != nil { + j.Logf(format, args...) + } +} + +// target is a pull request on one build module: the node chosen, the workspace its fetch made and what it said. +type target struct { + pull Pull + module string + node string + toolchain string + fetched Fetched + // priority are the Go packages (import paths) or the Node module directories whose test files the merge changes. + changed []string + plans map[string]*goPlan // by level +} + +// goPlan is what a Go target has to run at one level: the digests, the packages without a kept verdict and the tests taken. +type goPlan struct { + digests map[string]string + todo []string + // all says the digest step gave nothing to key a kept verdict by, so the level runs whole and keeps nothing. + all bool +} + +// acc collects a level's counts across targets. +type acc struct { + run, kept, skipped int + elapsed, setup float64 + over bool + slowest []TestTime +} + +// state is one judging. +type state struct { + j *Examiner + c *Case + targets []*target + levels map[string]*acc + nodes map[string]bool + clone time.Duration + slow []string +} + +// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot +// be judged at all. +func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { + st := &state{j: j, c: c, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}} + if v := st.fetchAll(ctx); v != nil { + return st.outcome(*v), nil + } + for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + for _, priority := range []bool{true, false} { + for _, t := range st.targets { + if v := st.runTarget(ctx, t, level, priority); v != nil { + return st.outcome(*v), nil + } + } + } + } + return st.outcome(delivery.Verdict{State: delivery.VerdictPass}), nil +} + +// outcome fills the verdict's levels, times, machines and composition from what ran. +func (st *state) outcome(v delivery.Verdict) Outcome { + if v.State == delivery.VerdictPass || len(v.Levels) == 0 { + v.Levels = map[string]delivery.LevelResult{} + for level, a := range st.levels { + v.Levels[level] = delivery.LevelResult{Run: a.run, Kept: a.kept, Skipped: a.skipped} + } + } + ms := func(s float64) int64 { return int64(s * 1000) } + u, i := st.levels[delivery.LevelUnit], st.levels[delivery.LevelIntegration] + v.Took = delivery.Took{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)} + for node := range st.nodes { + v.Machines = append(v.Machines, node) + } + sort.Strings(v.Machines) + for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + if st.levels[level].over { + v.OverBound = append(v.OverBound, level) + } + } + // The composition is the assigner's check (issue 1297): until it delivers a verdict says it was not run (ADR 0484 clause 6). + v.Composition = &delivery.Composition{State: delivery.CompositionNotRun} + var slow []string + for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + a := st.levels[level] + if !a.over { + continue + } + sort.SliceStable(a.slowest, func(x, y int) bool { return a.slowest[x].Elapsed > a.slowest[y].Elapsed }) + for n, s := range a.slowest { + if n == 3 { + break + } + slow = append(slow, fmt.Sprintf("%s %s %.1fs", level, path.Base(s.Package)+"."+s.Test, s.Elapsed)) + } + } + return Outcome{Verdict: v, Slowest: slow} +} + +// fail is a verdict of failure at a machine. +func fail(machine, words string) *delivery.Verdict { + v := failure(machine, words) + return &v +} + +// step runs one step of a build module on the node the target is on, holding a place there while it runs. +func (st *state) step(ctx context.Context, t *target, tool, label string, args map[string]any) (json.RawMessage, error) { + st.c.Progress.Set(label, t.node) + release, err := st.j.Places.Acquire(ctx, t.node) + if err != nil { + return nil, err + } + defer release() + return st.j.Steps.Run(ctx, t.module, t.node, toolPrefix[t.module]+tool, args) +} + +// choose picks the build node for a pull request's repository on a build module. +func (st *state) choose(bm string) (node, toolchain string, err error) { + language := buildrun.LanguageOf[bm] + qualifies := func(node string) bool { + return (st.j.Presence == nil || st.j.Presence.Online(node)) && (st.j.Health == nil || st.j.Health.Healthy(node, bm)) + } + nodes, tc, err := buildrun.ChooseNodes(language, st.j.Toolchains.Entries(language), st.j.Places, qualifies, 1) + if err != nil { + return "", "", err + } + return nodes[0], tc.Version, nil +} + +// fetchAll fetches every pull request's head merged onto its trunk on a build node of each build module the repository +// needs. A head that does not merge, or a step that fails, ends the check at once, running no test (ADR 0484 clause 13). +func (st *state) fetchAll(ctx context.Context) *delivery.Verdict { + began := st.j.now() + defer func() { st.clone = st.j.now().Sub(began) }() + for _, p := range st.c.Pulls { + for _, bm := range st.j.Modules.BuildModules(p.Repository) { + node, tc, err := st.choose(bm) + if err != nil { + return fail(bm, err.Error()) + } + t := &target{pull: p, module: bm, node: node, toolchain: tc, plans: map[string]*goPlan{}} + st.nodes[node] = true + args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk} + if bm == buildrun.GoBuildModule { + if beside := st.j.Beside(st.c, p); len(beside) > 0 { + args["beside"] = beside + } + } + raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", p.Repository, p.Number, node), args) + if err != nil { + return fail(node, fmt.Sprintf("fetch of %s#%d: %v", p.Repository, p.Number, err)) + } + if err := json.Unmarshal(raw, &t.fetched); err != nil || t.fetched.Workspace == "" { + return fail(node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, raw)) + } + if len(t.fetched.Conflicts) > 0 { + return fail(node, fmt.Sprintf("%s#%d does not merge into the trunk: files in conflict: %s", p.Repository, p.Number, strings.Join(t.fetched.Conflicts, ", "))) + } + t.changed = t.fetched.Changed + st.targets = append(st.targets, t) + } + } + return nil +} + +// isTestFile says whether a repository-relative path is a test file of the languages the checker judges: Go's _test.go and +// the TypeScript module's .test. and .spec. files and test directories. +func isTestFile(p string) bool { + switch { + case strings.HasSuffix(p, "_test.go"): + return true + case strings.Contains(p, ".test.") || strings.Contains(p, ".spec."): + return true + } + for _, seg := range strings.Split(path.Dir(p), "/") { + if seg == "__tests__" || seg == "test" || seg == "tests" { + return true + } + } + return false +} + +// changesTestOf says whether the merge changes a test file of the Go package with the import path pkg: the directory of the +// file and the import path agree on their last segments, whichever holds the module's directory and path in front. +func changesTestOf(changed []string, pkg string) bool { + for _, f := range changed { + if !strings.HasSuffix(f, "_test.go") { + continue + } + dir := path.Dir(f) + if dir == pkg || strings.HasSuffix(pkg, "/"+dir) || strings.HasSuffix(dir, "/"+pkg) { + return true + } + } + return false +} + +// changesTestIn says whether the merge changes a test file inside the directory of a Node module. +func changesTestIn(changed []string, dir string) bool { + for _, f := range changed { + if !isTestFile(f) || strings.HasSuffix(f, "_test.go") { + continue + } + if dir == "." || dir == "" || strings.HasPrefix(f, strings.TrimSuffix(dir, "/")+"/") { + return true + } + } + return false +} + +// failedName names a failure: its package and test. +func failedName(f Failure) string { + if f.Test == "" { + return f.Package + } + return f.Package + "." + f.Test +} + +// firstLine is the first non-empty line of a failure's output, shortened. +func firstLine(s string) string { + for _, l := range strings.Split(s, "\n") { + if l = strings.TrimSpace(l); l != "" { + if len(l) > 120 { + l = l[:120] + } + return l + } + } + return "" +} + +// failure turns a level that did not pass into a verdict naming what failed first. +func levelFailure(t *target, level string, res Tested) *delivery.Verdict { + v := delivery.Verdict{State: delivery.VerdictFail} + switch { + case len(res.Failed) > 0: + f := res.Failed[0] + name := failedName(f) + if t.module == buildrun.NodeBuildModule && f.Package == "" { + name = res.Module + } + v.Failed = delivery.FailedFirst{Test: level + ": " + name, Words: firstLine(f.Output)} + default: + why := res.Why + if why == "" { + why = "the " + level + " level did not pass" + } + v.Failed = delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why} + } + return &v +} + +// runTarget runs one level of a target: first the packages the change's own tests are in (priority), then the others. +func (st *state) runTarget(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict { + if t.module == buildrun.NodeBuildModule { + return st.runNode(ctx, t, level, priority) + } + return st.runGo(ctx, t, level, priority) +} + +// A level's work is planned once for a target: the digest of each package, the kept verdicts taken, the packages left to run. +func (st *state) planGo(ctx context.Context, t *target, level string) *delivery.Verdict { + if t.plans[level] != nil { + return nil + } + plan := &goPlan{digests: map[string]string{}} + t.plans[level] = plan + raw, err := st.step(ctx, t, "digest", fmt.Sprintf("digest the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node), + map[string]any{"workspace": t.fetched.Workspace, "level": level}) + var d Digested + if err == nil { + err = json.Unmarshal(raw, &d) + } + if ctx.Err() != nil { + return fail(t.node, ctx.Err().Error()) + } + if err != nil || len(d.Packages) == 0 { + // The build module named no digest (a level it cannot digest here): the level runs whole and keeps nothing. + if err != nil { + st.j.logf("%s#%d: no digest of the %s level, so it runs whole: %v", t.pull.Repository, t.pull.Number, level, err) + } + plan.all = true + return nil + } + plan.digests = d.Packages + a := st.levels[level] + pkgs := make([]string, 0, len(d.Packages)) + for p := range d.Packages { + pkgs = append(pkgs, p) + } + sort.Strings(pkgs) + for _, p := range pkgs { + k, found, err := st.j.Kept.Lookup(ctx, level, d.Packages[p]) + if err != nil { + st.j.logf("the kept verdict of %s at %s cannot be read, so it runs: %v", p, level, err) + } + if found { + a.kept += k.Tests + continue + } + plan.todo = append(plan.todo, p) + } + return nil +} + +func (st *state) runGo(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict { + if v := st.planGo(ctx, t, level); v != nil { + return v + } + plan := t.plans[level] + var run []string + switch { + case plan.all: + if priority { + return nil // nothing to put first: the level runs whole after + } + default: + for _, p := range plan.todo { + if changesTestOf(t.changed, p) == priority { + run = append(run, p) + } + } + if len(run) == 0 { + return nil + } + } + args := map[string]any{"workspace": t.fetched.Workspace, "level": level} + if run != nil { + args["packages"] = run + } + raw, err := st.step(ctx, t, "test", fmt.Sprintf("test the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node), args) + if err != nil { + return fail(t.node, fmt.Sprintf("the %s level of %s#%d: %v", level, t.pull.Repository, t.pull.Number, err)) + } + var res Tested + if err := json.Unmarshal(raw, &res); err != nil { + return fail(t.node, fmt.Sprintf("the %s level of %s#%d answered what is not a result: %v", level, t.pull.Repository, t.pull.Number, err)) + } + a := st.levels[level] + a.run += res.Tests + a.skipped += res.Skipped + a.elapsed += res.Elapsed + a.setup += res.Setup + a.over = a.over || res.OverBound + a.slowest = append(a.slowest, res.Slowest...) + if len(plan.digests) > 0 { + if _, err := st.j.Kept.KeepPasses(ctx, t.pull.Repository, level, t.toolchain, res, plan.digests, st.j.now()); err != nil { + st.j.logf("%s#%d: a passing package could not be kept: %v", t.pull.Repository, t.pull.Number, err) + } + } + if !res.Passed { + return levelFailure(t, level, res) + } + return nil +} + +func (st *state) runNode(ctx context.Context, t *target, level string, priority bool) *delivery.Verdict { + for _, dir := range t.fetched.Modules { + if changesTestIn(t.changed, dir) != priority { + continue + } + raw, err := st.step(ctx, t, "test", fmt.Sprintf("test the %s level of %s#%d (%s) on %s", level, t.pull.Repository, t.pull.Number, dir, t.node), + map[string]any{"workspace": t.fetched.Workspace, "module": dir, "level": level}) + if err != nil { + return fail(t.node, fmt.Sprintf("the %s level of %s#%d (%s): %v", level, t.pull.Repository, t.pull.Number, dir, err)) + } + var res Tested + if err := json.Unmarshal(raw, &res); err != nil { + return fail(t.node, fmt.Sprintf("the %s level of %s#%d (%s) answered what is not a result: %v", level, t.pull.Repository, t.pull.Number, dir, err)) + } + if res.Module == "" { + res.Module = dir + } + a := st.levels[level] + if !res.NoTests { + a.run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10) + } + a.elapsed += res.Elapsed + a.setup += res.Setup + a.over = a.over || res.OverBound + if !res.Passed { + return levelFailure(t, level, res) + } + } + return nil +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/README b/modules/mesh-delivery-checker/internal/check/testdata/captured/README new file mode 100644 index 0000000..e39f215 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/README @@ -0,0 +1,9 @@ +go-test-failing.json, go-test-slow.json, go-test-empty.json and go-digest.json are what mesh-catalog's mesh-build-go answers +(TestResult and DigestResult, cmd/mesh-build-go levels.go and digest.go), marshalled by the module's own code: a throwaway +test beside the module's test_test.go and digest_test.go (workspaceFor + runUnit on the fixtures testdata/captured/failing, +slow and empty, and digestFixture + digests on testdata/repos/digest) at mesh-catalog origin/main cdbdb474, captured on +2026-10-21 for issue 1679 and not kept there. The fixtures' package names are the build module's own (fixture/failing/bad...). + +The fields a fetch with a base adds (base, merge, conflicts, changed, trees) are the contract the lead relayed to the +mesh-catalog agent (ADR 0484 clause 3, 2026-10-21); mesh-catalog's fetch step is not on main yet, so the fetch fixtures in +the tests are written from that contract and are to be replaced by a capture once its fetch_test can marshal one. diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-digest.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-digest.json new file mode 100644 index 0000000..50f848b --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-digest.json @@ -0,0 +1,11 @@ +{ + "level": "unit", + "packages": { + "fixture/digest/a": "c288afe6252c38d9e7286ecb5f28b3c961780871fcc253670dcc3151c11c1df2", + "fixture/digest/c": "11abf5f32f0f12caafd76c565e4c2c2f3bf2e3ce6c2c7fcb6c2264a06db40087" + }, + "without_tests": [ + "fixture/digest/b", + "fixture/digest/d" + ] +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-empty.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-empty.json new file mode 100644 index 0000000..daee148 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-empty.json @@ -0,0 +1,21 @@ +{ + "level": "unit", + "passed": true, + "why": "the repository has no tests at the unit level", + "no_tests": true, + "tests": 0, + "skipped": 0, + "packages": [ + { + "dir": ".", + "package": "fixture/empty", + "passed": true, + "elapsed": 0, + "tests": 0, + "no_tests": true + } + ], + "elapsed": 0, + "bound": 5, + "over_bound": false +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-failing.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-failing.json new file mode 100644 index 0000000..2f6d90b --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-failing.json @@ -0,0 +1,45 @@ +{ + "level": "unit", + "passed": false, + "why": "1 failed: fixture/failing/bad TestThreeIsFour", + "tests": 2, + "skipped": 0, + "packages": [ + { + "dir": ".", + "package": "fixture/failing/bad", + "passed": false, + "elapsed": 0.003, + "tests": 1 + }, + { + "dir": ".", + "package": "fixture/failing/good", + "passed": true, + "elapsed": 0.002, + "tests": 1 + } + ], + "elapsed": 0.003, + "bound": 5, + "slowest": [ + { + "package": "fixture/failing/bad", + "test": "TestThreeIsFour", + "elapsed": 0 + }, + { + "package": "fixture/failing/good", + "test": "TestTwo", + "elapsed": 0 + } + ], + "failed": [ + { + "package": "fixture/failing/bad", + "test": "TestThreeIsFour", + "output": " bad_test.go:7: Three() = 3, want 4\n--- FAIL: TestThreeIsFour (0.00s)" + } + ], + "over_bound": false +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-slow.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-slow.json new file mode 100644 index 0000000..aa6e575 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-test-slow.json @@ -0,0 +1,30 @@ +{ + "level": "unit", + "passed": true, + "tests": 2, + "skipped": 0, + "packages": [ + { + "dir": ".", + "package": "fixture/slow", + "passed": true, + "elapsed": 0.065, + "tests": 2 + } + ], + "elapsed": 0.065, + "bound": 5, + "slowest": [ + { + "package": "fixture/slow", + "test": "TestTakesAWhile", + "elapsed": 0.06 + }, + { + "package": "fixture/slow", + "test": "TestQuick", + "elapsed": 0 + } + ], + "over_bound": false +}