package view import ( "encoding/json" "errors" "fmt" "strconv" "strings" "testing" "time" ) // usageNow is the fixture's clock. var usageNow = time.Date(2026, 10, 26, 12, 0, 0, 0, time.UTC) // at is the fixture's time hh:mm on its day. func at(hm string) time.Time { t, _ := time.Parse("2006-01-02 15:04", "2026-10-26 "+hm) return t.UTC() } // stepID is a step id made at t, as buildstep.NewID makes it, with suffix as its six hex digits. func stepID(t time.Time, suffix string) string { return strconv.FormatInt(t.UnixMilli(), 36) + suffix } func states(m map[string]string) StateRead { return StateRead{ Keys: func() ([]string, error) { var k []string for key := range m { k = append(k, key) } return k, nil }, Get: func(key string) (json.RawMessage, error) { if v, ok := m[key]; ok { return json.RawMessage(v), nil } return nil, nil }, } } func step(node, id, tool, state string, started, ended time.Time) string { e := "" if !ended.IsZero() { e = `,"ended":"` + ended.Format(time.RFC3339) + `"` } return fmt.Sprintf(`{"v":1,"id":%q,"tool":%q,"node":%q,"state":%q,"started":%q%s}`, id, tool, node, state, started.Format(time.RFC3339), e) } // usageFixture is four nodes: shanks builds Go at capacity 2 (two asks running, one left running, a try's ask, a check, // a recheck of the morning and a hand step ended); ace holds the Go seat at capacity 2 and is paused; g14 holds it at // capacity 0; the Node seat on shanks has no resolved capacity and takes the modules' default. func usageFixture() UsageReader { running := `{"v":1,"phase":"running","health":"ok"}` tryAsk := stepID(at("11:30"), "a00003") goSteps := map[string]string{} add := func(node, id, tool, state string, started, ended time.Time) { goSteps[node+"."+id] = step(node, id, tool, state, started, ended) } add("shanks", stepID(at("11:50"), "a00001"), "delivery ask", "running", at("11:50"), time.Time{}) add("shanks", stepID(at("11:55"), "a00002"), "check ask", "running", at("11:55"), time.Time{}) add("shanks", tryAsk, "check ask", "passed", at("11:30"), at("11:40")) add("shanks", stepID(at("11:20"), "a00004"), "check ask", "failed", at("11:20"), at("11:26")) add("shanks", stepID(at("06:00"), "a00005"), "recheck ask", "passed", at("06:00"), at("06:10")) add("shanks", stepID(at("10:00"), "a00006"), "check ask", "running", at("10:00"), time.Time{}) add("shanks", stepID(at("11:45"), "a00007"), "build_go_test", "passed", at("11:45"), at("11:46")) // Two keys a build module ended for being left running: their end is when it noticed, so neither is finished work. goSteps["shanks."+stepID(at("08:00"), "a00008")] = `{"v":1,"id":"` + stepID(at("08:00"), "a00008") + `","tool":"check ask","node":"shanks","state":"failed","started":"2026-10-26T08:00:00Z","ended":"2026-10-26T11:59:00Z","reason":"the build module restarted while it ran"}` goSteps["shanks."+stepID(at("09:00"), "a00009")] = `{"v":1,"id":"` + stepID(at("09:00"), "a00009") + `","tool":"delivery ask","node":"shanks","state":"failed","started":"2026-10-26T09:00:00Z","ended":"2026-10-26T11:58:00Z","reason":"past the step bound: its asker stopped waiting at 2026-10-26T09:30:00Z"}` goSteps["shanks.notastepkey"] = `{}` // a key that is no step id is passed over return UsageReader{ Now: func() time.Time { return usageNow }, Haves: states(map[string]string{"shanks.mesh-build-go": running, "shanks.mesh-build-node": running, "ace.mesh-build-go": running, "g14.mesh-build-go": running, "g14.mesh-build-node": running, "shanks.node": `{}`, "shanks.mesh-delivery-view": running}), Resolved: states(map[string]string{ "shanks.mesh-build-go.capacity": `{"node":"shanks","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "ace.mesh-build-go.capacity": `{"node":"ace","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "g14.mesh-build-go.capacity": `{"node":"g14","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "shanks.mesh-config.other": `{"value":"x"}`, "g14.mesh-build-node.capacity": `{"node":"g14","module":"mesh-build-node","key":"capacity","source":"unreadable"}`, }), Steps: map[string]StateRead{"go": states(goSteps), "node": states(map[string]string{}), "image": states(map[string]string{})}, Pauses: map[string]StateRead{"go": states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`}), "node": states(nil), "image": states(nil)}, Tries: states(map[string]string{ "try-" + strconv.FormatInt(at("11:29").UnixNano(), 10): `{"id":"try-1","repository":"novox/mesh-sdk","head":"h","trunk":"t","state":"pass","asks":["` + tryAsk + `"]}`, "try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","started":"2026-10-26T11:58:00Z"}`, }), Backlog: func() ([]BacklogRow, error) { return []BacklogRow{{Language: "go", Kind: "check", Waiting: 3, Held: 2, OldestSecs: 240}}, nil }, } } func seatOf(t *testing.T, a UsageAnswer, node, seat string) SeatUsage { t.Helper() for _, n := range a.Nodes { for _, s := range n.Seats { if n.Node == node && s.Seat == seat { return s } } } t.Fatalf("no seat %s on %s in %+v", seat, node, a.Nodes) return SeatUsage{} } // build_usage answers, from the states alone, what the operator asked in six calls: per node its seats, capacity, // eligibility, the asks running by kind and the ones left running, the last hour and day, and mesh-wide the backlog. func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { a := usageFixture().Usage() if testing.Verbose() { out, _ := json.MarshalIndent(a, "", " ") t.Logf("build_usage answers:\n%s", out) } sg := seatOf(t, a, "shanks", "node-build-go") eligible := map[string]string{} for _, n := range a.Nodes { eligible[n.Node] = n.Eligible } for _, c := range []struct { what string got, want any }{ {"nodes, novox named by the default capacity", len(a.Nodes), 4}, {"eligible", eligible, map[string]string{"shanks": "eligible", "ace": "not eligible", "g14": "not eligible", "novox": "not eligible"}}, {"a resolved key with no readable value: the puller uses 0", seatOf(t, a, "g14", "node-build-node").WhyNot, "its capacity on g14 cannot be read, so its puller uses 0"}, {"and is said so", seatOf(t, a, "g14", "node-build-node").CapacityFrom, "mesh-config's value for mesh-build-node on g14 cannot be read (mesh-config resolves it as unreadable), so its puller uses 0"}, {"novox why not", seatOf(t, a, "novox", "node-build-go").WhyNot, "mesh-build-go is not on novox"}, {"novox capacity, from the module's value resolved elsewhere", seatOf(t, a, "novox", "node-build-go").CapacityFrom, "mesh-config, resolved for mesh-build-go on ace (mesh-build-go is not assigned to novox)"}, {"ended by restart or bound, apart", len(sg.EndedStale), 2}, {"ace why not", seatOf(t, a, "ace", "node-build-go").WhyNot, "paused (node-build-go.resume takes asks again)"}, {"g14 why not", seatOf(t, a, "g14", "node-build-go").WhyNot, "its capacity on g14 is 0"}, {"shanks capacity", sg.Capacity, 2}, {"shanks node seat takes the default", seatOf(t, a, "shanks", "node-build-node").Capacity, 3}, {"running", sg.Running.Total, 2}, {"running by kind", sg.Running.ByKind, map[string]int{"delivery": 1, "check": 1}}, {"running steps, the hand step's ended", len(sg.Steps), 2}, {"left running", len(sg.LeftRunning), 1}, {"hour started", sg.LastHour.Started, 5}, // delivery, check, try, check, hand {"hour finished", sg.LastHour.Finished, 3}, // try 600 s, check 360 s, hand 60 s {"hour failed", sg.LastHour.Failed, 1}, {"hour total", sg.LastHour.TotalSecs, int64(1020)}, {"hour median", sg.LastHour.MedianSecs, int64(360)}, {"hour try", sg.LastHour.ByKind["try"], KindWindow{Finished: 1, TotalSecs: 600, MedianSecs: 600}}, {"hour hand", sg.LastHour.ByKind["hand"], KindWindow{Finished: 1, TotalSecs: 60, MedianSecs: 60}}, {"hour at capacity", sg.LastHour.AtCapacitySecs, int64(300)}, // 11:55-12:00, two asks of two {"hour at capacity share", sg.LastHour.AtCapacityShare, 0.083}, {"day finished", sg.LastDay.Finished, 4}, {"day recheck", sg.LastDay.ByKind["recheck"].Finished, 1}, {"day at capacity", sg.LastDay.AtCapacitySecs, int64(300)}, {"backlog", a.Backlog, []BacklogRow{{Language: "go", Kind: "check", Waiting: 3, Held: 2, OldestSecs: 240}}}, {"tries running", len(a.TriesRunning), 1}, {"said", a.Nodes[0].Said[:1], []string{"node-build-go: not eligible, paused (node-build-go.resume takes asks again); 0 of capacity 2 running; last hour 0 finished, median 0 s, at capacity 0% of it; last day 0 finished, median 0 s, at capacity 0.0% of it"}}, {"said shanks", a.Nodes[3].Said[0], "node-build-go: eligible; 2 of capacity 2 running (check 1, delivery 1), 1 left running; last hour 3 finished, median 360 s, at capacity 8% of it; last day 4 finished, median 360 s, at capacity 0.3% of it"}, {"cpu says where it is", strings.Contains(a.Nodes[0].CPU, "/disk-load.cpu_top"), true}, } { if fmt.Sprint(c.got) != fmt.Sprint(c.want) { t.Errorf("%s = %v, want %v", c.what, c.got, c.want) } } if !strings.Contains(sg.LeftRunning[0].Why, "past the step bound") { t.Errorf("left running says %q", sg.LeftRunning[0].Why) } // Nothing readable: each state is named in a note, the backlog's refusal too, and nothing is said held. none := UsageReader{Now: func() time.Time { return usageNow }, Backlog: func() ([]BacklogRow, error) { return nil, errors.New("no socket") }}.Usage() notes := strings.Join(none.Notes, " | ") for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} { if !strings.Contains(notes, want) { t.Errorf("the notes %q do not say %q", notes, want) } } if none.Backlog == nil { t.Errorf("nothing readable answered no backlog list") } // mesh-config unreadable to this tool: a seat that holds its module is not answered (never said not eligible), and its // node's verdict is not known; a seat known not eligible on other grounds (paused) stays so. u := usageFixture() u.Resolved = StateRead{} blind := u.Usage() sb := seatOf(t, blind, "shanks", "node-build-go") verdict := map[string]string{} for _, n := range blind.Nodes { verdict[n.Node] = n.Eligible } if sb.EligibleKnown || sb.Eligible != nil || !strings.HasPrefix(sb.WhyNot, "not known: ") || !strings.HasPrefix(sb.CapacityFrom, "capacity unknown to this tool: ") { t.Errorf("unknown capacity: known %v eligible %v why %q from %q", sb.EligibleKnown, sb.Eligible, sb.WhyNot, sb.CapacityFrom) } if verdict["shanks"] != "not known" || verdict["ace"] != "not eligible" { t.Errorf("verdicts with mesh-config unreadable = %v, want shanks not known, ace not eligible (paused)", verdict) } }