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

140 lines
6.2 KiB
Go

//go:build integration
package main
import (
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/inventory"
)
// A group is one batch and one plan (novox/hq ADR 0418 clauses 3 and 6): the batch holding a merge of a group with a
// member still to merge does not close by the merge window, however long that member takes, and a group with a
// member of each kind is one batch whatever order its members are heard in.
func TestAGroupsBatchWaitsForItsLastMemberHoweverLongItTakes(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
states := map[string]string{"novox/one": "ready", "novox/two": "ready"}
groupStates(t, "feat/g", states)
first := fromHead(repoMerge("one", "c1", t0), "feat/g")
hear(t, open, first, t0)
// Past the window and past its maximum, with the second member still to merge: one batch, no walk.
for _, after := range []time.Duration{90 * time.Second, 11 * time.Minute, 30 * time.Minute} {
cutAt(t, open, t0.Add(after))
ws, bs := walks(t, open)
if len(ws) != 0 || len(bs) != 1 || len(bs[0].Delivery.Batch.Holds) != 1 {
t.Fatalf("%s after the first member: %d walk(s), %d batch(es) %+v", after, len(ws), len(bs), bs)
}
}
// The last member merges: the window runs again from that merge.
at := t0.Add(30 * time.Minute)
states["novox/two"] = "merged"
hear(t, open, fromHead(repoMerge("two", "c2", at), "feat/g"), at.Add(time.Second))
cutAt(t, open, at.Add(time.Second+89*time.Second))
if ws, _ := walks(t, open); len(ws) != 0 {
t.Fatalf("cut before the window ran again: %+v", ws)
}
cutAt(t, open, at.Add(time.Second+90*time.Second))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 0 || ws[0].CommitOf("novox/one") != "c1" || ws[0].CommitOf("novox/two") != "c2" {
t.Fatalf("after the window: %d walk(s) %+v, %d batch(es)", len(ws), ws, len(bs))
}
}
// A group with a member of each kind is one batch and one walk, in both orders of hearing, started without
// mesh-delivery's word.
func TestAGroupWithAMemberOfEachKindIsOneBatchAndOneWalk(t *testing.T) {
for name, ownFirst := range map[string]bool{"controller member first": true, "catalogue member first": false} {
t.Run(strings.ReplaceAll(name, " ", "_"), func(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
states := map[string]string{"novox/mesh-controller": "ready", "novox/mesh-catalog": "ready"}
groupStates(t, "feat/both-kinds", states)
own := fromHead(repoMerge("mesh-controller", "k1", t0), "feat/both-kinds")
cat := fromHead(catalogueMerge("cat1aaaaaaaa", "app", t0), "feat/both-kinds")
first, second := cat, own
if ownFirst {
first, second = own, cat
}
second.MergedAt = t0.Add(10 * time.Second).Format(time.RFC3339Nano)
hear(t, open, first, t0)
states["novox/"+second.Repo] = "merged"
hear(t, open, second, t0.Add(10*time.Second))
ws, bs := walks(t, open)
if len(ws) != 0 || len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 {
t.Fatalf("both members heard: %d walk(s), %d batch(es) %+v; want one batch of both", len(ws), len(bs), bs)
}
cutAt(t, open, t0.Add(10*time.Second+90*time.Second))
ws, bs = walks(t, open)
if len(ws) != 1 || len(bs) != 0 || ws[0].CommitOf("novox/mesh-controller") != "k1" ||
ws[0].CommitOf("novox/mesh-catalog") != cat.Commit {
t.Fatalf("after the window: %d walk(s) %+v, %d batch(es)", len(ws), ws, len(bs))
}
if ws[0].Delivery != nil && ws[0].Delivery.Awaits != "" {
t.Fatalf("a plan carrying the controller waits for %s's word", ws[0].Delivery.Awaits)
}
})
}
}
// A group whose last member has not merged ten minutes after its first raises a condition naming it, and the held
// batch is not the batch S18 reads as a cut the controller failed to make.
func TestAHeldBatchRaisesTheGroupConditionAndNotTheBatchNotCutOne(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
groupStates(t, "feat/g", map[string]string{"novox/one": "ready", "novox/two": "ready"})
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/g"), t0)
now := t0.Add(12 * time.Minute)
batches, err := gatherBatches(t.Context(), open.inventory, now)
if err != nil || len(batches) != 1 {
t.Fatalf("batches %+v: %v", batches, err)
}
f := &signalFacts{now: now, batches: batches}
if got := watchBatchesNotCut(f); len(got) != 0 {
t.Fatalf("a held batch is read as not cut: %+v", got)
}
got := watchGroupsNotMerged(f)
if len(got) != 1 || got[0].Kind != kindGroupNotMerged || !strings.Contains(got[0].Summary, "novox/two") ||
!strings.Contains(got[0].Summary, "feat/g") {
t.Fatalf("the group's condition: %+v", got)
}
if early := watchGroupsNotMerged(&signalFacts{now: t0.Add(5 * time.Minute), batches: batches}); len(early) != 0 {
t.Fatalf("raised before the maximum: %+v", early)
}
}
// The batch held for a group survives a restart: the hold is in the record.
func TestAHeldBatchSurvivesARestart(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
groupStates(t, "feat/g", map[string]string{"novox/one": "ready", "novox/two": "ready"})
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/g"), t0)
reopened, err := open.inventory.Batches(t.Context())
if err != nil || len(reopened) != 1 || len(reopened[0].Delivery.Batch.Holds) != 1 {
t.Fatalf("the record of the held batch: %+v %v", reopened, err)
}
cutAt(t, open, t0.Add(time.Hour))
if ws, bs := walks(t, open); len(ws) != 0 || len(bs) != 1 || bs[0].State != inventory.PlanAssembling {
t.Fatalf("after an hour: %d walk(s), batches %+v", len(ws), bs)
}
}
// A mesh-delivery that does not yet say the group's gate is composed merges member two only after member one is
// delivered, so a batch held for it would wait for ever: the merge waits for the window, as before.
func TestAnOlderMeshDeliverysGroupIsNotHeldOpen(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
groupStatesFrom(t, "feat/g", map[string]string{"novox/one": "ready", "novox/two": "ready"}, false)
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/g"), t0)
if _, bs := walks(t, open); len(bs) != 1 || len(bs[0].Delivery.Batch.Holds) != 0 {
t.Fatalf("held for an older mesh-delivery's group: %+v", bs)
}
cutAt(t, open, t0.Add(90*time.Second))
if ws, bs := walks(t, open); len(ws) != 1 || len(bs) != 0 {
t.Fatalf("at its window: %d walk(s), %d batch(es)", len(ws), len(bs))
}
}