Swaps three import paths for mesh-sdk's go/catalogue, go/overlay and go/validate (the same packages, copied from this repository at 6c2a8d62) and lets gofmt order the groups. The six swap commits compile together once the pin commit (go.mod, modules.txt) is on the branch; each alone does not, because a file on the SDK's types cannot call one still on the old package.
699 lines
28 KiB
Go
699 lines
28 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
func windowed(t *testing.T) *stores {
|
|
t.Helper()
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
settings := t.TempDir() + "/merge-window.json"
|
|
if err := os.WriteFile(settings, []byte(`{"merge-window": "90s", "merge-window-at-most": "10m"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(mergeWindowFileVar, settings)
|
|
noGroups(t)
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"},
|
|
inventory.Source{Repository: "novox/mesh-controller", Seat: "git", Ref: "main", BuiltFrom: "c0",
|
|
Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []struct{ module, repository, path string }{
|
|
{"app", "novox/mesh-catalog", "modules/app"},
|
|
{"notes", "novox/mesh-catalog", "modules/notes"},
|
|
{"one", "novox/one", ""},
|
|
{"two", "novox/two", ""},
|
|
{"three", "novox/three", ""},
|
|
} {
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m.module, Version: "1"},
|
|
inventory.Source{Repository: m.repository, Seat: "git", Path: m.path, Ref: "main", BuiltFrom: "c0",
|
|
Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return open
|
|
}
|
|
|
|
func hear(t *testing.T, open *stores, m link.SourceMoved, now time.Time) {
|
|
t.Helper()
|
|
if err := (following{open: open}).hearMerge(t.Context(), m, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func cutAt(t *testing.T, open *stores, now time.Time) {
|
|
t.Helper()
|
|
if err := cutBatchesHeld(t.Context(), open, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestTwoMergesSecondsApartAreOneWalkAtTheLaterCommit(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asked := asksWithPaths(t)
|
|
claude := catalogueMerge("553b7191claude", "app", t0)
|
|
dunst := catalogueMerge("48bda475dunst", "notes", t0.Add(17*time.Second))
|
|
hear(t, open, claude, t0.Add(time.Second))
|
|
hear(t, open, dunst, t0.Add(18*time.Second))
|
|
if ws, bs := walks(t, open); len(ws) != 0 || len(bs) != 1 {
|
|
t.Fatalf("within the window: %d walk(s), %d batch(es)", len(ws), len(bs))
|
|
}
|
|
cutAt(t, open, t0.Add(18*time.Second+89*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 0 {
|
|
t.Fatalf("cut before the window closed: %+v", ws)
|
|
}
|
|
cutAt(t, open, t0.Add(18*time.Second+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)", len(ws), len(bs))
|
|
}
|
|
w := ws[0]
|
|
if w.Commit != dunst.Commit || len(w.Commits) != 1 || w.Commits[0].Commit != dunst.Commit {
|
|
t.Fatalf("the walk is at %s %+v, not the later commit", w.Commit, w.Commits)
|
|
}
|
|
for _, m := range []string{"app", "notes"} {
|
|
if _, in := w.Modules[m]; !in {
|
|
t.Fatalf("the walk does not build %s, which one of its merges moved: %v", m, w.Modules)
|
|
}
|
|
}
|
|
want := []inventory.PlanMerge{{Repository: "novox/mesh-catalog", Commit: claude.Commit, Carried: dunst.Commit},
|
|
{Repository: "novox/mesh-catalog", Commit: dunst.Commit}}
|
|
if w.Delivery == nil || !slices.EqualFunc(w.Delivery.Merges, want, sameMerge) {
|
|
t.Fatalf("the walk answers %+v, want %+v", w.Delivery, want)
|
|
}
|
|
if len(*asked) != 2 || (*asked)[0][2] != "main" || (*asked)[1][2] != "main" {
|
|
t.Fatalf("the walk did not ask its modules at the branch, which holds the commit it carries: %v", *asked)
|
|
}
|
|
// Heard again, as the bus may hand it over twice: never a second merge, nor a second walk.
|
|
hear(t, open, claude, t0.Add(5*time.Minute))
|
|
if ws, bs := walks(t, open); len(ws) != 1 || len(bs) != 0 {
|
|
t.Fatalf("a merge heard twice made %d walk(s) and %d batch(es)", len(ws), len(bs))
|
|
}
|
|
}
|
|
|
|
func TestThreeRepositoriesInOneWindowAreOneWalk(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "feat/three")
|
|
asksRecorded(t)
|
|
for i, r := range []string{"one", "two", "three"} {
|
|
hear(t, open, fromHead(repoMerge(r, "c-"+r, t0.Add(time.Duration(i)*20*time.Second)), "feat/three"),
|
|
t0.Add(time.Duration(i)*20*time.Second+time.Second))
|
|
}
|
|
cutAt(t, open, t0.Add(41*time.Second+90*time.Second))
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 0 {
|
|
t.Fatalf("%d walk(s), %d batch(es)", len(ws), len(bs))
|
|
}
|
|
w := ws[0]
|
|
if len(w.Commits) != 3 || len(w.Delivery.Merges) != 3 {
|
|
t.Fatalf("the walk carries %+v and answers %+v", w.Commits, w.Delivery.Merges)
|
|
}
|
|
for _, r := range []string{"one", "two", "three"} {
|
|
if w.CommitOf("novox/"+r) != "c-"+r {
|
|
t.Fatalf("the walk carries %s at %q", r, w.CommitOf("novox/"+r))
|
|
}
|
|
if _, in := w.Modules[r]; !in {
|
|
t.Fatalf("the walk does not build %s: %v", r, w.Modules)
|
|
}
|
|
}
|
|
// Each module's ask is recorded at its own repository's commit.
|
|
for _, r := range []string{"one", "two", "three"} {
|
|
q, found, err := open.inventory.BuildRequestByID(t.Context(), w.Modules[r].Build)
|
|
if err != nil || !found || q.Commit != "c-"+r {
|
|
t.Fatalf("%s's build was not recorded at its own commit: %+v %v %v", r, q, found, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMergeRestartsTheWindowAndTheMaximumClosesIt(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "feat/both", "feat/busy")
|
|
asksWithPaths(t)
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/both"), t0)
|
|
hear(t, open, fromHead(repoMerge("two", "c2", t0.Add(80*time.Second)), "feat/both"), t0.Add(80*time.Second))
|
|
cutAt(t, open, t0.Add(100*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 0 {
|
|
t.Fatal("the window closed 90 seconds after its first merge, not after its last")
|
|
}
|
|
cutAt(t, open, t0.Add(170*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 1 || len(ws[0].Commits) != 2 {
|
|
t.Fatalf("the window did not close 90 seconds after its last merge: %+v", ws)
|
|
}
|
|
|
|
busy := windowed(t)
|
|
groupsWaiting(t, "feat/both", "feat/busy")
|
|
asksWithPaths(t)
|
|
var last time.Time
|
|
for i := 0; i < 12; i++ {
|
|
last = t0.Add(time.Duration(i) * time.Minute)
|
|
repo := []string{"one", "two", "three"}[i%3]
|
|
hear(t, busy, fromHead(repoMerge(repo, "c"+string(rune('a'+i)), last), "feat/busy"), last)
|
|
if ws, _ := walks(t, busy); len(ws) > 0 {
|
|
if i != 10 {
|
|
t.Fatalf("a busy window closed at merge %d (minute %d), not at its maximum", i, i)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
ws, _ := walks(t, busy)
|
|
if len(ws) != 1 {
|
|
t.Fatalf("ten minutes of merges a minute apart were never cut: %d walk(s)", len(ws))
|
|
}
|
|
}
|
|
|
|
func TestTheAssemblingBatchIsShown(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "pr/553b7191claude")
|
|
asksWithPaths(t)
|
|
var said []inventory.Plan
|
|
was := inventory.PlanSaved
|
|
inventory.PlanSaved = func(_ *inventory.Inventory, p inventory.Plan) { said = append(said, p) }
|
|
t.Cleanup(func() { inventory.PlanSaved = was })
|
|
now := time.Now().UTC()
|
|
claude := catalogueMerge("553b7191claude", "app", now.Add(-20*time.Second))
|
|
claude.Number, claude.Title = 174, "claude-code: an agent proposes a section"
|
|
dunst := catalogueMerge("48bda475dunst", "notes", now.Add(-2*time.Second))
|
|
dunst.Number, dunst.Title = 175, "dunst: the font the operator chose"
|
|
hear(t, open, claude, now.Add(-19*time.Second))
|
|
hear(t, open, dunst, now.Add(-time.Second))
|
|
hear(t, open, fromHead(repoMerge("one", "a6bc0931one", now), dunst.Head), now)
|
|
out := captured(t, func() error { return plansCommand(t.Context(), nil) })
|
|
lines := strings.Split(out, "\n")
|
|
first := lines[0]
|
|
for _, want := range []string{"assembling: ", " s left (at the latest ", "grouped: novox/mesh-catalog@48bda475 " +
|
|
"(answers 553b7191), novox/one@a6bc0931; plan not yet calculated"} {
|
|
if !strings.Contains(first, want) {
|
|
t.Fatalf("plans' first line %q does not say %q", first, want)
|
|
}
|
|
}
|
|
// Under it, one line per repository: the pull request, what it answers, what it moves.
|
|
if len(lines) < 3 || strings.TrimSpace(lines[1]) != "mesh-catalog #175 dunst: the font the operator chose (answers "+
|
|
"#174 claude-code: an agent proposes a section) · app, notes" ||
|
|
strings.TrimSpace(lines[2]) != "one a6bc0931 · one" {
|
|
t.Fatalf("the grouped list reads %q", lines[1:4])
|
|
}
|
|
if len(said) == 0 || said[len(said)-1].State != inventory.PlanAssembling || said[len(said)-1].Delivery.Batch == nil ||
|
|
len(said[len(said)-1].Delivery.Merges) != 3 {
|
|
t.Fatalf("the batch was not said as assembling with its merges: %+v", said)
|
|
}
|
|
}
|
|
|
|
func captured(t *testing.T, run func() error) string {
|
|
t.Helper()
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was := os.Stdout
|
|
os.Stdout = w
|
|
runErr := run()
|
|
os.Stdout = was
|
|
_ = w.Close()
|
|
var b bytes.Buffer
|
|
_, _ = io.Copy(&b, r)
|
|
if runErr != nil {
|
|
t.Fatal(runErr)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func TestAMergeDuringAWalkJoinsTheNextBatch(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
hear(t, open, repoMerge("one", "c1", t0), t0)
|
|
cutAt(t, open, t0.Add(90*time.Second))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 || !ws[0].Open() {
|
|
t.Fatalf("the first batch was not cut: %+v", ws)
|
|
}
|
|
first := ws[0]
|
|
hear(t, open, repoMerge("one", "c2", t0.Add(2*time.Minute)), t0.Add(2*time.Minute))
|
|
cutAt(t, open, t0.Add(5*time.Minute))
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 1 || bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != first.ID {
|
|
t.Fatalf("the merge during the walk: %d walk(s), batches %+v", len(ws), bs)
|
|
}
|
|
if got, _ := open.inventory.PlanByID(t.Context(), first.ID); !got.Open() {
|
|
t.Fatalf("the started walk was %s by the next merge", got.State)
|
|
}
|
|
if !strings.HasPrefix(batchWords(bs[0], t0.Add(5*time.Minute)), "queued behind "+first.ID) {
|
|
t.Fatalf("a queued batch reads %q", batchWords(bs[0], t0.Add(5*time.Minute)))
|
|
}
|
|
first.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(t.Context(), &first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(6*time.Minute))
|
|
ws, bs = walks(t, open)
|
|
if len(ws) != 2 || len(bs) != 0 || ws[0].Commit != "c2" {
|
|
t.Fatalf("the queued batch was not cut when the walk ended: %+v %+v", ws, bs)
|
|
}
|
|
}
|
|
|
|
func TestAWaitingWalkIsFoldedIntoTheNextBatch(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
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)
|
|
}
|
|
hear(t, open, repoMerge("one", "c1", t0), t0)
|
|
cutAt(t, open, t0.Add(90*time.Second))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 || !ws[0].Waiting() {
|
|
t.Fatalf("the walk does not wait for its word: %+v", ws)
|
|
}
|
|
waiting := ws[0]
|
|
hear(t, open, repoMerge("one", "c2", t0.Add(2*time.Minute)), t0.Add(2*time.Minute))
|
|
cutAt(t, open, t0.Add(2*time.Minute+90*time.Second))
|
|
ws, bs := walks(t, open)
|
|
if len(bs) != 0 || len(ws) != 2 {
|
|
t.Fatalf("%d walk(s), %d batch(es)", len(ws), len(bs))
|
|
}
|
|
folded, err := open.inventory.PlanByID(ctx, waiting.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
newer := ws[0]
|
|
if folded.State != inventory.PlanSuperseded || folded.Delivery.TakenOverBy != newer.ID {
|
|
t.Fatalf("the waiting walk is %s, taken over by %q", folded.State, folded.Delivery.TakenOverBy)
|
|
}
|
|
if len(newer.Delivery.Merges) != 2 || newer.CommitOf("novox/one") != "c2" {
|
|
t.Fatalf("the newer walk answers %+v", newer.Delivery.Merges)
|
|
}
|
|
if _, in := newer.Modules["one"]; !in {
|
|
t.Fatalf("what the folded walk was to build is not built: %v", newer.Modules)
|
|
}
|
|
}
|
|
|
|
func TestALateMergeIsAnsweredByTheWalkOfTheLaterOne(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
later := catalogueMerge("48bda475dunst", "notes", t0.Add(17*time.Second))
|
|
hear(t, open, later, t0.Add(18*time.Second))
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
w := ws[0]
|
|
w.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Moves only notes, which the done walk built from the branch after it: answered there, at once.
|
|
hear(t, open, catalogueMerge("553b7191early", "notes", t0), t0.Add(15*time.Minute))
|
|
got, _ := open.inventory.PlanByID(ctx, w.ID)
|
|
if got.State != inventory.PlanDone || len(got.Delivery.Merges) != 2 ||
|
|
!sameMerge(got.Delivery.Merges[0], inventory.PlanMerge{Repository: "novox/mesh-catalog", Commit: "553b7191early",
|
|
Carried: later.Commit}) {
|
|
t.Fatalf("the late merge is not named by the walk that carried it: %+v", got.Delivery.Merges)
|
|
}
|
|
// Moves app, which that walk never built: the next batch walks it.
|
|
hear(t, open, catalogueMerge("1111aaaaapp", "app", t0.Add(time.Second)), t0.Add(16*time.Minute))
|
|
_, bs := walks(t, open)
|
|
if len(bs) != 1 || bs[0].Delivery.Merges[0].Commit != "1111aaaaapp" {
|
|
t.Fatalf("a late merge moving what the walk never built did not join the next batch: %+v", bs)
|
|
}
|
|
}
|
|
|
|
func TestAFailedWalkWalksItsEarlierMergesAlone(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asked := asksWithPaths(t)
|
|
ctx := t.Context()
|
|
// Made before the cut marks their modules seen, as merges are: walked again, they are news all the same.
|
|
oldest := catalogueMerge("aaaa0001", "app", t0.Add(-3*time.Minute))
|
|
middle := catalogueMerge("bbbb0002", "app", t0.Add(-3*time.Minute+10*time.Second))
|
|
newest := catalogueMerge("cccc0003", "notes", t0.Add(-3*time.Minute+20*time.Second))
|
|
for i, m := range []link.SourceMoved{oldest, middle, newest} {
|
|
hear(t, open, m, t0.Add(time.Duration(i)*10*time.Second+time.Second))
|
|
}
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
failed := ws[0]
|
|
failed.State, failed.Note = inventory.PlanFailed, "notes failed to build in tier 0"
|
|
if err := open.inventory.SavePlan(ctx, &failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(3*time.Minute))
|
|
got, _ := open.inventory.PlanByID(ctx, failed.ID)
|
|
if got.State != inventory.PlanFailed || len(got.Delivery.Merges) != 3 {
|
|
t.Fatalf("the failed walk's record changed: %s %+v", got.State, got.Delivery.Merges)
|
|
}
|
|
ws, _ = walks(t, open)
|
|
alone := ws[0]
|
|
if alone.ID == failed.ID || alone.Commit != middle.Commit || len(alone.Delivery.Merges) != 1 ||
|
|
!sameMerge(alone.Delivery.Merges[0], inventory.PlanMerge{Repository: "novox/mesh-catalog", Commit: middle.Commit}) {
|
|
t.Fatalf("the newest earlier merge was not walked alone on its own commit: %+v", alone)
|
|
}
|
|
if _, in := alone.Modules["app"]; !in || (*asked)[len(*asked)-1] != [3]string{"novox/mesh-catalog", "modules/app",
|
|
middle.Commit} {
|
|
t.Fatalf("the merge walked alone does not build app at its own commit: %v, asked %v", alone.Modules, *asked)
|
|
}
|
|
// Retried, the failed walk would name merges the search answers now.
|
|
if _, err := retryPlan(ctx, open, failed.ID); err == nil || !strings.Contains(err.Error(), "walked alone") {
|
|
t.Fatalf("a failed walk whose merges are searched was retried: %v", err)
|
|
}
|
|
alone.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &alone); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(4*time.Minute))
|
|
ws, bs := walks(t, open)
|
|
if ws[0].ID != alone.ID || len(bs) != 0 {
|
|
t.Fatalf("the search went on after a merge was delivered: %+v", ws[0])
|
|
}
|
|
done, _ := open.inventory.PlanByID(ctx, alone.ID)
|
|
if len(done.Delivery.Merges) != 2 || !sameMerge(done.Delivery.Merges[0], inventory.PlanMerge{Repository: "novox/mesh-catalog",
|
|
Commit: oldest.Commit, Carried: middle.Commit}) {
|
|
t.Fatalf("the oldest merge is not answered by the walk that delivered the one after it: %+v", done.Delivery.Merges)
|
|
}
|
|
// A stopped walk starts no search: a person ended it.
|
|
hear(t, open, catalogueMerge("dddd0004", "app", t0.Add(5*time.Minute)), t0.Add(5*time.Minute))
|
|
hear(t, open, catalogueMerge("eeee0005", "notes", t0.Add(5*time.Minute+time.Second)), t0.Add(5*time.Minute+time.Second))
|
|
cutAt(t, open, t0.Add(8*time.Minute))
|
|
ws, _ = walks(t, open)
|
|
if _, err := stopWalk(ctx, open.inventory, ws[0].ID, "mesh-delivery for jochen", "not now"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(9*time.Minute))
|
|
if again, _ := walks(t, open); again[0].ID != ws[0].ID {
|
|
t.Fatalf("a stopped walk's earlier merge was walked alone: %+v", again[0])
|
|
}
|
|
}
|
|
|
|
func TestAGroupsOrderBecomesTiersInsideTheWalk(t *testing.T) {
|
|
for _, grouped := range []bool{true, false} {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
for _, m := range []string{"mesh-host"} {
|
|
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"},
|
|
inventory.Source{Repository: "novox/" + m, Seat: "git", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
host := repoMerge("mesh-host", "h1", t0)
|
|
ctl := repoMerge("mesh-controller", "k1", t0.Add(time.Second))
|
|
host.Head, ctl.Head = "feat/together", "feat/together"
|
|
if !grouped {
|
|
ctl.Head = "feat/alone"
|
|
}
|
|
hear(t, open, ctl, t0.Add(2*time.Second))
|
|
hear(t, open, host, t0.Add(3*time.Second))
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 {
|
|
t.Fatalf("%d walks", len(ws))
|
|
}
|
|
tiers := ws[0].Tiers
|
|
if grouped {
|
|
if len(tiers) != 2 || !slices.Equal(tiers[0], []string{"mesh-host"}) ||
|
|
!slices.Equal(tiers[1], []string{"mesh-controller"}) {
|
|
t.Fatalf("the group's order is not the walk's tiers: %v", tiers)
|
|
}
|
|
} else if len(tiers) != 1 {
|
|
t.Fatalf("two merges of no group were ordered: %v", tiers)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestABatchSurvivesARestart(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "feat/both")
|
|
asksWithPaths(t)
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/both"), t0)
|
|
hear(t, open, fromHead(repoMerge("two", "c2", t0.Add(30*time.Second)), "feat/both"), t0.Add(30*time.Second))
|
|
|
|
// A new controller: nothing of the first one's but the store.
|
|
again, err := openStores(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(again.Close)
|
|
hear(t, again, fromHead(repoMerge("one", "c1", t0), "feat/both"), t0.Add(60*time.Second)) // the bus hands it over again
|
|
cutAt(t, again, t0.Add(119*time.Second))
|
|
if ws, bs := walks(t, again); len(ws) != 0 || len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("the restarted controller's batch: %d walk(s), %+v", len(ws), bs)
|
|
}
|
|
cutAt(t, again, t0.Add(120*time.Second))
|
|
ws, bs := walks(t, again)
|
|
if len(ws) != 1 || len(bs) != 0 || len(ws[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("the restarted controller did not cut the batch at its window: %+v %+v", ws, bs)
|
|
}
|
|
}
|
|
|
|
func TestALateMergeOfAOneModuleRepositoryIsNamed(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
c2 := repoMerge("one", "c2", t0.Add(-50*time.Second))
|
|
hear(t, open, c2, t0)
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
w := ws[0]
|
|
w.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hear(t, open, repoMerge("one", "c1", t0.Add(-60*time.Second)), t0.Add(15*time.Minute))
|
|
got, _ := open.inventory.PlanByID(ctx, w.ID)
|
|
if len(got.Delivery.Merges) != 2 || !sameMerge(got.Delivery.Merges[0], inventory.PlanMerge{Repository: "novox/one",
|
|
Commit: "c1", Carried: "c2"}) {
|
|
t.Fatalf("the late merge was not named by the walk that carried it: %+v", got.Delivery.Merges)
|
|
}
|
|
|
|
// A walk of two that built only two: a late merge of one is not its to name.
|
|
hear(t, open, repoMerge("two", "d2", t0.Add(16*time.Minute)), t0.Add(16*time.Minute))
|
|
cutAt(t, open, t0.Add(18*time.Minute))
|
|
ws, _ = walks(t, open)
|
|
w = ws[0]
|
|
delete(w.Modules, "two")
|
|
w.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hear(t, open, repoMerge("two", "d1", t0.Add(15*time.Minute)), t0.Add(20*time.Minute))
|
|
if got, _ := open.inventory.PlanByID(ctx, w.ID); len(got.Delivery.Merges) != 1 {
|
|
t.Fatalf("a walk that never built what the late merge moves named it: %+v", got.Delivery.Merges)
|
|
}
|
|
if _, bs := walks(t, open); len(bs) != 1 || bs[0].Delivery.Merges[0].Commit != "d1" {
|
|
t.Fatalf("the late merge did not join the next batch: %+v", bs)
|
|
}
|
|
}
|
|
|
|
func TestAWalkLetGoBesideAStartedOneStartsOnceItEnded(t *testing.T) {
|
|
open := windowed(t)
|
|
asked := asksWithPaths(t)
|
|
ctx := t.Context()
|
|
hear(t, open, repoMerge("one", "c1", t0), t0)
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
started, _ := walks(t, open)
|
|
waiting := inventory.Plan{ID: "plan-waiting", Repository: "novox/one", Branch: "main", Commit: "d1", Created: t0,
|
|
State: inventory.PlanBuilding, Tiers: [][]string{{"one"}}, Modules: map[string]*inventory.PlanModule{"one": {}},
|
|
Delivery: &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat}}
|
|
if err := open.inventory.SavePlan(ctx, &waiting); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := letGo(ctx, open.inventory, waiting.ID, catalogue.DeliverySeat, "its turn"); err != nil {
|
|
t.Fatalf("the word was refused beside %s: %v", started[0].ID, err)
|
|
}
|
|
before := len(*asked)
|
|
advanceHeld(ctx, open)
|
|
got, _ := open.inventory.PlanByID(ctx, waiting.ID)
|
|
if len(*asked) != before || !strings.Contains(got.Note, "starts once "+started[0].ID) {
|
|
t.Fatalf("a walk let go beside a started one asked (%d → %d) or does not say it waits: %q", before, len(*asked), got.Note)
|
|
}
|
|
// Read as a wait, an hour on: its note on the line, never LATE, and no tier of it late for S3.
|
|
later := time.Now().Add(time.Hour)
|
|
if line := planLine(got, later); !strings.Contains(line, "starts once "+started[0].ID) || strings.Contains(line, "LATE") {
|
|
t.Fatalf("a deferred walk reads %q", line)
|
|
}
|
|
facts, _, err := gatherPlans(ctx, open.inventory, later, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range facts {
|
|
if f.id == got.ID {
|
|
t.Fatalf("a deferred walk is watched as a tier running late: %+v", f)
|
|
}
|
|
}
|
|
done := started[0]
|
|
done.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &done); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
advanceHeld(ctx, open)
|
|
if len(*asked) != before+1 {
|
|
t.Fatalf("the walk did not start once the started one ended: asked %v", *asked)
|
|
}
|
|
}
|
|
|
|
func TestAMergeOnTheControllersPathNeverSharesABatch(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asked := asksWithPaths(t)
|
|
ctx := t.Context()
|
|
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)
|
|
}
|
|
hear(t, open, catalogueMerge("aaaa0001", "app", t0), t0)
|
|
hear(t, open, repoMerge("mesh-controller", "k1", t0.Add(10*time.Second)), t0.Add(10*time.Second))
|
|
hear(t, open, catalogueMerge("bbbb0002", "notes", t0.Add(20*time.Second)), t0.Add(20*time.Second))
|
|
_, bs := walks(t, open)
|
|
var ownBatch, catalogueBatch inventory.Plan
|
|
for _, b := range bs {
|
|
if b.OwnPath() {
|
|
ownBatch = b
|
|
} else {
|
|
catalogueBatch = b
|
|
}
|
|
}
|
|
if len(bs) != 2 || ownBatch.ID == "" || catalogueBatch.ID == "" || len(catalogueBatch.Delivery.Merges) != 2 ||
|
|
len(ownBatch.Delivery.Merges) != 1 {
|
|
t.Fatalf("a controller merge and two catalogue merges in one window: %+v", bs)
|
|
}
|
|
if !strings.Contains(batchWords(ownBatch, t0.Add(30*time.Second)), "own path") {
|
|
t.Fatalf("the own-path batch does not say so: %q", batchWords(ownBatch, t0.Add(30*time.Second)))
|
|
}
|
|
// Both windows closed, the controller's batch is cut first and starts; the catalogue batch queues behind it,
|
|
// is cut when the controller's walk ended, and waits for its word with both merges.
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 1 || ws[0].Waiting() || ws[0].CommitOf("novox/mesh-controller") != "k1" {
|
|
t.Fatalf("the controller's batch was not cut first into a started walk: %+v", ws)
|
|
}
|
|
ownWalk := ws[0]
|
|
for _, m := range []string{"app", "notes"} {
|
|
if _, in := ownWalk.Modules[m]; in {
|
|
t.Fatalf("the controller's walk builds %s, a catalogue module: it skipped its turn", m)
|
|
}
|
|
}
|
|
if len(bs) != 1 || bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != ownWalk.ID || bs[0].OwnPath() {
|
|
t.Fatalf("the catalogue batch does not queue behind the controller's walk: %+v", bs)
|
|
}
|
|
ownWalk.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &ownWalk); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(3*time.Minute))
|
|
ws, bs = walks(t, open)
|
|
if len(bs) != 0 || ws[0].ID != catalogueBatch.ID || !ws[0].Waiting() || len(ws[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("the catalogue batch was not cut into a waiting walk once the controller's ended: %+v %+v", ws, bs)
|
|
}
|
|
for _, m := range []string{"app", "notes"} {
|
|
if _, in := ws[0].Modules[m]; !in {
|
|
t.Fatalf("the catalogue walk does not build %s: %v", m, ws[0].Modules)
|
|
}
|
|
}
|
|
for _, a := range *asked {
|
|
if a[1] == "modules/app" || a[1] == "modules/notes" {
|
|
t.Fatalf("a catalogue module was asked without the word: %v", *asked)
|
|
}
|
|
}
|
|
|
|
// A catalogue walk waiting for its word when an own-path batch is cut keeps waiting beside the own-path
|
|
// walk, is not folded into it, and its word is taken meanwhile: it starts once the own-path walk ended.
|
|
catalogueWalk := ws[0]
|
|
hear(t, open, repoMerge("mesh-controller", "k2", t0.Add(4*time.Minute)), t0.Add(4*time.Minute))
|
|
cutAt(t, open, t0.Add(6*time.Minute))
|
|
ws, _ = walks(t, open)
|
|
kept, _ := open.inventory.PlanByID(ctx, catalogueWalk.ID)
|
|
if !kept.Waiting() || ws[0].CommitOf("novox/mesh-controller") != "k2" || ws[0].Waiting() {
|
|
t.Fatalf("the waiting catalogue walk was not kept waiting beside the controller's walk: %s %+v", kept.State, ws)
|
|
}
|
|
if _, in := ws[0].Modules["app"]; in {
|
|
t.Fatalf("the controller's walk folded the waiting catalogue walk in: %v", ws[0].Modules)
|
|
}
|
|
if _, err := letGo(ctx, open.inventory, catalogueWalk.ID, catalogue.DeliverySeat, "its turn"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := len(*asked)
|
|
advanceHeld(ctx, open)
|
|
if len(*asked) != before {
|
|
t.Fatalf("the catalogue walk started beside the controller's: asked %v", (*asked)[before:])
|
|
}
|
|
own2 := ws[0]
|
|
own2.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &own2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
advanceHeld(ctx, open)
|
|
if len(*asked) < before+1 || (*asked)[before][1] != "modules/app" {
|
|
t.Fatalf("the catalogue walk did not start once the controller's ended: %v", (*asked)[before:])
|
|
}
|
|
}
|
|
|
|
func TestALateCatalogueMergeIsNotAnsweredByAnOwnPathWalk(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
for _, m := range []struct {
|
|
module string
|
|
claims []catalogue.Claim
|
|
}{
|
|
{"nats", nil},
|
|
{"mesh-delivery", []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}},
|
|
} {
|
|
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m.module, Version: "1", Claims: m.claims},
|
|
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m.module, 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)
|
|
}
|
|
mixed := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "m1xed", MergedAt: t0.Format(time.RFC3339Nano),
|
|
Paths: []string{"modules/nats/module.json", "modules/app/module.json"}, ModuleDirs: []string{"modules/nats", "modules/app"},
|
|
ModuleDirsSaid: true}
|
|
hear(t, open, mixed, t0.Add(time.Second))
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 || ws[0].Waiting() {
|
|
t.Fatalf("the mixed merge's walk waited, or was not cut: %+v", ws)
|
|
}
|
|
done := ws[0]
|
|
done.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &done); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hear(t, open, catalogueMerge("ear1ier", "app", t0.Add(-30*time.Second)), t0.Add(3*time.Minute))
|
|
got, _ := open.inventory.PlanByID(ctx, done.ID)
|
|
_, bs := walks(t, open)
|
|
if len(got.Delivery.Merges) != 1 || len(bs) != 1 || bs[0].OwnPath() || bs[0].Delivery.Merges[0].Commit != "ear1ier" {
|
|
t.Fatalf("the late catalogue merge was answered by the own-path walk (%+v) rather than the next catalogue batch (%+v)",
|
|
got.Delivery.Merges, bs)
|
|
}
|
|
}
|