Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go
T
jschoubben fb89990e2b mesh-delivery-gate: ask a check the build queue lost again, and take a recheck from proposed when none is held (issue 1274)
A proposed delivery whose asked check no queue entry holds stayed proposed for good (met in 1311, 1336, 1726, 1792, 1795, 1833). The sweep reads the controller's queue and asks again past max(10 minutes, the repository's last check time); recheck from proposed is taken on the same fact; never while the queue holds one.
2026-10-22 16:34:33 +02:00

261 lines
11 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, EvLost:
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"}
if r.From[0] == Published {
// Its walk ended done before its word, and nothing delivered it (novox/hq issue 474).
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone, Waits: true, WaitedFor: "mesh-delivery", MovedSaid: true}
d.Stranded = "its walk plan-1 ended done before its word without building app"
}
}
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"
if r.From[0] == Published {
// Its walk ended done before its word; a later merge delivered its modules (novox/hq issue 474).
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone, Waits: true, WaitedFor: "mesh-delivery", MovedSaid: true}
}
if r.From[0] == Delivering {
// Answered by its successor in one walk (ADR 0276): the walk done, carrying a later merge.
d.CoveredBy, d.AnsweredBy, d.Carrier = "", "bbbb2222", "novox/app@bbbb (merged as bbbb2222, walk plan-2)"
d.Walk = &WalkSeen{ID: "plan-2", State: walkDone}
}
case EvRewalk:
d.MergedAs, d.AnsweredBy = "aaaa1111", "bbbb2222"
d.Walk = &WalkSeen{ID: "plan-2", State: walkFailed}
case EvTrunkMoved:
d.Check = &Verdict{Gate: "pass", Head: "c0ffee000000", Trunk: "d28ca95bbbbb"}
d.TrunkNow = "690966dccccccccc"
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 EvBackupFailed:
d.Walk = &WalkSeen{ID: "plan-1", State: walkBuilding, Revision: 4,
BackupsFailed: []WalkBackupFailure{{Machine: "ace", Module: "pg", Words: "no space left on device"}}}
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, EvLost, EvNewHead,
EvClosed, EvMerged, EvMergedUnchecked, EvGo, EvHold, EvRelease, EvDone, EvFailed, EvSuperseded, EvStop,
EvHistory, EvRetire, EvCovered, EvLifted, EvRewalk, EvTrunkMoved, EvBackupFailed}
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.
// And a merge whose walk carried a later commit for it and failed waits there for a walk of its own (ADR 0276).
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) &&
!(r.Event == EvRewalk && len(r.From) == 1 && r.From[0] == Delivering) {
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)
}
}
}