Add disk-load's watched items, their test and its whole test, which the binary's ignore line hid

The ignore line `disk-load` matched the directory cmd/disk-load, so new files there were never
added; it now names the built binary alone.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 00:15:27 +01:00
parent b712123f2b
commit 357a53d9ac
4 changed files with 541 additions and 1 deletions
@@ -0,0 +1,153 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
)
// ADR 0577 clauses 2, 3 (and the disk-load check of clause 5): the items measured come from the node-engine's keys of
// this machine as the watch hands them over, and none is known (none taken for gone) before the watch caught up; a key
// that changes after the watch began has its module measured again, and one deleted is measured no more; an item whose
// measurement fails is a failed item of the round, which measures the rest, and the round's key says so.
func TestTheItemsComeFromTheEnginesKeysAndARoundSaysHowFarItGot(t *testing.T) {
key := func(module string) string { k, _ := delivery.EngineDataKey("ace", module); return k }
value := func(module string, items ...catalogue.PlacedDataItem) json.RawMessage {
raw, err := delivery.EncodeEngineData(delivery.EngineData{Node: "ace", Module: module, Digest: "sha256:" + module, Items: items})
if err != nil {
t.Fatal(err)
}
return raw
}
pg := catalogue.PlacedDataItem{Module: "pg", ID: "data", Class: "valuable", Path: "/srv/pg", Measure: catalogue.MeasureShallow}
gone := catalogue.PlacedDataItem{Module: "pg", ID: "wal", Class: "valuable", Path: "/srv/missing", Measure: catalogue.MeasureShallow}
web := catalogue.PlacedDataItem{Module: "web", ID: "uploads", Class: "valuable", Path: "/srv/web", Measure: catalogue.MeasureShallow}
measured := map[string]int{}
run := func(_ context.Context, name string, args ...string) (string, error) {
switch {
case name == "test" && args[1] == "/srv/missing":
return "", errors.New("no such file")
case name == "test":
return "", nil
case name == "bash" && strings.Contains(args[1], "-maxdepth 1"):
measured[strings.Split(args[1], "'")[1]]++
return "1760000000 2\n", nil
}
return "", os.ErrNotExist
}
now := time.Date(2026, 11, 1, 0, 30, 0, 0, time.UTC)
readings, rounds := &memStore{m: map[string]json.RawMessage{}}, &memStore{m: map[string]json.RawMessage{}}
w := &Watched{Node: "ace"}
var said []string
k := &Keeper{Node: "ace", Items: w.All, Store: readings, Rounds: rounds,
Say: func(f string, a ...any) { said = append(said, f) },
Measurer: Measurer{Run: run, Now: func() time.Time { return now }}}
round := func() Round {
var r Round
raw, _ := rounds.Get("ace")
if err := json.Unmarshal(raw, &r); err != nil {
t.Fatalf("the round's key: %v", err)
}
return r
}
// Before the watch caught up nothing is known, and a round says so in its key rather than retiring every reading.
if _, _, err := k.Round(context.Background(), false); !errors.Is(err, ErrNotWatchedYet) {
t.Fatalf("a round before the watch: %v", err)
}
if r := round(); r.Ended == nil || !strings.Contains(r.Error, "not watched yet") {
t.Fatalf("the round before the watch is kept as %+v", r)
}
for _, step := range []struct {
name string
change stdio.StateChange
module string
changed bool
err bool
}{
{"a current key", stdio.StateChange{Key: key("pg"), Op: stdio.OpPut, Value: value("pg", pg, gone), Current: true}, "pg", true, false},
{"another current key", stdio.StateChange{Key: key("web"), Op: stdio.OpPut, Value: value("web", web), Current: true}, "web", true, false},
{"the same items again", stdio.StateChange{Key: key("web"), Op: stdio.OpPut, Value: value("web", web)}, "web", false, false},
{"another machine's items", stdio.StateChange{Key: key("x"), Op: stdio.OpPut, Value: json.RawMessage(
`{"v":1,"node":"g14","module":"x","digest":"d","items":[]}`)}, "", false, true},
{"a delete of a key never seen", stdio.StateChange{Key: key("y"), Op: stdio.OpDelete}, "", false, false},
} {
module, changed, err := w.Take(step.change)
if module != step.module || changed != step.changed || (err != nil) != step.err {
t.Errorf("%s: taken as %q, changed %v, %v", step.name, module, changed, err)
}
}
w.Ready()
// A round measures every item it was handed; the missing one fails, and the round goes on and says it.
if _, _, err := k.Round(context.Background(), false); err != nil {
t.Fatalf("the round: %v", err)
}
r := round()
if r.Ended == nil || r.Items != 3 || r.Measured != 3 || len(r.Failed) != 1 || r.Failed[0].Item != "pg.wal" ||
!strings.Contains(r.Failed[0].Why, "does not exist") || measured["/srv/pg"] != 1 || measured["/srv/web"] != 1 {
t.Fatalf("the round's key says %+v; measured %v", r, measured)
}
if !strings.Contains(strings.Join(said, "\n"), "the round ended") || !strings.Contains(strings.Join(said, "\n"), "starts") {
t.Errorf("the journal said %v", said)
}
// A key that changes after the watch began has its module measured again, at once, by Work.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go k.Work(ctx)
moved := web
moved.Path = "/srv/web2"
if module, changed, err := w.Take(stdio.StateChange{Key: key("web"), Op: stdio.OpPut, Value: value("web", moved)}); err != nil || !changed {
t.Fatalf("a changed key: %s %v %v", module, changed, err)
}
k.Due("web")
waitFor(t, func() bool {
raw := readingOf(readings, "ace.web.uploads")
return raw != nil && raw.Path == "/srv/web2"
})
// A key deleted: its module's items are measured no more, and its reading is retired.
if module, changed, _ := w.Take(stdio.StateChange{Key: key("web"), Op: stdio.OpDelete}); module != "web" || !changed {
t.Fatalf("a deleted key was taken as %q, %v", module, changed)
}
k.Due("web")
waitFor(t, func() bool { r := readingOf(readings, "ace.web.uploads"); return r != nil && !r.RetiredAt.IsZero() })
before := measured["/srv/web2"]
if _, _, err := k.Round(context.Background(), false); err != nil {
t.Fatal(err)
}
if measured["/srv/web2"] != before || round().Items != 2 {
t.Errorf("an item whose key was deleted was measured again: %d times, round %+v", measured["/srv/web2"]-before, round())
}
}
func readingOf(s *memStore, key string) *health.Reading {
raw, _ := s.Get(key)
if raw == nil {
return nil
}
r, err := health.DecodeReading(raw)
if err != nil {
return nil
}
return &r
}
func waitFor(t *testing.T, done func() bool) {
t.Helper()
for end := time.Now().Add(5 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
if done() {
return
}
}
t.Fatal("not done within 5 s")
}