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

178 lines
8.1 KiB
Go

//go:build integration
package main
import (
"context"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// Acceptance 1 of design 47, the controller's part: a feature across the controller and the catalogue with a new seat
// verb is delivered as one group, with no `after:` line — its merges are one batch, each module is built once and sent
// once, its commits are carried by that one plan, and the plan names the edge that placed each tier. The Approve and
// the one-pass merge are mesh-delivery's.
// manifestsHeld stubs the forge for a test: the manifest each (repository, path, commit) holds, and none anywhere else.
func manifestsHeld(t *testing.T, held map[[3]string]catalogue.Manifest) {
t.Helper()
was, wasParent := manifestAt, parentOf
t.Cleanup(func() { manifestAt, parentOf = was, wasParent })
manifestsReadMu.Lock()
clear(manifestsRead)
manifestsReadMu.Unlock()
parentOf = func(context.Context, string, string, string) (string, error) { return "", nil }
manifestAt = func(_ context.Context, _, repo, file, ref string) (catalogue.Manifest, bool, error) {
m, ok := held[[3]string{repo, file, ref}]
return m, ok, nil
}
}
func TestAFeatureWithANewSeatVerbIsOneBatchAndOnePlanInOrderWithNoAfterLine(t *testing.T) {
for name, controllerFirst := range map[string]bool{"controller_first": true, "catalogue_first": false} {
t.Run(name, func(t *testing.T) {
open := windowed(t)
asked := asksWithPaths(t)
ctx := t.Context()
const head = "feat/one-group-one-plan"
// mesh-delivery holds its seat, so a catalogue plan would wait for its word; this plan carries the controller.
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1",
Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
t.Fatal(err)
}
controller := fromHead(repoMerge("mesh-controller", "k1", t0), head)
controller.Paths = []string{"module.json", "cmd/mesh-controller/main.go"}
catalog := fromHead(catalogueMerge("cat1aaaaaaaa", "app", t0), head)
manifestsHeld(t, map[[3]string]catalogue.Manifest{
{"mesh-controller", "module.json", "k1"}: {Module: "mesh-controller",
DefinesSeats: []catalogue.SeatDeclaration{{Name: "seat", Serves: []catalogue.Verb{{Name: "verb"}}}}},
{"mesh-catalog", "modules/app/module.json", "cat1aaaaaaaa"}: {Module: "app", Invokes: []string{"seat:seat.verb"}},
})
states := map[string]string{"novox/mesh-controller": "ready", "novox/mesh-catalog": "ready"}
groupStates(t, head, states)
first, second := controller, catalog
if !controllerFirst {
first, second = catalog, controller
}
// The first member merges; the group waits for the second, however long it takes.
hear(t, open, first, t0)
cutAt(t, open, t0.Add(30*time.Minute))
if ws, bs := walks(t, open); len(ws) != 0 || len(bs) != 1 {
t.Fatalf("a member of a group alone: %d walk(s), %d batch(es)", len(ws), len(bs))
}
states["novox/"+second.Repo] = "merged"
second.MergedAt = t0.Add(30 * time.Minute).Format(time.RFC3339Nano)
hear(t, open, second, t0.Add(30*time.Minute))
cutAt(t, open, t0.Add(30*time.Minute+90*time.Second))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 0 {
t.Fatalf("after the window: %d walk(s), %d batch(es); want exactly one plan", len(ws), len(bs))
}
plan := ws[0]
if plan.CommitOf("novox/mesh-controller") != "k1" || plan.CommitOf("novox/mesh-catalog") != "cat1aaaaaaaa" {
t.Fatalf("the plan carries %+v", plan.Commits)
}
if len(plan.Tiers) != 2 || strings.Join(plan.Tiers[0], ",") != "mesh-controller" || strings.Join(plan.Tiers[1], ",") != "app" {
t.Fatalf("the plan's tiers are %v; want the controller, then the caller of its verb", plan.Tiers)
}
if placed := plan.Modules["app"].Placed; !strings.Contains(placed, "verb before caller: seat.verb") ||
!strings.Contains(placed, "after mesh-controller") {
t.Fatalf("the plan does not name the edge that placed the caller: %q", placed)
}
if plan.Modules["mesh-controller"].Placed != "" {
t.Fatalf("the first tier is placed by %q", plan.Modules["mesh-controller"].Placed)
}
if plan.Delivery != nil && (plan.Delivery.Awaits != "" || plan.Waiting()) {
t.Fatalf("a plan carrying the controller waits for %q's word", plan.Delivery.Awaits)
}
if plan.Modules["mesh-controller"].Build == "" || len(*asked) != 1 {
t.Fatalf("the first tier is asked once: build %q, %d ask(s) %v", plan.Modules["mesh-controller"].Build, len(*asked), *asked)
}
// Every merge is answered by that one plan.
for _, repository := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
merges, err := open.inventory.MergesOf(ctx, plan.ID)
if err != nil || len(merges) != 2 {
t.Fatalf("the plan answers %d merge(s) of %s: %v", len(merges), repository, err)
}
}
})
}
}
// Issue 1049: a move delivered half before its second merge. The controller adds a module the catalogue removes, in one
// group: the merges are one batch and one plan in either order, the module is built once and from the controller's
// repository, and the catalogue's removal does not forget it.
func TestAMoveOfAModuleAcrossTwoRepositoriesIsOnePlanAndBuildsItOnceFromTheNewOne(t *testing.T) {
for name, controllerFirst := range map[string]bool{"controller_first": true, "catalogue_first": false} {
t.Run(name, func(t *testing.T) {
open := windowed(t)
asked := asksWithPaths(t)
ctx := t.Context()
const head = "feat/whole-modules"
// The module runs on a machine from the catalogue's repository, so its removal there cannot forget it.
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "route-proxy", Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/route-proxy", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
if _, err := open.inventory.Assign(ctx, "anchor", "route-proxy"); err != nil {
t.Fatal(err)
}
manifestsHeld(t, nil)
controller := fromHead(repoMerge("mesh-controller", "k1", t0), head)
controller.Paths = []string{"modules/route-proxy/module.json", "modules/route-proxy/main.go"}
controller.ModuleDirs, controller.ModuleDirsSaid = []string{"modules/route-proxy"}, true
catalog := fromHead(catalogueMerge("cat1aaaaaaaa", "route-proxy", t0), head)
catalog.Removed = []string{"modules/route-proxy/module.json"}
catalog.ModuleDirs, catalog.ModuleDirsSaid = nil, true
states := map[string]string{"novox/mesh-controller": "ready", "novox/mesh-catalog": "ready"}
groupStates(t, head, states)
first, second := controller, catalog
if !controllerFirst {
first, second = catalog, controller
}
hear(t, open, first, t0)
states["novox/"+second.Repo] = "merged"
second.MergedAt = t0.Add(20 * time.Second).Format(time.RFC3339Nano)
hear(t, open, second, t0.Add(20*time.Second))
cutAt(t, open, t0.Add(20*time.Second+90*time.Second))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 0 {
t.Fatalf("a move: %d walk(s), %d batch(es); want exactly one plan", len(ws), len(bs))
}
var fromController int
for _, a := range *asked {
if strings.Contains(a[0], "mesh-controller") && a[1] == "modules/route-proxy" {
fromController++
}
if strings.Contains(a[0], "mesh-catalog") && a[1] == "modules/route-proxy" {
t.Errorf("route-proxy was built from the repository it left: %v", a)
}
}
if fromController != 1 {
t.Fatalf("route-proxy was built %d time(s) from the controller's repository: %v", fromController, *asked)
}
entries, err := open.inventory.Catalogued(ctx)
if err != nil {
t.Fatal(err)
}
kept := false
for _, e := range entries {
kept = kept || e.Manifest.Module == "route-proxy"
}
if !kept {
t.Fatal("the catalogue's removal forgot route-proxy, which a machine runs")
}
})
}
}