A held delivery asked a person a question with no right answer: four hq merges that build nothing, merges adding a module the controller sends nowhere, controller merges whose change a later walk of the same trunk already delivered, and a group member held because its composed check had not yet answered. Only stop could end them, which marks a delivered change as an error on its pull request. - A merge whose plan moves no module (nothing, or only a new module sent nowhere) is delivered at once. - A new final state, covered: every module a merge moves was sent by a finished walk of a later merge into the same trunk, which contains it. Shown as success on the pull request; a group counts it as delivered. - A group member waits published while its composed check is asked and unanswered, and a member held for it is lifted back to published once it passes for the heads that merged. Read again each tick, so the deliveries already held end by themselves after the rollout.
236 lines
9.4 KiB
Go
236 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The table walked (novox/hq ADR 0239 decision 2): every row it holds can be taken when its guard holds and
|
|
// is refused when it does not; every pair it does not hold is refused, by name; and the rules the table
|
|
// exists for are true of the table itself, not of the code around it.
|
|
|
|
// aDeliveryFor is a delivery whose facts make the row's guard hold.
|
|
func aDeliveryFor(r Row) (*Delivery, Facts) {
|
|
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
|
d := &Delivery{ID: "novox/app@c0ffee000000", Repository: "novox/app", Commit: "c0ffee000000", Number: 7}
|
|
f := Facts{Now: now}
|
|
switch r.Event {
|
|
case EvAppeared:
|
|
d.MergedAs, d.HeldWhy = "c0ffee000000", "no pull request"
|
|
case EvAdopted, EvGo:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkRolling}
|
|
if r.From[0] == Held {
|
|
d.Walk.WaitedFor, d.Walk.GoBy = "mesh-delivery", "a person (jochen)"
|
|
}
|
|
case EvNewHead:
|
|
d.NewerHead = "beef"
|
|
case EvClosed:
|
|
d.ClosedUnmerged = true
|
|
case EvMerged:
|
|
d.MergedAs, d.Walk = "aaaa1111", &WalkSeen{ID: "plan-1", State: walkBuilding, Waits: true, WaitedFor: "mesh-delivery"}
|
|
case EvMergedUnchecked:
|
|
d.MergedAs = "aaaa1111"
|
|
case EvHistory, EvRetire:
|
|
d.MergedAs, d.FromForge = "aaaa1111", true
|
|
d.MergedAt, d.Created = now.Add(-48*time.Hour), now
|
|
if r.Event == EvHistory {
|
|
d.History = "merged two days before this owner heard of it"
|
|
} else {
|
|
f.Why, f.By = "history adopted at the switch", "jochen"
|
|
}
|
|
case EvChecked, EvAccepted:
|
|
d.Check = &Verdict{Gate: "pass"}
|
|
case EvRefused:
|
|
d.Check = &Verdict{Gate: "fail"}
|
|
case EvRecheck, EvRelease, EvStop:
|
|
f.Why, f.By = "a person's reason", "jochen"
|
|
if r.Event == EvStop && !r.Act {
|
|
f = Facts{Now: now}
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkFailed, StoppedBy: "mesh-delivery for jochen"}
|
|
}
|
|
case EvReask:
|
|
f.Why, f.By = "delivery stalled", byH2
|
|
d.CheckAsk = &CheckAsk{ID: "check-1", At: now.Add(-2 * time.Hour), By: byOwner}
|
|
case EvHold:
|
|
d.HeldWhy, d.MergedAs = "its group's check did not pass", "aaaa1111"
|
|
case EvDone:
|
|
switch {
|
|
case r.From[0] == Published:
|
|
d.Plan = &DeliveryPlan{Summary: "moves nothing"}
|
|
case r.From[0] == Proposed:
|
|
d.MergedAs, d.Plan = "aaaa1111", &DeliveryPlan{Summary: "builds nothing"}
|
|
default:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone}
|
|
}
|
|
case EvCovered:
|
|
d.MergedAs, d.CoveredBy = "aaaa1111", "delivered with novox/app@bbbb (merged as bbbb2222, walk plan-2): app"
|
|
case EvLifted:
|
|
d.MergedAs, d.Lifted = "aaaa1111", "its group's composed check passed"
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkBuilding, Waits: true, WaitedFor: "mesh-delivery"}
|
|
case EvFailed:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkFailed}
|
|
case EvSuperseded:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkSuperseded}
|
|
}
|
|
return d, f
|
|
}
|
|
|
|
func TestEveryRowOfTheTableIsTakenWhenItsGuardHoldsAndRefusedWhenNot(t *testing.T) {
|
|
for _, r := range Table {
|
|
for _, from := range r.From {
|
|
d, f := aDeliveryFor(r)
|
|
d.State = from
|
|
got, err := Apply(d, r.Event, r.Act, f)
|
|
if err != nil {
|
|
t.Errorf("%s —%s→ %s was refused with its guard holding: %v", orNothing(from), r.Event, r.To, err)
|
|
continue
|
|
}
|
|
if d.State != r.To || got.From != from || got.To != r.To || len(d.Transitions) != 1 {
|
|
t.Errorf("%s —%s→ left it %s, transition %+v", orNothing(from), r.Event, d.State, got)
|
|
}
|
|
// The guard not holding: a delivery with no facts.
|
|
bare := &Delivery{ID: "novox/app@bare", State: from, Check: &Verdict{Gate: "pass"}}
|
|
if r.Event == EvAccepted || r.Event == EvChecked {
|
|
bare.Check = nil
|
|
if r.Event == EvAccepted {
|
|
bare.Check = &Verdict{Gate: "fail"}
|
|
}
|
|
}
|
|
if r.Event == EvRefused {
|
|
bare.Check = &Verdict{Gate: "pass"}
|
|
}
|
|
if _, err := Apply(bare, r.Event, r.Act, Facts{Now: f.Now}); err == nil {
|
|
t.Errorf("%s —%s→ %s was taken with no fact for its guard", orNothing(from), r.Event, r.To)
|
|
} else if bare.State != from || len(bare.Transitions) != 0 {
|
|
t.Errorf("a refused transition moved the delivery to %s", bare.State)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) {
|
|
events := []Event{EvAnnounced, EvAppeared, EvAdopted, EvChecked, EvAccepted, EvRefused, EvRecheck, EvReask, EvNewHead,
|
|
EvClosed, EvMerged, EvMergedUnchecked, EvGo, EvHold, EvRelease, EvDone, EvFailed, EvSuperseded, EvStop,
|
|
EvHistory, EvRetire, EvCovered, EvLifted}
|
|
for _, from := range append([]State{None}, AllStates...) {
|
|
for _, ev := range events {
|
|
for _, act := range []bool{false, true} {
|
|
if rowFor(from, ev, act) != nil {
|
|
continue
|
|
}
|
|
d := &Delivery{ID: "novox/app@c0ffee", State: from, MergedAs: "x", NewerHead: "y", Check: &Verdict{Gate: "pass"},
|
|
Walk: &WalkSeen{ID: "p", State: walkDone}, HeldWhy: "h", ClosedUnmerged: true, Number: 1, Commit: "c"}
|
|
_, err := Apply(d, ev, act, Facts{Now: time.Now(), Why: "w", By: "b"})
|
|
var refused ErrRefused
|
|
if !errors.As(err, &refused) || refused.Event != ev || refused.From != from {
|
|
t.Errorf("%s —%s (act %v)→ was not refused by name: %v", orNothing(from), ev, act, err)
|
|
}
|
|
if d.State != from {
|
|
t.Errorf("a pair the table does not hold moved %s to %s", orNothing(from), d.State)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The rules the table is for, read off the table.
|
|
func TestTheTableKeepsItsRules(t *testing.T) {
|
|
for _, r := range Table {
|
|
for _, from := range r.From {
|
|
// Nothing leaves a final state.
|
|
if from.Final() {
|
|
t.Errorf("%s is final and the table leaves it by %s", from, r.Event)
|
|
}
|
|
// Nothing goes back before the trunk once a delivery is on it.
|
|
if (from == Published || from == Delivering || from == Held) &&
|
|
(r.To == Proposed || r.To == Checked || r.To == Ready || r.To == Rejected) {
|
|
t.Errorf("%s goes back to %s by %s", from, r.To, r.Event)
|
|
}
|
|
}
|
|
// The trunk rule: only a ready delivery is published, by its merge — or a held one published again when
|
|
// what held it is gone (issue 316); nothing off the trunk delivers.
|
|
if r.To == Published && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) &&
|
|
!(r.Event == EvLifted && len(r.From) == 1 && r.From[0] == Held) {
|
|
t.Errorf("%v —%s→ published: only a ready delivery's merge publishes", r.From, r.Event)
|
|
}
|
|
// Nothing a person did not decide ends a delivery as stopped or failed by observation from held.
|
|
if contains(r.From, Held) && !r.Act && (r.To == Stopped || r.To == Failed) {
|
|
t.Errorf("held —%s→ %s by observation", r.Event, r.To)
|
|
}
|
|
if r.To == Delivering {
|
|
for _, from := range r.From {
|
|
if from == Proposed || from == Checked || from == Ready || from == Rejected {
|
|
t.Errorf("%s delivers by %s without reaching the trunk", from, r.Event)
|
|
}
|
|
}
|
|
}
|
|
// Held is left only by a person or a walk that started on a word that was not this owner's.
|
|
if contains(r.From, Held) && r.To == Delivering && r.Event == EvRelease && !r.Act {
|
|
t.Error("a held delivery is released by observation")
|
|
}
|
|
}
|
|
// Every state that is not final has a bound or is the operator's to wait in, and every H2 transition is
|
|
// a row the table holds from that state: observed once the walk's record is read again, or H2's own ask.
|
|
for state, b := range Bounds {
|
|
for _, ev := range b.H2 {
|
|
if rowFor(state, ev, false) == nil && rowFor(state, ev, true) == nil {
|
|
t.Errorf("H2 may take %s from %s, which the table does not hold", ev, state)
|
|
}
|
|
}
|
|
}
|
|
// H2 re-asks a check once: a second re-ask is refused by the table itself.
|
|
d0 := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, CheckAsk: &CheckAsk{ID: "check-1", Reasked: 1}}
|
|
if _, err := Apply(d0, EvReask, true, Facts{Now: time.Now(), By: byH2, Why: "stalled"}); err == nil {
|
|
t.Fatal("a check re-asked once was re-asked again")
|
|
}
|
|
// A ready delivery off the trunk is refused publication, whatever else is true of it.
|
|
d := &Delivery{ID: "novox/app@c0", State: Ready, Walk: &WalkSeen{ID: "plan-x", State: walkRolling}}
|
|
if _, err := Apply(d, EvMerged, false, Facts{Now: time.Now()}); err == nil {
|
|
t.Fatal("a commit not on the trunk was published")
|
|
}
|
|
}
|
|
|
|
func TestTheMachineStepTable(t *testing.T) {
|
|
for _, c := range []struct {
|
|
from, to StepState
|
|
may bool
|
|
}{
|
|
{"", StepSent, true}, {StepSent, StepJudging, true}, {StepJudging, StepPassed, true},
|
|
{StepJudging, StepFailed, true}, {StepFailed, StepRolledBack, true}, {StepSent, StepFailed, true},
|
|
{StepPassed, StepFailed, false}, {StepRolledBack, StepPassed, false}, {StepPassed, StepSent, false},
|
|
{StepJudging, StepSent, false}, {StepFailed, StepPassed, false},
|
|
} {
|
|
if StepMay(c.from, c.to) != c.may {
|
|
t.Errorf("%q → %q: may %v, wanted %v", c.from, c.to, !c.may, c.may)
|
|
}
|
|
}
|
|
}
|
|
|
|
// settle stands a delivery where its facts put it, whatever order they came in.
|
|
func TestSettleIsTheOrderOfTheFactsNotOfTheirArrival(t *testing.T) {
|
|
now := time.Now()
|
|
d := &Delivery{ID: "novox/app@c0", Number: 1, Commit: "c0"}
|
|
if _, err := Apply(d, EvAnnounced, false, Facts{Now: now}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The verdict, the merge and a finished walk, all known before anything settled.
|
|
d.Check = &Verdict{Gate: "pass"}
|
|
d.MergedAs = "m1"
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone}
|
|
ts := settle(d, Facts{Now: now})
|
|
var path []State
|
|
for _, t := range ts {
|
|
path = append(path, t.To)
|
|
}
|
|
want := []State{Checked, Ready, Published, Delivering, Delivered}
|
|
if len(path) != len(want) {
|
|
t.Fatalf("settled through %v, wanted %v", path, want)
|
|
}
|
|
for i := range want {
|
|
if path[i] != want[i] {
|
|
t.Fatalf("settled through %v, wanted %v", path, want)
|
|
}
|
|
}
|
|
}
|