package main import ( "context" "encoding/json" "sort" "sync" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // Issue 1687 (ADR 0483 clause 2): a delivery plan's key lives from the ask of its first build until every build of its last // tier is built, or the plan fails, stops or waits on the operator. type fakeBuildingStore struct { mu sync.Mutex keys map[string]link.PlanBuilding puts map[string]int } func newFakeBuildingStore() *fakeBuildingStore { return &fakeBuildingStore{keys: map[string]link.PlanBuilding{}, puts: map[string]int{}} } func (f *fakeBuildingStore) Put(_ context.Context, plan string, value []byte) error { f.mu.Lock() defer f.mu.Unlock() var v link.PlanBuilding if err := json.Unmarshal(value, &v); err != nil { return err } f.keys[plan] = v f.puts[plan]++ return nil } func (f *fakeBuildingStore) Delete(_ context.Context, plan string) error { f.mu.Lock() defer f.mu.Unlock() delete(f.keys, plan) return nil } func (f *fakeBuildingStore) Keys(context.Context) ([]string, error) { f.mu.Lock() defer f.mu.Unlock() var out []string for k := range f.keys { out = append(out, k) } sort.Strings(out) return out, nil } func aPlan(id, state string, tiers [][]string, states map[string]string) inventory.Plan { p := inventory.Plan{ID: id, State: state, Tiers: tiers, Modules: map[string]*inventory.PlanModule{}} now := time.Now().Add(-time.Minute) for m, st := range states { pm := &inventory.PlanModule{State: st} if st != "" { pm.AskedAt = &now } p.Modules[m] = pm } return p } func TestAPlanIsInItsBuildPhaseFromTheFirstAskToTheLastBuild(t *testing.T) { tiers := [][]string{{"a", "b"}, {"c"}} cases := []struct { name string plan inventory.Plan want bool }{ {"no build asked yet", aPlan("p", inventory.PlanBuilding, tiers, nil), false}, {"the first tier asked", aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "asked", "b": "asked"}), true}, // Between two tiers: the first built and sent (rolling), the second not asked yet. {"between two tiers", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built"}), true}, {"the last tier asked", aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "built", "b": "built", "c": "asked"}), true}, {"every build built", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built", "c": "built"}), false}, // A module the plan deletes is done as far as its tiers go (release_plan.go), so it holds no key through the rollout. {"a deleted module is done", aPlan("p", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built", "c": planDeleted}), false}, {"failed", aPlan("p", inventory.PlanFailed, tiers, map[string]string{"a": "failed", "b": "asked"}), false}, {"superseded", aPlan("p", inventory.PlanSuperseded, tiers, map[string]string{"a": "asked"}), false}, {"a batch whose window is open", aPlan("p", inventory.PlanAssembling, tiers, map[string]string{"a": "asked"}), false}, } for _, c := range cases { if got := buildPhaseOf(c.plan); got != c.want { t.Errorf("%s: in the build phase = %v, want %v", c.name, got, c.want) } } waiting := aPlan("p", inventory.PlanBuilding, tiers, map[string]string{"a": "asked"}) waiting.Delivery = &inventory.PlanDelivery{Awaits: "the operator's word"} if buildPhaseOf(waiting) { t.Error("a plan that waits on the operator is in its build phase") } } // The key's life: put, refreshed, deleted at the phase's end; a plan that left the phase unseen loses its key at the next look. func TestAPlanKeyIsPutRefreshedAndDeletedAtThePhasesEnd(t *testing.T) { ctx := context.Background() store := newFakeBuildingStore() tiers := [][]string{{"a"}, {"b"}} building := aPlan("plan-1", inventory.PlanBuilding, tiers, map[string]string{"a": "asked"}) if faults := syncBuilding(ctx, store, []inventory.Plan{building}, time.Now()); len(faults) > 0 { t.Fatal(faults) } if _, ok := store.keys["plan-1"]; !ok { t.Fatal("no key while the first build is asked") } first := store.keys["plan-1"] // Refreshed: put again, the phase's start kept. time.Sleep(2 * time.Millisecond) syncBuilding(ctx, store, []inventory.Plan{building}, time.Now()) if store.puts["plan-1"] != 2 || !store.keys["plan-1"].Seen.After(first.Seen) || !store.keys["plan-1"].Since.Equal(first.Since) { t.Errorf("the key was not refreshed with its start kept: %d puts, %+v then %+v", store.puts["plan-1"], first, store.keys["plan-1"]) } // Between the tiers: still there. between := aPlan("plan-1", inventory.PlanRolling, tiers, map[string]string{"a": "built"}) syncBuilding(ctx, store, []inventory.Plan{between}, time.Now()) if _, ok := store.keys["plan-1"]; !ok { t.Error("the key was gone between two tiers") } // A key of another plan that is no longer in the phase. store.keys["plan-0"] = link.PlanBuilding{Plan: "plan-0"} done := aPlan("plan-1", inventory.PlanRolling, tiers, map[string]string{"a": "built", "b": "built"}) syncBuilding(ctx, store, []inventory.Plan{done}, time.Now()) if len(store.keys) != 0 { t.Errorf("keys remain after the last build was built: %v", store.keys) } } // keepBuildingKey, told of each save, puts the key while the plan builds and deletes it when the plan fails. func TestASavedPlanPutsOrDeletesItsKey(t *testing.T) { ctx := context.Background() store := newFakeBuildingStore() var s buildingStore = store buildingKeys.Store(&s) defer buildingKeys.Store(nil) p := aPlan("plan-7", inventory.PlanBuilding, [][]string{{"a"}}, map[string]string{"a": "asked"}) keepBuildingKey(ctx, p) if _, ok := store.keys["plan-7"]; !ok { t.Fatal("the key was not put when the plan was saved building") } p.State = inventory.PlanFailed keepBuildingKey(ctx, p) if _, ok := store.keys["plan-7"]; ok { t.Fatal("the key stayed after the plan failed") } } // The plan's key is there before its first build is asked, so a check cannot take a place between the ask and the plan's save. func TestThePlansKeyIsPutBeforeItsFirstBuildIsAsked(t *testing.T) { asksOfEveryModule(t) store := newFakeBuildingStore() var s buildingStore = store buildingKeys.Store(&s) defer buildingKeys.Store(nil) p, byName := aTierOfThree() keyThere := false was := askABuild askABuild = func(context.Context, buildSource, string, string) (string, error) { _, keyThere = store.keys["plan-1"] return "build-x", nil } t.Cleanup(func() { askABuild = was }) askModule(t.Context(), p, "a", byName) if !keyThere { t.Fatal("the plan's key was not there when its first build was asked") } }