diff --git a/.kiro/specs/raft-sim-conformance/tasks.md b/.kiro/specs/raft-sim-conformance/tasks.md index 2f45971..0fc9fa2 100644 --- a/.kiro/specs/raft-sim-conformance/tasks.md +++ b/.kiro/specs/raft-sim-conformance/tasks.md @@ -168,7 +168,7 @@ ## 6. CI batch and randomized seed sweep -- [ ] 6.1 RED+GREEN: randomized M-seed sweep with zero violations +- [x] 6.1 RED+GREEN: randomized M-seed sweep with zero violations - Test: run `raft_sim_run` for a fixed `max_steps` across M seeds (M ≥ configured minimum) plus all targeted scenarios; fail if any seed/scenario violates any invariant - Implement the sweep harness in `test_raft.c`; bound `max_steps`, M, and array sizes so the target fits the library test-suite budget - Observable completion: the full batch runs clean and within budget diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 771dc51..b6d6f2c 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -8526,6 +8526,106 @@ static void test_sim_5_5_stale_rejoin(void) "post-stale-rejoin cluster reports more than one leader (Election Safety)"); } } + +/* + * test_sim_6_1_seed_sweep (raft-sim-conformance task 6.1; Req 6.1, 6.2, 6.4) + * + * CI conformance batch: run the randomized action scheduler (raft_sim_run) across a + * configured minimum of seeds for several cluster sizes, then run ALL FIVE targeted + * scenarios across a handful of seeds each — asserting ZERO invariant violations + * anywhere (R6.2). + * + * raft_sim_run drives raft_sim_step up to RAFT_SIM_SWEEP_STEPS actions, halting and + * returning < 0 on the FIRST invariant violation (the step already dumps the + * replayable trace to stderr) and returning 0 on a clean run. The library is correct + * (bug-1095/1096 fixed), so the entire batch MUST be clean: any rc < 0 here is a REAL + * divergence — NOT something to mask by shrinking M or skipping the offending seed. + * The failure message names the exact (seed, n) so the failing run is reproducible + * (R5/6.2): re-run raft_sim_conformance_init(&c, n, 2000, seed) + raft_sim_run. + * + * Bounds (R6.4): RAFT_SIM_SWEEP_M seeds (the configured minimum) over a small set of + * cluster sizes, each capped at RAFT_SIM_SWEEP_STEPS steps; the harness is fixed-array + * (no allocation) so the whole batch stays within the lib test-suite time budget. + */ +#define RAFT_SIM_SWEEP_M 32 /* configured minimum number of randomized seeds (R6.2) */ +#define RAFT_SIM_SWEEP_STEPS 400 /* per-run step cap — bounded for the suite budget (R6.4) */ + +static void test_sim_6_1_seed_sweep(void) +{ + /* Cluster sizes swept for the randomized batch. Raft rejects even peer counts + * (no quorum benefit, raft_init -EINVAL), so only ODD sizes <= RAFT_SIM_MAX_NODES. */ + static const int sizes[] = { 3, 5, 7 }; + const int election_timeout = 2000; + + /* --- Randomized seed sweep: M >= configured minimum seeds x each cluster size --- */ + for (int s = 0; s < RAFT_SIM_SWEEP_M; s++) { + /* Deterministic, well-spread seeds derived from the index s (documented): + * a large odd multiplier + offset so consecutive seeds differ in many bits. */ + uint64_t seed = (uint64_t)s * 0x9E3779B97F4A7C15ULL + 0xD1B54A32D192ED03ULL; + + for (size_t z = 0; z < sizeof(sizes) / sizeof(sizes[0]); z++) { + int n = sizes[z]; + + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, + raft_sim_conformance_init(&c, n, election_timeout, seed)); + + int rc = raft_sim_run(&c, RAFT_SIM_SWEEP_STEPS); + + /* Zero violations through the whole run. On failure, name (seed, n) so the + * run is reproducible — the step already dumped the replayable trace. */ + if (rc != 0) { + char msg[160]; + snprintf(msg, sizeof(msg), + "seed sweep surfaced invariant violation rc=%d at seed=%llu n=%d " + "(reproduce: raft_sim_conformance_init(&c,%d,%d,%lluULL); " + "raft_sim_run(&c,%d))", + rc, (unsigned long long)seed, n, + n, election_timeout, (unsigned long long)seed, RAFT_SIM_SWEEP_STEPS); + TEST_FAIL_MESSAGE(msg); + } + } + } + + /* --- All five targeted scenarios over a handful of seeds each (R6.2). Each + * scenario inits the caller's cluster internally from (n, seed) and returns 0 on + * zero violations, < 0 (dumping its trace) on any violation. --- */ + static const uint64_t scen_seeds[] = { 1ULL, 3ULL, 7ULL, 0x4A33ULL, 0xBEEFULL }; + + for (size_t k = 0; k < sizeof(scen_seeds) / sizeof(scen_seeds[0]); k++) { + uint64_t seed = scen_seeds[k]; + + /* n chosen per seed index, alternating 3- and 5-node clusters. */ + int n = (k % 2 == 0) ? 3 : 5; + + struct { const char *name; int (*fn)(raft_sim_cluster_t *, int, uint64_t); } scenarios[] = { + { "partition_heal", raft_sim_scenario_partition_heal }, + { "restart_churn", raft_sim_scenario_restart_churn }, + { "drop_dup_reorder",raft_sim_scenario_drop_dup_reorder}, + { "figure8", raft_sim_scenario_figure8 }, + { "stale_rejoin", raft_sim_scenario_stale_rejoin }, + }; + + for (size_t i = 0; i < sizeof(scenarios) / sizeof(scenarios[0]); i++) { + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + int rc = scenarios[i].fn(&c, n, seed); + + if (rc != 0) { + char msg[192]; + snprintf(msg, sizeof(msg), + "scenario '%s' surfaced violation rc=%d at seed=%lluULL n=%d " + "(reproduce: raft_sim_scenario_%s(&c,%d,%lluULL))", + scenarios[i].name, rc, (unsigned long long)seed, n, + scenarios[i].name, n, (unsigned long long)seed); + TEST_FAIL_MESSAGE(msg); + } + } + } +} #endif /* XCE_TESTING */ /* ---------- test_raft entry point ---------- */ @@ -8741,6 +8841,7 @@ void test_raft(void) RUN_TEST(test_sim_5_3_drop_dup_reorder); RUN_TEST(test_sim_5_4_figure8); RUN_TEST(test_sim_5_5_stale_rejoin); + RUN_TEST(test_sim_6_1_seed_sweep); /* Group A: basic election correctness (task 5.1) */ RUN_TEST(test_proto_A1_single_node_self_elects);