carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "os"
5 "path/filepath"
6 "reflect"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/registry"
11)
12
13func TestBuildStateKeySitsBesideTheLock(t *testing.T) {
14 if got := BuildStateKey("instances/a.example.com"); got != "instances/a.example.com/build-state.json" {
15 t.Fatalf("BuildStateKey = %q", got)
16 }
17}
18
19func TestConsumedRoundTrip(t *testing.T) {
20 opt := t.TempDir()
21 if c, err := ReadConsumed(opt, "a.example.com"); c != nil || err != nil {
22 t.Fatalf("absent = %+v, %v", c, err)
23 }
24 want := Consumed{Schema: 1, Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v2"}, Completed: 2,
25 Desired: "v2", Reason: string(ReasonRequest), At: time.Now().UTC().Truncate(time.Second)}
26 if err := WriteConsumed(opt, want); err != nil {
27 t.Fatal(err)
28 }
29 got, err := ReadConsumed(opt, "a.example.com")
30 if err != nil || got.Resolved.Target != "v2" || got.Completed != 2 || got.Reason != string(ReasonRequest) {
31 t.Fatalf("round trip = %+v, %v", got, err)
32 }
33 if ConsumedPath(opt, "a.example.com") != filepath.Join(opt, "consumed", "a.example.com") {
34 t.Fatal("the consumed file moved")
35 }
36}
37
38func TestConsumedRefusesAnotherHostsFile(t *testing.T) {
39 opt := t.TempDir()
40 if err := WriteConsumed(opt, Consumed{Schema: 1, Host: "b.example.com"}); err != nil {
41 t.Fatal(err)
42 }
43 if err := WriteFileAtomic(ConsumedPath(opt, "a.example.com"), mustRead(t, ConsumedPath(opt, "b.example.com")), 0o644); err != nil {
44 t.Fatal(err)
45 }
46 if _, err := ReadConsumed(opt, "a.example.com"); err == nil {
47 t.Fatal("a consumed file naming another host was accepted")
48 }
49}
50
51func TestWatermarkIsTheHighestOfBoth(t *testing.T) {
52 for _, c := range []struct {
53 c *Consumed
54 b *BuildState
55 want uint64
56 }{
57 {nil, nil, 0},
58 {&Consumed{Completed: 4}, nil, 4},
59 {nil, &BuildState{Completed: 6}, 6},
60 {&Consumed{Completed: 4}, &BuildState{Completed: 6}, 6},
61 {&Consumed{Completed: 9}, &BuildState{Completed: 6}, 9},
62 } {
63 if got := Watermark(c.c, c.b); got != c.want {
64 t.Errorf("Watermark(%+v, %+v) = %d, want %d", c.c, c.b, got, c.want)
65 }
66 }
67}
68
69func TestBuildStateCopyColumn(t *testing.T) {
70 c, err := ParseBuildStateCopy("")
71 if err != nil || c.Known {
72 t.Fatalf("an empty column is unknown: %+v %v", c, err)
73 }
74 in := BuildStateCopy{Known: true, Replica: true, State: &BuildState{Schema: 1, Host: "a.example.com", LastExecuted: "v1"}}
75 out, err := ParseBuildStateCopy(in.Column())
76 if err != nil || !out.Known || !out.Replica || out.State.LastExecuted != "v1" {
77 t.Fatalf("round trip = %+v %v", out, err)
78 }
79}
80
81func TestNextBuildState(t *testing.T) {
82 for _, tc := range []struct {
83 name string
84 cur *BuildState
85 c *Consumed
86 build string
87 want BuildState
88 }{
89 {"a: nil cur, nil consumed sets only LastExecuted", nil, nil, "v1",
90 BuildState{LastExecuted: "v1"}},
91 {"b: a consumed resolution above the watermark is carried and completes when its target runs",
92 &BuildState{Schema: 1, Host: "h", Completed: 2},
93 &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2}, "v3",
94 BuildState{Schema: 1, Host: "h", LastExecuted: "v3", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 3}},
95 {"c: running another build carries the resolution but does not complete it",
96 &BuildState{Schema: 1, Host: "h", Completed: 2},
97 &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2}, "v2",
98 BuildState{Schema: 1, Host: "h", LastExecuted: "v2", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2}},
99 {"d: a consumed resolution at or below the state's Completed is not carried",
100 &BuildState{Schema: 1, Host: "h", Completed: 3},
101 &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2}, "v3",
102 BuildState{Schema: 1, Host: "h", LastExecuted: "v3", Completed: 3}},
103 {"e: a higher consumed Completed raises the state's and carries CompletedFailed",
104 &BuildState{Schema: 1, Host: "h", Completed: 2},
105 &Consumed{Completed: 5, CompletedFailed: true}, "v1",
106 BuildState{Schema: 1, Host: "h", LastExecuted: "v1", Completed: 5, CompletedFailed: true}},
107 {"f: an equal Completed with consumed CompletedFailed marks the state failed and does not reopen",
108 &BuildState{Schema: 1, Host: "h", Completed: 4},
109 &Consumed{Completed: 4, CompletedFailed: true}, "v1",
110 BuildState{Schema: 1, Host: "h", LastExecuted: "v1", Completed: 4, CompletedFailed: true}},
111 {"g: the state's own resolution from another box completes when its target runs",
112 &BuildState{Schema: 1, Host: "h", Resolved: &ResolvedRequest{ID: 4, Target: "v3"}, Completed: 3},
113 nil, "v3",
114 BuildState{Schema: 1, Host: "h", LastExecuted: "v3", Resolved: &ResolvedRequest{ID: 4, Target: "v3"}, Completed: 4}},
115 {"h: the watermark never decreases",
116 &BuildState{Schema: 1, Host: "h", Completed: 6},
117 &Consumed{Completed: 4}, "v1",
118 BuildState{Schema: 1, Host: "h", LastExecuted: "v1", Completed: 6}},
119 {"i: a resolution below the consumed Completed never lowers the watermark",
120 &BuildState{Schema: 1, Host: "h", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2},
121 &Consumed{Completed: 5, CompletedFailed: true}, "v3",
122 BuildState{Schema: 1, Host: "h", LastExecuted: "v3", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 5, CompletedFailed: true}},
123 {"ii: a closed request whose target runs again stays closed and failed",
124 &BuildState{Schema: 1, Host: "h", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 3, CompletedFailed: true},
125 nil, "v3",
126 BuildState{Schema: 1, Host: "h", LastExecuted: "v3", Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 3, CompletedFailed: true}},
127 {"j: an older resolution in this box's consumed file does not replace the state's newer one",
128 &BuildState{Schema: 1, Host: "h", Resolved: &ResolvedRequest{ID: 5, Target: "v5"}, Completed: 3},
129 &Consumed{Resolved: &ResolvedRequest{ID: 4, Target: "v4"}, Completed: 3}, "v5",
130 BuildState{Schema: 1, Host: "h", LastExecuted: "v5", Resolved: &ResolvedRequest{ID: 5, Target: "v5"}, Completed: 5}},
131 } {
132 t.Run(tc.name, func(t *testing.T) {
133 var curBefore BuildState
134 if tc.cur != nil {
135 curBefore = *tc.cur
136 }
137 got := NextBuildState(tc.cur, tc.c, tc.build)
138 if !reflect.DeepEqual(got, tc.want) {
139 t.Fatalf("NextBuildState = %+v (resolved %+v), want %+v (resolved %+v)", got, got.Resolved, tc.want, tc.want.Resolved)
140 }
141 if tc.cur != nil && !reflect.DeepEqual(*tc.cur, curBefore) {
142 t.Fatalf("cur was mutated: %+v", *tc.cur)
143 }
144 })
145 }
146}
147
148func TestNextBuildStateDoesNotAliasConsumed(t *testing.T) {
149 c := &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}}
150 got := NextBuildState(nil, c, "v2")
151 c.Resolved.Target = "changed"
152 c.Resolved.ID = 99
153 if got.Resolved == nil || got.Resolved.Target != "v3" || got.Resolved.ID != 3 {
154 t.Fatalf("the state aliases the consumed file: %+v", got.Resolved)
155 }
156}
157
158func mustRead(t *testing.T, path string) []byte {
159 t.Helper()
160 b, err := os.ReadFile(path)
161 if err != nil {
162 t.Fatal(err)
163 }
164 return b
165}
166
167// Ruling R-SPEC-43A-2 (Astra round 1 finding 3): a graceful move carries the
168// source box's consumed request record into the build state: root's
169// resolution when it is newer than everything the state knows, and a failed
170// completion; never a successful completion, and never last_executed (only a
171// run changes that).
172func TestCarryRequestCarriesTheResolutionAndAFailedCompletion(t *testing.T) {
173 cur := &BuildState{Schema: 1, Host: "h", LastExecuted: "A", Completed: 1}
174 c := &Consumed{Host: "h", Resolved: &ResolvedRequest{ID: 3, Target: "T"}, Completed: 2, CompletedFailed: true}
175 got := CarryRequest(cur, c)
176 if got.LastExecuted != "A" || got.Completed != 2 || !got.CompletedFailed || got.Resolved == nil || got.Resolved.ID != 3 || got.Resolved.Target != "T" {
177 t.Fatalf("carried %+v; want A, completed 2 failed, {3 T}", got)
178 }
179 if cur.Completed != 1 || cur.Resolved != nil {
180 t.Fatalf("the input was changed: %+v", cur)
181 }
182 // A successful completion is the pre-exec write's alone.
183 if got := CarryRequest(cur, &Consumed{Host: "h", Completed: 5}); got.Completed != 1 || got.CompletedFailed {
184 t.Fatalf("a successful completion was carried: %+v", got)
185 }
186 // An older resolution never replaces a newer one; a same-id failure marks
187 // the completed request failed without reopening it.
188 newer := &BuildState{LastExecuted: "A", Completed: 2, Resolved: &ResolvedRequest{ID: 4, Target: "U"}}
189 if got := CarryRequest(newer, &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "T"}, Completed: 2, CompletedFailed: true}); got.Resolved.ID != 4 || got.Completed != 2 || !got.CompletedFailed {
190 t.Fatalf("carried %+v; want {4 U} kept, R2 completed failed", got)
191 }
192 // No state yet: the record alone makes one, with no last_executed.
193 if got := CarryRequest(nil, c); got.LastExecuted != "" || got.Completed != 2 || !got.CompletedFailed || got.Resolved == nil {
194 t.Fatalf("carried onto no state: %+v", got)
195 }
196}
197
198// The scenario: a sleeping manual instance last ran A. Root resolves R1 to
199// T, which this box holds as failed, so R1 completes failed in root's file
200// and nothing runs. A graceful move carries that into the build state, and
201// the destination never resolves R1 again (rule 2: a completed request never
202// re-enters), so it installs nothing new without another Update now. A box
203// that is lost carries nothing: the destination then resolves R1 again, the
204// spec's lost request (§4.2).
205func TestAFailedCompletionSurvivesAGracefulMove(t *testing.T) {
206 u := UpdateSettings{Updates: UpdatesManual, Channel: "stable", InstallRequest: 1}
207 route := registry.Route{Host: "h", Kind: "instance", UpdateSettings: u.Column()}
208 state := &BuildState{Schema: 1, Host: "h", LastExecuted: "A"}
209 src := TargetInput{Route: route, Pointer: &ChannelPointer{Version: "T"}, Settings: u,
210 Build: BuildStateCopy{Known: true, Replica: true, State: state}, Installed: "A",
211 Failed: func(v string) bool { return v == "T" }}
212 dec := Target(src)
213 if dec.Resolve == nil || dec.Fail != 1 {
214 t.Fatalf("source decision %+v; want R1 resolved to T and completed failed", dec)
215 }
216 consumed := NextConsumed(src, dec)
217 dst := func(st *BuildState) TargetDecision {
218 return Target(TargetInput{Route: route, Pointer: &ChannelPointer{Version: "H"}, Settings: u,
219 Build: BuildStateCopy{Known: true, Replica: true, State: st}})
220 }
221 carried := CarryRequest(state, &consumed)
222 if d := dst(&carried); d.Resolve != nil || d.Reason == ReasonRequest || d.Version != "A" {
223 t.Fatalf("destination after a graceful move: %+v; want R1 closed, held on A", d)
224 }
225 if d := dst(state); d.Resolve == nil || d.Resolve.ID != 1 || d.Version != "H" {
226 t.Fatalf("destination after a lost box: %+v; want R1 resolved again (a lost request)", d)
227 }
228}
229
230// Found by the simulation (seed 164 after Astra round 1's changes): a box
231// the instance returns to keeps its old row, and that copy's refreshes
232// failed while the instance was away (ReadErr), so it still says request R1
233// completed while the bucket says R5 completed failed. Root must read such a
234// copy as unread (R-TARGET-UNKNOWN): no request bookkeeping, so R5 is not
235// resolved again from a stale watermark, and a held instance is unknown
236// rather than recovered onto a stale last_executed.
237func TestTargetReadsACopyWhoseRefreshFailedAsUnread(t *testing.T) {
238 stale := BuildStateCopy{Known: true, Replica: true, ReadErr: "s3: timeout", State: &BuildState{LastExecuted: "b1", Completed: 1, CompletedFailed: true}}
239 for _, u := range []UpdateSettings{
240 {Updates: UpdatesAuto, Channel: "stable", InstallRequest: 5},
241 {Updates: UpdatesManual, Channel: "stable", InstallRequest: 5},
242 } {
243 in := TargetInput{Route: registry.Route{Host: "h", UpdateSettings: u.Column()}, Pointer: &ChannelPointer{Version: "b6"}, Settings: u,
244 Build: stale, Consumed: &Consumed{Host: "h", Completed: 1, CompletedFailed: true}, Installed: "b1"}
245 d := Target(in)
246 if d.Resolve != nil || d.Reason == ReasonRequest {
247 t.Fatalf("%s: %+v; a stale copy's watermark resolved R5 again", u.Updates, d)
248 }
249 if u.Updates == UpdatesManual && (d.Reason != ReasonUnknown || d.Version != "") {
250 t.Fatalf("manual: %+v; want unknown, nothing installed", d)
251 }
252 if next := NextConsumed(in, d); next.Baking != "" || (next.Resolved != nil && next.Resolved.ID == 5) {
253 t.Fatalf("%s: consumed %+v", u.Updates, next)
254 }
255 }
256}
257