carlos / platform Public

enginesim: exact chaos/calm split and list-only mode (#335 plan 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a54c5821b089b54f34031ba80c72e3b8fe3403e8 parent 95bf730
2 files changed, +62 −16
  • internal/sim/engines/sim_test.go +59 −14
  • internal/sim/engines/world_test.go +3 −2
diff --git a/internal/sim/engines/sim_test.go b/internal/sim/engines/sim_test.go
index f909ba937..08c06537e 100644
--- a/internal/sim/engines/sim_test.go
+++ b/internal/sim/engines/sim_test.go
@@ -1,7 +1,9 @@
package engines
import (
+ "fmt"
"os"
+ "os/exec"
"reflect"
"sort"
"strconv"
@@ -98,14 +100,51 @@ func TestSeedDeterministic(t *testing.T) {
}
}
-// TestRulesListed prints the rules a run would check, one per line, and runs
-// nothing. Without ENGINESIM_LIST it stands aside.
-func TestRulesListed(t *testing.T) {
- if os.Getenv("ENGINESIM_LIST") != "1" {
- t.Skip("ENGINESIM_LIST=1 prints the rules")
+// TestMain handles ENGINESIM_LIST=1 before any test runs: it prints the rules
+// a run would check, one per line, to stdout and exits 0 without running a
+// test (the mutation harness reads this list).
+func TestMain(m *testing.M) {
+ if os.Getenv("ENGINESIM_LIST") == "1" {
+ for _, r := range allRules {
+ fmt.Println(r)
+ }
+ os.Exit(0)
}
+ os.Exit(m.Run())
+}
+
+// TestListPrintsRulesAndRunsNothing runs this test binary as a subprocess
+// with ENGINESIM_LIST=1 and requires that its output is exactly the rule
+// list and that no test ran. A control run without the variable shows the
+// "=== RUN" marker is what a test run prints.
+func TestListPrintsRulesAndRunsNothing(t *testing.T) {
+ run := func(list bool) string {
+ cmd := exec.Command(os.Args[0], "-test.v", "-test.run=TestOnMutation")
+ cmd.Env = os.Environ()
+ for i := 0; i < len(cmd.Env); i++ {
+ if strings.HasPrefix(cmd.Env[i], "ENGINESIM_LIST=") {
+ cmd.Env = append(cmd.Env[:i], cmd.Env[i+1:]...)
+ i--
+ }
+ }
+ if list {
+ cmd.Env = append(cmd.Env, "ENGINESIM_LIST=1")
+ }
+ out, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("list=%v: %v", list, err)
+ }
+ return string(out)
+ }
+ if control := run(false); !strings.Contains(control, "=== RUN") {
+ t.Fatalf("control run printed no test markers: %q", control)
+ }
+ want := ""
for _, r := range allRules {
- t.Log(r)
+ want += r + "\n"
+ }
+ if got := run(true); got != want {
+ t.Fatalf("ENGINESIM_LIST=1 output = %q, want exactly the rule list %q", got, want)
}
}
@@ -175,16 +214,22 @@ func TestStepLoop(t *testing.T) {
calls := map[string]int{}
var chaosCalls, calmCalls int
+ phase := func(w *world) {
+ if w.calm() {
+ calmCalls++
+ } else {
+ chaosCalls++
+ }
+ }
actions = []action{
{name: "alpha", weight: 3, run: func(w *world) {
calls["alpha"]++
- if w.calm() {
- calmCalls++
- } else {
- chaosCalls++
- }
+ phase(w)
+ }},
+ {name: "beta", weight: 1, run: func(w *world) {
+ calls["beta"]++
+ phase(w)
}},
- {name: "beta", weight: 1, run: func(w *world) { calls["beta"]++ }},
{name: "never", weight: 0, run: func(w *world) { t.Error("a zero-weight action ran") }},
}
w = runSeed(3, false)
@@ -197,8 +242,8 @@ func TestStepLoop(t *testing.T) {
if calls["alpha"] < 2*calls["beta"] {
t.Fatalf("weights ignored: %v", calls)
}
- if chaosCalls == 0 || calmCalls == 0 {
- t.Fatalf("the walk must reach both phases: chaos %d calm %d", chaosCalls, calmCalls)
+ if chaosCalls != chaosSteps || calmCalls != calmSteps || chaosSteps != 400 || calmSteps != 200 {
+ t.Fatalf("phase split: chaos %d calm %d, want 400 and 200", chaosCalls, calmCalls)
}
if _, ok := w.stats["never"]; ok {
t.Fatal("a zero-weight action was counted")
diff --git a/internal/sim/engines/world_test.go b/internal/sim/engines/world_test.go
index 5bcb10d73..70b035ee3 100644
--- a/internal/sim/engines/world_test.go
+++ b/internal/sim/engines/world_test.go
@@ -156,8 +156,9 @@ func (w *world) logf(format string, args ...any) {
w.trace = append(w.trace, fmt.Sprintf("[%d] ", w.step)+fmt.Sprintf(format, args...))
}
-// calm is true once the chaos steps are over.
-func (w *world) calm() bool { return w.step >= chaosSteps }
+// calm is true once the chaos steps are over. doStep counts the step before
+// it runs the action, so steps 1 to chaosSteps are chaos and the rest calm.
+func (w *world) calm() bool { return w.step > chaosSteps }
// group is a row's group key: its owner for a minted instance row, else its
// own host.