carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "fmt"
5 "testing"
6)
7
8// stageByChannel finds one stage across every band, so the tests can assert
9// about a channel without caring which band the assembler filed it under.
10func stageByChannel(f FlowView, channel string) *FlowStage {
11 for _, band := range [][]FlowStage{f.Line, f.Tests, f.Extra} {
12 for i := range band {
13 if band[i].Channel == channel {
14 return &band[i]
15 }
16 }
17 }
18 return nil
19}
20
21// The line is the app's release path, and it is drawn whole even when the
22// app has only reached the first rung: a pipeline with its later steps
23// missing reads as "there is no later step", which is the opposite of true.
24func TestFlowLineIsAlwaysTheWholePath(t *testing.T) {
25 v := &AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}
26 f := assembleFlow(v, nil, "a1", "", "/a/x/apps/app/instances")
27
28 if len(f.Line) != 3 {
29 t.Fatalf("line has %d stages, want edge/beta/live: %+v", len(f.Line), f.Line)
30 }
31 for i, want := range []string{"edge", "beta", "stable"} {
32 if f.Line[i].Channel != want {
33 t.Errorf("line[%d] is %q, want %q", i, f.Line[i].Channel, want)
34 }
35 }
36 if f.Line[2].Name != "live" {
37 t.Errorf("the stable stage is labelled %q, want the word owners use: live", f.Line[2].Name)
38 }
39 if f.Line[1].Version != "" {
40 t.Errorf("beta holds nothing but reports version %q", f.Line[1].Version)
41 }
42}
43
44// The whole point of the picture is that an address hangs off the channel
45// feeding it. An address filed under the wrong stage would draw a lie.
46func TestFlowAddressesAttachToTheirChannel(t *testing.T) {
47 v := &AppView{Channels: []ChannelView{
48 {Name: "stable", Version: "a1"},
49 {Name: "canary/rc", Version: "b2"},
50 }}
51 rows := []InstanceRow{
52 {Host: "kass.dev", Channel: "stable", Version: "a1", HealthWord: "Running"},
53 {Host: "www.kass.dev", Channel: "stable", Version: "a1", HealthWord: "Running"},
54 {Host: "rc.kass.dev", Channel: "canary/rc", Version: "b2", HealthWord: "Asleep"},
55 }
56 f := assembleFlow(v, rows, "b2", "", "/a/x/apps/app/instances")
57
58 live := stageByChannel(f, "stable")
59 if live == nil || len(live.Addresses) != 2 {
60 t.Fatalf("live stage should carry both stable hosts: %+v", live)
61 }
62 if live.Addresses[0].Host != "kass.dev" || live.Addresses[1].Host != "www.kass.dev" {
63 t.Errorf("live addresses out of order: %+v", live.Addresses)
64 }
65 rc := stageByChannel(f, "canary/rc")
66 if rc == nil || len(rc.Addresses) != 1 || rc.Addresses[0].Host != "rc.kass.dev" {
67 t.Fatalf("the canary host should hang off canary/rc: %+v", rc)
68 }
69 if rc.Name != "rc" {
70 t.Errorf("canary stage is labelled %q, want the short name rc", rc.Name)
71 }
72 if len(f.Tests) != 1 {
73 t.Errorf("canary/rc belongs in the test band, got %d test stages", len(f.Tests))
74 }
75}
76
77// A running address that the diagram does not draw is worse than no diagram:
78// the picture claims to be the whole story. Whatever channel a route follows,
79// its host must appear somewhere.
80func TestFlowDrawsEveryRunningAddress(t *testing.T) {
81 v := &AppView{Channels: []ChannelView{{Name: "stable", Version: "a1"}}}
82 rows := []InstanceRow{
83 {Host: "kass.dev", Channel: "stable", HealthWord: "Running"},
84 {Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Running"},
85 {Host: "odd.kass.dev", Channel: "experiment", HealthWord: "Running"},
86 }
87 f := assembleFlow(v, rows, "a1", "", "/a/x/apps/app/instances")
88
89 for _, want := range []string{"kass.dev", "rc.kass.dev", "odd.kass.dev"} {
90 found := false
91 for _, band := range [][]FlowStage{f.Line, f.Tests, f.Extra} {
92 for _, s := range band {
93 for _, a := range s.Addresses {
94 if a.Host == want {
95 found = true
96 }
97 }
98 }
99 }
100 if !found {
101 t.Errorf("%s is running but appears nowhere in the diagram", want)
102 }
103 }
104 if len(f.Extra) != 1 || f.Extra[0].Channel != "experiment" {
105 t.Errorf("a route off the ladder needs its own band, got %+v", f.Extra)
106 }
107}
108
109// The safety delay is the single most confusing thing on the old ladder, so
110// the diagram states it — but only when it is actually switched on, and only
111// on the channels that hold.
112func TestFlowShowsTheSafetyDelayOnlyWhenItApplies(t *testing.T) {
113 chans := []ChannelView{{Name: "edge", Version: "a1"}, {Name: "beta", Version: "a1"}, {Name: "stable", Version: "a1"}}
114
115 on := assembleFlow(&AppView{Channels: chans, Production: ProductionView{On: true}}, nil, "a1", "", "/a/x/apps/app/instances")
116 if got := stageByChannel(on, "beta").Hold; got != "~24h" {
117 t.Errorf("beta hold is %q, want ~24h", got)
118 }
119 if got := stageByChannel(on, "stable").Hold; got != "~72h" {
120 t.Errorf("live hold is %q, want ~72h", got)
121 }
122 if got := stageByChannel(on, "edge").Hold; got != "" {
123 t.Errorf("edge holds nothing back but says %q", got)
124 }
125
126 off := assembleFlow(&AppView{Channels: chans}, nil, "a1", "", "/a/x/apps/app/instances")
127 for _, s := range off.Line {
128 if s.Hold != "" {
129 t.Errorf("safety delay is off, but %s advertises a %q wait", s.Channel, s.Hold)
130 }
131 }
132
133 // An unreadable flag is treated as on, because that is what the boxes do.
134 bad := assembleFlow(&AppView{Channels: chans, Production: ProductionView{Err: "unverifiable"}}, nil, "a1", "", "/a/x/apps/app/instances")
135 if stageByChannel(bad, "stable").Hold == "" {
136 t.Error("an unreadable production flag must still show the delay — boxes keep baking")
137 }
138}
139
140// Health is the third of the three questions the console exists to answer,
141// and the diagram is where it gets answered per address. Asleep must read as
142// idle, not as a problem: hibernating is the platform working.
143func TestFlowTonesAddressHealth(t *testing.T) {
144 rows := []InstanceRow{
145 {Host: "a.dev", Channel: "stable", HealthWord: "Running"},
146 {Host: "b.dev", Channel: "stable", HealthWord: "Asleep"},
147 {Host: "c.dev", Channel: "stable", HealthWord: "Not responding"},
148 {Host: "d.dev", Channel: "stable", HealthWord: "Failed"},
149 {Host: "e.dev", Channel: "stable", HealthWord: "Setting up"},
150 {Host: "f.dev", Channel: "stable", HealthWord: "Serving"},
151 {Host: "g.dev", Channel: "stable", HealthWord: "Something new"},
152 {Host: "h.dev", Channel: "stable", HealthWord: "No readings"},
153 }
154 f := assembleFlow(&AppView{Channels: []ChannelView{{Name: "stable", Version: "a1"}}}, rows, "a1", "", "/a/x/apps/app/instances")
155
156 want := map[string]string{
157 "a.dev": "ok", "b.dev": "idle", "c.dev": "bad", "d.dev": "bad",
158 "e.dev": "wait", "f.dev": "ok",
159 // A word this function has never been taught is missing information:
160 // neither reassurance nor alarm.
161 "g.dev": "idle",
162 // Nobody could have sampled it either — honest ignorance reads the
163 // same as hibernating, not as a problem.
164 "h.dev": "idle",
165 }
166 for _, a := range stageByChannel(f, "stable").Addresses {
167 if a.Tone != want[a.Host] {
168 t.Errorf("%s (%s): tone=%q, want %q", a.Host, a.Health, a.Tone, want[a.Host])
169 }
170 }
171}
172
173// The solid-vs-dashed arrow is the diagram's one claim beyond the words on
174// it: that a version really did travel this step. It must be exactly true.
175func TestFlowCarriesTheArrowOnlyWhereTheVersionTravelled(t *testing.T) {
176 // a1 shipped, promoted all the way to beta, not yet live.
177 f := assembleFlow(&AppView{Channels: []ChannelView{
178 {Name: "edge", Version: "a1"},
179 {Name: "beta", Version: "a1"},
180 {Name: "stable", Version: "old"},
181 }}, nil, "a1", "", "/a/x/apps/app/instances")
182 want := []bool{true, true, false}
183 for i, s := range f.Line {
184 if s.Carried != want[i] {
185 t.Errorf("%s: carried=%v, want %v", s.Channel, s.Carried, want[i])
186 }
187 }
188
189 // Two empty boxes in a row share the empty string, and no version has
190 // travelled between them — the arrow must stay dashed.
191 empty := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "a1", "", "/a/x/apps/app/instances")
192 if empty.Line[1].Carried || empty.Line[2].Carried {
193 t.Errorf("empty boxes must not chain a solid arrow: %+v", empty.Line)
194 }
195
196 // Nothing shipped at all: the very first arrow has nothing to carry.
197 none := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "", "", "/a/x/apps/app/instances")
198 if none.Line[0].Carried {
199 t.Error("no shipped version — the arrow into edge cannot be solid")
200 }
201}
202
203// A host following a channel nothing was ever published to is the failure the
204// ladder exists to make visible; the diagram must not quietly draw an empty
205// box instead.
206func TestFlowSaysWhenAChannelHasNothingPublished(t *testing.T) {
207 rows := []InstanceRow{{Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Not responding"}}
208 f := assembleFlow(&AppView{}, rows, "", "", "/a/x/apps/app/instances")
209
210 rc := stageByChannel(f, "canary/rc")
211 if rc == nil {
212 t.Fatal("a host follows canary/rc — the stage must be drawn")
213 }
214 if rc.Err == "" {
215 t.Error("canary/rc has no version and no pointer: the stage must say so")
216 }
217}
218
219// Nothing shipped and nothing running is not a diagram — it is an empty
220// state, and the page has better words for that than three blank boxes.
221func TestFlowHidesItselfWhenThereIsNothingToDraw(t *testing.T) {
222 if f := assembleFlow(&AppView{}, nil, "", "", "/a/x/apps/app/instances"); f.Show {
223 t.Error("an app with no channels and no addresses should not draw a diagram")
224 }
225 if f := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "a1", "", "/a/x/apps/app/instances"); !f.Show {
226 t.Error("a shipped version on edge is worth drawing")
227 }
228 rows := []InstanceRow{{Host: "kass.dev", Channel: "stable", HealthWord: "Running"}}
229 if f := assembleFlow(&AppView{}, rows, "", "", "/a/x/apps/app/instances"); !f.Show {
230 t.Error("a running address is worth drawing even with no readable pointer")
231 }
232}
233
234// The rollup exists for stages that anticipate hundreds of addresses; a
235// stage still inside the normal range must render exactly as it always has,
236// with no rollup fields engaged at all.
237func TestFlowSmallStagesUnchanged(t *testing.T) {
238 rows := make([]InstanceRow, 0, flowAddrMax)
239 for i := 0; i < flowAddrMax; i++ {
240 h := fmt.Sprintf("i%02d.example.test", i)
241 rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: "Running"})
242 }
243 view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}}
244 f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances")
245 st := f.Line[0]
246 if st.RolledUp {
247 t.Fatalf("%d addresses must not roll up: %+v", flowAddrMax, st)
248 }
249 if len(st.Addresses) != flowAddrMax {
250 t.Fatalf("all %d addresses should be listed, got %d: %+v", flowAddrMax, len(st.Addresses), st.Addresses)
251 }
252 if st.Total != 0 || st.Attention != 0 || st.Hidden != 0 || st.MoreURL != "" {
253 t.Fatalf("rollup fields must stay zero when not rolled up: %+v", st)
254 }
255}
256
257// The needs-attention list inside a rolled-up stage is capped so the diagram
258// never grows unbounded again by another route — the Running tab is where
259// the rest of the story lives, via MoreURL.
260func TestFlowRollupCapsExceptions(t *testing.T) {
261 rows := make([]InstanceRow, 0, 20)
262 for i := 0; i < 20; i++ {
263 h := fmt.Sprintf("i%02d.example.test", i)
264 w := "Running"
265 if i < 9 {
266 w = "Not responding"
267 }
268 rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: w})
269 }
270 view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}}
271 f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances")
272 st := f.Line[0]
273 if !st.RolledUp || st.Attention != 9 {
274 t.Fatalf("rollup: %+v", st)
275 }
276 if len(st.Addresses) != flowExceptionMax {
277 t.Fatalf("exceptions capped at %d, got %d: %+v", flowExceptionMax, len(st.Addresses), st.Addresses)
278 }
279 if st.Hidden != 4 {
280 t.Fatalf("hidden should be 9 attention - 5 shown = 4, got %d", st.Hidden)
281 }
282}
283
284// The exact test from the task brief, kept verbatim for full alignment with
285// the interface it specifies.
286func TestFlowRollsUpBigStages(t *testing.T) {
287 rows := make([]InstanceRow, 0, 12)
288 for i := 0; i < 12; i++ {
289 h := fmt.Sprintf("i%02d.example.test", i)
290 w := "Running"
291 if i == 3 || i == 7 {
292 w = "Not responding"
293 }
294 rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: w})
295 }
296 view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}}
297 f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances")
298 st := f.Line[0] // edge
299 if !st.RolledUp || st.Total != 12 || st.Attention != 2 || st.Hidden != 0 {
300 t.Fatalf("rollup: %+v", st)
301 }
302 if len(st.Addresses) != 2 || st.Addresses[0].Health != "Not responding" {
303 t.Fatalf("exceptions: %+v", st.Addresses)
304 }
305 if st.MoreURL != "/a/x/apps/app/instances" {
306 t.Fatalf("MoreURL: %q", st.MoreURL)
307 }
308}
309