From 6c94ff75e2f1a79cb7f94bddd6f5046e61fd1d1d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:11:57 +0100 Subject: [PATCH] DLV-B8: the tag ask's narration is a case of the builder's whole-module test, and two instances together joins the archive table The builder's integration tests fall from 17 to 16: the two-instance test is a row of the archive table; the bed holds the tag asks and their key-ttl bounds and the builder's process log. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../mesh-delivery-builder/integration_test.go | 110 ++++++++++++++---- 1 file changed, 89 insertions(+), 21 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 9cbaf12e..2b6b08b1 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "os" "path/filepath" "strings" @@ -31,6 +30,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/process" "mesh-delivery-builder/internal/build" + "mesh-delivery-builder/internal/versions" ) // busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the @@ -195,7 +195,10 @@ type bed struct { builds jetstream.KeyValue tools map[string]jetstream.KeyValue instances jetstream.KeyValue - stop func() // stops the builder started last + tagAsks jetstream.KeyValue + tagBounds jetstream.KeyValue + out *saidLog // the builder's process log, every instance's + stop func() // stops the builder started last fake *fakeBuildModules srvURL string dir string @@ -272,6 +275,13 @@ func startBedWith(t *testing.T, closed bool) *bed { mk(delivery.ManifestsBucket, delivery.ManifestsHistory) b.builds = mk(delivery.BuilderBuildsBucket, delivery.BuilderBuildsHistory) b.instances = mk(instancesBucket, 1) + // The tag asks and their bounds (design 51 *Tagged versions*): the bounds declared key-ttl, so an ended bound leaves the + // bus's marker a watch hears. + b.tagAsks = mk(versions.TagAsksBucket, 1) + if b.tagBounds, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: versions.TagAskBoundsBucket, History: 1, LimitMarkerTTL: time.Minute}); err != nil { + t.Fatal(err) + } + b.out = &saidLog{} // The builder's module log, as the runtime raises it (ADR 0297): its stream on the log's subjects. logStream := buscompose.LogStreamName(delivery.BuilderModule, delivery.BuilderLog) if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: logStream, Subjects: []string{buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog) + ".>"}, @@ -389,12 +399,48 @@ func (b *bed) start(t *testing.T, node string, env map[string]string) { } runCtx, stop := context.WithCancel(b.ctx) done := make(chan error, 1) - go func() { done <- run(runCtx, func(k string) string { return e[k] }, io.Discard) }() + go func() { done <- run(runCtx, func(k string) string { return e[k] }, b.out) }() var once sync.Once b.stop = func() { once.Do(func() { stop(); <-done }) } t.Cleanup(b.stop) } +// saidLog is the builder's process log as its instances write it. +type saidLog struct { + mu sync.Mutex + buf strings.Builder +} + +func (l *saidLog) Write(p []byte) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.Write(p) +} + +// awaitSaid reads the process log until it holds want, within 20 s, and answers the line. +func (b *bed) awaitSaid(t *testing.T, want string) string { + t.Helper() + deadline := time.Now().Add(20 * time.Second) + for { + b.out.mu.Lock() + all := b.out.buf.String() + b.out.mu.Unlock() + for _, line := range strings.Split(all, "\n") { + if strings.Contains(line, want) { + return line + } + } + if time.Now().After(deadline) { + t.Fatalf("the builder never said %q; it said:\n%s", want, all) + } + select { + case <-b.ctx.Done(): + t.Fatal(b.ctx.Err()) + case <-time.After(50 * time.Millisecond): + } + } +} + // await reads the build key until pred holds of it, within 20 s. func (b *bed) await(t *testing.T, module string, pred func(delivery.BuilderBuild) bool) delivery.BuilderBuild { t.Helper() @@ -426,7 +472,16 @@ func isState(state string) func(delivery.BuilderBuild) bool { func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 b := startBed(t) digest := b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck") - b.start(t, "n1", nil) + // DLV-B6: a tag ask kept and never numbered (no check builds its head) is narrated once its bound ends on the bus, with + // what it waits on, and its bound is written again; no timer runs in the builder. + key, ask, err := versions.NewTagAsk("novox/mesh-sdk", "feat/x", "abc", "a group member's check", "me@shanks", time.Now()) + if err != nil { + t.Fatal(err) + } + if _, err := b.tagAsks.Put(b.ctx, key, ask); err != nil { + t.Fatal(err) + } + b.start(t, "n1", map[string]string{"MESH_BUILDER_TAG_ASK_BOUND": "1s"}) v := b.await(t, "netcheck", isState(delivery.BuildDone)) if v.By != "n1.mesh-delivery-builder" || v.Manifest != digest || len(v.Artifacts) != 2 { t.Fatalf("build = %+v", v) @@ -455,38 +510,51 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 t.Errorf("%s called %d times, want %d (on one node, ADR 0478 clause 4)", tool, got, want) } } -} - -// TestTwoBuilderInstancesStartingTogetherEndWithOneDoneKey: two builders on two nodes start together on one module key of an -// archive only (no toolchain named, as the indexer writes it); the second takes over the key the first holds building -// (ADR 0478 clause 6), and the key ends done, written once. -func TestTwoBuilderInstancesStartingTogetherEndWithOneDoneKey(t *testing.T) { // 1607/AC7 - b := startBed(t) - b.archive(t, "ca-trust", "modules/ca-trust", commit) - b.start(t, "n1", nil) - b.start(t, "n2", nil) - v := b.await(t, "ca-trust", isState(delivery.BuildDone)) - if len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "archive" || v.Artifacts[0].Digest != digestOf("files") { - t.Fatalf("build = %+v, want the archive's digest", v) + said := b.awaitSaid(t, "the tag ask of novox/mesh-sdk feat/x at abc (feat-x) is not answered after") + if !strings.Contains(said, "the check's build of abc is not claimed yet") { + t.Errorf("the narration = %q, want what the ask waits on", said) } - if n := b.fake.count("mesh-build-go.build_go_build"); n < 1 { // each instance that built, on one node - t.Errorf("build_go_build was called %d times, want one for each instance that built", n) + if e, err := b.tagAsks.Get(b.ctx, key); err != nil { + t.Fatal(err) + } else if a, _ := versions.DecodeTagAsk(e.Value()); a.Answered.IsZero() || !a.Unanswered() { + t.Errorf("the tag ask = %+v, want kept and answered, no version yet", a) + } + for deadline := time.Now().Add(10 * time.Second); ; time.Sleep(50 * time.Millisecond) { + if _, err := b.tagBounds.Get(b.ctx, key); err == nil { + break + } else if time.Now().After(deadline) { + t.Fatalf("the bound is not written again after its narration: %v", err) + } } } // TestAnArchiveOnlyModuleAndARootModuleAreBuilt: the indexer gives an archive no language, so modules.ca-trust has no // toolchains; the builder builds it on the build module's nodes all the same (review round 4). A module whose path is "." -// is given to the build tools as it is (1615/AC4). One table: the two tests were merged to pay for the enrolment's. +// is given to the build tools as it is (1615/AC4). Two builders on two nodes start together on the archive's key: the +// second takes over the key the first holds building (ADR 0478 clause 6), and the key ends done, written once (1607/AC7). +// One table: three tests were merged, to pay for the enrolment's and to bring mesh-delivery's count down (DLV-B8). func TestAnArchiveOnlyModuleAndARootModuleAreBuilt(t *testing.T) { - for _, c := range []struct{ name, path string }{{"no toolchain", "modules/ca-trust"}, {"root", delivery.RootPath}} { + for _, c := range []struct { + name, path string + two bool + }{{"no toolchain", "modules/ca-trust", false}, {"root", delivery.RootPath, false}, {"two instances together", "modules/ca-trust", true}} { t.Run(c.name, func(t *testing.T) { b := startBed(t) b.archive(t, "ca-trust", c.path, commit) b.start(t, "n1", nil) + if c.two { + b.start(t, "n2", nil) + } v := b.await(t, "ca-trust", isState(delivery.BuildDone)) if c.name == "no toolchain" && (len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (")) { t.Errorf("build = %+v, want done naming no toolchain", v) } + if c.two && (len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "archive" || v.Artifacts[0].Digest != digestOf("files")) { + t.Errorf("build = %+v, want the archive's digest", v) + } + if n := b.fake.count("mesh-build-go.build_go_build"); c.two && n < 1 { // each instance that built, on one node + t.Errorf("build_go_build was called %d times, want one for each instance that built", n) + } }) } }