213 lines
10 KiB
Go
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)
|
|
}
|
|
}
|