267 lines
9.3 KiB
Go
267 lines
9.3 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"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 TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
next := planMovedHeard(t)
|
|
register(t, open, catalogue.Manifest{Module: "app", Version: "1"})
|
|
if _, err := assign(ctx, open, "anchor", "app"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cut := time.Now().UTC()
|
|
waiting := inventory.Plan{ID: "plan-308-live", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c8c8c8c8",
|
|
Created: cut, State: inventory.PlanBuilding, Tiers: [][]string{{"app"}},
|
|
Modules: map[string]*inventory.PlanModule{"app": {}},
|
|
Delivery: &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat},
|
|
Times: &inventory.PlanTimes{Cut: &cut, Class: inventory.ClassLeaf}}
|
|
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)
|
|
}
|
|
started := next(inventory.WalkStarted)
|
|
touches, _ := json.Marshal(started["touches"])
|
|
if started["walk"] != waiting.ID || string(touches) != `[{"machine":"anchor","modules":["app"]}]` ||
|
|
started["budget_class"] != inventory.ClassLeaf ||
|
|
started["bound_seconds"] != float64(2*deliveryBudgets[inventory.ClassLeaf]/time.Second) {
|
|
t.Fatalf("the start: %v", started)
|
|
}
|
|
if err := deliveryCommand(ctx, []string{"stop", waiting.ID, "--why", "a test", "--by", "a person"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ended := next(inventory.WalkEnded)
|
|
touches, _ = json.Marshal(ended["touches"])
|
|
if ended["walk"] != waiting.ID || ended["outcome"] != inventory.WalkStopped || string(touches) != `[]` {
|
|
t.Fatalf("the end, which sent nothing: %v", ended)
|
|
}
|
|
}
|
|
|
|
func planMovedHeard(t *testing.T) func(phase string) map[string]any {
|
|
t.Helper()
|
|
js, err := broker.Dial(testbus.URL(t))
|
|
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, 64)
|
|
sub, err := js.Conn().ChanSubscribe(link.SeatEventSubject(link.MeshControllerSeat, link.KeyPlanMoved), heard)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
|
return func(phase string) map[string]any {
|
|
t.Helper()
|
|
deadline := time.After(5 * time.Second)
|
|
for {
|
|
select {
|
|
case m := <-heard:
|
|
var said map[string]any
|
|
if err := json.Unmarshal(m.Data, &said); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said["phase"] == phase {
|
|
return said
|
|
}
|
|
case <-deadline:
|
|
t.Fatalf("no walk was said %s", phase)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
next := planMovedHeard(t)
|
|
register(t, open, catalogue.Manifest{Module: "app", Version: "1"})
|
|
if _, err := assign(ctx, open, "anchor", "app"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, verb := range []string{"stop", "close"} {
|
|
cut := time.Now().UTC()
|
|
walk := inventory.Plan{ID: "plan-308-" + verb, Repository: "novox/mesh-catalog", Branch: "main",
|
|
Commit: "c8c8c8c8", Created: cut, State: inventory.PlanBuilding, Tiers: [][]string{{"app"}},
|
|
Modules: map[string]*inventory.PlanModule{"app": {}},
|
|
Times: &inventory.PlanTimes{Cut: &cut, Started: &cut, Class: inventory.ClassLeaf}}
|
|
if err := open.inventory.SavePlan(ctx, &walk); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := plansCommand(ctx, []string{verb, walk.ID, "--why", "a test"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ended := next(inventory.WalkEnded)
|
|
if ended["walk"] != walk.ID || ended["outcome"] != inventory.WalkStopped {
|
|
t.Fatalf("plans %s: the end is %v", verb, ended)
|
|
}
|
|
p, err := open.inventory.PlanByID(ctx, walk.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Outcome() != inventory.WalkStopped || p.Delivery == nil || p.Delivery.StoppedWhy != "a test" {
|
|
t.Fatalf("plans %s kept the walk %s, %+v", verb, p.Outcome(), p.Delivery)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAWalkRetriedByAPersonIsSaidStartedAgain(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
asksRecorded(t)
|
|
next := planMovedHeard(t)
|
|
twoTiers(t, open)
|
|
before := time.Now().UTC().Add(-time.Hour)
|
|
ended := before.Add(time.Minute)
|
|
failed := inventory.Plan{ID: "plan-308-retry", Repository: "novox/a", Branch: "main", Commit: "c0ffee",
|
|
Created: before, State: inventory.PlanFailed, Tier: 0, Tiers: [][]string{{"a"}, {"b"}},
|
|
Note: "a failed to build in tier 0",
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "failed", AskedAt: &before, Build: "build-1",
|
|
Why: link.KilledByHand}},
|
|
Times: &inventory.PlanTimes{Cut: &before, Started: &before, Ended: &ended, Class: inventory.ClassLeaf}}
|
|
if err := open.inventory.SavePlan(ctx, &failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := plansCommand(ctx, []string{"retry", failed.ID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
started := next(inventory.WalkStarted)
|
|
if started["walk"] != failed.ID || started["budget_class"] != inventory.ClassLeaf {
|
|
t.Fatalf("the retried start: %v", started)
|
|
}
|
|
}
|
|
|
|
func TestABacklogReleasedByAPersonIsSaidStarted(t *testing.T) {
|
|
b := aBacklog(t)
|
|
next := planMovedHeard(t)
|
|
if err := backlogCommand(t.Context(), "release-backlog", []string{"--why", "a test"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
started := next(inventory.WalkStarted)
|
|
if p := b.release(t); started["walk"] != p.ID {
|
|
t.Fatalf("the release's start: %v", started)
|
|
}
|
|
}
|
|
|
|
func asTyped(t *testing.T, args ...string) error {
|
|
t.Helper()
|
|
was := os.Args
|
|
os.Args = append([]string{"mesh-controller"}, args...)
|
|
t.Cleanup(func() { os.Args = was })
|
|
err := run()
|
|
// run gives back the lease as the process would end; the test's process goes on, and takes it again.
|
|
theLease.mu.Lock()
|
|
theLease.held, theLease.kv, theLease.noBus = nil, nil, false
|
|
theLease.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
func TestAWalkEndedByCancelOrClearIsSaidEnded(t *testing.T) {
|
|
_, ask := aBuildQueue(t)
|
|
open := aMesh(t)
|
|
next := planMovedHeard(t)
|
|
twoTiers(t, open)
|
|
for _, verb := range [][]string{{"cancel"}, {"clear"}} {
|
|
id := link.NewBuildID(time.Now())
|
|
ask(id, "https://forge.example/novox/a.git")
|
|
asked, _ := link.BuildAskedAt(id)
|
|
walk := aStartedWalkAsking(t, open, "plan-308-"+verb[0], id, asked)
|
|
args := verb
|
|
if verb[0] == "cancel" {
|
|
args = append(args, id)
|
|
}
|
|
if err := asTyped(t, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ended := next(inventory.WalkEnded)
|
|
if ended["walk"] != walk.ID || ended["outcome"] != inventory.WalkFailed {
|
|
t.Fatalf("%s: the end is %v", verb[0], ended)
|
|
}
|
|
// Ended, and so the next walk may be cut on the same repository.
|
|
p, _ := open.inventory.PlanByID(t.Context(), walk.ID)
|
|
if p.State != inventory.PlanFailed {
|
|
t.Fatalf("%s left the walk %s", verb[0], p.State)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAWalkRestartedByARebuildIsSaidStarted(t *testing.T) {
|
|
open := aMesh(t)
|
|
asksRecorded(t)
|
|
next := planMovedHeard(t)
|
|
twoTiers(t, open)
|
|
before := time.Now().UTC().Add(-time.Hour)
|
|
ended := before.Add(time.Minute)
|
|
failed := inventory.Plan{ID: "plan-308-rebuild", Repository: "novox/a", Branch: "main", Commit: "c0ffee",
|
|
Created: before, State: inventory.PlanFailed, Tiers: [][]string{{"a"}, {"b"}},
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "failed", AskedAt: &before, Build: "build-1",
|
|
Why: link.CancelledByHand}},
|
|
Times: &inventory.PlanTimes{Cut: &before, Started: &before, Ended: &ended, Class: inventory.ClassLeaf}}
|
|
if err := open.inventory.SavePlan(t.Context(), &failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := asTyped(t, "rebuild", "a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if started := next(inventory.WalkStarted); started["walk"] != failed.ID {
|
|
t.Fatalf("the rebuilt walk's start: %v", started)
|
|
}
|
|
}
|
|
|
|
func TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
asksRecorded(t)
|
|
twoTiers(t, open)
|
|
for _, again := range [][]string{{"plans", "retry"}, {"rebuild", "a"}} {
|
|
before := time.Now().UTC().Add(-time.Hour)
|
|
walk := aStartedWalkAsking(t, open, "plan-308-again-"+again[0], "build-1", before)
|
|
if err := asTyped(t, "plans", "stop", walk.ID, "--why", "a test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The build asked before the stop fails after it.
|
|
p, _ := open.inventory.PlanByID(ctx, walk.ID)
|
|
p.Modules["a"].State, p.Modules["a"].Why = "failed", "it broke"
|
|
if err := open.inventory.SavePlan(ctx, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args := again
|
|
if again[0] == "plans" {
|
|
args = append(append([]string(nil), again...), walk.ID)
|
|
}
|
|
if err := asTyped(t, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p, _ = open.inventory.PlanByID(ctx, walk.ID)
|
|
if p.State != inventory.PlanBuilding || (p.Delivery != nil && p.Delivery.Stopped != "") {
|
|
t.Fatalf("%s: the walk is %s and keeps its stop: %+v", again[0], p.State, p.Delivery)
|
|
}
|
|
asking, _ := link.BuildAskedAt(p.Modules["a"].Build)
|
|
planBuilt(ctx, open, "a", "c0ffee", "it broke again", asking, p.Modules["a"].Build)
|
|
if p, _ = open.inventory.PlanByID(ctx, walk.ID); p.State != inventory.PlanFailed || p.Outcome() != inventory.WalkFailed {
|
|
t.Fatalf("%s: failing again, the walk is %s and ends %s", again[0], p.State, p.Outcome())
|
|
}
|
|
}
|
|
}
|