//go:build integration package main import ( "errors" "reflect" "slices" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" ) func TestAStopWithoutAMergeWalksTheOthersAgain(t *testing.T) { open := windowed(t) groupsWaiting(t, "pr/cat1aaaaaaaa") endsItsLine(t) asksRecorded(t) ctx := t.Context() cat := catalogueMerge("cat1aaaaaaaa", "app", t0) one := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), cat.Head) hear(t, open, cat, t0.Add(time.Second)) hear(t, open, one, t0.Add(6*time.Second)) cutAt(t, open, t0.Add(2*time.Minute)) ws, _ := walks(t, open) if len(ws) != 1 || !ws[0].Open() { t.Fatalf("no open walk to stop: %+v", ws) } stopped := ws[0] p, said, err := stopWalkWithout(ctx, open.inventory, stopped.ID, "mesh-delivery for jochen", "its group stopped", []string{"novox/mesh-catalog@" + cat.Commit}, t0.Add(3*time.Minute)) if err != nil { t.Fatal(err) } if p.State != inventory.PlanFailed || p.Delivery.Stopped == "" || p.Delivery.WalkedAgainBy == "" || !reflect.DeepEqual(p.Delivery.LeftOut, []inventory.PlanLeftOut{{Repository: "novox/mesh-catalog", Commit: cat.Commit}}) { t.Fatalf("the stopped walk reads %s %+v", p.State, p.Delivery) } if said.WalkedAgainBy != p.Delivery.WalkedAgainBy || !reflect.DeepEqual(said.WalkedAgain, []string{"novox/one@" + one.Commit}) { t.Fatalf("the stop answered %+v", said) } again, err := open.inventory.MergesOf(ctx, p.Delivery.WalkedAgainBy) if err != nil || len(again) != 1 || again[0].Commit != one.Commit { t.Fatalf("the fresh batch answers %+v (%v); want the other repository's merge alone", again, err) } if left, _ := open.inventory.MergesOf(ctx, stopped.ID); len(left) != 1 || left[0].Commit != cat.Commit { t.Fatalf("the stopped walk still answers %+v; want the left-out merge alone", left) } cutAt(t, open, t0.Add(4*time.Minute)) fresh, err := open.inventory.PlanByID(ctx, p.Delivery.WalkedAgainBy) if err != nil || fresh.Batch() || !fresh.Open() { t.Fatalf("the fresh batch was not cut into a walk: %s %v", fresh.State, err) } if _, in := fresh.Modules["one"]; !in { t.Fatalf("the fresh walk does not build one: %v", fresh.Modules) } if _, in := fresh.Modules["app"]; in { t.Fatalf("the fresh walk builds app, whose merge was left out: %v", fresh.Modules) } if len(fresh.Delivery.Merges) != 1 || fresh.Delivery.Merges[0].Commit != one.Commit { t.Fatalf("the fresh walk answers %+v", fresh.Delivery.Merges) } // A stopped walk starts no search of its own (ADR 0276 decision 3 is for a failed one). cutAt(t, open, t0.Add(5*time.Minute)) if alone, _ := open.inventory.AloneMerges(ctx); len(alone) != 0 { t.Fatalf("a merge of the stopped walk waits to be walked alone: %+v", alone) } } func TestAStopWithoutAMergeEndsTheMergesThatContainIt(t *testing.T) { open := windowed(t) groupsWaiting(t, "pr/cat1aaaaaaaa") endsItsLine(t) asksRecorded(t) ctx := t.Context() first := catalogueMerge("cat1aaaaaaaa", "app", t0) later := catalogueMerge("cat2cccccccc", "notes", t0.Add(10*time.Second)) one := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), first.Head) hear(t, open, first, t0.Add(time.Second)) hear(t, open, one, t0.Add(6*time.Second)) hear(t, open, later, t0.Add(11*time.Second)) cutAt(t, open, t0.Add(2*time.Minute)) ws, _ := walks(t, open) walk := ws[0] if _, _, err := stopWalkWithout(ctx, open.inventory, walk.ID, "mesh-delivery for jochen", "x", []string{"novox/mesh-catalog@ffffffffffff"}, t0.Add(3*time.Minute)); err == nil { t.Fatal("a stop leaving out a merge the walk does not answer was taken") } if p, _ := open.inventory.PlanByID(ctx, walk.ID); !p.Open() { t.Fatalf("a refused stop ended the walk: %s", p.State) } p, said, err := stopWalkWithout(ctx, open.inventory, walk.ID, "mesh-delivery for jochen", "its group stopped", []string{"novox/mesh-catalog@" + first.Commit}, t0.Add(3*time.Minute)) if err != nil { t.Fatal(err) } want := []inventory.PlanLeftOut{{Repository: "novox/mesh-catalog", Commit: first.Commit}, {Repository: "novox/mesh-catalog", Commit: later.Commit, Contains: first.Commit}} if !reflect.DeepEqual(p.Delivery.LeftOut, want) || !reflect.DeepEqual(said.LeftOut, want) { t.Fatalf("left out %+v, said %+v; want %+v", p.Delivery.LeftOut, said.LeftOut, want) } if !strings.Contains(p.Note, later.Commit[:8]) || !strings.Contains(p.Note, p.Delivery.WalkedAgainBy) { t.Fatalf("the stopped walk's note does not name the merge ended with it and the fresh batch: %q", p.Note) } again, _ := open.inventory.MergesOf(ctx, p.Delivery.WalkedAgainBy) if len(again) != 1 || again[0].Commit != one.Commit { t.Fatalf("walked again: %+v; want the other repository's merge alone", again) } // Nothing left once the other repository's merge is left out too: the fresh walk is stopped, and nothing more. cutAt(t, open, t0.Add(4*time.Minute)) fresh, _ := open.inventory.PlanByID(ctx, p.Delivery.WalkedAgainBy) q, said, err := stopWalkWithout(ctx, open.inventory, fresh.ID, "mesh-delivery for jochen", "that one too", []string{"novox/one@" + one.Commit}, t0.Add(5*time.Minute)) if err != nil || q.State != inventory.PlanFailed || q.Delivery.WalkedAgainBy != "" || said.WalkedAgainBy != "" { t.Fatalf("a stop leaving nothing to walk: %s %+v %+v %v", q.State, q.Delivery, said, err) } if _, bs := walks(t, open); len(bs) != 0 { t.Fatalf("a batch of nothing was made: %+v", bs) } } func TestAStopWalksTheOthersAgainInTheOpenBatch(t *testing.T) { open := windowed(t) groupsWaiting(t, "pr/cat1aaaaaaaa") endsItsLine(t) asksRecorded(t) ctx := t.Context() cat := catalogueMerge("cat1aaaaaaaa", "app", t0) one1 := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), cat.Head) hear(t, open, cat, t0.Add(time.Second)) hear(t, open, one1, t0.Add(6*time.Second)) cutAt(t, open, t0.Add(2*time.Minute)) ws, _ := walks(t, open) walk := ws[0] one2 := repoMerge("one", "one2cccccccc", t0.Add(3*time.Minute)) hear(t, open, one2, t0.Add(3*time.Minute)) _, bs := walks(t, open) if len(bs) != 1 { t.Fatalf("the merge heard while the walk ran is in %d batches", len(bs)) } waiting := bs[0] p, said, err := stopWalkWithout(ctx, open.inventory, walk.ID, "mesh-delivery for jochen", "its group stopped", []string{"novox/mesh-catalog@" + cat.Commit}, t0.Add(3*time.Minute+10*time.Second)) if err != nil { t.Fatal(err) } if p.Delivery.WalkedAgainBy != waiting.ID || said.WalkedAgainBy != waiting.ID { t.Fatalf("walked again by %q (said %q); want the open batch %s", p.Delivery.WalkedAgainBy, said.WalkedAgainBy, waiting.ID) } if _, bs := walks(t, open); len(bs) != 1 { t.Fatalf("%d batches; want the open one alone", len(bs)) } b, _ := open.inventory.PlanByID(ctx, waiting.ID) if b.Delivery.Batch == nil || !b.Delivery.Batch.Closed || !slices.Contains(b.Delivery.Batch.Withheld, "app") { t.Fatalf("the open batch reads %+v; want its window closed and app withheld", b.Delivery.Batch) } // Its window closed by the stop, not by the clock: cut at once. cutAt(t, open, t0.Add(3*time.Minute+11*time.Second)) fresh, _ := open.inventory.PlanByID(ctx, waiting.ID) if fresh.Batch() || fresh.CommitOf("novox/one") != one2.Commit { t.Fatalf("the batch is %s at %q; want it cut, walking the newer commit", fresh.State, fresh.CommitOf("novox/one")) } var answered []string for _, m := range fresh.Delivery.Merges { answered = append(answered, m.Commit+">"+m.Carried) } if !slices.Contains(answered, one1.Commit+">"+one2.Commit) || !slices.Contains(answered, one2.Commit+">") { t.Fatalf("the walk answers %v; want the earlier merge carried by the later", answered) } } func TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt(t *testing.T) { open := windowed(t) groupsWaiting(t, "pr/cat1aaaaaaaa") endsItsLine(t) asksRecorded(t) ctx := t.Context() cat := catalogueMerge("cat1aaaaaaaa", "app", t0) one1 := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), cat.Head) hear(t, open, cat, t0.Add(time.Second)) hear(t, open, one1, t0.Add(6*time.Second)) cutAt(t, open, t0.Add(2*time.Minute)) ws, _ := walks(t, open) walk := ws[0] sent := t0.Add(150 * time.Second) for name, m := range walk.Modules { if name == "app" || name == "one" { m.First, m.FirstAt = []string{"anchor"}, &sent } } if err := open.inventory.SavePlan(ctx, &walk); err != nil { t.Fatal(err) } cat2 := catalogueMerge("cat2dddddddd", "notes", t0.Add(3*time.Minute)) hear(t, open, cat2, t0.Add(3*time.Minute)) _, said, err := stopWalkWithout(ctx, open.inventory, walk.ID, "mesh-delivery for jochen", "its group stopped", []string{"novox/mesh-catalog@" + cat.Commit}, t0.Add(3*time.Minute+10*time.Second)) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(said.Kept, []inventory.PlanKept{{Module: "app", Machines: []string{"anchor"}}}) { t.Fatalf("kept %+v; want app on anchor alone (one is walked again)", said.Kept) } p, _ := open.inventory.PlanByID(ctx, walk.ID) if !reflect.DeepEqual(p.Delivery.Kept, said.Kept) || !strings.Contains(p.Note, "app on anchor") { t.Fatalf("the stopped walk's record does not name what stays: %+v %q", p.Delivery.Kept, p.Note) } if len(said.StillCarriedBy) != 1 || !strings.Contains(said.StillCarriedBy[0], cat2.Commit) { t.Fatalf("still carried by %v; want the later catalogue merge waiting in the batch", said.StillCarriedBy) } if _, err := retryPlan(ctx, open, walk.ID); err == nil || !strings.Contains(err.Error(), "was stopped without") { t.Fatalf("a walk stopped without some merges was retried, or refused for another reason: %v", err) } } func TestAStopWithoutAMergeLeavesItOutOfABatch(t *testing.T) { open := windowed(t) everyHeadWaits(t) endsItsLine(t) asksRecorded(t) ctx := t.Context() cat := catalogueMerge("cat1aaaaaaaa", "app", t0) one := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), cat.Head) hear(t, open, cat, t0.Add(time.Second)) hear(t, open, one, t0.Add(6*time.Second)) _, bs := walks(t, open) b := bs[0] r, said, err := stopWalkWithout(ctx, open.inventory, b.ID, "mesh-delivery for jochen", "its group stopped", []string{"novox/mesh-catalog@" + cat.Commit}, t0.Add(10*time.Second)) if err != nil { t.Fatal(err) } if said.WalkedAgainBy != b.ID || said.Stopped == b.ID { t.Fatalf("said %+v; want a stopped record of its own, and the batch walking the others", said) } if left, _ := open.inventory.MergesOf(ctx, said.Stopped); len(left) != 1 || left[0].Commit != cat.Commit { t.Fatalf("the stopped record holds %+v", left) } if stopped, _ := open.inventory.PlanByID(ctx, said.Stopped); stopped.State != inventory.PlanFailed || stopped.Delivery.Stopped == "" || stopped.Delivery.WalkedAgainBy != b.ID { t.Fatalf("the stopped record reads %s %+v", stopped.State, stopped.Delivery) } _ = r cutAt(t, open, t0.Add(3*time.Minute)) w, _ := open.inventory.PlanByID(ctx, b.ID) if _, in := w.Modules["app"]; in || w.Batch() { t.Fatalf("the batch's walk is %s and builds %v; want it cut without app", w.State, w.Modules) } // A batch whose every merge is left out is stopped itself. hear(t, open, catalogueMerge("cat3eeeeeeee", "notes", t0.Add(4*time.Minute)), t0.Add(4*time.Minute)) _, bs = walks(t, open) last := bs[0] if _, said, err := stopWalkWithout(ctx, open.inventory, last.ID, "mesh-delivery for jochen", "x", []string{"novox/mesh-catalog@cat3eeeeeeee"}, t0.Add(4*time.Minute+time.Second)); err != nil || said.Stopped != last.ID { t.Fatalf("a batch left with nothing: %+v %v", said, err) } if p, _ := open.inventory.PlanByID(ctx, last.ID); p.State != inventory.PlanFailed || p.Batch() { t.Fatalf("the emptied batch is %s", p.State) } } func TestSavingPlansAndMovingMergesIsOneAct(t *testing.T) { open := windowed(t) everyHeadWaits(t) endsItsLine(t) ctx := t.Context() hear(t, open, catalogueMerge("cat1aaaaaaaa", "app", t0), t0.Add(time.Second)) _, bs := walks(t, open) stale := bs[0] moved := stale moved.Note = "written by somebody else" if err := open.inventory.SavePlan(ctx, &moved); err != nil { t.Fatal(err) } fresh := inventory.Plan{ID: "plan-fresh", Repository: "novox/mesh-catalog", Branch: "main", Commit: "x", Created: t0, State: inventory.PlanAssembling, Tiers: [][]string{}, Modules: map[string]*inventory.PlanModule{}} err := open.inventory.SavePlansMoving(ctx, []*inventory.Plan{&fresh, &stale}, []inventory.MergeMove{{Repository: "novox/mesh-catalog", Commit: "cat1aaaaaaaa", Plan: "plan-fresh"}}) if !errors.Is(err, inventory.ErrPlanMoved) { t.Fatalf("a stale plan in the act: %v", err) } if _, err := open.inventory.PlanByID(ctx, "plan-fresh"); err == nil { t.Fatal("the new plan was written though the act was refused") } if m, _, _ := open.inventory.MergeOf(ctx, "novox/mesh-catalog", "cat1aaaaaaaa"); m.Plan != stale.ID { t.Fatalf("the merge moved to %q though the act was refused", m.Plan) } }