197 lines
7.0 KiB
Go
197 lines
7.0 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
asked := asksRecorded(t)
|
|
withConditionsInMemory(t)
|
|
for _, name := range []string{"app", "mesh-delivery"} {
|
|
m := catalogue.Manifest{Module: name, Version: "1"}
|
|
if name == "mesh-delivery" {
|
|
m.Claims = []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}
|
|
}
|
|
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git",
|
|
Path: "modules/" + name, Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
merge := func(commit string, paths ...string) inventory.Plan {
|
|
t.Helper()
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: commit, Paths: paths,
|
|
ModuleDirs: []string{"modules/app", "modules/mesh-delivery"}, ModuleDirsSaid: true}
|
|
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recent, err := inv.RecentPlans(ctx, 1)
|
|
if err != nil || len(recent) != 1 || recent[0].Commit != commit {
|
|
t.Fatalf("no plan for %s: %v %v", commit, recent, err)
|
|
}
|
|
return recent[0]
|
|
}
|
|
|
|
// finish ends a walk as done, so the next merge has nothing of it to take over.
|
|
finish := func(id string) {
|
|
t.Helper()
|
|
w, err := inv.PlanByID(ctx, id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.State = inventory.PlanDone
|
|
if err := inv.SavePlan(ctx, &w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// No holder on record: the merge starts its walk, as before.
|
|
p := merge("c1aaaaaaaa", "modules/app/index.ts")
|
|
if p.Waiting() || len(*asked) != 1 {
|
|
t.Fatalf("with no holder on record the walk waited (%v) or asked %v", p.Waiting(), *asked)
|
|
}
|
|
|
|
// The holder on record: the next merge waits, asking nothing, and an advance asks nothing either. One walk
|
|
// is open at a time (novox/hq ADR 0276): the first ends before the next merge's batch is cut.
|
|
finish(p.ID)
|
|
if _, err := inv.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p = merge("c2bbbbbbbb", "modules/app/index.ts")
|
|
if !p.Waiting() || len(*asked) != 1 || !strings.Contains(p.Note, "plans go "+p.ID) {
|
|
t.Fatalf("the walk did not wait for its word: waiting %v, asked %v, note %q", p.Waiting(), *asked, p.Note)
|
|
}
|
|
advanceHeld(ctx, open)
|
|
if len(*asked) != 1 {
|
|
t.Fatalf("a waiting walk was advanced into asking: %v", *asked)
|
|
}
|
|
if line := planLine(p, p.Created); strings.Contains(line, "LATE") || !strings.Contains(line, "waits for") {
|
|
t.Errorf("a waiting walk reads %q", line)
|
|
}
|
|
|
|
// The delivery's word starts it.
|
|
if err := deliveryCommand(ctx, []string{"go", p.ID, "--by", catalogue.DeliverySeat, "--why", "its turn"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := deliveryCommand(ctx, []string{"go", p.ID}); err == nil {
|
|
t.Fatal("a walk was let go twice")
|
|
}
|
|
advanceHeld(ctx, open)
|
|
if len(*asked) != 2 {
|
|
t.Fatalf("the word did not start the walk: %v", *asked)
|
|
}
|
|
got, err := inv.PlanByID(ctx, p.ID)
|
|
if err != nil || got.Delivery == nil || got.Delivery.By != catalogue.DeliverySeat || got.Delivery.Why != "its turn" {
|
|
t.Fatalf("the word was not kept: %+v %v", got.Delivery, err)
|
|
}
|
|
|
|
// The delivery's owner's own merge never waits for it. A walk that started is never taken over (novox/hq
|
|
// ADR 0276): the merge's batch is cut once it ended.
|
|
finish(p.ID)
|
|
p = merge("c3cccccccc", "modules/mesh-delivery/main.go")
|
|
if p.Waiting() || len(*asked) != 3 {
|
|
t.Fatalf("mesh-delivery's own walk waited for mesh-delivery: %v %v", p.Waiting(), *asked)
|
|
}
|
|
|
|
// The owner down: a person starts a waiting walk, with why.
|
|
finish(p.ID)
|
|
p = merge("c4dddddddd", "modules/app/index.ts")
|
|
if !p.Waiting() {
|
|
t.Fatal("the walk did not wait")
|
|
}
|
|
if err := plansCommand(ctx, []string{"go", p.ID}); err == nil || !strings.Contains(err.Error(), "--why") {
|
|
t.Fatalf("a walk was started by hand without why: %v", err)
|
|
}
|
|
if err := plansCommand(ctx, []string{"go", p.ID, "--why", "mesh-delivery is down"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
advanceHeld(ctx, open)
|
|
got, _ = inv.PlanByID(ctx, p.ID)
|
|
if got.Waiting() || !strings.HasPrefix(got.Delivery.By, "a person") || len(*asked) < 4 {
|
|
t.Fatalf("a person's word did not start the walk: %+v, asked %v", got.Delivery, *asked)
|
|
}
|
|
|
|
// The owner's stop: failed, said as stopped by it.
|
|
finish(p.ID)
|
|
p = merge("c5eeeeeeee", "modules/app/index.ts")
|
|
if err := deliveryCommand(ctx, []string{"stop", p.ID, "--why", "the operator stopped it", "--by",
|
|
"mesh-delivery for jochen"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ = inv.PlanByID(ctx, p.ID)
|
|
if got.State != inventory.PlanFailed || got.Delivery.Stopped != "mesh-delivery for jochen" ||
|
|
!strings.Contains(got.Note, "the operator stopped it") {
|
|
t.Fatalf("the stop was kept as %s %+v %q", got.State, got.Delivery, got.Note)
|
|
}
|
|
|
|
// Unassigned: the mesh is back on the controller's own path.
|
|
if err := inv.Unassign(ctx, "anchor", "mesh-delivery"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
asks := len(*asked)
|
|
if p = merge("c6ffffffff", "modules/app/index.ts"); p.Waiting() || len(*asked) != asks+1 {
|
|
t.Fatalf("with the holder gone the walk waited: %v", p.Waiting())
|
|
}
|
|
}
|
|
|
|
func TestAWalkLetGoByAVerbIsSaidAsPlanMoved(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
url := testbus.URL(t)
|
|
js, err := broker.Dial(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(js.Close)
|
|
if err := broker.AssertMeshStreams(js); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := handActConn
|
|
handActConn = js.Conn()
|
|
t.Cleanup(func() { handActConn = before })
|
|
heard := make(chan *nats.Msg, 4)
|
|
sub, err := js.Conn().ChanSubscribe(link.SeatEventSubject(link.MeshControllerSeat, link.KeyPlanMoved), heard)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
|
|
|
waiting := inventory.Plan{ID: "plan-waits", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c7c7c7c7",
|
|
Created: time.Now().UTC(), State: inventory.PlanBuilding, Tiers: [][]string{{"app"}},
|
|
Modules: map[string]*inventory.PlanModule{"app": {}},
|
|
Delivery: &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat}}
|
|
if err := open.inventory.SavePlan(ctx, &waiting); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case m := <-heard:
|
|
var said inventory.Plan
|
|
if err := json.Unmarshal(m.Data, &said); err != nil || said.ID != waiting.ID || said.Delivery == nil ||
|
|
said.Delivery.Go == nil {
|
|
t.Fatalf("plan-moved said %s (%v)", m.Data, err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("a walk let go by a verb was not said")
|
|
}
|
|
if checkEvents != nil || inventory.PlanSaved != nil {
|
|
t.Fatal("the command's own bus was left in place")
|
|
}
|
|
}
|