| index 987f99832..f5f7c7586 100644 |
| --- a/internal/console/flow_test.go |
| +++ b/internal/console/flow_test.go |
| @@ -1,6 +1,9 @@ |
| package console |
| |
| -import "testing" |
| +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. |
| @@ -20,7 +23,7 @@ func stageByChannel(f FlowView, channel string) *FlowStage { |
| // 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", "") |
| + 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) |
| @@ -50,7 +53,7 @@ func TestFlowAddressesAttachToTheirChannel(t *testing.T) { |
| {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", "") |
| + f := assembleFlow(v, rows, "b2", "", "/a/x/apps/app/instances") |
| |
| live := stageByChannel(f, "stable") |
| if live == nil || len(live.Addresses) != 2 { |
| @@ -81,7 +84,7 @@ func TestFlowDrawsEveryRunningAddress(t *testing.T) { |
| {Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Running"}, |
| {Host: "odd.kass.dev", Channel: "experiment", HealthWord: "Running"}, |
| } |
| - f := assembleFlow(v, rows, "a1", "") |
| + f := assembleFlow(v, rows, "a1", "", "/a/x/apps/app/instances") |
| |
| for _, want := range []string{"kass.dev", "rc.kass.dev", "odd.kass.dev"} { |
| found := false |
| @@ -109,7 +112,7 @@ func TestFlowDrawsEveryRunningAddress(t *testing.T) { |
| 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", "") |
| + 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) |
| } |
| @@ -120,7 +123,7 @@ func TestFlowShowsTheSafetyDelayOnlyWhenItApplies(t *testing.T) { |
| t.Errorf("edge holds nothing back but says %q", got) |
| } |
| |
| - off := assembleFlow(&AppView{Channels: chans}, nil, "a1", "") |
| + 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) |
| @@ -128,7 +131,7 @@ func TestFlowShowsTheSafetyDelayOnlyWhenItApplies(t *testing.T) { |
| } |
| |
| // 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", "") |
| + 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") |
| } |
| @@ -148,7 +151,7 @@ func TestFlowTonesAddressHealth(t *testing.T) { |
| {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", "") |
| + 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", |
| @@ -175,7 +178,7 @@ func TestFlowCarriesTheArrowOnlyWhereTheVersionTravelled(t *testing.T) { |
| {Name: "edge", Version: "a1"}, |
| {Name: "beta", Version: "a1"}, |
| {Name: "stable", Version: "old"}, |
| - }}, nil, "a1", "") |
| + }}, nil, "a1", "", "/a/x/apps/app/instances") |
| want := []bool{true, true, false} |
| for i, s := range f.Line { |
| if s.Carried != want[i] { |
| @@ -185,13 +188,13 @@ func TestFlowCarriesTheArrowOnlyWhereTheVersionTravelled(t *testing.T) { |
| |
| // 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", "") |
| + 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, "", "") |
| + 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") |
| } |
| @@ -202,7 +205,7 @@ func TestFlowCarriesTheArrowOnlyWhereTheVersionTravelled(t *testing.T) { |
| // box instead. |
| func TestFlowSaysWhenAChannelHasNothingPublished(t *testing.T) { |
| rows := []InstanceRow{{Host: "rc.kass.dev", Channel: "canary/rc", HealthWord: "Not responding"}} |
| - f := assembleFlow(&AppView{}, rows, "", "") |
| + f := assembleFlow(&AppView{}, rows, "", "", "/a/x/apps/app/instances") |
| |
| rc := stageByChannel(f, "canary/rc") |
| if rc == nil { |
| @@ -216,14 +219,90 @@ func TestFlowSaysWhenAChannelHasNothingPublished(t *testing.T) { |
| // 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, "", ""); f.Show { |
| + 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", ""); !f.Show { |
| + 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, "", ""); !f.Show { |
| + 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) |
| + } |
| +} |