Files
mesh-delivery/modules/mesh-delivery-view/internal/view/usage_test.go
T
jschoubben ee2f7a5415 View: build_usage answers how the build nodes are used in one call; the checker's tries name their asks
The operator's question (how much are the build nodes used, is ace eligible) took six calls over four tools.
build_usage reads it from the bus's states: seats held (have), capacity (mesh-config resolved), pauses, the
build modules' steps by kind with left-running keys, the last hour and day with total and median time and time
at capacity, the running tries, and the backlog the view program reads from the seats' streams. A try now names
the asks it put, and the checker announces its tries readable, so the view tells a try's asks from a check's.
2026-10-26 14:46:02 +01:00

169 lines
8.3 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"))
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, "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","source":"mesh"}`,
"ace.mesh-build-go.capacity": `{"node":"ace","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0","source":"mesh"}`,
"g14.mesh-build-go.capacity": `{"node":"g14","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0","source":"mesh"}`,
"shanks.mesh-config.other": `{"value":"x"}`,
}),
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]bool{}
for _, n := range a.Nodes {
eligible[n.Node] = n.Eligible
}
for _, c := range []struct {
what string
got, want any
}{
{"nodes", len(a.Nodes), 3},
{"eligible", eligible, map[string]bool{"shanks": true, "ace": false, "g14": false}},
{"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, []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[2].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", "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 len(none.Nodes) != 0 || none.Backlog == nil {
t.Errorf("nothing readable answered nodes %v, backlog %v", none.Nodes, none.Backlog)
}
}