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 "testing"
5)
6
7func ip(n int) *int { return &n }
8
9type tcase struct {
10 name string
11 in TargetInput
12 want TargetDecision
13}
14
15func head(v string) *ChannelPointer { return &ChannelPointer{Version: v} }
16
17func known(replica bool, b *BuildState) BuildStateCopy {
18 return BuildStateCopy{Known: true, Replica: replica, State: b}
19}
20
21func runTargetCases(t *testing.T, cases []tcase) {
22 t.Helper()
23 for _, c := range cases {
24 got := Target(c.in)
25 w := c.want
26 if got.Version != w.Version || got.Reason != w.Reason || got.Evidence != w.Evidence || got.Bake != w.Bake ||
27 got.Fail != w.Fail || (got.Resolve == nil) != (w.Resolve == nil) ||
28 (got.Resolve != nil && *got.Resolve != *w.Resolve) {
29 t.Errorf("%s:\n got %+v\nwant %+v", c.name, got, w)
30 }
31 }
32}
33
34func auto() UpdateSettings { return UpdateSettings{Updates: UpdatesAuto, Channel: "stable"} }
35func manual() UpdateSettings { return UpdateSettings{Updates: UpdatesManual, Channel: "stable"} }
36
37func TestTargetRules(t *testing.T) {
38 failedV9 := func(v string) bool { return v == "v9" }
39 runTargetCases(t, []tcase{
40 {"auto: today's adoption", TargetInput{Pointer: head("v3"), Settings: auto(), Installed: "v2"},
41 TargetDecision{Version: "v3", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}},
42 {"pin: the pinned build, a record, no bake", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1"}, Installed: "v3"},
43 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
44 // Astra I3 #6: the request is still resolved at the start of the pass,
45 // pin or no pin, onto the head eligible now; the pin decides what runs.
46 {"pin outranks manual and a request, which still resolves", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, Pin: "v1", InstallRequest: 5}, Build: known(false, nil)},
47 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 5, Target: "v3"}}},
48 {"pin, request already resolved to T: the pin runs, T is kept for later", TargetInput{Pointer: head("v4"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2},
49 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 1}},
50 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
51 {"pin, the request's T failed: completes failed under the pin", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2},
52 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v9"}, Completed: 1}, Failed: failedV9, Build: known(false, nil)},
53 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Fail: 2}},
54 {"pin, request waiting on bake: unresolved", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2}, Build: known(false, nil)},
55 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
56 {"pending request, head eligible: resolves to the head", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
57 Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"},
58 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer, Resolve: &ResolvedRequest{ID: 2, Target: "v3"}}},
59 {"pending request, head baking: unresolved, held instance recovers", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
60 Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v0"},
61 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
62 {"pending request, head baking, FastTrack: resolves anyway", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2, InstallFastTrack: true},
63 Build: known(true, &BuildState{Completed: 1}), Installed: "v1"},
64 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer, Resolve: &ResolvedRequest{ID: 2, Target: "v3"}}},
65 {"resolved request, T no longer the head: a record", TargetInput{Pointer: head("v4"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
66 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 1}, Installed: "v1", Build: known(false, nil)},
67 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}},
68 {"resolved request ignores manual and major", TargetInput{Pointer: head("v3"), HeadTag: "2.0.0", Settings: UpdateSettings{Updates: UpdatesManual, Major: ip(1), InstallRequest: 2},
69 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}}, Build: known(false, nil)},
70 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer}},
71 {"resolved request whose T failed: completes failed, installs nothing", TargetInput{Pointer: head("v9"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
72 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v9"}}, Failed: failedV9, Build: known(true, &BuildState{LastExecuted: "v1"}), Installed: "v1"},
73 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Fail: 2}},
74 {"manual, build state differs from installed: install last_executed", TargetInput{Pointer: head("v3"), Settings: manual(),
75 Build: known(true, &BuildState{LastExecuted: "v1"}), Installed: "v2"},
76 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
77 {"manual, no build state, something installed: keep it", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(true, nil), Installed: "v2"},
78 TargetDecision{Version: "v2", Reason: ReasonManual, Evidence: EvidenceRecord}},
79 {"manual, no build state, nothing installed, a replica: unknown", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(true, nil)},
80 TargetDecision{Reason: ReasonUnknown}},
81 {"manual, none of these: new, gets the eligible head", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(false, nil)},
82 TargetDecision{Version: "v3", Reason: ReasonNew, Evidence: EvidencePointer, Bake: true}},
83 {"major matching head tag: today's adoption", TargetInput{Pointer: head("v3"), HeadTag: "2.1.0", Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)}, Installed: "v2"},
84 TargetDecision{Version: "v3", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}},
85 {"major, head of another major: held", TargetInput{Pointer: head("v3"), HeadTag: "3.0.0", Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)},
86 Build: known(true, &BuildState{LastExecuted: "v2"}), Installed: "v2"},
87 TargetDecision{Version: "v2", Reason: ReasonMajor, Evidence: EvidenceRecord}},
88 {"major, untagged head: held", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)},
89 Build: known(true, &BuildState{LastExecuted: "v2"}), Installed: "v2"},
90 TargetDecision{Version: "v2", Reason: ReasonMajor, Evidence: EvidenceRecord}},
91 {"auto onto a failed head: nothing new", TargetInput{Pointer: head("v9"), Settings: auto(), Installed: "v2", Failed: failedV9},
92 TargetDecision{Reason: ReasonFailed}},
93 })
94}
95
96// Review Focus 3.
97func TestFailureRecordsOutrankAPin(t *testing.T) {
98 runTargetCases(t, []tcase{{"pin of a failed build", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v9"},
99 Installed: "v2", Failed: func(v string) bool { return v == "v9" }}, TargetDecision{Reason: ReasonFailed}}})
100}
101
102// Review Focus 2.
103func TestARequestBelowTheWatermarkNeverRuns(t *testing.T) {
104 runTargetCases(t, []tcase{{"completed in the build state, older local file", TargetInput{Pointer: head("v4"),
105 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 3},
106 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2},
107 Build: known(true, &BuildState{LastExecuted: "v3", Completed: 3}), Installed: "v3"},
108 TargetDecision{Version: "v3", Reason: ReasonManual, Evidence: EvidenceRecord}}})
109}
110
111// Review Focus 4.
112func TestAnUnreadBuildStateIsNeverNew(t *testing.T) {
113 runTargetCases(t, []tcase{{"held, build state never read", TargetInput{Pointer: head("v3"), Settings: manual(), Build: BuildStateCopy{}},
114 TargetDecision{Reason: ReasonUnknown}}})
115}
116
117// Review Focus 5.
118func TestAResolutionInTheBuildStateIsReused(t *testing.T) {
119 runTargetCases(t, []tcase{{"resolved on another box", TargetInput{Pointer: head("v5"),
120 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 4},
121 Build: known(true, &BuildState{LastExecuted: "v2", Resolved: &ResolvedRequest{ID: 4, Target: "v3"}, Completed: 3})},
122 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}}})
123}
124
125// Astra I1 #10: T ran (the request completed at the pre-exec write), then
126// failed its health check. The completed request is marked failed once,
127// and is not reopened.
128func TestAFailureAfterCompletionMarksTheRequestFailed(t *testing.T) {
129 failedV3 := func(v string) bool { return v == "v3" }
130 runTargetCases(t, []tcase{
131 {"completed, then T failed", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
132 Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 2}),
133 Installed: "v1", Failed: failedV3},
134 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Fail: 2}},
135 {"already recorded as failed: not again", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
136 Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 2, CompletedFailed: true}),
137 Installed: "v1", Failed: failedV3},
138 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
139 })
140}
141
142// Astra I1 #9: rules 1-3 do not need a readable head.
143func TestIndependentTargetsNeedNoPointer(t *testing.T) {
144 runTargetCases(t, []tcase{
145 {"pin, no pointer", TargetInput{Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1"}},
146 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
147 {"resolved request, no pointer", TargetInput{Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
148 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}}, Build: known(false, nil)},
149 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}},
150 {"held recovery, no pointer", TargetInput{Settings: manual(), Build: known(true, &BuildState{LastExecuted: "v1"})},
151 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
152 {"new held instance, no pointer: nothing yet", TargetInput{Settings: manual(), Build: known(false, nil)},
153 TargetDecision{Reason: ReasonNew}},
154 {"auto, no pointer: nothing", TargetInput{Settings: auto()}, TargetDecision{}},
155 })
156}
157
158// Spec §8: a held sleeper with a pending request wakes onto T, which the
159// wake check reads from this same decision.
160func TestAPendingTOutranksLastExecuted(t *testing.T) {
161 runTargetCases(t, []tcase{{"resolved T pending while last_executed is older", TargetInput{Pointer: head("v3"),
162 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
163 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}},
164 Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v3"},
165 TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer}}})
166}
167
168// R-TARGET-UNKNOWN: with the build state unread the watermark sees only the
169// consumed file, so a request that already completed would look pending on a
170// fresh box. Target then does no request bookkeeping at all.
171func TestAnUnreadBuildStateDoesNoRequestBookkeeping(t *testing.T) {
172 runTargetCases(t, []tcase{
173 {"held, completed request looks pending: unknown, nothing resolved", TargetInput{Pointer: head("v7"),
174 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 5}, Build: BuildStateCopy{}},
175 TargetDecision{Reason: ReasonUnknown}},
176 {"auto, FastTrack, baking head: today's adoption, nothing resolved", TargetInput{Pointer: head("v7"), HeadHeld: "held 20h",
177 Settings: UpdateSettings{Updates: UpdatesAuto, InstallRequest: 5, InstallFastTrack: true}, Build: BuildStateCopy{}},
178 TargetDecision{Version: "v7", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}},
179 {"pin, pending request: the pin, nothing resolved", TargetInput{Pointer: head("v7"),
180 Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 5}, Build: BuildStateCopy{}},
181 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
182 {"resolved T failed, build state unread: no failure completion", TargetInput{Pointer: head("v7"),
183 Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 5},
184 Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 5, Target: "v9"}}, Failed: func(v string) bool { return v == "v9" }},
185 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
186 })
187}
188
189// R-TARGET-FAILEDHEAD: a pending request whose eligible head this box holds
190// as failed is resolved to that head and completed failed in the same pass.
191// It installs nothing itself; the later rules decide what runs, and the
192// failed head never becomes the Version.
193func TestAPendingRequestOnAFailedHeadCompletesFailed(t *testing.T) {
194 failedV9 := func(v string) bool { return v == "v9" }
195 runTargetCases(t, []tcase{
196 {"manual: the held build runs, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9,
197 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
198 Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"},
199 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}},
200 {"auto: nothing new, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9,
201 Settings: UpdateSettings{Updates: UpdatesAuto, InstallRequest: 2}, Build: known(true, nil), Installed: "v2"},
202 TargetDecision{Reason: ReasonFailed, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}},
203 {"pin: the pin runs, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9,
204 Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2}, Build: known(true, nil)},
205 TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}},
206 {"baking failed head: still unresolved", TargetInput{Pointer: head("v9"), Failed: failedV9, HeadHeld: "held 20h",
207 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
208 Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"},
209 TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
210 })
211}
212
213// Ruling R-RESOLUTION-STATE-WINS (Task 11 sweep, seed 531): box A resolved R
214// to v1 and lost the instance to a move before any wake; box B resolved R
215// again, to v4, and a wake carried that into the build state; back on A, the
216// file and the build state name different builds for the same R. The build
217// state's T is followed, root's file is rewritten to it, and the wake that
218// runs it completes R. Following the file ran v1 forever with R pending.
219func TestTheBuildStatesResolutionOutranksTheBoxsFile(t *testing.T) {
220 in := TargetInput{Pointer: head("v5"), HeadTag: "1.5.0",
221 Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 1, InstallFastTrack: true},
222 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 1, Target: "v1"}},
223 Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 1, Target: "v4"}}), Installed: "v1"}
224 runTargetCases(t, []tcase{{"same request, two builds: the build state's T", in,
225 TargetDecision{Version: "v4", Reason: ReasonRequest, Evidence: EvidenceRecord}}})
226 dec := Target(in)
227 c := NextConsumed(in, dec)
228 if c.Resolved == nil || c.Resolved.ID != 1 || c.Resolved.Target != "v4" {
229 t.Fatalf("consumed after the pass = %+v; want the build state's R1->v4", c.Resolved)
230 }
231 st := NextBuildState(in.Build.State, &c, dec.Version)
232 if st.LastExecuted != "v4" || st.Completed != 1 || st.CompletedFailed {
233 t.Fatalf("build state after the wake = %+v; want R1 completed by v4", st)
234 }
235}
236