Files
mesh-delivery/modules/mesh-delivery-view/internal/view/usage_test.go
T
jschoubben 4f722899df
mesh/delivery merged: source.main moved to novox/mesh-delivery@feef96ed160e
mesh/check checking 4f722899df25 on feef96ed160e
View: build_usage does not answer eligibility it cannot read; a resolved capacity key with no readable value is the puller's 0
A capacity this tool cannot read is unknown to it, not 0: the seat says eligible_known false and the node not known.
A key mesh-config holds without a readable value is what the puller reads as 0, and was dropped silently.
2026-10-26 14:58:47 +01:00

195 lines
11 KiB
Go

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)
}
}