carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
4 "testing"
5 "time"
6
7 "github.com/carlosframework/platform/internal/activator"
8)
9
10func noBlocks(string, string, bool) bool { return false }
11
12// awakeQueue is an app queue whose hosts all serve v1, their good build.
13func awakeQueue(blocks func(build, host string, served bool) bool, hosts ...string) *appQueue {
14 q := newAppQueue("bab", "blog", blocks)
15 for _, h := range hosts {
16 q.observe(h, "v1", "v1", t0)
17 }
18 return q
19}
20
21func TestAReleaseOntoTheServingBuildIsDropped(t *testing.T) {
22 q := awakeQueue(noBlocks, "a")
23 q.wantBuild("a", activator.CauseRelease, "v1", t0)
24 if d, ok := q.pending(q.hosts["a"]); ok {
25 t.Fatalf("pending = %+v, want nothing: v1 is already serving", d.ID)
26 }
27}
28
29func TestRestartAndConfigOntoTheServingBuildAreRealWork(t *testing.T) {
30 q := awakeQueue(noBlocks, "a", "b")
31 q.wantBuild("a", activator.CauseRelease, "v1", t0)
32 q.wantRestart("a", "r1", t0)
33 d, ok := q.pending(q.hosts["a"])
34 if !ok || d.ID != (Identity{Host: "a", Cause: activator.CauseRestart, Build: "v1", RestartID: "r1"}) {
35 t.Fatalf("restart pending = %+v, %v", d.ID, ok)
36 }
37 if d.Class != classRecover {
38 t.Fatal("a restart is recovery: no failure record, no halt")
39 }
40 q.wantConfig("b", "c1", t0)
41 d, ok = q.pending(q.hosts["b"])
42 if !ok || d.ID != (Identity{Host: "b", Cause: activator.CauseConfig, Build: "v1", ConfigRev: "c1"}) {
43 t.Fatalf("config pending = %+v, %v", d.ID, ok)
44 }
45}
46
47func TestDedupeComparesTheWholeIdentity(t *testing.T) {
48 q := awakeQueue(noBlocks, "a")
49 if !q.wantBuild("a", activator.CauseRelease, "v2", t0) {
50 t.Fatal("the first desire of v2 must register")
51 }
52 rev := q.newest("a")
53 if q.wantBuild("a", activator.CauseRelease, "v2", t0) {
54 t.Fatal("the same desire again is the same attempt")
55 }
56 if q.newest("a") != rev {
57 t.Fatal("a duplicate moved the newest revision")
58 }
59 if !q.wantBuild("a", activator.CauseRetry, "v2", t0) {
60 t.Fatal("a retry onto the same build is a different identity")
61 }
62}
63
64func TestANewerDesiredBuildSupersedesPerInstance(t *testing.T) {
65 q := awakeQueue(noBlocks, "a", "b")
66 q.wantBuild("a", activator.CauseRelease, "v2", t0)
67 q.wantBuild("b", activator.CauseRelease, "v2", t0)
68 ra := q.newest("a")
69 q.wantBuild("a", activator.CauseRelease, "v3", t0.Add(20*time.Second))
70 d, _ := q.pending(q.hosts["a"])
71 if d.ID.Build != "v3" || d.SchedRev != q.newest("a") || q.newest("a") == ra {
72 t.Fatalf("a pending %+v rev %d newest %d (was %d)", d.ID, d.SchedRev, q.newest("a"), ra)
73 }
74 if d, _ := q.pending(q.hosts["b"]); d.ID.Build != "v2" {
75 t.Fatal("supersession is per instance: b keeps v2 until its own desire moves")
76 }
77}
78
79func TestRestartAndConfigCoalesceAndDoNotSupersede(t *testing.T) {
80 q := awakeQueue(noBlocks, "a")
81 q.wantBuild("a", activator.CauseRelease, "v2", t0)
82 q.wantRestart("a", "r1", t0)
83 q.wantRestart("a", "r2", t0)
84 q.wantConfig("a", "c1", t0)
85 q.wantConfig("a", "c2", t0)
86 d, _ := q.pending(q.hosts["a"])
87 want := Identity{Host: "a", Cause: activator.CauseRelease, Build: "v2", ConfigRev: "c2", RestartID: "r2"}
88 if d.ID != want {
89 t.Fatalf("pending = %+v, want %+v: one attempt covers the newest restart and config", d.ID, want)
90 }
91 rev := q.newest("a")
92 q.wantRestart("a", "r3", t0)
93 q.wantConfig("a", "c3", t0)
94 if q.newest("a") != rev {
95 t.Fatal("a restart or config request must not abandon an attempt past spawn")
96 }
97}
98
99func TestABlockedBuildFallsThroughToRecovery(t *testing.T) {
100 held := func(build, _ string, served bool) bool { return build == "v2" && !served }
101 q := awakeQueue(held, "a")
102 q.wantBuild("a", activator.CauseRelease, "v2", t0)
103 if _, ok := q.pending(q.hosts["a"]); ok {
104 t.Fatal("a held build must not be attempted")
105 }
106 q.wantRestart("a", "r1", t0)
107 d, ok := q.pending(q.hosts["a"])
108 if !ok || d.ID.Cause != activator.CauseRestart || d.ID.Build != "v1" {
109 t.Fatalf("pending = %+v: the restart onto the serving build still goes ahead", d.ID)
110 }
111}
112
113func TestAMoveBackToTheGoodBuildIsRecovery(t *testing.T) {
114 q := newAppQueue("bab", "blog", func(_, _ string, served bool) bool { return !served })
115 q.observe("a", "v2", "v1", t0) // serving v2, which has not settled; good v1
116 q.wantBuild("a", activator.CauseRelease, "v1", t0)
117 d, ok := q.pending(q.hosts["a"])
118 if !ok || d.Class != classRecover {
119 t.Fatalf("pending = %+v %v: moving back onto a build it has served is recovery", d, ok)
120 }
121}
122
123func TestNothingIsSwappedOnASleeper(t *testing.T) {
124 q := newAppQueue("bab", "blog", noBlocks)
125 q.observe("a", "", "v1", t0)
126 q.wantBuild("a", activator.CauseRelease, "v2", t0)
127 if _, ok := q.pending(q.hosts["a"]); ok {
128 t.Fatal("a sleeping instance boots its installed build on its next wake; there is nothing to swap")
129 }
130}
131