Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue459_test.go
T

339 lines
14 KiB
Go

package main
import (
"fmt"
"strings"
"testing"
"time"
)
// novox/hq issue 459, ADR 0299: since the merge window (ADR 0276) one walk carries every merge of its batch. A group
// stop asked the controller to end the stopped member's walk, and with it every other delivery on it, which no one
// stopped. Now a group stop asks `delivery-stop` with the stopped members' merges to leave out: the walk ends, and
// the controller walks its other merges again in a fresh batch; a later merge of a stopped merge's repository
// contains it and ends with it, named. A person's stop of a delivery on a shared walk answers first with what
// ending the walk would take down, and acts only on a way given: `again` or `walk`.
// shape459 is a world of a delivery group feat/g (its members in the repositories group names, in that order) and
// lone deliveries (the repositories alone names), each checked and merged. The group's first member is on a walk of
// its own, plan-a; every other merge is carried by one walk, plan-2, waiting for its word.
type shape459 struct {
*world
ids, heads, merges map[string]string // by repository
}
func open459(t *testing.T, group, alone []string) *shape459 {
t.Helper()
w := newWorld(t)
s := &shape459{world: w, ids: map[string]string{}, heads: map[string]string{}, merges: map[string]string{}}
n := 0
add := func(repo, branch string) {
n++
head := fmt.Sprintf("%02d%s", n, strings.Repeat("h", 14))
s.ids[repo], s.heads[repo] = IDOf(repo, head), head
s.merges[repo] = fmt.Sprintf("%02d%s", n, strings.Repeat("m", 10))
w.h.PullUpdated(pr(repo, n, head, branch, "x"))
w.h.Checked(verdict(repo, n, head, "pass"))
}
for _, r := range group {
add(r, "feat/g")
}
for _, r := range alone {
add(r, "fix/"+r[strings.LastIndex(r, "/")+1:])
}
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/g", ID: "check-1", Verdict: "pass", Summary: "compose"})
w.settleAll()
for i, r := range append(append([]string{}, group...), alone...) {
w.h.PullMerged(merged(r, i+1, s.heads[r], s.merges[r]))
}
first := aWalk("plan-a", group[0], s.merges[group[0]], true)
first.Delivery.Merges = []WalkMerge{{Repository: group[0], Commit: s.merges[group[0]]}}
w.h.WalkMoved(w.ctl.put(first))
two := aWalk("plan-2", alone[0], s.merges[alone[0]], true)
two.Created = two.Created.Add(time.Minute)
for _, r := range append(append([]string{}, group[1:]...), alone...) {
two.Delivery.Merges = append(two.Delivery.Merges, WalkMerge{Repository: r, Commit: s.merges[r]})
}
w.h.WalkMoved(w.ctl.put(two))
w.settleAll()
return s
}
// failFirst fails the group's first member's walk: the members after it are stopped by their group.
func (s *shape459) failFirst() {
a := s.ctl.walk("plan-a")
a.State, a.Note = walkFailed, "the first member failed its gate on anchor"
s.h.WalkMoved(s.ctl.put(a))
s.settleAll()
}
// heard is the controller's records of the stopped walk and the walk that walks its merges again, as plan-moved says
// them, and then a walk done.
func (s *shape459) heard(walks ...string) {
for _, id := range walks {
s.h.WalkMoved(s.ctl.walk(id))
}
s.settleAll()
}
// cutAgain is the controller cutting the batch that walks a stopped walk's merges again, as plan-moved says it.
func (s *shape459) cutAgain(id string) {
s.h.WalkMoved(s.ctl.cut(id))
s.settleAll()
}
func (s *shape459) done(walk string) {
r := s.ctl.walk(walk)
sent := s.now
for m := range r.Modules {
r.Modules[m] = &WalkModule{State: "built", SentAt: &sent}
}
r.State = walkDone
s.h.WalkMoved(s.ctl.put(r))
s.settleAll()
}
func lastWhy(d *Delivery) string {
if len(d.Transitions) == 0 {
return ""
}
return d.Transitions[len(d.Transitions)-1].Why
}
// retarget makes the delivery of repo `later` a later merge of repository `of`: same repository, carried by plan-2
// for the merge of `of`.
func (s *shape459) retarget(later, of string) {
s.t.Helper()
s.h.mu.Lock()
d := s.h.deliveries[s.ids[later]]
d.Repository, d.MergedAt = of, d.MergedAt.Add(time.Minute)
s.h.mu.Unlock()
s.ctl.mu.Lock()
s.ctl.mergedAt[s.merges[of]], s.ctl.mergedAt[s.merges[later]] = d.MergedAt.Add(-time.Minute), d.MergedAt
s.ctl.mu.Unlock()
two := s.ctl.walk("plan-2")
merges := []WalkMerge{}
for _, m := range two.Delivery.Merges {
switch m.Repository {
case of:
m.Carried = s.merges[later]
case later:
m.Repository = of
}
merges = append(merges, m)
}
wd := *two.Delivery
wd.Merges = merges
two.Delivery = &wd
s.h.WalkMoved(s.ctl.put(two))
s.settleAll()
}
// shared is two lone deliveries, y and z, let go together on one walk, plan-2.
func shared459(t *testing.T) *shape459 {
t.Helper()
s := open459(t, []string{"novox/a"}, []string{"novox/y", "novox/z"})
for _, r := range []string{"novox/y", "novox/z"} {
if st := s.state(s.ids[r]); st != Delivering {
t.Fatalf("%s is %s, let go %v; want both delivering on plan-2", r, st, s.ctl.delivers)
}
}
return s
}
// A person's stop of y, which shares its walk with z, does nothing at first: it names z and the two ways.
func TestAPersonsStopOfASharedWalkAsksFirst(t *testing.T) {
s := shared459(t)
said, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "")
if err == nil {
t.Fatalf("the stop acted at once: %q", said)
}
for _, want := range []string{s.ids["novox/z"], "plan-2", "again", "walk", "not stopped"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("the stop said %q; want it to name %q", err, want)
}
}
if st := s.state(s.ids["novox/y"]); st != Delivering || len(s.ctl.stops)+len(s.ctl.withouts) != 0 {
t.Fatalf("y is %s, the controller was asked %v %v; want nothing done", st, s.ctl.stops, s.ctl.withouts)
}
if _, err := s.h.StopWay(s.ids["novox/y"], "x", "jochen", "sideways"); err == nil {
t.Fatal("a way that is neither again nor walk was taken")
}
}
// With `again`, y is stopped, the walk is stopped without y's merge, and z goes back to published and follows the
// walk that walks it again.
func TestAPersonsStopAgainWalksTheOthersAgain(t *testing.T) {
s := shared459(t)
said, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "again")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(said, "plan-2-again") || !strings.Contains(said, "novox/z@") {
t.Fatalf("the stop said %q; want it to name the walk that walks z again", said)
}
if want := "plan-2 without novox/y@" + s.merges["novox/y"]; !containsString(s.ctl.withouts, want) ||
containsString(s.ctl.stops, "plan-2") {
t.Fatalf("the controller was asked %v, ended whole %v; want %q", s.ctl.withouts, s.ctl.stops, want)
}
s.heard("plan-2")
if z := s.delivery(s.ids["novox/z"]); z.State != Published {
t.Fatalf("z is %s once plan-2 was stopped and walked again; want published, waiting for that walk", z.State)
}
s.heard("plan-2-again")
s.cutAgain("plan-2-again")
if z := s.delivery(s.ids["novox/z"]); z.Walk == nil || z.Walk.ID != "plan-2-again" || z.State != Delivering {
t.Fatalf("z is %s on %+v; want it delivering on plan-2-again", z.State, z.Walk)
}
if st := s.state(s.ids["novox/y"]); st != Stopped {
t.Fatalf("y is %s", st)
}
}
// With `walk`, the whole walk is stopped, and z ends stopped, naming y's stop.
func TestAPersonsStopOfTheWalkStopsEveryDelivery(t *testing.T) {
s := shared459(t)
if _, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "walk"); err != nil {
t.Fatal(err)
}
if !containsString(s.ctl.stops, "plan-2") || len(s.ctl.withouts) != 0 {
t.Fatalf("the controller was asked %v %v; want plan-2 ended whole", s.ctl.stops, s.ctl.withouts)
}
s.heard("plan-2")
z := s.delivery(s.ids["novox/z"])
if z.State != Stopped || !strings.Contains(lastWhy(z), s.ids["novox/y"]) {
t.Fatalf("z is %s (%q); want it stopped, naming y's stop", z.State, lastWhy(z))
}
}
// Whatever brought it there, a walk that carries a stopped delivery's merge is asked to leave it out, and is never
// given its word while it carries it.
func TestAWalkCarryingAStoppedMergeIsAskedToLeaveItOutAndNeverLetGo(t *testing.T) {
s := shared459(t)
if _, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "again"); err != nil {
t.Fatal(err)
}
// A walk the controller made that carries y's merge anyway, waiting for its word, with z on it.
x := aWalk("plan-x", "novox/z", s.merges["novox/z"], true)
x.Created = x.Created.Add(10 * time.Minute)
x.Delivery.Merges = []WalkMerge{{Repository: "novox/y", Commit: s.merges["novox/y"]},
{Repository: "novox/z", Commit: s.merges["novox/z"]}}
s.ctl.refuseWithout = true // it cannot be left out: the walk is stopped whole
s.h.WalkMoved(s.ctl.put(x))
s.settleAll()
if want := "plan-x without novox/y@" + s.merges["novox/y"]; !containsString(s.ctl.withouts, want) {
t.Fatalf("the controller was asked %v; want %q", s.ctl.withouts, want)
}
if containsString(s.ctl.delivers, "plan-x") {
t.Fatalf("a walk carrying a stopped merge was let go: %v", s.ctl.delivers)
}
if !containsString(s.ctl.stops, "plan-x") {
t.Fatalf("refused the leave-out, the walk was not stopped whole: %v", s.ctl.stops)
}
}
// A person's stop the controller cannot make leaves the delivery as it was, and says how to go on.
func TestAPersonsStopThatCannotBeMadeStopsNothing(t *testing.T) {
s := shared459(t)
s.ctl.refuseWithout = true
_, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "again")
if err == nil || !strings.Contains(err.Error(), "is not stopped") || !strings.Contains(err.Error(), "plans stop") {
t.Fatalf("the refused stop said %v", err)
}
if st := s.state(s.ids["novox/y"]); st != Delivering {
t.Fatalf("y is %s; want it delivering, its stop not made", st)
}
}
// What a stop that left merges out says: the walk that walks the others, what it left out and what each contains,
// as the controller says it, what stays on which machines, and what will still deliver the stopped change.
func TestAStopSaysWhatStaysAndWhatStillCarriesIt(t *testing.T) {
said := againWords(StopAnswer{Stopped: "plan-2", WalkedAgainBy: "plan-3", WalkedAgain: []string{"novox/z@zz"},
LeftOut: []WalkLeftOut{{Repository: "novox/b", Commit: "b1b1b1b1b1b1"},
{Repository: "novox/b", Commit: "b2b2b2b2b2b2", Contains: "b1b1b1b1b1b1"}},
Kept: []WalkKept{{Module: "lab", Machines: []string{"anchor"}}},
StillCarriedBy: []string{"novox/b@b3b3 (in plan-4)"}})
for _, want := range []string{"plan-3", "novox/z@zz", "b2b2b2b2b2b2 (contains b1b1b1b1b1b1)", "lab on anchor",
"stays on the machines", "novox/b@b3b3 (in plan-4)", "revert"} {
if !strings.Contains(said, want) {
t.Fatalf("the stop said %q; want %q in it", said, want)
}
}
}
// A person's first answer names the deliveries that would be left out for moving a module this merge moves, and says
// the controller may leave out more.
func TestAPersonsFirstAnswerNamesWhatSharesItsModules(t *testing.T) {
s := shared459(t)
_, err := s.h.StopWay(s.ids["novox/y"], "x", "jochen", "")
if err == nil || !strings.Contains(err.Error(), "for moving a module this merge moves: "+s.ids["novox/z"]+" (moves app)") ||
!strings.Contains(err.Error(), "may leave out more") {
t.Fatalf("the first answer said %v", err)
}
}
// The safety net holds back only what a person or a group stopped: a delivery ended by what its walk did (the
// observed rows, which name nobody) is not asked out of a walk that carries its merge.
func TestTheSafetyNetLeavesAStrandedStopAlone(t *testing.T) {
s := shared459(t)
s.h.mu.Lock()
y := s.h.deliveries[s.ids["novox/y"]]
y.State = Stopped
y.Transitions = append(y.Transitions, Transition{At: s.now, From: Published, To: Stopped, Event: EvStop,
Why: "its walk ended done before its word"})
s.h.mu.Unlock()
s.settleAll()
if len(s.ctl.withouts) != 0 {
t.Fatalf("a stop nobody asked for was held back: %v", s.ctl.withouts)
}
}
// A walk the safety net holds — its leave-out and its whole stop both refused — says why on the deliveries waiting on
// it, in `waits`, and in `stalled` past their bound.
func TestAHeldWalkIsSaidOnItsDeliveriesAndStalls(t *testing.T) {
s := shared459(t)
if _, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "again"); err != nil {
t.Fatal(err)
}
s.ctl.refuseWithout, s.ctl.refuseStop = true, true
x := aWalk("plan-x", "novox/z", s.merges["novox/z"], true)
x.Created = x.Created.Add(10 * time.Minute)
x.Delivery.Merges = []WalkMerge{{Repository: "novox/y", Commit: s.merges["novox/y"]},
{Repository: "novox/z", Commit: s.merges["novox/z"]}}
s.h.WalkMoved(s.ctl.put(x))
s.settleAll()
z := s.delivery(s.ids["novox/z"])
if !strings.Contains(z.HeldBack, "plan-x") || !strings.Contains(z.HeldBack, "refused") {
t.Fatalf("z says it is held back by %q; want plan-x and the refusal", z.HeldBack)
}
if w := s.h.waitsFor(z); w != z.HeldBack {
t.Fatalf("z waits for %q", w)
}
s.later(Bounds[Published].For + time.Minute)
s.settleAll()
for _, l := range s.h.Stalled() {
if l.ID == z.ID && strings.Contains(l.Says, "plan-x") {
return
}
}
t.Fatalf("z is not listed stalled naming plan-x: %+v", s.h.Stalled())
}
// A person's stop of a delivery whose merge waits in a batch asks that batch now to leave it out, and says so.
func TestAPersonsStopOfAMergeInABatchAsksTheBatch(t *testing.T) {
s := shared459(t)
if _, err := s.h.StopWay(s.ids["novox/y"], "not wanted after all", "jochen", "again"); err != nil {
t.Fatal(err)
}
s.heard("plan-2", "plan-2-again")
said, err := s.h.StopWay(s.ids["novox/z"], "this one too", "jochen", "")
if err != nil {
t.Fatal(err)
}
if want := "plan-2-again without novox/z@" + s.merges["novox/z"]; !containsString(s.ctl.withouts, want) ||
!strings.Contains(said, "batch plan-2-again") {
t.Fatalf("said %q, asked %v; want the batch asked now, and said", said, s.ctl.withouts)
}
}