160 lines
7.4 KiB
Go
160 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq ADR 0419: a group's order, from the module graph and the manifests' change alone, the same on every
|
|
// reading — what a module stands on before it, the controller before a manifest that needs it, the node-engine
|
|
// before the controller, a verb's server before its caller — and no `after:` line read.
|
|
func TestAGroupIsOrderedByTheGraphAndTheManifestsChange(t *testing.T) {
|
|
members := []orderMember{
|
|
{ID: "cat", Repository: "novox/mesh-catalog"},
|
|
{ID: "ctl", Repository: "novox/mesh-controller"},
|
|
{ID: "host", Repository: "novox/mesh-host"},
|
|
{ID: "base", Repository: "novox/base"},
|
|
{ID: "app", Repository: "novox/app"},
|
|
{ID: "lab", Repository: "novox/lab"},
|
|
}
|
|
reach := map[string]orderReach{
|
|
"cat": {Moved: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}},
|
|
"ctl": {Moved: []string{"mesh-controller"}, Manifests: []string{"module.json"}},
|
|
"host": {Moved: []string{"mesh-host"}, Manifests: []string{"module.json"}},
|
|
"base": {Moved: []string{"base"}},
|
|
"app": {Moved: []string{"app"}},
|
|
"lab": {Moved: []string{"lab"}},
|
|
}
|
|
edges := []inventory.Edge{{From: "app", To: "base", Kind: inventory.EdgeStandsOn},
|
|
{From: "gitea", To: "postgres", Kind: inventory.EdgeDeclared}}
|
|
changes := map[string]moduleChange{"gitea": {Manifest: true}, "mesh-controller": {Manifest: true},
|
|
"mesh-host": {Manifest: true}, "lab": {Serves: []string{"seat.v"}}, "app": {Invokes: []string{"seat.v"}}}
|
|
got := orderOf(members, reach, edges, changes)
|
|
if len(got.Cycle) > 0 {
|
|
t.Fatalf("a cycle where there is none: %v", got.Cycle)
|
|
}
|
|
// By repository among those with nothing before them: app waits for base and lab; gitea's catalogue for the controller.
|
|
want := []string{"base", "lab", "app", "host", "ctl", "cat"}
|
|
if !reflect.DeepEqual(got.Order, want) {
|
|
t.Fatalf("ordered %v, wanted %v; pairs %+v", got.Order, want, got.Pairs)
|
|
}
|
|
why := map[string]string{}
|
|
for _, p := range got.Pairs {
|
|
why[p.Before+">"+p.After] = p.Why
|
|
}
|
|
for pair, reason := range map[string]string{"host>ctl": ruleEngineFirst, "ctl>cat": ruleVersionSkew,
|
|
"ctl>host": "", "base>app": orderBuiltBy, "lab>app": ruleVerbBeforeCaller + ": seat.v"} {
|
|
if why[pair] != reason {
|
|
t.Errorf("%s is ordered %q, wanted %q", pair, why[pair], reason)
|
|
}
|
|
}
|
|
// The same members in another order read the same.
|
|
shuffled := []orderMember{members[5], members[3], members[0], members[4], members[2], members[1]}
|
|
if again := orderOf(shuffled, reach, edges, changes); !reflect.DeepEqual(again.Order, want) {
|
|
t.Fatalf("another reading ordered %v", again.Order)
|
|
}
|
|
}
|
|
|
|
// A cycle is the one refusal, and it names every module and edge. A description's `after:` line changes nothing.
|
|
func TestAGroupInACycleIsNamedAndNothingElseIsRefused(t *testing.T) {
|
|
members := []orderMember{
|
|
{ID: "x", Repository: "novox/x", Number: 7},
|
|
{ID: "y", Repository: "novox/y", Number: 9},
|
|
}
|
|
reach := map[string]orderReach{"x": {Moved: []string{"mx"}}, "y": {Moved: []string{"my"}}}
|
|
changes := map[string]moduleChange{
|
|
"mx": {Serves: []string{"s.a"}, Invokes: []string{"s.b"}},
|
|
"my": {Serves: []string{"s.b"}, Invokes: []string{"s.a"}},
|
|
}
|
|
got := orderOf(members, reach, nil, changes)
|
|
if !reflect.DeepEqual(got.Cycle, []string{"x", "y"}) || len(got.Order) > 0 {
|
|
t.Fatalf("a cycle was ordered: %v, cycle %v", got.Order, got.Cycle)
|
|
}
|
|
for _, w := range []string{"mx", "my", "s.a", "s.b"} {
|
|
if !strings.Contains(got.CycleSaid, w) {
|
|
t.Errorf("the cycle does not say %q: %s", w, got.CycleSaid)
|
|
}
|
|
}
|
|
// Without the edges the same members are ordered by repository.
|
|
got = orderOf(members, reach, nil, nil)
|
|
if !reflect.DeepEqual(got.Order, []string{"x", "y"}) || len(got.Cycle) > 0 || len(got.Pairs) > 0 {
|
|
t.Fatalf("members with no edge: %v %v %+v", got.Order, got.Cycle, got.Pairs)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0239 decision 8: a walk waits for the delivery's owner only while the seat has a holder on
|
|
// record, and never one that moves a module on the controller's own path.
|
|
func TestAWalkWaitsOnlyWhileTheDeliverySeatIsHeldAndNeverForTheCore(t *testing.T) {
|
|
holder := inventory.Entry{Manifest: catalogue.Manifest{Module: "mesh-delivery",
|
|
Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}}, On: []string{"anchor"}}
|
|
unassigned := holder
|
|
unassigned.On = nil
|
|
for _, c := range []struct {
|
|
entries []inventory.Entry
|
|
moved []string
|
|
waits bool
|
|
}{
|
|
{nil, []string{"gitea"}, false},
|
|
{[]inventory.Entry{unassigned}, []string{"gitea"}, false},
|
|
{[]inventory.Entry{holder}, []string{"gitea", "plex"}, true},
|
|
{[]inventory.Entry{holder}, []string{"gitea", "mesh-controller"}, false},
|
|
{[]inventory.Entry{holder}, []string{"mesh-host"}, false},
|
|
{[]inventory.Entry{holder}, []string{"node-tools"}, false},
|
|
{[]inventory.Entry{holder}, []string{"nats"}, false},
|
|
{[]inventory.Entry{holder}, []string{"mesh-delivery", "gitea"}, false},
|
|
} {
|
|
d := awaitsFor(c.entries, c.moved)
|
|
if (d != nil) != c.waits {
|
|
t.Errorf("held %v, moving %v: waits %v, wanted %v", len(c.entries) > 0 && len(c.entries[0].On) > 0,
|
|
c.moved, d != nil, c.waits)
|
|
}
|
|
if d != nil && d.Awaits != catalogue.DeliverySeat {
|
|
t.Errorf("waits for %q", d.Awaits)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0239: with mesh-delivery on record, a merge opens its walk and asks nothing until the
|
|
// delivery's word; the word starts it; the core's own merge never waits; a person starts a waiting walk by
|
|
// hand when the owner is down, and the owner's stop ends one as stopped.
|
|
|
|
// The verbs mesh-delivery asks with become the commands they name, and nothing a caller sends is passed over.
|
|
func TestTheDeliveryVerbsComposeTheirCommands(t *testing.T) {
|
|
for _, c := range []struct {
|
|
verb string
|
|
args map[string]any
|
|
want []string
|
|
}{
|
|
{"deliver", map[string]any{"plan": "plan-1", "why": "its turn"},
|
|
[]string{"delivery", "go", "plan-1", "--by", "mesh-delivery", "--why", "its turn"}},
|
|
{"delivery-stop", map[string]any{"plan": "plan-1", "why": "w", "by": "jochen"},
|
|
[]string{"delivery", "stop", "plan-1", "--why", "w", "--by", "mesh-delivery for jochen"}},
|
|
{"delivery-walks", map[string]any{}, []string{"delivery", "walks"}},
|
|
{"delivery-walks", map[string]any{"plan": "plan-1"}, []string{"delivery", "walks", "--plan", "plan-1"}},
|
|
{"delivery-order", map[string]any{"members": "[]"}, []string{"delivery", "order", "--members", "[]"}},
|
|
{"delivery-check", map[string]any{"group": "feat/x", "members": "[]"},
|
|
[]string{"delivery", "check", "--group", "feat/x", "--members", "[]"}},
|
|
{"delivery-plan", map[string]any{"repository": "novox/a", "paths": "x", "head": "c0"},
|
|
[]string{"delivery", "plan", "--repository", "novox/a", "--paths", "x", "--head", "c0"}},
|
|
{"plans", map[string]any{"go": "plan-1", "why": "down"}, []string{"plans", "go", "plan-1", "--why", "down"}},
|
|
} {
|
|
got, err := argvFor(c.verb, c.args)
|
|
if err != nil || !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("%s %v → %v %v, wanted %v", c.verb, c.args, got, err, c.want)
|
|
}
|
|
}
|
|
if _, err := argvFor("deliver", map[string]any{}); err == nil {
|
|
t.Error("deliver without a walk was composed")
|
|
}
|
|
if _, err := argvFor("delivery-stop", map[string]any{"plan": "p"}); err == nil {
|
|
t.Error("a stop without why was composed")
|
|
}
|
|
}
|
|
|
|
// A verb runs as a command of its own: what it keeps of a walk is still said as plan-moved, on a bus of the
|
|
// command's own, so the delivery's owner hears it at once and not only when it reads the walks back.
|