Files
mesh-controller/cmd/mesh-controller/delivery_edges_test.go
T

213 lines
10 KiB
Go

package main
import (
"context"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// The order of a plan's modules is read from the module graph and the manifests' change alone (novox/hq ADR 0419):
// no `after:` line, no member of a group. Pure, like orderOf was.
func hasEdge(edges []inventory.Edge, from, to string) (inventory.Edge, bool) {
for _, e := range edges {
if e.From == from && e.To == to {
return e, true
}
}
return inventory.Edge{}, false
}
// A module whose change adds a verb to a seat goes before the module whose change adds a call of it: the caller
// depends on the server, the plan puts them in two tiers, and the edge says why.
func TestAVerbIsServedBeforeItsCallerIsSent(t *testing.T) {
changes := map[string]moduleChange{
"server": {Serves: []string{"mesh-delivery.groups"}, Manifest: true},
"caller": {Invokes: []string{"mesh-delivery.groups"}, Manifest: true},
}
got := orderEdges([]string{"caller", "server"}, changes, nil)
e, ok := hasEdge(got.Edges, "caller", "server")
if !ok || !strings.Contains(e.Why, "verb before caller") || !strings.Contains(e.Why, "mesh-delivery.groups") {
t.Fatalf("the caller does not follow the server by the verb: %+v", got.Edges)
}
if len(got.Cycle) != 0 {
t.Fatalf("a cycle: %+v", got.Cycle)
}
if tiers := tiersOf([]string{"caller", "server"}, got.Edges); len(tiers) != 2 || tiers[0][0] != "server" {
t.Fatalf("tiers %v, want the server first", tiers)
}
}
// Edges of one rule both ways are a cycle: every module and edge is named.
func TestEdgesBothWaysAreACycleNamingEveryModuleAndEdge(t *testing.T) {
changes := map[string]moduleChange{
"x": {Serves: []string{"s.a"}, Invokes: []string{"s.b"}},
"y": {Serves: []string{"s.b"}, Invokes: []string{"s.a"}},
}
got := orderEdges([]string{"x", "y"}, changes, nil)
if len(got.Cycle) < 2 {
t.Fatalf("no cycle found: %+v", got)
}
said := got.CycleSaid()
for _, want := range []string{"x", "y", "s.a", "s.b"} {
if !strings.Contains(said, want) {
t.Fatalf("the cycle is said as %q, without %s", said, want)
}
}
}
// The node-engine goes before the controller by default, and says so; an edge of the manifests the other way
// overrules it, and the edge that did is said.
func TestTheEngineGoesBeforeTheControllerUnlessAnEdgeSaysOtherwise(t *testing.T) {
got := orderEdges([]string{"mesh-controller", "mesh-host"}, nil, nil)
e, ok := hasEdge(got.Edges, "mesh-controller", "mesh-host")
if !ok || e.Why != "engine before controller" {
t.Fatalf("the engine does not go first: %+v", got.Edges)
}
changes := map[string]moduleChange{
"mesh-host": {Serves: []string{"s.v"}},
"mesh-controller": {Invokes: []string{"s.v"}},
}
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil)
if _, ok := hasEdge(got.Edges, "mesh-controller", "mesh-host"); !ok || len(got.Edges) != 1 ||
!strings.Contains(got.Edges[0].Why, "verb before caller") {
t.Fatalf("the verb edge does not place the controller after the engine: %+v", got.Edges)
}
// The other way: the engine calls what the controller brings.
changes = map[string]moduleChange{
"mesh-controller": {Serves: []string{"s.v"}},
"mesh-host": {Invokes: []string{"s.v"}},
}
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil)
e, ok = hasEdge(got.Edges, "mesh-host", "mesh-controller")
if !ok || len(got.Edges) != 1 || !strings.Contains(e.Why, "overrules engine before controller") {
t.Fatalf("the verb edge does not say it overruled the default: %+v", got.Edges)
}
}
// The controller goes before every module whose manifest the plan changes (version skew), the node-engine excepted.
func TestTheControllerGoesBeforeEveryChangedManifest(t *testing.T) {
changes := map[string]moduleChange{"app": {Manifest: true}, "other": {}}
got := orderEdges([]string{"mesh-controller", "app", "other"}, changes, nil)
if e, ok := hasEdge(got.Edges, "app", "mesh-controller"); !ok || e.Why != "version skew" {
t.Fatalf("the changed manifest does not follow the controller: %+v", got.Edges)
}
if _, ok := hasEdge(got.Edges, "other", "mesh-controller"); ok {
t.Fatalf("a module with an unchanged manifest was ordered: %+v", got.Edges)
}
if got := orderEdges([]string{"app", "other"}, changes, nil); len(got.Edges) != 0 {
t.Fatalf("version skew ordered a plan that does not move the controller: %+v", got.Edges)
}
}
// The order is per module, not per repository: of two modules of one repository only the one with the edge moves.
func TestTheOrderIsPerModuleNotPerRepository(t *testing.T) {
changes := map[string]moduleChange{
"a": {Serves: []string{"s.v"}},
"c": {Invokes: []string{"s.v"}},
}
got := orderEdges([]string{"a", "b", "c"}, changes, nil)
tiers := tiersOf([]string{"a", "b", "c"}, got.Edges)
if len(tiers) != 2 || strings.Join(tiers[0], ",") != "a,b" || strings.Join(tiers[1], ",") != "c" {
t.Fatalf("tiers %v, want [a b] then [c]", tiers)
}
}
// The edges a plan's order was drawn with are kept on the plan, and its gates read them: a later tier waits for the
// earlier to be applied although the catalogue's graph holds no such edge (ADR 0419, the lead's settlement Q4).
func TestAPlansOwnOrderGatesItsTiersWhateverTheStoreGraphHolds(t *testing.T) {
p := inventory.Plan{Tiers: [][]string{{"server"}, {"caller"}}, Tier: 0,
Delivery: &inventory.PlanDelivery{Order: []inventory.Edge{{From: "caller", To: "server", Kind: edgeChangeOrder,
Why: ruleVerbBeforeCaller + ": s.v"}}}}
if got := gates(p, nil, func(string) bool { return true }); len(got) != 1 || got[0] != "server" {
t.Fatalf("the caller's tier waits for %v; want the server", got)
}
p.Delivery.Order = nil
if got := gates(p, nil, func(string) bool { return true }); len(got) != 0 {
t.Fatalf("a plan with no order waits for %v", got)
}
}
// A verb an earlier merge of the same repository in the batch added is added by the batch: the change is compared with
// what the repository held before the batch, not with the first parent of its latest merge.
func TestAVerbAnEarlierMergeOfTheBatchAddedDrawsItsEdge(t *testing.T) {
was, wasParent := manifestAt, parentOf
t.Cleanup(func() { manifestAt, parentOf = was, wasParent })
manifestsReadMu.Lock()
clear(manifestsRead)
manifestsReadMu.Unlock()
const first, second, before = "1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222", "0000000000000000000000000000000000000000"
parentOf = func(_ context.Context, _, _, commit string) (string, error) {
if commit == first {
return before, nil
}
return first, nil // the latest merge's parent holds the verb already
}
manifestAt = func(_ context.Context, _, _, _, ref string) (catalogue.Manifest, bool, error) {
m := catalogue.Manifest{Module: "server"}
if ref != before {
m.DefinesSeats = []catalogue.SeatDeclaration{{Name: "s", Serves: []catalogue.Verb{{Name: "v"}}}}
}
return m, true, nil
}
src := changeSource{Owner: "novox", Repo: "r", New: second, Paths: []string{"module.json"}}
if got, err := changesOf(context.Background(), []changeSource{src}); err != nil || len(got["server"].Serves) != 0 {
t.Fatalf("against the first parent of the latest merge: %+v %v", got, err)
}
src.Since = first
got, err := changesOf(context.Background(), []changeSource{src})
if err != nil || len(got["server"].Serves) != 1 || got["server"].Serves[0] != "s.v" {
t.Fatalf("against what the batch found: %+v %v", got, err)
}
}
// A change that names only a beginning of its files is read as changing the manifests of the modules it moves.
func TestATruncatedChangeIsReadAsChangingTheManifestsOfWhatItMoves(t *testing.T) {
entries := []inventory.Entry{
{Manifest: catalogue.Manifest{Module: "a"}, Source: inventory.Source{Repository: "novox/mesh-catalog", Path: "modules/a"}},
{Manifest: catalogue.Manifest{Module: "b"}, Source: inventory.Source{Repository: "novox/mesh-catalog", Path: "modules/b"}},
{Manifest: catalogue.Manifest{Module: "c"}, Source: inventory.Source{Repository: "novox/other", Path: "modules/c"}},
}
got := truncatedPaths(entries, "novox", "mesh-catalog", []string{"a", "c"})
if len(got) != 1 || got[0] != "modules/a/module.json" {
t.Fatalf("read as changed: %v", got)
}
members := []orderMember{{ID: "m", Repository: "novox/mesh-catalog", Head: "h", PathsTruncated: true, Paths: []string{"x"}}}
src := changeSourcesOf(members, entries, map[string]orderReach{"m": {Moved: []string{"a"}}})
if len(src) != 1 || src[0].Truncated || len(src[0].Paths) != 1 || src[0].Paths[0] != "modules/a/module.json" {
t.Fatalf("the member's change is read as %+v", src)
}
}
// A lone pull request's cycle fails its check, naming every module and edge, with no group and no members.
func TestALonePullRequestInACycleFailsWithNoGroup(t *testing.T) {
was, wasParent := manifestAt, parentOf
t.Cleanup(func() { manifestAt, parentOf = was, wasParent })
manifestsReadMu.Lock()
clear(manifestsRead)
manifestsReadMu.Unlock()
const head = "3333333333333333333333333333333333333333"
parentOf = func(context.Context, string, string, string) (string, error) { return "", nil }
manifestAt = func(_ context.Context, _, _, file, ref string) (catalogue.Manifest, bool, error) {
if ref == "main" {
return catalogue.Manifest{}, false, nil
}
if file == "modules/x/module.json" {
return catalogue.Manifest{Module: "mx", DefinesSeats: []catalogue.SeatDeclaration{{Name: "s", Serves: []catalogue.Verb{{Name: "a"}}}},
Invokes: []string{"seat:s.b"}}, true, nil
}
return catalogue.Manifest{Module: "my", DefinesSeats: []catalogue.SeatDeclaration{{Name: "s", Serves: []catalogue.Verb{{Name: "b"}}}},
Invokes: []string{"seat:s.a"}}, true, nil
}
m := orderMember{ID: "novox/r@" + head[:12], Repository: "novox/r", Base: "main", Head: head, Number: 5,
Paths: []string{"modules/x/module.json", "modules/y/module.json"}}
c, cyclic, err := cycleVerdict(context.Background(), "", []orderMember{m}, map[string]orderReach{m.ID: {Moved: []string{"mx", "my"}}}, nil, nil)
if err != nil || !cyclic || c.Verdict != "fail" || c.Group != "" || len(c.Members) != 0 || !strings.Contains(c.Summary, "s.a") ||
!strings.Contains(c.Summary, "my") {
t.Fatalf("the verdict is %+v (cyclic %v, %v)", c, cyclic, err)
}
}