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 c446fde..80d1623 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 @@ -220,7 +220,7 @@ func startBed(t *testing.T) *bed { for name, history := range map[string]int{delivery.CheckerVerdictsBucket: delivery.CheckerVerdictsHistory, delivery.CheckerKeptBucket: delivery.CheckerKeptHistory, delivery.CheckerOwedBucket: delivery.CheckerOwedHistory, delivery.PreviewsBucket: delivery.PreviewsHistory, delivery.CandidatesBucket: delivery.CandidatesHistory, delivery.ModulesBucket: delivery.ModulesHistory, delivery.ManifestsBucket: delivery.ManifestsHistory, delivery.SourceBucket: delivery.SourceHistory, - delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory} { + delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory, delivery.BuilderBuildsBucket: delivery.BuilderBuildsHistory} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)}) if err != nil { t.Fatal(err) @@ -526,3 +526,28 @@ func TestNoCheckStartsWhileADeliveryBuildsAndOneStartsWhenItEnds(t *testing.T) { t.Errorf("verdict = %+v", v) } } + +// TestTheLineSaysTheStateOfTheBuildsOfTheCandidatesModules: the builder wrote builds.avahi. done for the fingerprint the +// candidate carries for avahi; the status's line says avahi is built (ADR 0484 clause 7). +func TestTheLineSaysTheStateOfTheBuildsOfTheCandidatesModules(t *testing.T) { // N7/AC1 + b := startBed(t) + fp := "src2:" + strings.Repeat("a", 64) + buildKey, _ := delivery.BuilderBuildKey("avahi", fp) + done, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, By: "n1.mesh-delivery-builder", Manifest: strings.Repeat("0a", 32), + Artifacts: []delivery.BuiltArtifact{{Name: "avahi", Kind: "bundle", Digest: "sha256:" + strings.Repeat("a", 64)}}}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, buildKey, done); err != nil { + t.Fatal(err) + } + p := pull(repo, 7, head1) + p.Modules = []string{"avahi"} + p.Fps = map[string]delivery.CandidateModule{"avahi": {FP2: fp, Manifest: strings.Repeat("0a", 32)}} + key := b.candidate(t, "feat/x", p) + b.start(t, nil) + b.verdict(t, key, ended) + if o := b.owed(t, 7, head1, "success"); !strings.Contains(o.Text, "builds: avahi built") { + t.Errorf("owed = %+v, want the builds in the line", o) + } +} 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 46a0100..61fe261 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -24,12 +24,13 @@ type judgeDeps struct { previews check.KV repos *repoModules announce check.Announcer + builds check.BuildStates logf func(format string, args ...any) } 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, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} } // manifestKV reads a manifest by its digest. @@ -110,3 +111,19 @@ func (r *repoModules) BuildModules(repository string) []string { sort.Strings(out) return out } + +// buildReader reads the builder's build of a module at a fingerprint (mesh-delivery-builder_builds). +type buildReader struct{ kv kvStore } + +func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) { + key, err := delivery.BuilderBuildKey(module, fp2) + if err != nil { + return delivery.BuilderBuild{}, false, err + } + value, _, found, err := r.kv.Get(ctx, key) + if err != nil || !found { + return delivery.BuilderBuild{}, false, err + } + b, err := delivery.DecodeBuilderBuild(value) + return b, err == nil, err +} 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 0d1186d..7f01c48 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -198,6 +198,17 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Link: link, Logf: logger.Printf, } + // The builder's builds say how the candidates' fingerprints stand (issue 1684); where its bucket is not there yet the line + // says nothing of the builds, until the checker starts again. + var builds check.BuildStates + switch kv, err := js.KeyValue(ctx, delivery.BuilderBuildsBucket); { + case errors.Is(err, jetstream.ErrBucketNotFound): + logger.Printf("the bucket %s does not exist yet: the line says nothing of the builds until the checker starts again", delivery.BuilderBuildsBucket) + case err != nil: + return fmt.Errorf("the bucket %s: %w", delivery.BuilderBuildsBucket, err) + default: + builds = buildReader{kvStore{kv}} + } holds := newHoldWatch(ctx, js, ch.Reconsider, logger) ch.Hold = holds.Held announce := func(ctx context.Context, event, key string) { @@ -210,7 +221,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro ch.Judge = newJudge(judgeDeps{ runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks, kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, - repos: repos, logf: logger.Printf, + repos: repos, builds: builds, logf: logger.Printf, announce: announce, }) ch.Announce = announce diff --git a/modules/mesh-delivery-checker/internal/check/builds.go b/modules/mesh-delivery-checker/internal/check/builds.go new file mode 100644 index 0000000..f70115f --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/builds.go @@ -0,0 +1,66 @@ +package check + +import ( + "context" + "sort" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The states of a build the status line says (ADR 0484 clause 7). +const ( + BuildBuilt = "built" + BuildFailed = "failed" + BuildWaiting = "waiting" +) + +// BuildStates reads the builder's build of a module at a fingerprint of the new form (builds..): the +// value and whether the key exists. +type BuildStates interface { + Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) +} + +// NoteOf is the state of a build as the line says it: done is built, failed is failed, and one queued, building or not +// begun is waiting. +func NoteOf(b delivery.BuilderBuild, found bool) string { + switch { + case !found: + return BuildWaiting + case b.State == delivery.BuildDone: + return BuildBuilt + case b.State == delivery.BuildFailed: + return BuildFailed + } + return BuildWaiting +} + +// buildNotes are the state of each build the case's modules carry a fingerprint of, by module, sorted. A module two pull +// requests give is said once. +func (j *Examiner) buildNotes(ctx context.Context, c *Case) []BuildNote { + if j.Builds == nil { + return nil + } + fps := map[string]string{} + for _, p := range c.Pulls { + for module, cm := range p.Fps { + if cm.FP2 != "" { + fps[module] = cm.FP2 + } + } + } + modules := make([]string, 0, len(fps)) + for m := range fps { + modules = append(modules, m) + } + sort.Strings(modules) + var notes []BuildNote + for _, m := range modules { + b, found, err := j.Builds.Build(ctx, m, fps[m]) + if err != nil { + j.logf("the build of %s at %s cannot be read: %v", m, fps[m], err) + found = false + } + notes = append(notes, BuildNote{Module: m, State: NoteOf(b, found)}) + } + return notes +} diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index f5b239b..ec6c3aa 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -522,3 +522,33 @@ func TestTheIndexerIsToldOfAPreviewOnceWhenItIsCreated(t *testing.T) { // 1445/A t.Errorf("told %v, want once of %s", told, key) } } + +type fakeBuilds map[string]delivery.BuilderBuild + +func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) { + b, ok := f[module+" "+fp2] + return b, ok, nil +} + +func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T) { // N7/AC1 (the checker reads the builder's keys) + b := newBench(t) + fpA := "src2:" + strings.Repeat("a", 64) + fpB := "src2:" + strings.Repeat("b", 64) + fpC := "src2:" + strings.Repeat("c", 64) + b.ex.Builds = fakeBuilds{ + "avahi " + fpA: {State: delivery.BuildDone}, + "mesh-tools " + fpB: {State: delivery.BuildFailed, Error: "boom"}, + // netcheck has no key yet + } + p := pullOf(repoA, 7, head1) + p.Modules = []string{"avahi", "mesh-tools", "netcheck", "nofp"} + p.Fps = map[string]delivery.CandidateModule{"avahi": {FP2: fpA}, "mesh-tools": {FP2: fpB}, "netcheck": {FP2: fpC}, "nofp": {None2: "toolchain not yet reported"}} + out := b.judge(t, caseOf(t, p)) + var got []string + for _, n := range out.Builds { + got = append(got, n.Module+" "+n.State) + } + if strings.Join(got, ", ") != "avahi built, mesh-tools failed, netcheck waiting" { + t.Errorf("builds = %v, want one note for each module that has a fingerprint, sorted", got) + } +} diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 1785c5d..21dc121 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -50,6 +50,9 @@ type Examiner struct { // Previews is the previews bucket; nil writes none. Previews KV Modules BuildModules + // Builds says how the build of a fingerprint the candidate carries stands (the builder's builds bucket); nil says nothing + // of the builds. + Builds BuildStates // Announce says a preview was written under the key (EventPreviewWritten); nil says nothing. Announce Announcer // Trunks are the trunk heads of every repository, for the siblings a repository's tests read. @@ -115,18 +118,25 @@ type state struct { 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 + return st.finish(ctx, *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.finish(ctx, *v), nil } } } } - return st.outcome(delivery.Verdict{State: delivery.VerdictPass}), nil + return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil +} + +// finish is the outcome of the verdict with the state of the builds the candidate's modules carry. +func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome { + out := st.outcome(v) + out.Builds = st.j.buildNotes(ctx, st.c) + return out } // outcome fills the verdict's levels, times, machines and composition from what ran.