Assert the key takes where a walk asks no build, and hold keys where a walk must stand still (issue 1931)

This commit is contained in:
2026-10-24 05:14:41 +02:00
parent 3cfcf5e3e4
commit 9db1459cb0
8 changed files with 92 additions and 17 deletions
@@ -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
}
@@ -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"} {
@@ -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)
}
}
@@ -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)
}
}
@@ -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 {
+7 -2
View File
@@ -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)
@@ -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()
@@ -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)
}