mesh/merge-gate pass: builds mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery-group group feat/459-a-stop-re-walks-the-others checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
A merge walked again into a batch and then stopped by its group stayed in that batch, which was later let go. Any walk or batch that names a stopped delivery's merge is now asked to leave it out and is never let go; a refused leave-out stops the whole walk. A person's stop marks the delivery stopped only after its walk is, and the answer says what stays on the machines and what will still carry the change.
384 lines
16 KiB
Go
384 lines
16 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
|
|
}
|
|
|
|
// A group member stopped on a walk it shares with a lone delivery: the walk is stopped without the member's merge,
|
|
// never whole, and the lone delivery follows the walk that walks its merge again and is delivered by it.
|
|
func TestAGroupStopWalksTheOtherDeliveriesAgainWithoutIt(t *testing.T) {
|
|
s := open459(t, []string{"novox/a", "novox/b"}, []string{"novox/z"})
|
|
if containsString(s.ctl.delivers, "plan-2") {
|
|
t.Fatalf("plan-2 was let go before b's turn: %v", s.ctl.delivers)
|
|
}
|
|
s.failFirst()
|
|
if b := s.delivery(s.ids["novox/b"]); b.State != Stopped || !strings.Contains(lastWhy(b), s.ids["novox/a"]) {
|
|
t.Fatalf("b is %s (%q); want it stopped by its group, naming a", b.State, lastWhy(b))
|
|
}
|
|
if containsString(s.ctl.stops, "plan-2") {
|
|
t.Fatalf("plan-2 was ended whole %v, and with it z, which is in no group and was not stopped", s.ctl.stops)
|
|
}
|
|
if want := "plan-2 without novox/b@" + s.merges["novox/b"]; !containsString(s.ctl.withouts, want) {
|
|
t.Fatalf("the controller was asked %v; want %q", s.ctl.withouts, want)
|
|
}
|
|
s.heard("plan-2", "plan-2-again")
|
|
if containsString(s.ctl.delivers, "plan-2-again") {
|
|
t.Fatal("a batch not yet cut was given its word")
|
|
}
|
|
s.cutAgain("plan-2-again")
|
|
z := s.delivery(s.ids["novox/z"])
|
|
if z.State.Final() || z.Walk == nil || z.Walk.ID != "plan-2-again" || !containsString(s.ctl.delivers, "plan-2-again") {
|
|
t.Fatalf("z is %s on %+v, let go %v; want it following plan-2-again, let go", z.State, z.Walk, s.ctl.delivers)
|
|
}
|
|
s.done("plan-2-again")
|
|
if z, b := s.state(s.ids["novox/z"]), s.state(s.ids["novox/b"]); z != Delivered || b != Stopped {
|
|
t.Fatalf("once plan-2-again is done, z is %s and b %s; want z delivered and b stopped", z, b)
|
|
}
|
|
}
|
|
|
|
// Two members after the failed one, both on the shared walk: one ask leaves both out, so neither is walked again
|
|
// because its own stop came second.
|
|
func TestAGroupStopLeavesOutEveryMemberItStopsOnOneWalk(t *testing.T) {
|
|
s := open459(t, []string{"novox/a", "novox/b", "novox/c"}, []string{"novox/z"})
|
|
s.failFirst()
|
|
if len(s.ctl.withouts) != 1 || !strings.Contains(s.ctl.withouts[0], "novox/b@"+s.merges["novox/b"]) ||
|
|
!strings.Contains(s.ctl.withouts[0], "novox/c@"+s.merges["novox/c"]) {
|
|
t.Fatalf("the controller was asked %v; want one stop of plan-2 leaving out b and c", s.ctl.withouts)
|
|
}
|
|
s.heard("plan-2", "plan-2-again")
|
|
again := s.ctl.walk("plan-2-again")
|
|
if len(again.Delivery.Merges) != 1 || again.Delivery.Merges[0].Repository != "novox/z" {
|
|
t.Fatalf("plan-2-again walks %+v; want z's merge alone", again.Delivery.Merges)
|
|
}
|
|
for _, r := range []string{"novox/b", "novox/c"} {
|
|
if st := s.state(s.ids[r]); st != Stopped {
|
|
t.Fatalf("%s is %s", r, st)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A lone delivery whose merge is a later merge of the stopped member's repository contains the member's merge: it
|
|
// is left out and ends stopped, naming the merge it contains; the other lone delivery is walked again.
|
|
func TestAGroupStopEndsAMergeThatContainsTheStoppedOne(t *testing.T) {
|
|
s := open459(t, []string{"novox/a", "novox/b"}, []string{"novox/z", "novox/b2"})
|
|
// b2 is a later merge of b's repository: the walk carries it for b.
|
|
s.retarget("novox/b2", "novox/b")
|
|
s.failFirst()
|
|
s.heard("plan-2", "plan-2-again")
|
|
s.cutAgain("plan-2-again")
|
|
b2 := s.delivery(s.ids["novox/b2"])
|
|
if b2.State != Stopped || !strings.Contains(lastWhy(b2), shortOf(s.merges["novox/b"], 8)) ||
|
|
!strings.Contains(lastWhy(b2), "contains") {
|
|
t.Fatalf("b2 is %s (%q); want it stopped, naming b's merge it contains", b2.State, lastWhy(b2))
|
|
}
|
|
if z := s.delivery(s.ids["novox/z"]); z.Walk == nil || z.Walk.ID != "plan-2-again" || z.State.Final() {
|
|
t.Fatalf("z is %s on %+v; want it following plan-2-again", z.State, z.Walk)
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
|
|
// The reviewer's case (review of #221): group [a, b]; y and z ride plan-2 with b. y is stopped `way: again`, and the
|
|
// batch that walks plan-2's others again carries b and z. Then a fails: b is stopped by its group, and its merge is
|
|
// left out of that batch — never shipped by it — and z alone is walked and delivered.
|
|
func TestAMergeWalkedAgainAndThenStoppedByItsGroupNeverShips(t *testing.T) {
|
|
s := open459(t, []string{"novox/a", "novox/b"}, []string{"novox/y", "novox/z"})
|
|
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")
|
|
s.failFirst()
|
|
if b := s.state(s.ids["novox/b"]); b != Stopped {
|
|
t.Fatalf("b is %s", b)
|
|
}
|
|
if want := "plan-2-again without novox/b@" + s.merges["novox/b"]; !containsString(s.ctl.withouts, want) {
|
|
t.Fatalf("the controller was asked %v; want %q", s.ctl.withouts, want)
|
|
}
|
|
s.heard("plan-2-again", "plan-2-again-left")
|
|
s.cutAgain("plan-2-again")
|
|
again := s.ctl.walk("plan-2-again")
|
|
for _, m := range again.Delivery.Merges {
|
|
if m.Repository == "novox/b" {
|
|
t.Fatalf("the walk %s still carries b's stopped merge: %+v", again.ID, again.Delivery.Merges)
|
|
}
|
|
}
|
|
if !containsString(s.ctl.delivers, "plan-2-again") {
|
|
t.Fatalf("z's walk was not let go: %v", s.ctl.delivers)
|
|
}
|
|
s.done("plan-2-again")
|
|
if z := s.state(s.ids["novox/z"]); z != Delivered {
|
|
t.Fatalf("z is %s", 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 group stop the controller refuses to make without the merge (an older controller) stops the whole walk: the
|
|
// stopped change is held back, and the delivery riding with it ends stopped, saying why.
|
|
func TestARefusedLeaveOutStopsTheWholeWalk(t *testing.T) {
|
|
s := open459(t, []string{"novox/a", "novox/b"}, []string{"novox/z"})
|
|
s.ctl.refuseWithout = true
|
|
s.failFirst()
|
|
if len(s.ctl.withouts) != 1 || !containsString(s.ctl.stops, "plan-2") {
|
|
t.Fatalf("asked %v, ended whole %v; want the leave-out tried, then plan-2 ended whole", s.ctl.withouts, s.ctl.stops)
|
|
}
|
|
s.heard("plan-2")
|
|
if z := s.delivery(s.ids["novox/z"]); z.State != Stopped || !strings.Contains(lastWhy(z), "could not be left out") {
|
|
t.Fatalf("z is %s (%q); want it stopped, saying the leave-out was refused", z.State, lastWhy(z))
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|