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

231 lines
9.6 KiB
Go

package main
import (
"errors"
"testing"
"time"
)
// The table walked (novox/hq ADR 0239 decision 2, cut by ADR 0539 §17): 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 EvNewHead:
d.NewerHead = "beef"
case EvClosed:
d.ClosedUnmerged = true
case EvMerged, EvMergedUnchecked:
d.MergedAs = "aaaa1111"
case EvAccepted:
d.Check = &Verdict{Gate: "pass"}
case EvRefused:
d.Check = &Verdict{Gate: "fail"}
case EvRecheck, EvLost:
f.Why, f.By = "a person's reason", "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 EvTrunkMoved:
d.Check = &Verdict{Gate: "pass", Head: "c0ffee000000", Trunk: "d28ca95bbbbb"}
d.TrunkNow = "690966dccccccccc"
case EvSettled:
d.MergedAs, d.HeldWhy = "aaaa1111", HeldUnchecked
f.Why, f.By = "settled: the operator chose Settle", "the operator"
}
return d, f
}
// The merge's states only (ADR 0539 §17): proposed, ready, rejected, merged, held and closed. Nothing after the merge.
func TestTheTableHoldsTheSixStatesOfTheMergeAndNothingAfter(t *testing.T) {
want := map[State]bool{Proposed: true, Ready: true, Rejected: true, Merged: true, Held: true, Closed: true}
if len(AllStates) != len(want) {
t.Fatalf("the states are %v, wanted exactly proposed, ready, rejected, merged, held and closed", AllStates)
}
for _, s := range AllStates {
if !want[s] {
t.Errorf("%q is a state of the table: ADR 0539 §17 keeps six", s)
}
}
gone := []Event{"appeared", "adopted", "checked", "go", "hold", "release", "done", "failed", "superseded", "stop",
"history", "retire", "covered", "lifted", "rewalk", "backup failed"}
for _, r := range Table {
for _, g := range gone {
if r.Event == g {
t.Errorf("the table holds the event %q: it belongs to a delivery state that is gone", g)
}
}
for _, s := range append([]State{r.To}, r.From...) {
if s != None && !want[s] {
t.Errorf("the row %v —%s→ %s names the state %q", r.From, r.Event, r.To, s)
}
}
}
for _, s := range []State{"checked", "published", "delivering", "delivered", "failed", "superseded", "stopped",
"retired", "covered"} {
if rowFor(s, EvMerged, false) != nil || contains(AllStates, s) {
t.Errorf("%q is still a state", s)
}
}
}
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 {
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, EvAccepted, EvRefused, EvRecheck, EvReask, EvLost, EvNewHead, EvClosed, EvMerged,
EvMergedUnchecked, EvTrunkMoved, EvSettled}
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"},
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 {
// The one row out of a final state is a person's settle of a merge held without a passing check, taken on the
// operator's answer (novox/hq ADR 0556 clause 5): it is already on the trunk, and moves from held to merged.
settle := r.Event == EvSettled && r.Act && len(r.From) == 1 && r.From[0] == Held && r.To == Merged
for _, from := range r.From {
// Nothing else leaves a final state.
if from.Final() && !settle {
t.Errorf("%s is final and the table leaves it by %s", from, r.Event)
}
}
// Only a ready delivery is merged, and nothing but its merge, or a merge without a passing check, puts one on the trunk.
if r.To == Merged && !settle && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) {
t.Errorf("%v —%s→ merged: only a ready delivery's merge merges", r.From, r.Event)
}
if r.To == Held && r.Event != EvMergedUnchecked {
t.Errorf("%v —%s→ held: only a merge without a passing check holds", r.From, r.Event)
}
}
// Every H2 transition is a row the table holds from that state.
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")
}
// While the check gates no merge (check-gates-merge false): a proposed delivery is accepted with no verdict or a failed
// one, none is refused, and a trunk's move puts no ready one back to proposed.
off := Facts{Now: time.Now(), Ungated: true}
for _, check := range []*Verdict{nil, {Gate: "fail"}} {
p := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, Check: check}
if _, err := Apply(p, EvRefused, false, off); err == nil {
t.Fatalf("ungated, a verdict %+v refused the delivery", check)
}
if _, err := Apply(p, EvAccepted, false, off); err != nil || p.State != Ready {
t.Fatalf("ungated, a delivery with the verdict %+v is %s: %v", check, p.State, err)
}
}
stale := &Delivery{ID: "novox/app@c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"}
if _, err := Apply(stale, EvTrunkMoved, false, off); err == nil {
t.Fatal("ungated, a trunk's move put a ready delivery back to proposed")
}
// A ready delivery off the trunk is refused its merge, whatever else is true of it.
d := &Delivery{ID: "novox/app@c0", State: Ready}
if _, err := Apply(d, EvMerged, false, Facts{Now: time.Now()}); err == nil {
t.Fatal("a commit not on the trunk was merged")
}
}
// A delivery an earlier owner kept in a state that is gone reads as the merge's state it came to (ADR 0539 §17).
func TestAStateAnEarlierOwnerKeptReadsAsTheMergeStateItCameTo(t *testing.T) {
for kept, want := range map[State]State{
"checked": Proposed, "published": Merged, "delivering": Merged, "delivered": Merged, "covered": Merged,
"retired": Merged, "failed": Merged, "superseded": Closed, "stopped": Closed,
Proposed: Proposed, Ready: Ready, Rejected: Rejected, Merged: Merged, Closed: Closed,
} {
if got := legacyState(&Delivery{State: kept}); got != want {
t.Errorf("kept as %q, read as %q, wanted %q", kept, got, want)
}
}
if got := legacyState(&Delivery{State: Held, HeldWhy: "merged without a passing check: only a person decides"}); got != Held {
t.Errorf("a merge without a passing check was read as %q", got)
}
if got := legacyState(&Delivery{State: Held, HeldWhy: "its group's composed check did not pass"}); got != Merged {
t.Errorf("a delivery held for a walk's sake was read as %q", got)
}
}
// 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 and the merge, both known before anything settled.
d.Check = &Verdict{Gate: "pass"}
d.MergedAs = "m1"
ts := settle(d, Facts{Now: now})
var path []State
for _, t := range ts {
path = append(path, t.To)
}
want := []State{Ready, Merged}
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)
}
}
}