package engines import ( "fmt" "os" "os/exec" "reflect" "sort" "strconv" "strings" "testing" ) // seedsFromEnv is the sweep size: ENGINESIM_SEEDS, else def. func seedsFromEnv(def int) int { if s := os.Getenv("ENGINESIM_SEEDS"); s != "" { if n, err := strconv.Atoi(s); err == nil && n > 0 { return n } } return def } // TestEngines is the sweep. ENGINESIM_SEEDS sets the seed count (the gate's // default is 200); ENGINESIM_SEED=n runs one seed and prints its trace. func TestEngines(t *testing.T) { if s := os.Getenv("ENGINESIM_SEED"); s != "" { seed, err := strconv.ParseInt(s, 10, 64) if err != nil { t.Fatalf("ENGINESIM_SEED=%q: %v", s, err) } w := runSeed(seed, true) for _, line := range w.trace { t.Log(line) } for _, v := range w.violations { t.Errorf("seed %d: %s", seed, v) } return } n := seedsFromEnv(200) failed := 0 count := map[string]int{} firstSeed := map[string]int64{} 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 } failed++ for _, v := range w.violations { id := invariantOf(v) count[id]++ if _, ok := firstSeed[id]; !ok { firstSeed[id] = seed t.Logf("seed %d: %s", seed, v) } } } t.Logf("%d seeds, %d failed, %d residuals; mutated=%q", n, failed, residuals, mutated) 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) } sort.Strings(ids) for _, id := range ids { t.Logf(" %4d x %s (first seed %d)", count[id], id, firstSeed[id]) } if failed > 0 { t.Fatalf("%d of %d seeds violated an invariant", failed, n) } // the default world sets no residual flag, so it never generates one of // the spec's stated residuals (TestResidualsAreCountedNotFailed) if residuals > 0 { t.Fatalf("the default world recorded %d residuals; only a test's flag may generate one", residuals) } } // TestSeedDeterministic: one seed gives one trace, or no reported seed can // be replayed. func TestSeedDeterministic(t *testing.T) { for _, verbose := range []bool{false, true} { a, b := runSeed(7, verbose), runSeed(7, verbose) if len(a.trace) == 0 { t.Fatalf("verbose=%v: the trace is empty, so equality proves nothing", verbose) } 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.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) } } if reflect.DeepEqual(newWorld(7, false).builds, newWorld(8, false).builds) && reflect.DeepEqual(newWorld(7, false).builds, newWorld(9, false).builds) && reflect.DeepEqual(newWorld(7, false).builds, newWorld(10, false).builds) { t.Fatal("builds do not vary with the seed") } } // 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 { want += r + "\n" } if got := run(true); got != want { t.Fatalf("ENGINESIM_LIST=1 output = %q, want exactly the rule list %q", got, want) } } // TestNewWorldShape pins the starting world the brief describes. func TestNewWorldShape(t *testing.T) { w := newWorld(1, false) if len(w.boxes) != 2 || w.boxes[0].ID != 0 || w.boxes[1].ID != 1 { t.Fatalf("want boxes 0 and 1, got %d", len(w.boxes)) } const eng = "eng.acct.apps.example" r := w.boxes[0].Rows[eng] if r == nil || !r.Platform || r.Kind != "instance" || r.Owner != "eng" || r.Backend == "" { t.Fatalf("box 0 lacks the minted platform row %s: %+v", eng, r) } if got := w.group(w.boxes[0], eng); got != "eng" { t.Fatalf("group of the minted row = %q, want eng", got) } if m := w.boxes[0].Mark[eng]; m != markInternal { t.Fatalf("the engine row's mark = %v, want markInternal", m) } s := w.boxes[1].Rows["shop"] if s == nil || s.Platform || s.Owner != "" || s.Backend == "" { t.Fatalf("box 1 lacks the public app row shop: %+v", s) } if got := w.group(w.boxes[1], "shop"); got != "shop" { t.Fatalf("group of an unminted row = %q, want its host", got) } if m := w.boxes[1].Mark["shop"]; m != markPublic { t.Fatalf("shop's mark = %v, want markPublic", m) } for v := 1; v <= 4; v++ { if b, ok := w.builds[v]; !ok || b.Ver != v { t.Fatalf("build %d missing or mislabelled: %+v", v, b) } } if len(w.builds) != 4 { t.Fatalf("want builds 1 to 4, got %d", len(w.builds)) } for _, b := range w.boxes { if b.Rows == nil || b.Mark == nil || b.Live == nil || b.Good == nil || b.Staged == nil { t.Fatalf("box %d has a nil map", b.ID) } } // a service row and a static row group by their own host b := w.boxes[1] b.Rows["svc"] = &row{Host: "svc", Kind: "service", Owner: "x"} if got := w.group(b, "svc"); got != "svc" { t.Fatalf("group of a service row = %q, want svc", got) } } // TestStepLoop: with no actions a step does nothing but advance w.step; with // 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 }() actions = nil w := runSeed(3, false) if w.step != chaosSteps+calmSteps { t.Fatalf("w.step = %d, want %d", w.step, chaosSteps+calmSteps) } if len(w.stats) != 0 || len(w.violations) != 0 { t.Fatalf("no actions: stats %v violations %v", w.stats, w.violations) } 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) bool { calls["alpha"]++ phase(w) return true }}, {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) 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) } if calls["alpha"] < 2*calls["beta"] { t.Fatalf("weights ignored: %v", calls) } 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") } } // TestFailOncePerInvariant: fail records the first violation of each // invariant id and ignores the repeats. func TestFailOncePerInvariant(t *testing.T) { w := newWorld(1, false) w.fail("INV-X", "first %d", 1) w.fail("INV-X", "second %d", 2) w.fail("INV-Y", "other") if len(w.violations) != 2 { t.Fatalf("violations = %v, want two", w.violations) } if !strings.HasPrefix(w.violations[0], "INV-X: first 1") || !strings.HasPrefix(w.violations[1], "INV-Y: other") { t.Fatalf("violations = %v", w.violations) } if invariantOf(w.violations[0]) != "INV-X" { t.Fatalf("invariantOf = %q", invariantOf(w.violations[0])) } } // TestOnMutation: on is false only for a rule named in the mutation set. func TestOnMutation(t *testing.T) { saved := mutated defer func() { mutated = saved }() mutated = "" if !on("any-rule") { t.Fatal("no mutation: every rule is on") } mutated = "a-rule,b-rule" if on("a-rule") || on("b-rule") || !on("c-rule") { t.Fatal("only the named rules are off") } } // TestEveryActionFires: over 200 seeds of the unmutated walk every registered // 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 = "" defer func() { mutated = saved }() if len(actions) == 0 { t.Fatal("no actions registered") } names := map[string]bool{} for _, a := range actions { if names[a.name] { t.Errorf("action %q registered twice", a.name) } names[a.name] = true if a.weight <= 0 { t.Errorf("action %q has weight %d: the walk never picks it", a.name, a.weight) } } done, events := map[string]int{}, map[string]int{} for seed := int64(1); seed <= 200; seed++ { 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 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) }