517 lines
23 KiB
Go
517 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"mesh-delivery-view/internal/view"
|
|
)
|
|
|
|
type fake map[string]string
|
|
|
|
func (f fake) reader() view.Reader {
|
|
return view.Reader{State: view.StateRead{
|
|
Keys: func() ([]string, error) {
|
|
var k []string
|
|
for key := range f {
|
|
k = append(k, key)
|
|
}
|
|
return k, nil
|
|
},
|
|
Get: func(key string) (json.RawMessage, error) {
|
|
if v, ok := f[key]; ok {
|
|
return json.RawMessage(v), nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
}}
|
|
}
|
|
|
|
func run(t *testing.T, r view.Reader, name string, args map[string]any) (any, error) {
|
|
t.Helper()
|
|
for _, tl := range Tools(r, view.UsageReader{}) {
|
|
if tl.Name == name {
|
|
return tl.Run(args)
|
|
}
|
|
}
|
|
t.Fatalf("no tool %s", name)
|
|
return nil, nil
|
|
}
|
|
|
|
const (
|
|
withFP2 = `{"v":3,"fp":"a1","fp2":"b2","manifest":"m1","inputs2":{"tree":"t1","bases":["x"]},"repository":"novox/r","commit":"c1","path":".","toolchains":{"go":{"version":"go1.27.1","recipe":"2"}}}`
|
|
withNone = `{"v":3,"none":"no successful trunk build recorded its inputs","none2":"builder's fingerprint not yet known","repository":"novox/r","commit":"c2","path":"modules/b"}`
|
|
)
|
|
|
|
// 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there.
|
|
func TestModuleToolAnswersTheStoredValue(t *testing.T) {
|
|
r := fake{"a": withFP2, "b": withNone}.reader()
|
|
for key, want := range map[string]string{"a": withFP2, "b": withNone} {
|
|
out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := string(out.(json.RawMessage)); got != want {
|
|
t.Errorf("%s = %s, want %s", key, got, want)
|
|
}
|
|
}
|
|
out, _ := run(t, r, view.ModuleReadTool, map[string]any{"module": "a"})
|
|
var m map[string]any
|
|
if err := json.Unmarshal(out.(json.RawMessage), &m); err != nil || m["fp"] != "a1" || m["fp2"] != "b2" || m["path"] != "." || m["inputs2"] == nil || m["toolchains"] == nil {
|
|
t.Errorf("stored fields are not all there: %v %v", m, err)
|
|
}
|
|
// A module with no key, and a name that is no key, are said so.
|
|
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") {
|
|
t.Errorf("a missing module: %v", err)
|
|
}
|
|
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": ""}); err == nil {
|
|
t.Errorf("an empty name was answered")
|
|
}
|
|
// 1280 (ADR 0556): view_reach answers the view's reach of one repository as stored, or of every one, and says a
|
|
// repository it holds none of.
|
|
const reach = `{"v":1,"repository":"novox/web","reached":"c1","reached_src":12}`
|
|
r.ReachState = fake{"novox/web": reach}.reader().State
|
|
if out, err := run(t, r, view.ReachReadTool, map[string]any{"repository": "novox/Web"}); err != nil || string(out.(json.RawMessage)) != reach {
|
|
t.Errorf("view_reach novox/Web = %v, %v", out, err)
|
|
}
|
|
if out, err := run(t, r, view.ReachReadTool, map[string]any{}); err != nil || !strings.Contains(fmt.Sprint(out), "novox/web") {
|
|
t.Errorf("view_reach of every repository = %v, %v", out, err)
|
|
}
|
|
if _, err := run(t, r, view.ReachReadTool, map[string]any{"repository": "novox/gone"}); err == nil || !strings.Contains(err.Error(), "no reach of novox/gone") {
|
|
t.Errorf("a repository with no reach: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1562/AC1: indexer_modules answers every key as stored, and with none2 only those that say why they have no FP2.
|
|
func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
|
|
r := fake{"a": withFP2, "b": withNone}.reader()
|
|
out, err := run(t, r, view.ModulesReadTool, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all := out.(map[string]any)["modules"].(map[string]json.RawMessage)
|
|
if len(all) != 2 || string(all["a"]) != withFP2 || string(all["b"]) != withNone {
|
|
t.Errorf("modules = %v", all)
|
|
}
|
|
out, err = run(t, r, view.ModulesReadTool, map[string]any{"none2": true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
only := out.(map[string]any)["modules"].(map[string]json.RawMessage)
|
|
if len(only) != 1 || string(only["b"]) != withNone {
|
|
t.Errorf("none2 modules = %v", only)
|
|
}
|
|
|
|
// 1126 (ADR 0549 §2): indexer_placement answers a node's key as stored, every node placing a module, or every node's,
|
|
// and lists a key that does not read.
|
|
const ace, g14 = `{"v":1,"node":"ace","revision":"m1","modules":[{"module":"plex"}]}`, `{"v":1,"node":"g14","revision":"m1","modules":[{"module":"zsh"},{"module":"plex"}]}`
|
|
p := view.Reader{PlacementState: fake{"placement.ace": ace, "placement.g14": g14, "placement.bad": `{"v":1}`, "other": "{}"}.reader().State}
|
|
got := func(args map[string]any) map[string]json.RawMessage {
|
|
t.Helper()
|
|
out, err := run(t, p, view.PlacementReadTool, args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.(map[string]any)["placement"].(map[string]json.RawMessage)
|
|
}
|
|
if one := got(map[string]any{"node": "ace"}); len(one) != 1 || string(one["ace"]) != ace {
|
|
t.Errorf("node ace = %v", one)
|
|
}
|
|
if zsh := got(map[string]any{"module": "zsh"}); len(zsh) != 1 || string(zsh["g14"]) != g14 {
|
|
t.Errorf("module zsh = %v", zsh)
|
|
}
|
|
out, err = run(t, p, view.PlacementReadTool, map[string]any{})
|
|
if all := out.(map[string]any); err != nil || len(all["placement"].(map[string]json.RawMessage)) != 2 || len(all["unreadable"].([]string)) != 2 {
|
|
t.Errorf("every node = %v, %v", all, err)
|
|
}
|
|
if _, err := run(t, p, view.PlacementReadTool, map[string]any{"node": "novox"}); err == nil || !strings.Contains(err.Error(), "no placement of novox") {
|
|
t.Errorf("a node with no key: %v", err)
|
|
}
|
|
if c := out.(map[string]any)["candidates"].([]view.CandidateRow); len(c) != 0 {
|
|
t.Errorf("with no candidate open: %v", c)
|
|
}
|
|
|
|
// ADR 0559 §7 (issue 1126): beside the trunk's, each open candidate key and the node's answer from its <node>.node:
|
|
// answered at the key's head and revision, at an earlier head (stale), or not at all (none); a node the trunk does not
|
|
// place yet is shown by its candidate alone; ace, placed on the trunk with its file unchanged (no key of its own), is
|
|
// shown with its answer, counted by the head alone.
|
|
const h1, h2 = "1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222"
|
|
cand := func(node, head string, placed bool) string {
|
|
pl := `,"placement":{"v":1,"node":"` + node + `","modules":[{"module":"plex"}]}`
|
|
if !placed {
|
|
pl = ""
|
|
}
|
|
return `{"v":1,"group":"feat/p","pull":{"repository":"novox/mesh-catalog","number":9,"head":"` + head + `"}` + pl + `}`
|
|
}
|
|
cands := fake{"feat/p.g14": cand("g14", h2, false), "feat/p.novox": cand("novox", h2, true)}
|
|
revs := map[string]uint64{"feat/p.g14": 5, "feat/p.novox": 6}
|
|
p.PlacementCandidatesState = view.StateRead{Keys: cands.reader().State.Keys, GetEntry: func(key string) (json.RawMessage, uint64, error) {
|
|
raw, _ := cands.reader().State.Get(key)
|
|
return raw, revs[key], nil
|
|
}}
|
|
p.HavesState = fake{
|
|
"ace.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h2 + `","verdict":"same","same":["plex"]}}}`,
|
|
"g14.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h1 + `","revision":2,"verdict":"same"}}}`,
|
|
}.reader().State
|
|
out, err = run(t, p, view.PlacementReadTool, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
answered := map[string]string{}
|
|
for _, c := range out.(map[string]any)["candidates"].([]view.CandidateRow) {
|
|
answered[c.Node] = c.Answered
|
|
}
|
|
if want := map[string]string{"ace": "same", "g14": view.AnswerStale, "novox": view.AnswerNone}; fmt.Sprint(answered) != fmt.Sprint(want) {
|
|
t.Errorf("answers %v, want %v", answered, want)
|
|
}
|
|
out, err = run(t, p, view.PlacementReadTool, map[string]any{"node": "novox"})
|
|
if c := out.(map[string]any)["candidates"].([]view.CandidateRow); err != nil || len(c) != 1 || c[0].Pull.Head != h2 || len(out.(map[string]any)["placement"].(map[string]json.RawMessage)) != 0 {
|
|
t.Errorf("a node placed by its candidate alone: %v, %v", out, err)
|
|
}
|
|
out, _ = run(t, p, view.PlacementReadTool, map[string]any{"module": "plex"})
|
|
if c := out.(map[string]any)["candidates"].([]view.CandidateRow); len(c) != 2 {
|
|
t.Errorf("the candidates placing plex: %v (g14's removes its file)", c)
|
|
}
|
|
}
|
|
|
|
const (
|
|
fp2A = "src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
fp2B = "src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
keyA = "netcheck.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
keyB = "zsh.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
doneBuild = `{"v":1,"state":"done","by":"novox.mesh-delivery-builder","step":"built on shanks, ace","manifest":"m","artifacts":[{"name":"program","kind":"bundle","digest":"sha256:d1"}]}`
|
|
failBuild = `{"v":1,"state":"failed","by":"novox.mesh-delivery-builder","error":"refused kind: package"}`
|
|
)
|
|
|
|
func both(builds, modules fake) view.Reader {
|
|
r := modules.reader()
|
|
r.Builds = builds.reader().State
|
|
return r
|
|
}
|
|
|
|
// 1723/AC1: builder_builds lists every key as stored (module, fingerprint, state, digests, error), and narrows by module and state.
|
|
func TestBuilderBuildsListsKeysAsStored(t *testing.T) {
|
|
r := both(fake{keyA: doneBuild, keyB: failBuild, "junk": "{}"}, fake{})
|
|
out, err := run(t, r, view.BuilderBuildsReadTool, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows := out.(view.BuildsAnswer).Builds
|
|
if u := out.(view.BuildsAnswer).Unreadable; len(u) != 1 || u[0] != "junk" {
|
|
t.Errorf("unreadable = %v, want [junk] (issue 1818)", u)
|
|
}
|
|
if len(rows) != 2 || rows[0].Key != keyA || rows[0].Module != "netcheck" || rows[0].Fingerprint != fp2A || string(rows[0].Build) != doneBuild || string(rows[1].Build) != failBuild {
|
|
t.Errorf("rows = %+v", rows)
|
|
}
|
|
for args, want := range map[string]string{"state": keyB, "module": keyA} {
|
|
out, _ := run(t, r, view.BuilderBuildsReadTool, map[string]any{args: map[string]string{keyB: "failed", keyA: "netcheck"}[want]})
|
|
got := out.(view.BuildsAnswer).Builds
|
|
if len(got) != 1 || got[0].Key != want {
|
|
t.Errorf("%s filter = %+v, want %s", args, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1723/AC2: builder_build answers one key by module and fp2, and by module alone through the indexer's fp2 (joined at its commit).
|
|
func TestBuilderBuildJoinsTheIndexersFingerprint(t *testing.T) {
|
|
idx := `{"v":3,"fp2":"` + fp2A + `","commit":"c1","path":"."}`
|
|
r := both(fake{keyA: doneBuild}, fake{"netcheck": idx, "zsh": `{"v":3,"none2":"not known","commit":"c9"}`})
|
|
for _, args := range []map[string]any{{"module": "netcheck", "fp2": fp2A}, {"module": "netcheck"}, {"module": "netcheck", "commit": "c1"}} {
|
|
out, err := run(t, r, view.BuilderBuildReadTool, args)
|
|
if err != nil {
|
|
t.Fatalf("%v: %v", args, err)
|
|
}
|
|
if row := out.(view.BuilderRow); row.Key != keyA || string(row.Build) != doneBuild {
|
|
t.Errorf("%v = %+v", args, row)
|
|
}
|
|
}
|
|
for args, want := range map[string]string{"netcheck/c2": "not c2", "zsh": "not known", "gone": "no module gone"} {
|
|
a := map[string]any{"module": strings.Split(args, "/")[0]}
|
|
if strings.Contains(args, "/") {
|
|
a["commit"] = "c2"
|
|
}
|
|
if _, err := run(t, r, view.BuilderBuildReadTool, a); err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("%v: err = %v, want %q", a, err, want)
|
|
}
|
|
}
|
|
if _, err := run(t, r, view.BuilderBuildReadTool, map[string]any{"module": "netcheck", "fp2": fp2B}); err == nil || !strings.Contains(err.Error(), "holds no build") {
|
|
t.Errorf("an unbuilt fingerprint: %v", err)
|
|
}
|
|
}
|
|
|
|
// bigBuilds is a bucket of n keys over 100 modules that counts the values read.
|
|
type bigBuilds struct {
|
|
fake fake
|
|
reads *atomic.Int64
|
|
}
|
|
|
|
func newBig(n int) bigBuilds {
|
|
f := fake{}
|
|
for i := 0; i < n; i++ {
|
|
f[fmt.Sprintf("mod%02d.%064x", i%100, i)] = doneBuild
|
|
}
|
|
return bigBuilds{fake: f, reads: new(atomic.Int64)}
|
|
}
|
|
|
|
func (b bigBuilds) reader() view.Reader {
|
|
r := b.fake.reader()
|
|
get := r.State.Get
|
|
r.Builds = view.StateRead{Keys: r.State.Keys, Get: func(k string) (json.RawMessage, error) { b.reads.Add(1); return get(k) },
|
|
GetEntry: func(k string) (json.RawMessage, uint64, error) {
|
|
b.reads.Add(1)
|
|
raw, err := get(k)
|
|
var rev uint64
|
|
fmt.Sscanf(k[strings.Index(k, ".")+1:], "%x", &rev) // the fake's revision is the key's number
|
|
return raw, rev + 1, err
|
|
}}
|
|
return r
|
|
}
|
|
|
|
// 1827/AC1: with 2,000 keys, a module's builds are found by key prefix, so only that module's values are read (20, not 2,000),
|
|
// newest revision first, within the limit, and the answer says it was cut.
|
|
func TestBuilderBuildsFiltersByPrefixBeforeAnyGet(t *testing.T) {
|
|
b := newBig(2000)
|
|
out, err := run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{"module": "mod07", "limit": float64(5)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := out.(view.BuildsAnswer)
|
|
if b.reads.Load() != 20 {
|
|
t.Errorf("read %d values for a module of 20 keys, want 20", b.reads.Load())
|
|
}
|
|
if len(a.Builds) != 5 || !a.Cut || !strings.Contains(a.Note, "5") {
|
|
t.Errorf("answer = %d rows, cut %v, note %q", len(a.Builds), a.Cut, a.Note)
|
|
}
|
|
for i := 1; i < len(a.Builds); i++ {
|
|
if a.Builds[i-1].Revision < a.Builds[i].Revision {
|
|
t.Errorf("not newest first: %d before %d", a.Builds[i-1].Revision, a.Builds[i].Revision)
|
|
}
|
|
}
|
|
if a.Builds[0].Revision != 1908 {
|
|
t.Errorf("newest revision = %d, want 1908", a.Builds[0].Revision)
|
|
}
|
|
}
|
|
|
|
// 1827/AC2: with no module the answer reads at most the limit's worth in key order (default 50), never the 2,000.
|
|
func TestBuilderBuildsWithoutAModuleIsBounded(t *testing.T) {
|
|
b := newBig(2000)
|
|
out, err := run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := out.(view.BuildsAnswer)
|
|
if len(a.Builds) != view.DefaultBuildsLimit || !a.Cut || b.reads.Load() > view.DefaultBuildsLimit {
|
|
t.Errorf("%d rows, cut %v, %d reads", len(a.Builds), a.Cut, b.reads.Load())
|
|
}
|
|
b = newBig(2000)
|
|
out, _ = run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{"state": "failed"})
|
|
if a := out.(view.BuildsAnswer); b.reads.Load() != view.MaxBuildReads || !a.Cut || len(a.Builds) != 0 {
|
|
t.Errorf("a state matching nothing: %d reads (want the bound %d), cut %v", b.reads.Load(), view.MaxBuildReads, a.Cut)
|
|
}
|
|
}
|
|
|
|
// 1818: fp2 together with commit is refused, naming both.
|
|
func TestBuilderBuildRefusesFP2WithCommit(t *testing.T) {
|
|
r := both(fake{keyA: doneBuild}, fake{"netcheck": `{"v":3,"fp2":"` + fp2A + `","commit":"c1","path":"."}`})
|
|
_, err := run(t, r, view.BuilderBuildReadTool, map[string]any{"module": "netcheck", "fp2": fp2A, "commit": "c1"})
|
|
if err == nil || !strings.Contains(err.Error(), "fp2") || !strings.Contains(err.Error(), "commit") || !strings.Contains(err.Error(), "not both") {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
}
|
|
|
|
// 1681: indexer_candidates and indexer_candidate answer the stored value; a pull request's fp2 per touched module shows.
|
|
func TestCandidateToolsAnswerTheStoredValue(t *testing.T) {
|
|
cand := `{"v":1,"group":"feat/x","base":3,"heads":"h","pulls":[{"modules":["zsh"],"fps":{"zsh":{"fp2":"` + fp2B + `","manifest":"m"}}}]}`
|
|
other := `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`
|
|
r := fake{"feat/x": cand, "fix": other, "bad": "{"}.reader()
|
|
r.CandidatesState = r.State
|
|
out, err := run(t, r, view.CandidateReadTool, map[string]any{"group": "feat/x"})
|
|
if err != nil || string(out.(json.RawMessage)) != cand || !strings.Contains(string(out.(json.RawMessage)), fp2B) {
|
|
t.Fatalf("candidate = %v, %v", out, err)
|
|
}
|
|
if _, err := run(t, r, view.CandidateReadTool, map[string]any{"group": "gone"}); err == nil || !strings.Contains(err.Error(), "no candidate gone") {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
out, err = run(t, r, view.CandidatesReadTool, map[string]any{})
|
|
m := out.(map[string]any)
|
|
if err != nil || len(m["candidates"].(map[string]json.RawMessage)) != 2 || len(m["unreadable"].([]string)) != 1 {
|
|
t.Errorf("all = %v, %v", m, err)
|
|
}
|
|
out, _ = run(t, r, view.CandidatesReadTool, map[string]any{"module": "zsh"})
|
|
if c := out.(map[string]any)["candidates"].(map[string]json.RawMessage); len(c) != 1 || c["feat/x"] == nil {
|
|
t.Errorf("by module = %v", c)
|
|
}
|
|
}
|
|
|
|
// 1906/D1 AC1: assigner_wants, assigner_want, assigner_differences and node_haves answer as stored, narrowed by node and module.
|
|
func keptReader() view.Reader {
|
|
st := func(f fake) view.StateRead { return f.reader().State }
|
|
return view.Reader{
|
|
WantsState: st(fake{
|
|
"want.ace.zsh": `{"fp":"` + fp2A + `"}`,
|
|
"want.novox.zsh": `{"fp":"` + fp2B + `"}`,
|
|
"want.ace.netcheck": `{"fp":"` + fp2A + `","rule":3}`,
|
|
"held." + keyB: `{"node":"ace","words":"failed"}`,
|
|
}),
|
|
DifferencesState: st(fake{"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`}),
|
|
HavesState: st(fake{"ace.zsh": `{"fp":"x"}`, "ace.node": `{"facts":1}`, "novox.zsh": `{"fp":"y"}`}),
|
|
}
|
|
}
|
|
|
|
// slowBucket is a bucket of the live shape (2026-10-25: node_haves and assigner_wants timed out at 30 s): keys over 4 nodes
|
|
// and perNode modules each, written by key(node, module), whose Get takes delay and counts the values read, the lists of
|
|
// the bucket and the most Gets in flight at once.
|
|
type slowBucket struct {
|
|
fake fake
|
|
delay time.Duration
|
|
reads, lists atomic.Int64
|
|
inFlight, maxFlight atomic.Int64
|
|
}
|
|
|
|
func newSlow(perNode int, delay time.Duration, key func(node string, i int) string) *slowBucket {
|
|
b := &slowBucket{fake: fake{}, delay: delay}
|
|
for _, n := range []string{"ace", "g14", "novox", "shanks"} {
|
|
for i := 0; i < perNode; i++ {
|
|
b.fake[key(n, i)] = `{"fp":"x"}`
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *slowBucket) state() view.StateRead {
|
|
f := b.fake.reader().State
|
|
return view.StateRead{
|
|
Keys: func() ([]string, error) { b.lists.Add(1); return f.Keys() },
|
|
Get: func(k string) (json.RawMessage, error) {
|
|
b.reads.Add(1)
|
|
now := b.inFlight.Add(1)
|
|
for m := b.maxFlight.Load(); now > m && !b.maxFlight.CompareAndSwap(m, now); m = b.maxFlight.Load() {
|
|
}
|
|
time.Sleep(b.delay)
|
|
b.inFlight.Add(-1)
|
|
return f.Get(k)
|
|
}}
|
|
}
|
|
|
|
func TestAssignerWantsToolAnswersAsStoredAndNarrows(t *testing.T) {
|
|
r := keptReader()
|
|
out, err := run(t, r, view.WantsReadTool, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := out.(view.WantsAnswer)
|
|
if len(a.Wants) != 3 || len(a.Held) != 1 || string(a.Wants["want.ace.zsh"]) != `{"fp":"`+fp2A+`"}` {
|
|
t.Errorf("all = %+v", a)
|
|
}
|
|
out, _ = run(t, r, view.WantsReadTool, map[string]any{"node": "ace"})
|
|
if got := out.(view.WantsAnswer).Wants; len(got) != 2 {
|
|
t.Errorf("node ace = %v", got)
|
|
}
|
|
out, _ = run(t, r, view.WantsReadTool, map[string]any{"module": "zsh"})
|
|
if got := out.(view.WantsAnswer).Wants; len(got) != 2 {
|
|
t.Errorf("module zsh = %v", got)
|
|
}
|
|
|
|
// On the live shape (4 nodes, 150 wants each, 1 ms a Get): a node and a module read that one key and list nothing;
|
|
// a node reads only its own keys; the whole answer reads its values in parallel.
|
|
slow := newSlow(150, time.Millisecond, func(n string, i int) string { return fmt.Sprintf("want.%s.m%03d", n, i) })
|
|
r = view.Reader{WantsState: slow.state()}
|
|
for _, row := range []struct {
|
|
args map[string]any
|
|
wants int
|
|
reads, lists int64
|
|
}{
|
|
{map[string]any{"node": "ace", "module": "m007"}, 1, 1, 0},
|
|
{map[string]any{"node": "ace", "module": "gone"}, 0, 1, 0},
|
|
{map[string]any{"node": "g14"}, 150, 150, 1},
|
|
{map[string]any{"module": "m007"}, 4, 4, 1},
|
|
{map[string]any{}, 600, 600, 1},
|
|
} {
|
|
slow.reads.Store(0)
|
|
slow.lists.Store(0)
|
|
start := time.Now()
|
|
out, err := run(t, r, view.WantsReadTool, row.args)
|
|
took := time.Since(start)
|
|
if err != nil {
|
|
t.Fatalf("%v: %v", row.args, err)
|
|
}
|
|
a := out.(view.WantsAnswer)
|
|
if len(a.Wants) != row.wants || slow.reads.Load() != row.reads || slow.lists.Load() != row.lists || a.Cut {
|
|
t.Errorf("%v: %d wants, %d reads, %d lists, cut %v; want %d, %d, %d", row.args, len(a.Wants), slow.reads.Load(), slow.lists.Load(), a.Cut, row.wants, row.reads, row.lists)
|
|
}
|
|
t.Logf("assigner_wants %v: %d values in %v (one after the other: %v)", row.args, slow.reads.Load(), took, time.Duration(slow.reads.Load())*slow.delay)
|
|
}
|
|
if slow.maxFlight.Load() < 2 {
|
|
t.Errorf("the values were read one at a time (at most %d in flight)", slow.maxFlight.Load())
|
|
}
|
|
}
|
|
|
|
func TestAssignerWantToolAnswersOneAndSaysWhenThereIsNone(t *testing.T) {
|
|
r := keptReader()
|
|
out, err := run(t, r, view.WantReadTool, map[string]any{"node": "ace", "module": "zsh"})
|
|
if err != nil || string(out.(json.RawMessage)) != `{"fp":"`+fp2A+`"}` {
|
|
t.Errorf("want = %v %v", out, err)
|
|
}
|
|
if _, err := run(t, r, view.WantReadTool, map[string]any{"node": "ace", "module": "gone"}); err == nil || !strings.Contains(err.Error(), "no want") {
|
|
t.Errorf("a missing want: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAssignerDifferencesAndNodeHavesToolsAnswerAsStored(t *testing.T) {
|
|
r := keptReader()
|
|
out, err := run(t, r, view.DifferencesReadTool, map[string]any{"node": "novox"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := out.(view.KeptAnswer).Keys; len(got) != 1 || string(got["novox.zsh"]) != `{"rule":3}` {
|
|
t.Errorf("differences = %v", got)
|
|
}
|
|
out, _ = run(t, r, view.HavesReadTool, map[string]any{"module": "zsh"})
|
|
if got := out.(view.KeptAnswer).Keys; len(got) != 2 {
|
|
t.Errorf("haves of zsh = %v", got)
|
|
}
|
|
out, _ = run(t, r, view.HavesReadTool, map[string]any{"node": "ace"})
|
|
if got := out.(view.KeptAnswer).Keys; len(got) != 2 || got["ace.node"] == nil {
|
|
t.Errorf("haves of ace (the node's own key too) = %v", got)
|
|
}
|
|
|
|
// More haves than one call reads (4 nodes, 300 each, 1 ms a Get): a node and a module read that one key and list
|
|
// nothing; the whole answer reads MaxStateReads values in parallel and says it was cut.
|
|
slow := newSlow(300, time.Millisecond, func(n string, i int) string { return fmt.Sprintf("%s.m%03d", n, i) })
|
|
r = view.Reader{HavesState: slow.state(), DifferencesState: slow.state()}
|
|
for _, row := range []struct {
|
|
tool string
|
|
args map[string]any
|
|
keys int
|
|
reads, lists int64
|
|
cut bool
|
|
}{
|
|
{view.HavesReadTool, map[string]any{"node": "shanks", "module": "m042"}, 1, 1, 0, false},
|
|
{view.DifferencesReadTool, map[string]any{"node": "shanks", "module": "m042"}, 1, 1, 0, false},
|
|
{view.HavesReadTool, map[string]any{"node": "ace"}, 300, 300, 1, false},
|
|
{view.HavesReadTool, map[string]any{"module": "m042"}, 4, 4, 1, false},
|
|
{view.HavesReadTool, map[string]any{}, view.MaxStateReads, view.MaxStateReads, 1, true},
|
|
} {
|
|
slow.reads.Store(0)
|
|
slow.lists.Store(0)
|
|
start := time.Now()
|
|
out, err := run(t, r, row.tool, row.args)
|
|
took := time.Since(start)
|
|
if err != nil {
|
|
t.Fatalf("%s %v: %v", row.tool, row.args, err)
|
|
}
|
|
a := out.(view.KeptAnswer)
|
|
if len(a.Keys) != row.keys || slow.reads.Load() != row.reads || slow.lists.Load() != row.lists || a.Cut != row.cut || (a.Cut && !strings.Contains(a.Note, "narrow")) {
|
|
t.Errorf("%s %v: %d keys, %d reads, %d lists, cut %v %q; want %d, %d, %d, %v", row.tool, row.args, len(a.Keys), slow.reads.Load(), slow.lists.Load(), a.Cut, a.Note, row.keys, row.reads, row.lists, row.cut)
|
|
}
|
|
t.Logf("%s %v: %d values in %v (one after the other: %v)", row.tool, row.args, slow.reads.Load(), took, time.Duration(slow.reads.Load())*slow.delay)
|
|
}
|
|
if slow.maxFlight.Load() < 2 {
|
|
t.Errorf("the values were read one at a time (at most %d in flight)", slow.maxFlight.Load())
|
|
}
|
|
}
|