187 lines
11 KiB
Go
187 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"mesh-delivery-view/internal/view"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
|
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
|
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
|
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
|
)
|
|
|
|
// encoded is v as its contract's encoder writes it, failing the test when it refuses.
|
|
func encoded(t *testing.T, v []byte, err error) string {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(v)
|
|
}
|
|
|
|
// short is a fingerprint as the assigner's words shorten it.
|
|
func short(fp string) string { return fp[:len("src2:")+12] }
|
|
|
|
// tiersToolSaysEachModulesWait: view_tiers orders a source revision's modules by the module graph and names each one's
|
|
// wait (ADR 0603 §6): a consumer after its provider, held by the assigner's provision rule and its provision not provided;
|
|
// a module whose build waits for a package; a provider with no wait; a node offline named away; older modules left out.
|
|
func tiersToolSaysEachModulesWait(t *testing.T) {
|
|
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
|
|
manifest := strings.Repeat("d", 64)
|
|
mod := func(f string, src uint64) string {
|
|
b, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: f, Manifest: manifest, Src: int64(src), FpSrc: int64(src), Repository: "novox/mesh-catalog", Commit: "c"})
|
|
return encoded(t, b, err)
|
|
}
|
|
node := func(g graph.Node) string {
|
|
b, err := graph.Encode(g)
|
|
return encoded(t, b, err)
|
|
}
|
|
build := func(b builds.BuilderBuild) string {
|
|
v, err := builds.EncodeBuilderBuild(b)
|
|
return encoded(t, v, err)
|
|
}
|
|
want := func(w meshwantsclient.Want) string {
|
|
v, err := meshwantsclient.EncodeWant(w)
|
|
return encoded(t, v, err)
|
|
}
|
|
have := func(h nodeengine.HaveByNodeModuleValue) string {
|
|
v, err := nodeengineclient.EncodeHave(h)
|
|
return encoded(t, v, err)
|
|
}
|
|
key := func(k string, err error) string {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return k
|
|
}
|
|
since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC)
|
|
r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader()
|
|
r.GraphState = fake{
|
|
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
|
|
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}),
|
|
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
|
|
}.reader().State
|
|
r.Builds = fake{
|
|
key(builds.BuilderBuildKey("db", fp("a"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
|
|
key(builds.BuilderBuildKey("web", fp("b"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
|
|
key(builds.BuilderBuildKey("lib", fp("c"))): build(builds.BuilderBuild{State: builds.BuildQueued, Waits: "@novox/x@1.2.0"}),
|
|
}.reader().State
|
|
r.WantsState = fake{
|
|
key(meshwantsclient.WantKey("ace", "db")): want(meshwantsclient.Want{Fp: fp("a"), Self: true}),
|
|
key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
|
|
key(meshwantsclient.WantKey("g14", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleCanary, Words: "the canary"}),
|
|
}.reader().State
|
|
r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil }
|
|
r.HavesState = fake{key(nodeengineclient.HaveKey("ace", "db")): have(nodeengine.HaveByNodeModuleValue{Fp: fp("a"), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})}.reader().State
|
|
p, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: "database", Consumer: meshprovisionerclient.End{Module: "web", Node: "ace"},
|
|
Provider: meshprovisionerclient.End{Module: "db", Node: "ace"}, State: meshprovisionerclient.Asked, Why: "the build db runs does not offer it", At: since})
|
|
r.ProvisionsState = fake{key(meshprovisionerclient.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State
|
|
off, err := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: false})
|
|
r.PresenceState = fake{key(meshwatchdogsclient.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State
|
|
|
|
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
|
|
got, _ := out.(view.Tiers)
|
|
if err != nil || got.Revision != 9 || got.Modules != 3 || len(got.Tiers) != 2 || !reflect.DeepEqual(got.Away, []string{"g14"}) || got.Graph != "" {
|
|
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
|
|
}
|
|
first := fmt.Sprint(got.Tiers[0])
|
|
if len(got.Tiers[0]) != 2 || got.Tiers[0][0].Module != "db" || len(got.Tiers[0][0].Waits) != 0 || got.Tiers[0][1].Module != "lib" ||
|
|
len(got.Tiers[0][1].Waits) != 1 || !strings.Contains(got.Tiers[0][1].Waits[0].Words, "waiting for the package @novox/x@1.2.0") {
|
|
t.Errorf("the first tier = %s, want db with no wait and lib's build waiting for its package", first)
|
|
}
|
|
web := got.Tiers[1][0]
|
|
if web.Module != "web" || !reflect.DeepEqual(web.After, []string{"db"}) || len(web.Waits) != 2 ||
|
|
web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != meshwantsclient.RuleProvisions || !web.Waits[0].Since.Equal(since) ||
|
|
web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") {
|
|
t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web)
|
|
}
|
|
// ADR 0633 §5 (1585/T24): a module the indexer holds back, read through indexer_pending, waits on the edge take-in from the
|
|
// wait's since; a held module the revision does not move is named in the first tier with that wait alone, a group's wait
|
|
// says its group, and a tool that cannot be asked is said and names no module.
|
|
heldSince := time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC)
|
|
r.Pending = func() (takein.PendingAnswer, error) {
|
|
return takein.PendingAnswer{Waits: []takein.PendingWait{
|
|
{Key: "m.00000000000000000042", What: takein.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}},
|
|
{Key: "g.feat", What: takein.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil
|
|
}
|
|
out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
|
|
held, _ := out.(view.Tiers)
|
|
if err != nil || len(held.Tiers) != 2 || held.Pending != "" {
|
|
t.Fatalf("view_tiers with waits = %+v, %v", out, err)
|
|
}
|
|
web = held.Tiers[1][0]
|
|
if last := web.Waits[len(web.Waits)-1]; last.Edge != view.TakeInEdge || !last.Since.Equal(heldSince) || !strings.Contains(last.Words, "src2:abc") {
|
|
t.Errorf("web = %+v, want its last wait on the edge take-in since %s", web, heldSince)
|
|
}
|
|
if lib := held.Tiers[0][1]; lib.Module != "lib" || lib.Waits[len(lib.Waits)-1].Edge != view.TakeInEdge || !strings.Contains(lib.Waits[len(lib.Waits)-1].Words, "group feat") {
|
|
t.Errorf("lib = %+v, want its last wait the group's", lib)
|
|
}
|
|
if nm := held.Tiers[0][len(held.Tiers[0])-1]; nm.Module != "newmod" || len(nm.Waits) != 1 || nm.Waits[0].Edge != view.TakeInEdge {
|
|
t.Errorf("the first tier = %+v, want newmod, which the revision does not move, named with its take-in wait alone", held.Tiers[0])
|
|
}
|
|
r.Pending = func() (takein.PendingAnswer, error) { return takein.PendingAnswer{}, errors.New("no answer") }
|
|
if out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || !strings.Contains(out.(view.Tiers).Pending, "no answer") ||
|
|
strings.Contains(fmt.Sprint(out.(view.Tiers).Tiers), view.TakeInEdge) {
|
|
t.Errorf("view_tiers with no answer of indexer_pending = %+v, %v", out, err)
|
|
}
|
|
r.Pending = nil
|
|
// No revision named: the gate's newest, its source key's revision.
|
|
r.SourceState = view.StateRead{GetEntry: func(string) (json.RawMessage, uint64, error) { return json.RawMessage(`{}`), 9, nil }}
|
|
if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 {
|
|
t.Errorf("view_tiers of the gate's newest = %+v, %v", out, err)
|
|
}
|
|
// ADR 0614 §2-§5: a want held on a verb, a reader capability or bus state is named by its edge; bus state is no
|
|
// provisioner key (none is ever made for it), so a module asking it shows no provision wait of it; and each verb edge
|
|
// the graph leaves out on a cycle is named.
|
|
r.GraphState = fake{
|
|
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
|
|
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database", graph.BusState}, Awaits: []string{"database", graph.BusState},
|
|
Invokes: []string{"seat:x.v", "db.loop"}, LeftOut: []string{"db.loop"}}),
|
|
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
|
|
}.reader().State
|
|
for words, edge := range map[string]string{
|
|
graph.VerbWaitWords("seat:x.v", "web", "ace"): "verb seat:x.v",
|
|
graph.ReaderWaitWords("ace", []string{"tool-runner"}, "web", short(fp("b"))): "reader capability tool-runner",
|
|
"the provision ace.web.database of web on ace is asked, not provided": "want",
|
|
} {
|
|
r.WantsState = fake{key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: words})}.reader().State
|
|
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
|
|
got, _ := out.(view.Tiers)
|
|
if err != nil || len(got.Tiers) != 2 {
|
|
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
|
|
}
|
|
web := got.Tiers[1][0]
|
|
if len(web.Waits) != 2 || web.Waits[0].Edge != edge || web.Waits[1].Edge != "provision database" ||
|
|
!reflect.DeepEqual(web.LeftOut, []string{"db.loop"}) {
|
|
t.Errorf("web held by %q = %+v, want its want named %s, no wait for a bus-state key and db.loop left out", words, web, edge)
|
|
}
|
|
}
|
|
// The graph not readable: every module in the first tier, said; past 500 modules the answer says it is cut short.
|
|
r.GraphState = view.StateRead{Keys: func() ([]string, error) { return nil, errors.New("no bucket") }, Get: r.GraphState.Get}
|
|
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || len(out.(view.Tiers).Tiers) != 1 ||
|
|
!strings.Contains(out.(view.Tiers).Graph, "cannot be read") {
|
|
t.Errorf("view_tiers with no graph = %+v, %v", out, err)
|
|
}
|
|
many := fake{}
|
|
for i := 0; i < view.TiersMost+2; i++ {
|
|
many[fmt.Sprintf("m%03d", i)] = mod(fp("a"), 9)
|
|
}
|
|
r.State = many.reader().State
|
|
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": "9"}); err != nil || out.(view.Tiers).Modules != view.TiersMost+2 ||
|
|
!strings.Contains(out.(view.Tiers).Cut, "the first 500") {
|
|
t.Errorf("view_tiers past its bound = %+v, %v", out.(view.Tiers).Cut, err)
|
|
}
|
|
}
|