enginesim: clean baseline env, immediate listener checks, completed-transition coverage (#335 plan 1, final review)
mutate.sh's unmutated baseline sets ENGINESIM_MUTATE= explicitly. The ports witness asserts the listener is gone right after the adoption and connects with no capability pass in between; a second witness does the same after --grant doc-engine; TestPortsListenerGoneOnNull checks the upgrade's NULL write. An action's run reports whether its transition completed; the walk counts selections (stats) and completions (done) apart, plus deliveries and engine calls that reached a process (events). TestEveryActionFires checks completions and both events; TestStepLoop pins the split.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 files changed,
+315
−123
internal/sim/engines/calls_test.go+57 −30internal/sim/engines/classify_test.go+37 −21internal/sim/engines/dedicated_test.go+25 −15internal/sim/engines/edge_test.go+1 −0internal/sim/engines/launch_test.go+62 −28internal/sim/engines/mutate.sh+3 −2internal/sim/engines/sim_test.go+42 −15internal/sim/engines/witness_test.go+70 −7internal/sim/engines/world_test.go+18 −5
diff --git a/internal/sim/engines/calls_test.go b/internal/sim/engines/calls_test.go| index 78221e170..c38bdbb59 100644 |
| --- a/internal/sim/engines/calls_test.go |
| +++ b/internal/sim/engines/calls_test.go |
| @@ -484,6 +484,7 @@ func (w *world) engineAdmit(home, from *box, mark caller, hostname, path, via, r |
| return false |
| } |
| w.logf("box%d: engine call reaches process %d (%s v%d); the access log records caller %s", home.ID, q.ID, q.Group, q.Ver, mark.app()) |
| + w.events["engine-call-reached"]++ |
| w.checkCall(home, from, mark, via, real, p, q) |
| return true |
| } |
| @@ -666,11 +667,11 @@ func init() { |
| } |
| }) |
| actions = append(actions, |
| - action{name: "engine-call", weight: 4, run: func(w *world) { |
| + action{name: "engine-call", weight: 4, run: func(w *world) bool { |
| b := w.pickBox() |
| gs := w.callers(b) |
| if len(gs) == 0 || len(b.Procs) == 0 { |
| - return |
| + return false |
| } |
| host := gs[w.rng.Intn(len(gs))] |
| // usually the instance's own process; one call in three, any |
| @@ -682,12 +683,13 @@ func init() { |
| c := call{from: b, p: p, uid: "carlos", host: host, engine: w.pickEngine(), path: w.pickPath()} |
| c.forged = w.maybeForged() |
| w.placeCall(c) |
| + return true |
| }}, |
| - action{name: "door-call", weight: 1, run: func(w *world) { |
| + action{name: "door-call", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| gs := w.groups(b) |
| if len(gs) == 0 { |
| - return |
| + return false |
| } |
| host := gs[w.rng.Intn(len(gs))] |
| c := call{from: b, viaDoor: true, host: host, engine: w.pickEngine(), path: w.pickPath()} |
| @@ -701,49 +703,54 @@ func init() { |
| } |
| c.forged = w.maybeForged() |
| w.placeCall(c) |
| + return true |
| }}, |
| - action{name: "client-hop", weight: 1, run: func(w *world) { |
| + action{name: "client-hop", weight: 1, run: func(w *world) bool { |
| hosts := []string{engHost, engSite} |
| c := callerOf(walkApps[w.rng.Intn(len(walkApps))], w.pickEngine()) |
| w.clientHop(w.pickBox(), hosts[w.rng.Intn(2)], w.pickPath(), c) |
| + return true |
| }}, |
| - action{name: "console-grant", weight: 1, run: func(w *world) { w.consoleGrant(w.pickKey()) }}, |
| - action{name: "console-revoke", weight: 1, run: func(w *world) { |
| + action{name: "console-grant", weight: 1, run: func(w *world) bool { return w.consoleGrant(w.pickKey()) }}, |
| + action{name: "console-revoke", weight: 1, run: func(w *world) bool { |
| keys := sortedKeys(w.console.Grants) |
| - if len(keys) > 0 { |
| - w.consoleRevoke(keys[w.rng.Intn(len(keys))]) |
| - } |
| + return len(keys) > 0 && w.consoleRevoke(keys[w.rng.Intn(len(keys))]) |
| }}, |
| - action{name: "console-resign", weight: 1, run: func(w *world) { |
| - if !w.console.Down { |
| - w.publish(w.console.Rev + 1) |
| + action{name: "console-resign", weight: 1, run: func(w *world) bool { |
| + if w.console.Down { |
| + return false |
| } |
| + w.publish(w.console.Rev + 1) |
| + return true |
| }}, |
| - action{name: "console-down", weight: 1, run: func(w *world) { |
| + action{name: "console-down", weight: 1, run: func(w *world) bool { |
| w.console.Down = !w.console.Down && !w.calm() |
| w.logf("console: down = %v", w.console.Down) |
| + return true |
| }}, |
| - action{name: "bucket-down", weight: 1, run: func(w *world) { |
| + action{name: "bucket-down", weight: 1, run: func(w *world) bool { |
| w.setBucketDown(!w.console.BucketDown && !w.calm()) |
| + return true |
| }}, |
| - action{name: "fast-forward", weight: 1, run: func(w *world) { |
| + action{name: "fast-forward", weight: 1, run: func(w *world) bool { |
| if w.calm() { |
| - return |
| + return false |
| } |
| if w.rng.Intn(8) == 0 { |
| w.console.Down = true |
| w.logf("console: down = true") |
| w.advance(lifetimeTicks + staleTicks) // the console is down for three days |
| - return |
| + return true |
| } |
| w.advance(staleTicks) // ten minutes with no poll |
| + return true |
| }}, |
| // any older signed record, at any time: the freshness bound, not the |
| // walk, limits what a replay can do (R5) |
| - action{name: "replay", weight: 1, run: func(w *world) { |
| - w.walkReplay() |
| + action{name: "replay", weight: 1, run: func(w *world) bool { |
| + return w.walkReplay() |
| }}, |
| - action{name: "restart-edge", weight: 2, run: func(w *world) { |
| + action{name: "restart-edge", weight: 2, run: func(w *world) bool { |
| switch w.rng.Intn(3) { |
| case 0: // a release roll restarts every edge |
| for _, b := range w.boxes { |
| @@ -759,14 +766,29 @@ func init() { |
| w.walkReplay() |
| } |
| } |
| + return true |
| + }}, |
| + action{name: "arm", weight: 2, run: func(w *world) bool { |
| + b := w.pickBox() |
| + if b.Armed { |
| + return false |
| + } |
| + w.arm(b) |
| + return true |
| + }}, |
| + action{name: "disarm", weight: 1, run: func(w *world) bool { |
| + b := w.pickBox() |
| + if !b.Armed { |
| + return false |
| + } |
| + w.disarm(b) |
| + return true |
| }}, |
| - action{name: "arm", weight: 2, run: func(w *world) { w.arm(w.pickBox()) }}, |
| - action{name: "disarm", weight: 1, run: func(w *world) { w.disarm(w.pickBox()) }}, |
| - action{name: "launch-unisolated", weight: 1, run: func(w *world) { |
| + action{name: "launch-unisolated", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| kind := unisolatedKinds[w.rng.Intn(len(unisolatedKinds))] |
| if !w.launchUnisolated(b, kind) || !w.flatEraTamper || b.Armed { |
| - return |
| + return true |
| } |
| var gs []string |
| for _, g := range w.groups(b) { |
| @@ -777,8 +799,9 @@ func init() { |
| if len(gs) > 0 { |
| w.tamper(b, w.newest(b, kind), gs[w.rng.Intn(len(gs))]) |
| } |
| + return true |
| }}, |
| - action{name: "unisolated-exit", weight: 1, run: func(w *world) { |
| + action{name: "unisolated-exit", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| var ps []*proc |
| for _, p := range b.Procs { |
| @@ -787,7 +810,7 @@ func init() { |
| } |
| } |
| if len(ps) == 0 { |
| - return |
| + return false |
| } |
| p := ps[w.rng.Intn(len(ps))] |
| for i, x := range b.Procs { |
| @@ -797,12 +820,16 @@ func init() { |
| } |
| } |
| w.logf("box%d: process %d (%s) exited", b.ID, p.ID, p.Slice) |
| + return true |
| }}, |
| - action{name: "move-to-nested", weight: 1, run: func(w *world) { |
| + action{name: "move-to-nested", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - if ks := sortedKeys(b.Unisolated); len(ks) > 0 { |
| - w.moveToNested(b, ks[w.rng.Intn(len(ks))]) |
| + ks := sortedKeys(b.Unisolated) |
| + if len(ks) == 0 { |
| + return false |
| } |
| + w.moveToNested(b, ks[w.rng.Intn(len(ks))]) |
| + return true |
| }}, |
| ) |
| } |
diff --git a/internal/sim/engines/classify_test.go b/internal/sim/engines/classify_test.go| index 5e20dbc3c..84f6a9c53 100644 |
| --- a/internal/sim/engines/classify_test.go |
| +++ b/internal/sim/engines/classify_test.go |
| @@ -111,8 +111,9 @@ func (w *world) mint(b *box, src, host string, recordOwner bool) { |
| // reconcileOwners is the reconciler's backfill pass: it records the owner of |
| // every minted instance row that has none. The row takes its owner group's |
| -// mark (one group, one mark). |
| -func (w *world) reconcileOwners(b *box) { |
| +// mark (one group, one mark). It reports whether it recorded any. |
| +func (w *world) reconcileOwners(b *box) bool { |
| + did := false |
| for _, h := range hostsOf(b) { |
| r := b.Rows[h] |
| if !r.Platform || r.Kind != "instance" || r.Owner != "" || b.Rows[r.Source] == nil { |
| @@ -122,8 +123,10 @@ func (w *world) reconcileOwners(b *box) { |
| r.Owner = r.Source |
| b.Mark[h] = m |
| b.Wrote++ |
| + did = true |
| w.logf("box%d: registry change %d: owner of %s recorded as %s (mark %v)", b.ID, b.Wrote, h, r.Owner, m) |
| } |
| + return did |
| } |
| // provision creates instance route host (a new route: NULL, unclassified), |
| @@ -487,31 +490,38 @@ const maxRows = 24 |
| func init() { |
| actions = append(actions, |
| - action{name: "upgrade", weight: 1, run: func(w *world) { |
| + action{name: "upgrade", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| if why := w.residualAtUpgrade(b); why != "" && !w.unwrittenManifestAcrossUpgrade { |
| w.logf("box%d: upgrade not generated: the stated residual (%s)", b.ID, why) |
| - return |
| + return false |
| } |
| w.upgrade(b) |
| + return true |
| }}, |
| - action{name: "grant", weight: 1, run: func(w *world) { |
| + action{name: "grant", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - if h := w.pickInstanceRow(b); h != "" { |
| - w.grant(b, h) |
| + h := w.pickInstanceRow(b) |
| + if h == "" || b.Rows[h].Grant { |
| + return false |
| } |
| + w.grant(b, h) |
| + return true |
| }}, |
| - action{name: "revoke", weight: 1, run: func(w *world) { |
| + action{name: "revoke", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - if h := w.pickInstanceRow(b); h != "" { |
| - w.revoke(b, h) |
| + h := w.pickInstanceRow(b) |
| + if h == "" || !b.Rows[h].Grant { |
| + return false |
| } |
| + w.revoke(b, h) |
| + return true |
| }}, |
| - action{name: "operator-public", weight: 2, run: func(w *world) { |
| + action{name: "operator-public", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| if g == "" { |
| - return |
| + return false |
| } |
| switch w.rng.Intn(5) { |
| case 1: // an adoption tries to run during the clear |
| @@ -542,17 +552,21 @@ func init() { |
| w.clearStopMisses = w.rng.Intn(4) == 0 |
| w.operatorPublic(b, g) |
| w.clearHook, w.clearStopMisses = nil, false |
| + return true |
| }}, |
| - action{name: "adopt-awake", weight: 2, run: func(w *world) { |
| + action{name: "adopt-awake", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| - if g := w.pickGroup(b); g != "" { |
| - w.repointAwake(b, g, w.pickVer()) |
| + g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| } |
| + w.repointAwake(b, g, w.pickVer()) |
| + return true |
| }}, |
| - action{name: "add-tcp-instance", weight: 1, run: func(w *world) { |
| + action{name: "add-tcp-instance", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| if len(b.Rows) >= maxRows { |
| - return |
| + return false |
| } |
| w.nextID++ |
| host := fmt.Sprintf("t%d", w.nextID) |
| @@ -560,17 +574,19 @@ func init() { |
| addr := fmt.Sprintf("10.0.%d.1:%d", b.ID, 9100+w.nextID) // unique: no ambiguous owner in the walk |
| w.provision(b, host, app, addr, w.pickVer()) |
| w.mint(b, host, host+".acct.apps.example", w.rng.Intn(2) == 0) |
| + return true |
| }}, |
| - action{name: "add-static-app", weight: 1, run: func(w *world) { |
| + action{name: "add-static-app", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| if len(b.Rows) >= maxRows { |
| - return |
| + return false |
| } |
| w.nextID++ |
| w.addStaticApp(b, fmt.Sprintf("s%d", w.nextID)) |
| + return true |
| }}, |
| - action{name: "reconcile-owners", weight: 1, run: func(w *world) { |
| - w.reconcileOwners(w.pickBox()) |
| + action{name: "reconcile-owners", weight: 1, run: func(w *world) bool { |
| + return w.reconcileOwners(w.pickBox()) |
| }}, |
| ) |
| } |
diff --git a/internal/sim/engines/dedicated_test.go b/internal/sim/engines/dedicated_test.go| index 47c4e9062..620dc809e 100644 |
| --- a/internal/sim/engines/dedicated_test.go |
| +++ b/internal/sim/engines/dedicated_test.go |
| @@ -340,6 +340,7 @@ func (w *world) reach(b *box, p *proc, public bool, how string) *proc { |
| return nil |
| } |
| w.logf("box%d: %s reached process %d (%s v%d) past the edge", b.ID, how, p.ID, p.Group, p.Ver) |
| + w.events["delivered"]++ |
| // a ports listener exists only because the group is public: a path the |
| // upgrade's 0 opens; any other way past the edge is the ingress rule's |
| listener := strings.HasPrefix(how, "the ports listener") |
| @@ -427,10 +428,10 @@ func (w *world) localBackends(b *box) []string { |
| func init() { |
| actions = append(actions, |
| - action{name: "add-service-row", weight: 2, run: func(w *world) { |
| + action{name: "add-service-row", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| if len(b.Rows) >= maxRows { |
| - return |
| + return false |
| } |
| w.nextID++ |
| host := fmt.Sprintf("svc%d.example", w.nextID) |
| @@ -438,12 +439,13 @@ func init() { |
| // aimed at box 0's address (codex R13-1) |
| ports := append(w.addressablePorts(w.boxes[0]), "1") |
| w.addServiceRow(b, host, boxAddr(w.boxes[0])+":"+ports[w.rng.Intn(len(ports))]) |
| - return |
| + return true |
| } |
| bs := w.localBackends(b) |
| w.addServiceRow(b, host, bs[w.rng.Intn(len(bs))]) |
| + return true |
| }}, |
| - action{name: "delete-service-row", weight: 2, run: func(w *world) { |
| + action{name: "delete-service-row", weight: 2, run: func(w *world) bool { |
| type at struct { |
| b *box |
| host string |
| @@ -457,31 +459,37 @@ func init() { |
| } |
| } |
| if len(rows) == 0 { |
| - return |
| + return false |
| } |
| x := rows[w.rng.Intn(len(rows))] |
| w.deleteRow(x.b, x.host) |
| + return true |
| }}, |
| - action{name: "own-port", weight: 1, run: func(w *world) { |
| + action{name: "own-port", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| if len(b.Procs) == 0 { |
| - return |
| + return false |
| } |
| w.bindOwnPort(b, b.Procs[w.rng.Intn(len(b.Procs))]) |
| + return true |
| }}, |
| - action{name: "direct-connect", weight: 2, run: func(w *world) { |
| + action{name: "direct-connect", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| ports := append(w.addressablePorts(b), "1") |
| w.directConnect(b, ports[w.rng.Intn(len(ports))], true) |
| + return true |
| }}, |
| - action{name: "ship-ports", weight: 1, run: func(w *world) { |
| + action{name: "ship-ports", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - if g := w.pickGroup(b); g != "" { |
| - w.ship(b, g, w.pickVer(), true) |
| + g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| } |
| + w.ship(b, g, w.pickVer(), true) |
| + return true |
| }}, |
| - action{name: "caps-pass", weight: 1, run: func(w *world) { w.capsPass(w.pickBox()) }}, |
| - action{name: "remove-route", weight: 1, run: func(w *world) { |
| + action{name: "caps-pass", weight: 1, run: func(w *world) bool { w.capsPass(w.pickBox()); return true }}, |
| + action{name: "remove-route", weight: 1, run: func(w *world) bool { |
| // an instance route the walk added (the starting eng and shop stay) |
| b := w.pickBox() |
| var gs []string |
| @@ -490,9 +498,11 @@ func init() { |
| gs = append(gs, g) |
| } |
| } |
| - if len(gs) > 0 { |
| - w.removeGroup(b, gs[w.rng.Intn(len(gs))]) |
| + if len(gs) == 0 { |
| + return false |
| } |
| + w.removeGroup(b, gs[w.rng.Intn(len(gs))]) |
| + return true |
| }}, |
| ) |
| } |
diff --git a/internal/sim/engines/edge_test.go b/internal/sim/engines/edge_test.go| index 42d9b7e8a..f6506eee8 100644 |
| --- a/internal/sim/engines/edge_test.go |
| +++ b/internal/sim/engines/edge_test.go |
| @@ -302,6 +302,7 @@ func (w *world) deliver(r *req) { |
| return |
| } |
| r.Reached = p.ID |
| + w.events["delivered"]++ |
| w.logf("box%d: request %d delivered to process %d (%s v%d)", b.ID, r.ID, p.ID, p.Group, p.Ver) |
| if r.Public && w.builds[p.Ver].Declares { |
| w.failProc(b, p, "INV-P", true, "box%d: public request %d (row %s) delivered to process %d of %s running v%d, which declares doc_engine", b.ID, r.ID, r.Row, p.ID, p.Group, p.Ver) |
diff --git a/internal/sim/engines/launch_test.go b/internal/sim/engines/launch_test.go| index 61ce48892..b0fe537dd 100644 |
| --- a/internal/sim/engines/launch_test.go |
| +++ b/internal/sim/engines/launch_test.go |
| @@ -231,8 +231,9 @@ func (w *world) stagedForApp(b *box, g string) []int { |
| // wake is the activator waking group g by its wake plan: "recovery" starts |
| // the recovery build, "fallback" first fails to start the live build and |
| // falls back; both choose GoodVersion. The triggering request, if any, is |
| -// delivered afterwards (or finds the fence cancelled it). |
| -func (w *world) wake(b *box, g string, mode string, trigger *req) { |
| +// delivered afterwards (or finds the fence cancelled it). It reports whether |
| +// there was a build to launch. |
| +func (w *world) wake(b *box, g string, mode string, trigger *req) bool { |
| ver := b.Good[g] |
| if mode == "fallback" { |
| w.logf("box%d: wake %s: live v%d fails to start; falling back to good v%d", b.ID, g, b.Live[g], ver) |
| @@ -245,6 +246,7 @@ func (w *world) wake(b *box, g string, mode string, trigger *req) { |
| if trigger != nil { |
| w.deliver(trigger) |
| } |
| + return ver != 0 |
| } |
| // moveLive is anything other than adoption moving group g's live link (a |
| @@ -261,22 +263,25 @@ func (w *world) moveLive(b *box, g string, ver int, who string) bool { |
| } |
| // systemdRestart is Restart=always: the unit's main process exits and |
| -// systemd starts /opt/carlos/live/<host> again, with no activator. |
| -func (w *world) systemdRestart(b *box, g string) { |
| +// systemd starts /opt/carlos/live/<host> again, with no activator. False |
| +// when the route has no live release. |
| +func (w *world) systemdRestart(b *box, g string) bool { |
| if b.Live[g] == 0 { |
| - return |
| + return false |
| } |
| w.stopGroup(b, g) |
| w.launch(b, g, b.Live[g], "systemd") |
| + return true |
| } |
| // socketActivate is carlos-app@.socket starting a stopped unit from the live |
| -// link, with no activator. |
| -func (w *world) socketActivate(b *box, g string) { |
| +// link, with no activator. False when there is nothing to start. |
| +func (w *world) socketActivate(b *box, g string) bool { |
| if b.Live[g] == 0 || w.running(b, g) { |
| - return |
| + return false |
| } |
| w.launch(b, g, b.Live[g], "socket") |
| + return true |
| } |
| // ---------- the random walk ---------- |
| @@ -344,9 +349,12 @@ func (w *world) heldFor(b *box, g string) *req { |
| func init() { |
| actions = append(actions, |
| - action{name: "adopt", weight: 3, run: func(w *world) { |
| + action{name: "adopt", weight: 3, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| ver := w.pickVer() |
| var between func() |
| switch w.rng.Intn(3) { |
| @@ -361,30 +369,33 @@ func init() { |
| } |
| } |
| w.adopt(b, g, ver, between) |
| + return true |
| }}, |
| - action{name: "reload", weight: 4, run: func(w *world) { w.reload(w.pickBox()) }}, |
| - action{name: "reload-fails", weight: 1, run: func(w *world) { |
| + action{name: "reload", weight: 4, run: func(w *world) bool { w.reload(w.pickBox()); return true }}, |
| + action{name: "reload-fails", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| b.ReloadFails = !b.ReloadFails && !w.calm() |
| w.logf("box%d: reload fails = %v", b.ID, b.ReloadFails) |
| + return true |
| }}, |
| // 9, not 6 (Task 6): fence-box-wide's sweep catch needs a request held |
| // through a service row that is deleted before a declaring adoption's |
| // fence; at 6 the 3000-seed sweep caught it 7 times, at 9 12 times |
| - action{name: "public-request", weight: 9, run: func(w *world) { |
| + action{name: "public-request", weight: 9, run: func(w *world) bool { |
| b := w.pickBox() |
| r := w.randomPublic(b) |
| if r == nil { |
| - return |
| + return false |
| } |
| if len(b.Admitted) <= 8 && w.rng.Intn(2) == 0 { |
| r.Held = true |
| w.logf("box%d: request %d held in the activator's queue", b.ID, r.ID) |
| - return |
| + return true |
| } |
| w.deliver(r) |
| + return true |
| }}, |
| - action{name: "resume", weight: 3, run: func(w *world) { |
| + action{name: "resume", weight: 3, run: func(w *world) bool { |
| var held []*req |
| for _, b := range w.boxes { |
| for _, r := range b.Admitted { |
| @@ -394,24 +405,31 @@ func init() { |
| } |
| } |
| if len(held) == 0 { |
| - return |
| + return false |
| } |
| sort.Slice(held, func(i, j int) bool { return held[i].ID < held[j].ID }) |
| w.deliver(held[w.rng.Intn(len(held))]) |
| + return true |
| }}, |
| - action{name: "wake", weight: 3, run: func(w *world) { |
| + action{name: "wake", weight: 3, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| mode := "recovery" |
| if w.rng.Intn(2) == 0 { |
| mode = "fallback" |
| } |
| w.stopGroup(b, g) // the group had gone to sleep |
| - w.wake(b, g, mode, w.heldFor(b, g)) |
| + return w.wake(b, g, mode, w.heldFor(b, g)) |
| }}, |
| - action{name: "swap", weight: 2, run: func(w *world) { |
| + action{name: "swap", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| if w.launch(b, g, w.pickVer(), "swap") { |
| // the incoming generation serves; the outgoing ones stop |
| newest := b.Procs[len(b.Procs)-1] |
| @@ -423,33 +441,49 @@ func init() { |
| } |
| b.Procs = kept |
| } |
| + return true |
| }}, |
| - action{name: "seed", weight: 1, run: func(w *world) { |
| + action{name: "seed", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| w.stopGroup(b, g) |
| w.launch(b, g, w.pickVer(), "seed") |
| + return true |
| }}, |
| - action{name: "cmd", weight: 1, run: func(w *world) { |
| + action{name: "cmd", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - w.launch(b, w.pickGroup(b), w.pickVer(), "cmd") |
| + g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| + w.launch(b, g, w.pickVer(), "cmd") |
| + return true |
| }}, |
| - action{name: "systemd-restart", weight: 2, run: func(w *world) { |
| + action{name: "systemd-restart", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| - w.systemdRestart(b, w.pickGroup(b)) |
| + return w.systemdRestart(b, w.pickGroup(b)) |
| }}, |
| - action{name: "socket-activation", weight: 2, run: func(w *world) { |
| + action{name: "socket-activation", weight: 2, run: func(w *world) bool { |
| b := w.pickBox() |
| g := w.pickGroup(b) |
| w.stopGroup(b, g) // the unit exited idle; a connection arrives |
| - w.socketActivate(b, g) |
| + ok := w.socketActivate(b, g) |
| if r := w.heldFor(b, g); r != nil { |
| w.deliver(r) |
| } |
| + return ok |
| }}, |
| - action{name: "repoint-live", weight: 1, run: func(w *world) { |
| + action{name: "repoint-live", weight: 1, run: func(w *world) bool { |
| b := w.pickBox() |
| - w.moveLive(b, w.pickGroup(b), w.pickVer(), "a rollout tool") |
| + g := w.pickGroup(b) |
| + if g == "" { |
| + return false |
| + } |
| + w.moveLive(b, g, w.pickVer(), "a rollout tool") |
| + return true |
| }}, |
| ) |
| } |
diff --git a/internal/sim/engines/mutate.sh b/internal/sim/engines/mutate.sh| index a0b05813a..c16aae224 100755 |
| --- a/internal/sim/engines/mutate.sh |
| +++ b/internal/sim/engines/mutate.sh |
| @@ -83,9 +83,10 @@ for f in "$SCRATCH"/verdicts/*; do |
| done |
| # the unmutated sweep first, at the same seed count: if the model violates |
| -# with every rule in place, no catch below means anything |
| +# with every rule in place, no catch below means anything (ENGINESIM_MUTATE |
| +# set empty here, so a value in the caller's environment cannot leak in) |
| blog="$SCRATCH/logs/unmutated.log" |
| -ENGINESIM_SEEDS=$SEEDS "$BIN" -test.run '^TestEngines$' -test.count=1 -test.v -test.timeout 180m >"$blog" 2>&1 |
| +ENGINESIM_MUTATE= ENGINESIM_SEEDS=$SEEDS "$BIN" -test.run '^TestEngines$' -test.count=1 -test.v -test.timeout 180m >"$blog" 2>&1 |
| brc=$? |
| if [ $brc -ne 0 ] || [ "$(grep -c "^ sim_test.go:[0-9]*: $SEEDS seeds, 0 failed, 0 residuals; mutated=\"\"" "$blog")" -ne 1 ]; then |
| echo "BASELINE unmutated: FAILED (rc=$brc); see $blog" >&2 |
diff --git a/internal/sim/engines/sim_test.go b/internal/sim/engines/sim_test.go| index b5bef5d7e..fc66252fb 100644 |
| --- a/internal/sim/engines/sim_test.go |
| +++ b/internal/sim/engines/sim_test.go |
| @@ -42,13 +42,19 @@ func TestEngines(t *testing.T) { |
| failed := 0 |
| count := map[string]int{} |
| firstSeed := map[string]int64{} |
| - stats := map[string]int{} |
| + stats, done, events := map[string]int{}, map[string]int{}, map[string]int{} |
| residuals := 0 |
| for seed := int64(1); seed <= int64(n); seed++ { |
| w := runSeed(seed, false) |
| for k, v := range w.stats { |
| stats[k] += v |
| } |
| + for k, v := range w.done { |
| + done[k] += v |
| + } |
| + for k, v := range w.events { |
| + events[k] += v |
| + } |
| residuals += len(w.residuals) |
| if len(w.violations) == 0 { |
| continue |
| @@ -64,7 +70,9 @@ func TestEngines(t *testing.T) { |
| } |
| } |
| t.Logf("%d seeds, %d failed, %d residuals; mutated=%q", n, failed, residuals, mutated) |
| - t.Logf("actions: %v", stats) |
| + t.Logf("actions chosen: %v", stats) |
| + t.Logf("actions completed: %v", done) |
| + t.Logf("events: %v", events) |
| ids := make([]string, 0, len(count)) |
| for id := range count { |
| ids = append(ids, id) |
| @@ -94,7 +102,7 @@ func TestSeedDeterministic(t *testing.T) { |
| if !reflect.DeepEqual(a.trace, b.trace) { |
| t.Fatalf("verbose=%v: two runs of seed 7 differ", verbose) |
| } |
| - if !reflect.DeepEqual(a.stats, b.stats) || !reflect.DeepEqual(a.violations, b.violations) { |
| + if !reflect.DeepEqual(a.stats, b.stats) || !reflect.DeepEqual(a.done, b.done) || !reflect.DeepEqual(a.events, b.events) || !reflect.DeepEqual(a.violations, b.violations) { |
| t.Fatalf("verbose=%v: two runs of seed 7 differ in stats or violations", verbose) |
| } |
| } |
| @@ -202,8 +210,9 @@ func TestNewWorldShape(t *testing.T) { |
| } |
| // TestStepLoop: with no actions a step does nothing but advance w.step; with |
| -// actions registered each run is counted under the action's name, the walk |
| -// is weighted, and 400 chaos plus 200 calm steps run. |
| +// actions registered each run is counted under the action's name (in done |
| +// only when it reports a completed transition), the walk is weighted, and |
| +// 400 chaos plus 200 calm steps run. |
| func TestStepLoop(t *testing.T) { |
| saved := actions |
| defer func() { actions = saved }() |
| @@ -227,20 +236,25 @@ func TestStepLoop(t *testing.T) { |
| } |
| } |
| actions = []action{ |
| - {name: "alpha", weight: 3, run: func(w *world) { |
| + {name: "alpha", weight: 3, run: func(w *world) bool { |
| calls["alpha"]++ |
| phase(w) |
| + return true |
| }}, |
| - {name: "beta", weight: 1, run: func(w *world) { |
| + {name: "beta", weight: 1, run: func(w *world) bool { |
| calls["beta"]++ |
| phase(w) |
| + return false // chosen, but found nothing to do |
| }}, |
| - {name: "never", weight: 0, run: func(w *world) { t.Error("a zero-weight action ran") }}, |
| + {name: "never", weight: 0, run: func(w *world) bool { t.Error("a zero-weight action ran"); return true }}, |
| } |
| w = runSeed(3, false) |
| if w.stats["alpha"] != calls["alpha"] || w.stats["beta"] != calls["beta"] { |
| t.Fatalf("stats %v do not match calls %v", w.stats, calls) |
| } |
| + if w.done["alpha"] != calls["alpha"] || w.done["beta"] != 0 { |
| + t.Fatalf("done %v: want alpha's every run and none of beta's (calls %v)", w.done, calls) |
| + } |
| if calls["alpha"]+calls["beta"] != chaosSteps+calmSteps { |
| t.Fatalf("each step should run one action: %v", calls) |
| } |
| @@ -288,8 +302,11 @@ func TestOnMutation(t *testing.T) { |
| } |
| // TestEveryActionFires: over 200 seeds of the unmutated walk every registered |
| -// action is chosen at least once. A rule that guards an action no seed fires |
| -// is untested however green the sweep looks (Review Focus 2). |
| +// action completes its transition at least once (chosen is not enough: an |
| +// action that always finds nothing to do exercises nothing), and public |
| +// requests and engine calls really reach processes. A rule that guards an |
| +// action no seed fires is untested however green the sweep looks (Review |
| +// Focus 2; final review, minor 3). |
| func TestEveryActionFires(t *testing.T) { |
| saved := mutated |
| mutated = "" |
| @@ -307,15 +324,25 @@ func TestEveryActionFires(t *testing.T) { |
| t.Errorf("action %q has weight %d: the walk never picks it", a.name, a.weight) |
| } |
| } |
| - stats := map[string]int{} |
| + done, events := map[string]int{}, map[string]int{} |
| for seed := int64(1); seed <= 200; seed++ { |
| - for k, v := range runSeed(seed, false).stats { |
| - stats[k] += v |
| + w := runSeed(seed, false) |
| + for k, v := range w.done { |
| + done[k] += v |
| + } |
| + for k, v := range w.events { |
| + events[k] += v |
| } |
| } |
| for _, a := range actions { |
| - if stats[a.name] == 0 { |
| - t.Errorf("action %q never fired over 200 seeds", a.name) |
| + if done[a.name] == 0 { |
| + t.Errorf("action %q never completed a transition over 200 seeds", a.name) |
| + } |
| + } |
| + for _, e := range []string{"delivered", "engine-call-reached"} { |
| + if events[e] == 0 { |
| + t.Errorf("no %s over 200 seeds", e) |
| } |
| } |
| + t.Logf("completed: %v; events: %v", done, events) |
| } |
diff --git a/internal/sim/engines/witness_test.go b/internal/sim/engines/witness_test.go| index 1fa64571f..cbc759729 100644 |
| --- a/internal/sim/engines/witness_test.go |
| +++ b/internal/sim/engines/witness_test.go |
| @@ -90,7 +90,9 @@ var witnesses = []witness{ |
| // Task 4: the dedicated engine box and ingress |
| {"ports-excluded", "ports-excluded", "INV-P", |
| - "R11-1: the engine route holds a port grant from a plain release, so the box renders its listener; adoption of a declaring build marks the group; a box that keeps the listener lets a public connection to the port, which the firewall admits as granted, reach the engine past the edge", wPortsExcluded}, |
| + "R11-1: the engine route holds a port grant from a plain release, so the box renders its listener; adoption of a declaring build marks the group; a box that keeps the listener lets a public connection to the port, which the firewall admits as granted, reach the engine past the edge, with no capability pass in between", wPortsExcluded}, |
| + {"ports-excluded", "ports-excluded", "INV-P", |
| + "R11-1, by the grant: the public engine route on a plain build holds a port grant and its listener; --grant doc-engine marks the group; a box that keeps the listener lets a public connection reach the granted group past the edge, with no capability pass in between", wPortsExcludedGrant}, |
| {"dedicated-launch-check", "dedicated-launch-check,dedicated-edge-refusal", "INV-P", |
| "R12-2: redundant alone: a service row aimed at the engine's backend exists before a declaring adoption; the launch check refuses the adoption; without it the edge's refusal still refuses the row once the group is internal; with both gone a public request through the row reaches the engine", wDedicatedLaunchCheck}, |
| {"dedicated-edge-refusal", "dedicated-edge-refusal", "INV-P", |
| @@ -1112,11 +1114,10 @@ func (w *world) runningEngine(t *testing.T) (*box, *proc) { |
| return b, b.Procs[len(b.Procs)-1] |
| } |
| -// wPortsExcluded (R11-1): the public engine route ships a plain release |
| -// declaring ports, and the box renders its listener; then adoption of a |
| -// declaring build; the capability pass runs; a public connection arrives on |
| -// the granted port. |
| -func wPortsExcluded(t *testing.T, w *world) { |
| +// portsEngine is the public engine route shipping a plain release declaring |
| +// ports: the box renders its listener, and a public connection to the port |
| +// reaches the plain build. It returns the port. |
| +func (w *world) portsEngine(t *testing.T) (*box, string) { |
| b := w.publicEngine(t) |
| if !w.ship(b, eng, vPlain, true) { |
| t.Fatalf("witness: a plain release declaring ports was refused: %s", w.boxesState()) |
| @@ -1129,15 +1130,77 @@ func wPortsExcluded(t *testing.T, w *world) { |
| t.Fatalf("witness: the granted port of a public route reached nothing: %s", w.boxesState()) |
| } |
| w.check() |
| + return b, port |
| +} |
| + |
| +// listenerGone fails the witness if the write that closed the group left |
| +// port's listener in place, while ports-excluded holds: the write removes it |
| +// itself, before any fence and with no capability pass (final review, minor |
| +// 2). |
| +func (w *world) listenerGone(t *testing.T, b *box, port, after string) { |
| + if on("ports-excluded") && b.Listen[port] != "" { |
| + t.Fatalf("witness: the listener on %s is still there right after %s: %s", port, after, w.boxesState()) |
| + } |
| +} |
| + |
| +// wPortsExcluded (R11-1): the public engine route ships a plain release |
| +// declaring ports, and the box renders its listener; then adoption of a |
| +// declaring build; at once, with no capability pass, a public connection |
| +// arrives on the granted port. |
| +func wPortsExcluded(t *testing.T, w *world) { |
| + b, port := w.portsEngine(t) |
| if !w.adopt(b, eng, vEngine, nil) { |
| t.Fatalf("witness: adoption of the engine refused: %s", w.boxesState()) |
| } |
| + w.listenerGone(t, b, port, "the adoption") |
| + w.check() |
| + w.directConnect(b, port, true) |
| + w.check() |
| +} |
| + |
| +// wPortsExcludedGrant (R11-1, by the grant): the public engine route serves a |
| +// plain build on its ports listener; the operator grants doc-engine on it; at |
| +// once, with no capability pass, a public connection arrives on the port. |
| +func wPortsExcludedGrant(t *testing.T, w *world) { |
| + b, port := w.portsEngine(t) |
| + w.grant(b, engHost) |
| + w.listenerGone(t, b, port, "the grant") |
| w.check() |
| - w.capsPass(b) |
| w.directConnect(b, port, true) |
| w.check() |
| } |
| +// TestPortsListenerGoneOnNull (final review, minor 2): the upgrade leaves a |
| +// route with no live release NULL; the write of NULL removes its ports |
| +// listener at once, before the capability pass runs, and a public connection |
| +// to the port is refused by the firewall. |
| +func TestPortsListenerGoneOnNull(t *testing.T) { |
| + w := newWitness() |
| + b := w.boxes[1] |
| + b.PortGrant["shop"] = "40001" |
| + w.capsPass(b) |
| + if b.Listen["40001"] != "shop" { |
| + t.Fatalf("fixture: no listener for shop's grant: %s", w.boxesState()) |
| + } |
| + w.upgrade(b) |
| + if m := w.groupMark(b, "shop"); m != markNull { |
| + t.Fatalf("fixture: shop is %v after the upgrade, want NULL", m) |
| + } |
| + if b.Listen["40001"] != "" { |
| + t.Errorf("the listener is still there right after the upgrade's NULL: %s", w.boxesState()) |
| + } |
| + if p := w.directConnect(b, "40001", true); p != nil { |
| + t.Errorf("a public connection to the NULL route's port reached process %d", p.ID) |
| + } |
| + w.check() |
| + if len(w.violations) > 0 { |
| + t.Errorf("violations: %v", w.violations) |
| + } |
| + if t.Failed() { |
| + dump(t, w) |
| + } |
| +} |
| + |
| // wDedicatedLaunchCheck (R12-2): the public engine route has a service row |
| // beside it aimed at its backend; adoption of a declaring build; a public |
| // request arrives through the service row. |
diff --git a/internal/sim/engines/world_test.go b/internal/sim/engines/world_test.go| index f3d8196d4..69497a060 100644 |
| --- a/internal/sim/engines/world_test.go |
| +++ b/internal/sim/engines/world_test.go |
| @@ -162,8 +162,10 @@ type world struct { |
| trace []string |
| violations []string |
| residuals []string |
| - stats map[string]int |
| - nextID int // process and request ids |
| + stats map[string]int // actions selected, by name |
| + done map[string]int // actions whose transition completed, by name |
| + events map[string]int // deliveries: public requests and engine calls that reached a process |
| + nextID int // process and request ids |
| // clearHook, when set, runs inside operatorPublic at each stage |
| // ("locked": step 1 passed; "stopped": step 2 done; "confirmed": step 3 |
| @@ -193,10 +195,16 @@ type world struct { |
| // action is one thing the random walk can do. Tasks 2 to 5 append theirs |
| // from their own files' init; the walk picks among them by weight. |
| +// |
| +// run reports whether the transition completed: false when the action found |
| +// nothing to act on (no group, no held request, a full table) and returned |
| +// without calling into the model. A refusal by the protocol is a completed |
| +// transition. The walk counts selections (stats) and completions (done) |
| +// apart, so coverage can be checked on what really ran. |
| type action struct { |
| name string |
| weight int |
| - run func(*world) |
| + run func(*world) bool |
| } |
| var actions []action |
| @@ -211,6 +219,8 @@ func newWorld(seed int64, verbose bool) *world { |
| rng: rand.New(rand.NewSource(seed)), |
| builds: map[int]build{}, |
| stats: map[string]int{}, |
| + done: map[string]int{}, |
| + events: map[string]int{}, |
| verbose: verbose, |
| } |
| newBox := func(id int) *box { |
| @@ -311,7 +321,8 @@ func (w *world) pick() *action { |
| } |
| // doStep advances one tick: it counts the step, runs one chosen action and |
| -// counts it under its name. |
| +// counts it under its name, in stats when chosen and in done when its |
| +// transition completed. |
| func (w *world) doStep() { |
| w.step++ |
| for _, h := range stepHooks { |
| @@ -326,7 +337,9 @@ func (w *world) doStep() { |
| } |
| w.stats[a.name]++ |
| w.logf("action %s", a.name) // always traced: the determinism test compares the action sequence |
| - a.run(w) |
| + if a.run(w) { |
| + w.done[a.name]++ |
| + } |
| w.check() |
| } |