177 lines
6.5 KiB
Go
177 lines
6.5 KiB
Go
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")
|
|
}
|
|
}
|