package main import ( "context" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/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) } }