diff --git a/cmd/mesh-controller/away_integration_test.go b/cmd/mesh-controller/away_integration_test.go index 69099dc7..45156cdd 100644 --- a/cmd/mesh-controller/away_integration_test.go +++ b/cmd/mesh-controller/away_integration_test.go @@ -241,3 +241,23 @@ func (k keysHolding) Build(ctx context.Context, module, fp string) (builderkeys. } return k.planKeyReads.Build(ctx, module, fp) } + +// holdKeys has the stand-in builder's keys of these modules (at any commit) unbuilt until the returned flag is cleared, +// so a walk that took them stays on its tier as it did while its build ran. It restores what it replaced at the end of +// the test, and the flag is set until the test lowers it. +func holdKeys(t *testing.T, modules ...string) *atomic.Bool { + t.Helper() + held := &atomic.Bool{} + held.Store(true) + was := openPlanKeys + openPlanKeys = func() (planKeyReads, func(), error) { + all, done, err := was() + var keys planKeyReads = all + for _, m := range modules { + keys = keysHolding{keys, m, held, ""} + } + return keys, done, err + } + t.Cleanup(func() { openPlanKeys = was }) + return held +} diff --git a/cmd/mesh-controller/one_group_one_plan_test.go b/cmd/mesh-controller/one_group_one_plan_test.go index 60fd5201..f37d07b0 100644 --- a/cmd/mesh-controller/one_group_one_plan_test.go +++ b/cmd/mesh-controller/one_group_one_plan_test.go @@ -94,8 +94,10 @@ func TestAFeatureWithANewSeatVerbIsOneBatchAndOnePlanInOrderWithNoAfterLine(t *t if plan.Delivery != nil && (plan.Delivery.Awaits != "" || plan.Waiting()) { t.Fatalf("a plan carrying the controller waits for %q's word", plan.Delivery.Awaits) } - if plan.Modules["mesh-controller"].Build == "" || len(*asked) != 1 { - t.Fatalf("the first tier is asked once: build %q, %d ask(s) %v", plan.Modules["mesh-controller"].Build, len(*asked), *asked) + // The cut takes the first tier from the controller's key at its commit and asks no build; the take-in itself is a + // later look's, which this test does not run, so nothing is recorded as asked. + if c := plan.Modules["mesh-controller"]; !c.Key || c.State != "asked" || c.KeyAt != "k1" || len(*asked) != 0 { + t.Fatalf("the first tier is not taken from its key once: %+v, %d ask(s) %v", c, len(*asked), *asked) } // Every merge is answered by that one plan. for _, repository := range []string{"novox/mesh-controller", "novox/mesh-catalog"} { diff --git a/cmd/mesh-controller/pending_integration_test.go b/cmd/mesh-controller/pending_integration_test.go index 1f2b246a..2e7b833c 100644 --- a/cmd/mesh-controller/pending_integration_test.go +++ b/cmd/mesh-controller/pending_integration_test.go @@ -266,9 +266,18 @@ func TestAPlansBuildRequestIsThePlans(t *testing.T) { if err := (following{open: open}).SourceMoved(ctx, m); err != nil { t.Fatal(err) } + // A walk asks no build, so it leaves no build request (askModule takes the module from its key): the plan's app is + // taken from the key at the commit the plan carries. requests, err := open.inventory.RequestsNamed(ctx, "app") - if err != nil || len(requests) != 1 || requests[0].For != "plan" { - t.Fatalf("the plan's request: %+v %v", requests, err) + if err != nil || len(requests) != 0 { + t.Fatalf("a request was recorded for the plan: %+v %v", requests, err) + } + recent, err := open.inventory.RecentPlans(ctx, 5) + if err != nil || len(recent) != 1 { + t.Fatalf("the plans: %+v %v", recent, err) + } + if app := recent[0].Modules["app"]; app == nil || !app.Key || app.KeyAt != "c1aaaaaaaa" { + t.Fatalf("the plan's app is not taken from its key at the commit it carries: %+v", app) } } diff --git a/cmd/mesh-controller/plan_clock_integration_test.go b/cmd/mesh-controller/plan_clock_integration_test.go index 460db64f..51e968d8 100644 --- a/cmd/mesh-controller/plan_clock_integration_test.go +++ b/cmd/mesh-controller/plan_clock_integration_test.go @@ -15,6 +15,9 @@ func TestAPlanStandingStillIsNotSavedAgain(t *testing.T) { ctx := t.Context() inv := open.inventory asked := asksRecorded(t) + // app's key is not built, so the plan stands on its tier as it did while the build ran; with the stand-in builder's + // key done at once, one step would carry it to done. + holdKeys(t, "app") if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { @@ -27,11 +30,12 @@ func TestAPlanStandingStillIsNotSavedAgain(t *testing.T) { } advanceHeld(ctx, open) recent, err := inv.RecentPlans(ctx, 1) - if err != nil || len(recent) != 1 || len(*asked) != 1 { + if err != nil || len(recent) != 1 || len(*asked) != 0 { t.Fatalf("no plan building: %v %v, asked %v", recent, err, *asked) } before := recent[0] - if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" { + if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" || + !before.Modules["app"].Key || before.Modules["app"].KeyAt != "c1aaaaaaaa" { t.Fatalf("the plan is not waiting on its build: %+v", before) } @@ -46,7 +50,7 @@ func TestAPlanStandingStillIsNotSavedAgain(t *testing.T) { t.Fatalf("a plan that did not move was saved again: revision %d → %d, saved %s → %s", before.Revision, after.Revision, before.Updated, after.Updated) } - if len(*asked) != 1 { - t.Fatalf("advancing asked again: %v", *asked) + if len(*asked) != 0 { + t.Fatalf("advancing took the key in: %v", *asked) } } diff --git a/cmd/mesh-controller/replays_integration_test.go b/cmd/mesh-controller/replays_integration_test.go index 37a4531d..76f28db8 100644 --- a/cmd/mesh-controller/replays_integration_test.go +++ b/cmd/mesh-controller/replays_integration_test.go @@ -316,6 +316,8 @@ func TestReplay296APlanStandingStillInItsTierIsNotSavedAgain(t *testing.T) { ctx := t.Context() inv := open.inventory asked := asksRecorded(t) + // app's key is not built, so the plan stands on its tier as it did while the build ran (see plan_clock_integration_test.go). + holdKeys(t, "app") if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { @@ -328,11 +330,12 @@ func TestReplay296APlanStandingStillInItsTierIsNotSavedAgain(t *testing.T) { } advanceHeld(ctx, open) recent, err := inv.RecentPlans(ctx, 1) - if err != nil || len(recent) != 1 || len(*asked) != 1 { + if err != nil || len(recent) != 1 || len(*asked) != 0 { t.Fatalf("no plan waiting on its build: %v %v, asked %v", recent, err, *asked) } before := recent[0] - if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" { + if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" || + !before.Modules["app"].Key || before.Modules["app"].KeyAt != "3ad22356aa" { t.Fatalf("the plan is not waiting on its build: %+v", before) } for range 3 { diff --git a/cmd/mesh-controller/rollout_events_test.go b/cmd/mesh-controller/rollout_events_test.go index c86e12a4..3944773b 100644 --- a/cmd/mesh-controller/rollout_events_test.go +++ b/cmd/mesh-controller/rollout_events_test.go @@ -167,6 +167,9 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) { return p } + // Tier 1's module b has its key unbuilt while the first controller runs, so the plan stands at tier 1 as it did while + // its build ran; with the key done at once, the same look would take it and send tier 1 as well. + heldB := holdKeys(t, "b") // The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything. first, stopFirst := context.WithCancel(ctx) defer stopFirst() @@ -181,10 +184,11 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) { if err := inv.SavePlan(ctx, &p); err != nil { t.Fatal(err) } - eventually(t, "tier 0 sent to both machines and tier 1 asked, on the save alone", func() bool { + eventually(t, "tier 0 sent to both machines and tier 1 taken from its key, on the save alone", func() bool { mu.Lock() defer mu.Unlock() - return len(sends) == 1 && len(*asked) == 1 && plan().Tier == 1 + b := plan().Modules["b"] + return len(sends) == 1 && len(*asked) == 0 && plan().Tier == 1 && b != nil && b.Key && b.KeyAt == "c2" }) eventually(t, "a key on the bus for each machine tier 0 was sent to", func() bool { return len(heldKeys()) == 2 }) @@ -194,6 +198,7 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) { time.Sleep(300 * time.Millisecond) reports("anchor") + heldB.Store(false) // b's key is built by the time the new controller looks second, stopSecond := context.WithCancel(ctx) defer stopSecond() waitSecond := startRolloutEvents(second, open, js) diff --git a/cmd/mesh-controller/silent_mid_tier_integration_test.go b/cmd/mesh-controller/silent_mid_tier_integration_test.go index 2593d8b1..4d403b24 100644 --- a/cmd/mesh-controller/silent_mid_tier_integration_test.go +++ b/cmd/mesh-controller/silent_mid_tier_integration_test.go @@ -65,6 +65,9 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT var mu sync.Mutex var sends [][]string asksRecorded(t) // each module is taken from a done key as the walk reaches it + // Except tier 1's module b, whose key is not built until the test says so: a walk that takes it in the same look goes on + // through tier 1's send and overwrites the note of the move before the test reads it (see away_integration_test.go). + heldB := holdKeys(t, "b") digests := 0 wasSend := sendRollout sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) { @@ -190,7 +193,12 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT if !strings.Contains(moved, "laptop") || strings.Contains(moved, "anchor") { t.Errorf("1160/AC5: the plan says %q; want laptop named silent and not anchor", moved) } - // Tier 1's module is taken from its key, which the walk's look does by itself. + if b := plan().Modules["b"]; b == nil || !b.Key { + t.Errorf("tier 1's module is not taken from its key: %+v", b) + } + // Tier 1's module is taken from its key once that is built, which the walk's look does by itself. + heldB.Store(false) + askToLook() eventually(t, "tier 1 sent to anchor alone", func() bool { mu.Lock() defer mu.Unlock() diff --git a/cmd/mesh-controller/walk_said_integration_test.go b/cmd/mesh-controller/walk_said_integration_test.go index 25aba361..52f09231 100644 --- a/cmd/mesh-controller/walk_said_integration_test.go +++ b/cmd/mesh-controller/walk_said_integration_test.go @@ -3,14 +3,17 @@ package main import ( + "context" "encoding/json" "os" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/builderkeys" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" @@ -205,11 +208,17 @@ func TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain(t *testing.T) { if p.State != inventory.PlanBuilding || (p.Delivery != nil && p.Delivery.Stopped != "") { t.Fatalf("%s: the walk is %s and keeps its stop: %+v", again[0], p.State, p.Delivery) } - // The build asked fails again: its record says so, and the look settles the walk from it. - if err := open.inventory.RecordBuild(ctx, inventory.Build{ID: p.Modules["a"].Build, Module: "a", Commit: "c0ffee", - Repository: "novox/a", At: time.Now().UTC(), Asked: before, Failed: "it broke again"}); err != nil { - t.Fatal(err) + // The retry takes a from its key (it asks no build), at the commit the walk carries. + if a := p.Modules["a"]; a == nil || !a.Key || a.KeyAt != "c0ffee" { + t.Fatalf("%s: the retried walk does not take a from its key: %+v", again[0], a) } + // The key fails again: the builder's record says so, and the look settles the walk from it. + wholeKeys := openPlanKeys + openPlanKeys = func() (planKeyReads, func(), error) { + all, done, err := wholeKeys() + return keysFailing{all, "a", "it broke again"}, done, err + } + t.Cleanup(func() { openPlanKeys = wholeKeys }) advancePlans(ctx, open) if p, _ = open.inventory.PlanByID(ctx, walk.ID); p.State != inventory.PlanFailed || p.Outcome() != inventory.WalkFailed { t.Fatalf("%s: failing again, the walk is %s and ends %s", again[0], p.State, p.Outcome()) @@ -259,3 +268,18 @@ func TestAWalksStartIsSaidAgainWithAMachineWhoseDeclarationChanged(t *testing.T) t.Fatalf("said again: %v", again) } } + +// keysFailing is the stand-in builder's keys but for one module, whose key is failed in these words. +type keysFailing struct { + planKeyReads + module string + words string +} + +func (k keysFailing) Build(ctx context.Context, module, fp string) (builderkeys.Key, error) { + if module == k.module { + return builderkeys.Key{Module: module, Fingerprint: fp, Found: true, Revision: 1, + Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildFailed, Error: k.words}}, nil + } + return k.planKeyReads.Build(ctx, module, fp) +}