cancel, clear and rebuild kept a walk's end or restart without saying it, as plans stop, retry and release-backlog did before them. The controller's run now says every save a verb makes on the bus, so the next verb cannot lose a mark the same way. A stop is cleared when a walk is resumed, so one that fails again ends failed and the search for its merge runs (review 21, ADR 0308, issue 532).
131 lines
5.2 KiB
Go
131 lines
5.2 KiB
Go
package inventory
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A walk is said started on the save that finds it live, ended on the save that finds it ended after it started,
|
|
// and neither on any other save (novox/hq ADR 0308 decision 1).
|
|
func TestAWalkIsSaidStartedOnceAndEndedOnce(t *testing.T) {
|
|
now := time.Date(2026, 10, 12, 11, 0, 0, 0, time.UTC)
|
|
cut := now.Add(-time.Second)
|
|
p := Plan{ID: "plan-1", State: PlanBuilding, Tiers: [][]string{{"messenger"}},
|
|
Modules: map[string]*PlanModule{"messenger": {}}, Times: &PlanTimes{Cut: &cut, Class: ClassLeaf}}
|
|
p.markWalk(now)
|
|
if p.WalkSaid != WalkStarted || p.Times.Started == nil || !p.Times.Started.Equal(now) {
|
|
t.Fatalf("a live walk's first save starts it: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
p.State = PlanRolling
|
|
p.markWalk(now.Add(time.Minute))
|
|
if p.WalkSaid != "" || !p.Times.Started.Equal(now) {
|
|
t.Fatalf("a later save says nothing and keeps the start: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
p.State = PlanDone
|
|
p.markWalk(now.Add(2 * time.Minute))
|
|
if p.WalkSaid != WalkEnded || p.Times.Ended == nil || p.Outcome() != WalkDelivered {
|
|
t.Fatalf("the save that finds it done ends it, delivered: %q %+v %q", p.WalkSaid, p.Times, p.Outcome())
|
|
}
|
|
p.markWalk(now.Add(3 * time.Minute))
|
|
if p.WalkSaid != "" {
|
|
t.Fatalf("an ended walk saved again says nothing: %q", p.WalkSaid)
|
|
}
|
|
// Retried, it walks again: a new start.
|
|
p.State = PlanBuilding
|
|
p.markWalk(now.Add(4 * time.Minute))
|
|
if p.WalkSaid != WalkStarted || p.Times.Ended != nil || !p.Times.Started.Equal(now.Add(4*time.Minute)) {
|
|
t.Fatalf("a retried walk starts again: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
}
|
|
|
|
// What is never said: a batch, a walk waiting for its word, a walk that ends without having started, and a walk
|
|
// kept before its times were. A release plan is said.
|
|
func TestWhatIsNotAWalkStartIsNotSaid(t *testing.T) {
|
|
now := time.Now()
|
|
for name, p := range map[string]Plan{
|
|
"a batch": {State: PlanAssembling, Times: &PlanTimes{}},
|
|
"waiting for a word": {State: PlanBuilding, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery"}},
|
|
"ended, never begun": {State: PlanDone, Times: &PlanTimes{}},
|
|
"kept before times": {State: PlanBuilding},
|
|
"stopped while waits": {State: PlanFailed, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery", Stopped: "x"}},
|
|
} {
|
|
p.markWalk(now)
|
|
if p.WalkSaid != "" {
|
|
t.Errorf("%s: said %q", name, p.WalkSaid)
|
|
}
|
|
}
|
|
r := Plan{State: PlanRolling, Release: &PlanRelease{Order: []string{"ace"}}}
|
|
r.markWalk(now)
|
|
if r.WalkSaid != WalkStarted || r.Times == nil || r.Times.Started == nil || r.Times.Class != "" {
|
|
t.Fatalf("a release plan is said started, its times holding only that: %q %+v", r.WalkSaid, r.Times)
|
|
}
|
|
// Let go, the walk starts.
|
|
go1 := now
|
|
w := Plan{State: PlanBuilding, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery", Go: &go1}}
|
|
w.markWalk(now)
|
|
if w.WalkSaid != WalkStarted {
|
|
t.Fatalf("a walk let go starts: %q", w.WalkSaid)
|
|
}
|
|
}
|
|
|
|
// An ended walk's outcome: delivered, failed, or stopped by a person or the delivery's owner, or by a newer walk.
|
|
func TestAWalksOutcome(t *testing.T) {
|
|
for want, p := range map[string]Plan{
|
|
WalkDelivered: {State: PlanDone},
|
|
WalkFailed: {State: PlanFailed},
|
|
WalkStopped: {State: PlanFailed, Delivery: &PlanDelivery{Stopped: "mesh-delivery"}},
|
|
"": {State: PlanRolling},
|
|
} {
|
|
if got := p.Outcome(); got != want {
|
|
t.Errorf("%s: outcome %q, want %q", p.State, got, want)
|
|
}
|
|
}
|
|
if (Plan{State: PlanSuperseded}).Outcome() != WalkStopped {
|
|
t.Error("a superseded walk is stopped")
|
|
}
|
|
}
|
|
|
|
// The marks are kept with the walk, so a controller replaced mid-walk neither says its start again nor misses
|
|
// its end; and a save refused leaves the walk unmarked, so the save that is made says it.
|
|
func TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := t.Context()
|
|
cut := time.Now().UTC().Add(-time.Minute)
|
|
p := Plan{ID: "plan-308", Repository: "novox/mesh-catalog", Commit: "abc", Created: cut, State: PlanBuilding,
|
|
Tiers: [][]string{{"messenger"}}, Modules: map[string]*PlanModule{"messenger": {}},
|
|
Times: &PlanTimes{Cut: &cut, Class: ClassLeaf}}
|
|
var said []string
|
|
was := PlanSaved
|
|
PlanSaved = func(_ *Inventory, p Plan) { said = append(said, p.WalkSaid) }
|
|
t.Cleanup(func() { PlanSaved = was })
|
|
if err := inv.SavePlan(ctx, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := inv.PlanByID(ctx, p.ID)
|
|
if err != nil || back.Times == nil || back.Times.Started == nil || back.Times.Class != ClassLeaf {
|
|
t.Fatalf("the start is kept with the walk: %v %+v", err, back.Times)
|
|
}
|
|
// Read back by another controller and saved: not said again.
|
|
back.State = PlanRolling
|
|
if err := inv.SavePlan(ctx, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A stale copy ending it is refused, and unmarked.
|
|
stale := p
|
|
stale.Revision = 1
|
|
stale.State = PlanDone
|
|
if err := inv.SavePlan(ctx, &stale); err == nil {
|
|
t.Fatal("a stale save is refused")
|
|
}
|
|
if stale.WalkSaid != "" || stale.Times.Ended != nil {
|
|
t.Fatalf("a refused save is unmarked: %q %+v", stale.WalkSaid, stale.Times)
|
|
}
|
|
back.State = PlanDone
|
|
if err := inv.SavePlan(ctx, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(said) != 3 || said[0] != WalkStarted || said[1] != "" || said[2] != WalkEnded {
|
|
t.Fatalf("said: %q, want started, nothing, ended", said)
|
|
}
|
|
}
|