package console import ( "fmt" "testing" ) // stageByChannel finds one stage across every band, so the tests can assert // about a channel without caring which band the assembler filed it under. func stageByChannel(f FlowView, channel string) *FlowStage { for _, band := range [][]FlowStage{f.Line, f.Tests, f.Extra} { for i := range band { if band[i].Channel == channel { return &band[i] } } } return nil } // The line is the app's release path, and it is drawn whole even when the // app has only reached the first rung: a pipeline with its later steps // missing reads as "there is no later step", which is the opposite of true. func TestFlowLineIsAlwaysTheWholePath(t *testing.T) { v := &AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}} f := assembleFlow(v, nil, "a1", "", "/a/x/apps/app/instances") if len(f.Line) != 3 { t.Fatalf("line has %d stages, want edge/beta/live: %+v", len(f.Line), f.Line) } for i, want := range []string{"edge", "beta", "stable"} { if f.Line[i].Channel != want { t.Errorf("line[%d] is %q, want %q", i, f.Line[i].Channel, want) } } if f.Line[2].Name != "live" { t.Errorf("the stable stage is labelled %q, want the word owners use: live", f.Line[2].Name) } if f.Line[1].Version != "" { t.Errorf("beta holds nothing but reports version %q", f.Line[1].Version) } } // The whole point of the picture is that an address hangs off the channel // feeding it. An address filed under the wrong stage would draw a lie. func TestFlowAddressesAttachToTheirChannel(t *testing.T) { v := &AppView{Channels: []ChannelView{ {Name: "stable", Version: "a1"}, {Name: "canary/rc", Version: "b2"}, }} rows := []InstanceRow{ {Host: "kass.dev", Channel: "stable", Version: "a1", HealthWord: "Running"}, {Host: "www.kass.dev", Channel: "stable", Version: "a1", HealthWord: "Running"}, {Host: "rc.kass.dev", Channel: "canary/rc", Version: "b2", HealthWord: "Asleep"}, } f := assembleFlow(v, rows, "b2", "", "/a/x/apps/app/instances") live := stageByChannel(f, "stable") if live == nil || len(live.Addresses) != 2 { t.Fatalf("live stage should carry both stable hosts: %+v", live) } if live.Addresses[0].Host != "kass.dev" || live.Addresses[1].Host != "www.kass.dev" { t.Errorf("live addresses out of order: %+v", live.Addresses) } rc := stageByChannel(f, "canary/rc") if rc == nil || len(rc.Addresses) != 1 || rc.Addresses[0].Host != "rc.kass.dev" { t.Fatalf("the canary host should hang off canary/rc: %+v", rc) } if rc.Name != "rc" { t.Errorf("canary stage is labelled %q, want the short name rc", rc.Name) } if len(f.Tests) != 1 { t.Errorf("canary/rc belongs in the test band, got %d test stages", len(f.Tests)) } } // A running address that the diagram does not draw is worse than no diagram: // the picture claims to be the whole story. Whatever channel a route follows, // its host must appear somewhere. func TestFlowDrawsEveryRunningAddress(t *testing.T) { v := &AppView{Channels: []ChannelView{{Name: "stable", Version: "a1"}}} rows := []InstanceRow{ {Host: "kass.dev", Channel: "stable", HealthWord: "Running"}, {Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Running"}, {Host: "odd.kass.dev", Channel: "experiment", HealthWord: "Running"}, } f := assembleFlow(v, rows, "a1", "", "/a/x/apps/app/instances") for _, want := range []string{"kass.dev", "rc.kass.dev", "odd.kass.dev"} { found := false for _, band := range [][]FlowStage{f.Line, f.Tests, f.Extra} { for _, s := range band { for _, a := range s.Addresses { if a.Host == want { found = true } } } } if !found { t.Errorf("%s is running but appears nowhere in the diagram", want) } } if len(f.Extra) != 1 || f.Extra[0].Channel != "experiment" { t.Errorf("a route off the ladder needs its own band, got %+v", f.Extra) } } // The safety delay is the single most confusing thing on the old ladder, so // the diagram states it — but only when it is actually switched on, and only // on the channels that hold. func TestFlowShowsTheSafetyDelayOnlyWhenItApplies(t *testing.T) { chans := []ChannelView{{Name: "edge", Version: "a1"}, {Name: "beta", Version: "a1"}, {Name: "stable", Version: "a1"}} on := assembleFlow(&AppView{Channels: chans, Production: ProductionView{On: true}}, nil, "a1", "", "/a/x/apps/app/instances") if got := stageByChannel(on, "beta").Hold; got != "~24h" { t.Errorf("beta hold is %q, want ~24h", got) } if got := stageByChannel(on, "stable").Hold; got != "~72h" { t.Errorf("live hold is %q, want ~72h", got) } if got := stageByChannel(on, "edge").Hold; got != "" { t.Errorf("edge holds nothing back but says %q", got) } off := assembleFlow(&AppView{Channels: chans}, nil, "a1", "", "/a/x/apps/app/instances") for _, s := range off.Line { if s.Hold != "" { t.Errorf("safety delay is off, but %s advertises a %q wait", s.Channel, s.Hold) } } // An unreadable flag is treated as on, because that is what the boxes do. bad := assembleFlow(&AppView{Channels: chans, Production: ProductionView{Err: "unverifiable"}}, nil, "a1", "", "/a/x/apps/app/instances") if stageByChannel(bad, "stable").Hold == "" { t.Error("an unreadable production flag must still show the delay — boxes keep baking") } } // Health is the third of the three questions the console exists to answer, // and the diagram is where it gets answered per address. Asleep must read as // idle, not as a problem: hibernating is the platform working. func TestFlowTonesAddressHealth(t *testing.T) { rows := []InstanceRow{ {Host: "a.dev", Channel: "stable", HealthWord: "Running"}, {Host: "b.dev", Channel: "stable", HealthWord: "Asleep"}, {Host: "c.dev", Channel: "stable", HealthWord: "Not responding"}, {Host: "d.dev", Channel: "stable", HealthWord: "Failed"}, {Host: "e.dev", Channel: "stable", HealthWord: "Setting up"}, {Host: "f.dev", Channel: "stable", HealthWord: "Serving"}, {Host: "g.dev", Channel: "stable", HealthWord: "Something new"}, {Host: "h.dev", Channel: "stable", HealthWord: "No readings"}, } f := assembleFlow(&AppView{Channels: []ChannelView{{Name: "stable", Version: "a1"}}}, rows, "a1", "", "/a/x/apps/app/instances") want := map[string]string{ "a.dev": "ok", "b.dev": "idle", "c.dev": "bad", "d.dev": "bad", "e.dev": "wait", "f.dev": "ok", // A word this function has never been taught is missing information: // neither reassurance nor alarm. "g.dev": "idle", // Nobody could have sampled it either — honest ignorance reads the // same as hibernating, not as a problem. "h.dev": "idle", } for _, a := range stageByChannel(f, "stable").Addresses { if a.Tone != want[a.Host] { t.Errorf("%s (%s): tone=%q, want %q", a.Host, a.Health, a.Tone, want[a.Host]) } } } // The solid-vs-dashed arrow is the diagram's one claim beyond the words on // it: that a version really did travel this step. It must be exactly true. func TestFlowCarriesTheArrowOnlyWhereTheVersionTravelled(t *testing.T) { // a1 shipped, promoted all the way to beta, not yet live. f := assembleFlow(&AppView{Channels: []ChannelView{ {Name: "edge", Version: "a1"}, {Name: "beta", Version: "a1"}, {Name: "stable", Version: "old"}, }}, nil, "a1", "", "/a/x/apps/app/instances") want := []bool{true, true, false} for i, s := range f.Line { if s.Carried != want[i] { t.Errorf("%s: carried=%v, want %v", s.Channel, s.Carried, want[i]) } } // Two empty boxes in a row share the empty string, and no version has // travelled between them — the arrow must stay dashed. empty := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "a1", "", "/a/x/apps/app/instances") if empty.Line[1].Carried || empty.Line[2].Carried { t.Errorf("empty boxes must not chain a solid arrow: %+v", empty.Line) } // Nothing shipped at all: the very first arrow has nothing to carry. none := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "", "", "/a/x/apps/app/instances") if none.Line[0].Carried { t.Error("no shipped version — the arrow into edge cannot be solid") } } // A host following a channel nothing was ever published to is the failure the // ladder exists to make visible; the diagram must not quietly draw an empty // box instead. func TestFlowSaysWhenAChannelHasNothingPublished(t *testing.T) { rows := []InstanceRow{{Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Not responding"}} f := assembleFlow(&AppView{}, rows, "", "", "/a/x/apps/app/instances") rc := stageByChannel(f, "canary/rc") if rc == nil { t.Fatal("a host follows canary/rc — the stage must be drawn") } if rc.Err == "" { t.Error("canary/rc has no version and no pointer: the stage must say so") } } // Nothing shipped and nothing running is not a diagram — it is an empty // state, and the page has better words for that than three blank boxes. func TestFlowHidesItselfWhenThereIsNothingToDraw(t *testing.T) { if f := assembleFlow(&AppView{}, nil, "", "", "/a/x/apps/app/instances"); f.Show { t.Error("an app with no channels and no addresses should not draw a diagram") } if f := assembleFlow(&AppView{Channels: []ChannelView{{Name: "edge", Version: "a1"}}}, nil, "a1", "", "/a/x/apps/app/instances"); !f.Show { t.Error("a shipped version on edge is worth drawing") } rows := []InstanceRow{{Host: "kass.dev", Channel: "stable", HealthWord: "Running"}} if f := assembleFlow(&AppView{}, rows, "", "", "/a/x/apps/app/instances"); !f.Show { t.Error("a running address is worth drawing even with no readable pointer") } } // The rollup exists for stages that anticipate hundreds of addresses; a // stage still inside the normal range must render exactly as it always has, // with no rollup fields engaged at all. func TestFlowSmallStagesUnchanged(t *testing.T) { rows := make([]InstanceRow, 0, flowAddrMax) for i := 0; i < flowAddrMax; i++ { h := fmt.Sprintf("i%02d.example.test", i) rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: "Running"}) } view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}} f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances") st := f.Line[0] if st.RolledUp { t.Fatalf("%d addresses must not roll up: %+v", flowAddrMax, st) } if len(st.Addresses) != flowAddrMax { t.Fatalf("all %d addresses should be listed, got %d: %+v", flowAddrMax, len(st.Addresses), st.Addresses) } if st.Total != 0 || st.Attention != 0 || st.Hidden != 0 || st.MoreURL != "" { t.Fatalf("rollup fields must stay zero when not rolled up: %+v", st) } } // The needs-attention list inside a rolled-up stage is capped so the diagram // never grows unbounded again by another route — the Running tab is where // the rest of the story lives, via MoreURL. func TestFlowRollupCapsExceptions(t *testing.T) { rows := make([]InstanceRow, 0, 20) for i := 0; i < 20; i++ { h := fmt.Sprintf("i%02d.example.test", i) w := "Running" if i < 9 { w = "Not responding" } rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: w}) } view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}} f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances") st := f.Line[0] if !st.RolledUp || st.Attention != 9 { t.Fatalf("rollup: %+v", st) } if len(st.Addresses) != flowExceptionMax { t.Fatalf("exceptions capped at %d, got %d: %+v", flowExceptionMax, len(st.Addresses), st.Addresses) } if st.Hidden != 4 { t.Fatalf("hidden should be 9 attention - 5 shown = 4, got %d", st.Hidden) } } // The exact test from the task brief, kept verbatim for full alignment with // the interface it specifies. func TestFlowRollsUpBigStages(t *testing.T) { rows := make([]InstanceRow, 0, 12) for i := 0; i < 12; i++ { h := fmt.Sprintf("i%02d.example.test", i) w := "Running" if i == 3 || i == 7 { w = "Not responding" } rows = append(rows, InstanceRow{Host: h, Channel: "edge", HealthWord: w}) } view := &AppView{Channels: []ChannelView{{Name: "edge", Version: "v1"}}} f := assembleFlow(view, rows, "v1", "", "/a/x/apps/app/instances") st := f.Line[0] // edge if !st.RolledUp || st.Total != 12 || st.Attention != 2 || st.Hidden != 0 { t.Fatalf("rollup: %+v", st) } if len(st.Addresses) != 2 || st.Addresses[0].Health != "Not responding" { t.Fatalf("exceptions: %+v", st.Addresses) } if st.MoreURL != "/a/x/apps/app/instances" { t.Fatalf("MoreURL: %q", st.MoreURL) } }