package rollout import ( "testing" "time" "github.com/carlosframework/platform/internal/activator" ) func noBlocks(string, string, bool) bool { return false } // awakeQueue is an app queue whose hosts all serve v1, their good build. func awakeQueue(blocks func(build, host string, served bool) bool, hosts ...string) *appQueue { q := newAppQueue("bab", "blog", blocks) for _, h := range hosts { q.observe(h, "v1", "v1", t0) } return q } func TestAReleaseOntoTheServingBuildIsDropped(t *testing.T) { q := awakeQueue(noBlocks, "a") q.wantBuild("a", activator.CauseRelease, "v1", t0) if d, ok := q.pending(q.hosts["a"]); ok { t.Fatalf("pending = %+v, want nothing: v1 is already serving", d.ID) } } func TestRestartAndConfigOntoTheServingBuildAreRealWork(t *testing.T) { q := awakeQueue(noBlocks, "a", "b") q.wantBuild("a", activator.CauseRelease, "v1", t0) q.wantRestart("a", "r1", t0) d, ok := q.pending(q.hosts["a"]) if !ok || d.ID != (Identity{Host: "a", Cause: activator.CauseRestart, Build: "v1", RestartID: "r1"}) { t.Fatalf("restart pending = %+v, %v", d.ID, ok) } if d.Class != classRecover { t.Fatal("a restart is recovery: no failure record, no halt") } q.wantConfig("b", "c1", t0) d, ok = q.pending(q.hosts["b"]) if !ok || d.ID != (Identity{Host: "b", Cause: activator.CauseConfig, Build: "v1", ConfigRev: "c1"}) { t.Fatalf("config pending = %+v, %v", d.ID, ok) } } func TestDedupeComparesTheWholeIdentity(t *testing.T) { q := awakeQueue(noBlocks, "a") if !q.wantBuild("a", activator.CauseRelease, "v2", t0) { t.Fatal("the first desire of v2 must register") } rev := q.newest("a") if q.wantBuild("a", activator.CauseRelease, "v2", t0) { t.Fatal("the same desire again is the same attempt") } if q.newest("a") != rev { t.Fatal("a duplicate moved the newest revision") } if !q.wantBuild("a", activator.CauseRetry, "v2", t0) { t.Fatal("a retry onto the same build is a different identity") } } func TestANewerDesiredBuildSupersedesPerInstance(t *testing.T) { q := awakeQueue(noBlocks, "a", "b") q.wantBuild("a", activator.CauseRelease, "v2", t0) q.wantBuild("b", activator.CauseRelease, "v2", t0) ra := q.newest("a") q.wantBuild("a", activator.CauseRelease, "v3", t0.Add(20*time.Second)) d, _ := q.pending(q.hosts["a"]) if d.ID.Build != "v3" || d.SchedRev != q.newest("a") || q.newest("a") == ra { t.Fatalf("a pending %+v rev %d newest %d (was %d)", d.ID, d.SchedRev, q.newest("a"), ra) } if d, _ := q.pending(q.hosts["b"]); d.ID.Build != "v2" { t.Fatal("supersession is per instance: b keeps v2 until its own desire moves") } } func TestRestartAndConfigCoalesceAndDoNotSupersede(t *testing.T) { q := awakeQueue(noBlocks, "a") q.wantBuild("a", activator.CauseRelease, "v2", t0) q.wantRestart("a", "r1", t0) q.wantRestart("a", "r2", t0) q.wantConfig("a", "c1", t0) q.wantConfig("a", "c2", t0) d, _ := q.pending(q.hosts["a"]) want := Identity{Host: "a", Cause: activator.CauseRelease, Build: "v2", ConfigRev: "c2", RestartID: "r2"} if d.ID != want { t.Fatalf("pending = %+v, want %+v: one attempt covers the newest restart and config", d.ID, want) } rev := q.newest("a") q.wantRestart("a", "r3", t0) q.wantConfig("a", "c3", t0) if q.newest("a") != rev { t.Fatal("a restart or config request must not abandon an attempt past spawn") } } func TestABlockedBuildFallsThroughToRecovery(t *testing.T) { held := func(build, _ string, served bool) bool { return build == "v2" && !served } q := awakeQueue(held, "a") q.wantBuild("a", activator.CauseRelease, "v2", t0) if _, ok := q.pending(q.hosts["a"]); ok { t.Fatal("a held build must not be attempted") } q.wantRestart("a", "r1", t0) d, ok := q.pending(q.hosts["a"]) if !ok || d.ID.Cause != activator.CauseRestart || d.ID.Build != "v1" { t.Fatalf("pending = %+v: the restart onto the serving build still goes ahead", d.ID) } } func TestAMoveBackToTheGoodBuildIsRecovery(t *testing.T) { q := newAppQueue("bab", "blog", func(_, _ string, served bool) bool { return !served }) q.observe("a", "v2", "v1", t0) // serving v2, which has not settled; good v1 q.wantBuild("a", activator.CauseRelease, "v1", t0) d, ok := q.pending(q.hosts["a"]) if !ok || d.Class != classRecover { t.Fatalf("pending = %+v %v: moving back onto a build it has served is recovery", d, ok) } } func TestNothingIsSwappedOnASleeper(t *testing.T) { q := newAppQueue("bab", "blog", noBlocks) q.observe("a", "", "v1", t0) q.wantBuild("a", activator.CauseRelease, "v2", t0) if _, ok := q.pending(q.hosts["a"]); ok { t.Fatal("a sleeping instance boots its installed build on its next wake; there is nothing to swap") } }