339 lines
14 KiB
Go
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)
|
|
}
|
|
}
|