diff --git a/.kiro/specs/raft-sim-conformance/tasks.md b/.kiro/specs/raft-sim-conformance/tasks.md index ca5dc00..6168dc2 100644 --- a/.kiro/specs/raft-sim-conformance/tasks.md +++ b/.kiro/specs/raft-sim-conformance/tasks.md @@ -192,7 +192,7 @@ - _Boundary: raft_sim.c_ - _Depends: 5.4, 5.5_ -- [ ] 7.2 RED+GREEN: negative test asserts both directions +- [x] 7.2 RED+GREEN: negative test asserts both directions - Test: under the broken configuration the Figure-8 / divergent-suffix scenario triggers a Log Matching / Leader Completeness / State Machine Safety violation (with trace dump); under the correct configuration the same scenario passes — both asserted in one test - Observable completion: the broken build is caught and the correct build passes, proving checker sensitivity - _Requirements: 7.1, 7.2, 7.4_ diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 53e5353..5ddd28b 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -8640,6 +8640,101 @@ static void test_sim_7_1_fault_switch_mechanism(void) } } +/* + * test_sim_7_2_negative_both_directions (raft-sim-conformance task 7.2; Req 7.1, 7.2, 7.4): + * the oracle's-teeth negative test — ONE test that proves checker sensitivity in BOTH + * directions within a SINGLE binary: + * + * (1) BROKEN: with RAFT_SIM_BREAK_TERM_STAMP set (on_persist_entry stamps every entry + * term 0 — emulating bug-851 G1), the randomized action scheduler driving the same + * (n, seed) reaches a state where two nodes share a (index, term) yet hold differing + * prefixes (term-0 collapse makes a term-A and a term-B entry at the same index look + * identical to the matching test). raft_sim_check_log_matching FIRES — a GENUINE Log + * Matching SAFETY violation, surfaced as rc == -(int)RAFT_SIM_INV_LOG_MATCHING with + * the replayable trace dumped to stderr (expected for a deliberately-failing run). + * (2) CORRECT: a FRESH cluster with the SAME (n, seed) and the fault CLEARED runs the + * SAME scheduler to completion with ZERO violations (rc == 0). + * + * Both asserted here, so the checker's ability to BOTH catch the broken build AND pass the + * correct one is itself verified (R7.4). The (fault, driver, n, seed) is fixed and named so + * the trip is reproducible: RAFT_SIM_BREAK_TERM_STAMP + raft_sim_run(&c, 400) at n=3, + * seed=1 (empirically the first index that exercises replication far enough to expose the + * term-0 collapse). raft_sim_run is preferred over the targeted scenarios because the + * scenarios self-init their cluster INTERNALLY and so cannot have the fault set BEFORE init; + * the action scheduler, by contrast, is driven over a caller-owned, pre-faulted cluster. + */ +#define RAFT_SIM_NEG_N 3 /* cluster size for the negative-test trip (named, reproducible) */ +#define RAFT_SIM_NEG_SEED 1ULL /* seed that exercises replication far enough to trip log-matching */ +#define RAFT_SIM_NEG_STEPS 400 /* bounded scheduler budget (matches the conformance sweep cap) */ + +static void test_sim_7_2_negative_both_directions(void) +{ + /* ---- (1) BROKEN direction: fault ON must trip a GENUINE safety invariant ---- */ + int broken_rc; + { + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + TEST_ASSERT_EQUAL_INT(0, + raft_sim_conformance_init(&c, RAFT_SIM_NEG_N, 2000, RAFT_SIM_NEG_SEED)); + + /* Corrupt the persist adapter: every entry is stamped term 0 (bug-851 G1). */ + raft_sim_set_fault_inject(&c, RAFT_SIM_BREAK_TERM_STAMP); + TEST_ASSERT_EQUAL_UINT(RAFT_SIM_BREAK_TERM_STAMP, c.fault_inject); + + /* Drive the randomized scheduler. It halts and returns < 0 (dumping the + * replayable trace to stderr — EXPECTED for this deliberately-broken run) on + * the FIRST invariant violation raft_sim_check_all detects. */ + broken_rc = raft_sim_run(&c, RAFT_SIM_NEG_STEPS); + + /* The broken build is CAUGHT: a real violation fired (rc < 0). */ + TEST_ASSERT_TRUE_MESSAGE(broken_rc < 0, + "BROKEN config (term-0 stamp) did NOT trip any invariant — the oracle lacks teeth"); + + /* And the failing invariant is a genuine SAFETY property, not merely a different + * return code: decode the negated return and require Log Matching / Leader + * Completeness / State Machine Safety (the three core safety invariants). */ + raft_sim_invariant_t inv = (raft_sim_invariant_t)(-broken_rc); + int is_safety = (inv == RAFT_SIM_INV_LOG_MATCHING) || + (inv == RAFT_SIM_INV_LEADER_COMPLETENESS) || + (inv == RAFT_SIM_INV_STATE_MACHINE_SAFETY); + char msg[160]; + snprintf(msg, sizeof(msg), + "BROKEN config tripped '%s' (rc=%d) — expected a Log Matching / Leader " + "Completeness / State Machine Safety violation", + raft_sim_invariant_name(inv), broken_rc); + TEST_ASSERT_TRUE_MESSAGE(is_safety, msg); + + /* For this fixed key the deterministic first trip is Log Matching (term-0 collapse + * makes two distinct-term entries at one index look identical to the prefix check). */ + TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_SIM_INV_LOG_MATCHING, inv, + "BROKEN config at the fixed key must deterministically trip Log Matching"); + } + + /* ---- (2) CORRECT direction: SAME (n, seed), fault OFF, ZERO violations ---- */ + { + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + TEST_ASSERT_EQUAL_INT(0, + raft_sim_conformance_init(&c, RAFT_SIM_NEG_N, 2000, RAFT_SIM_NEG_SEED)); + + /* Explicitly clear the fault (also the zero-init default) — the correct build. */ + raft_sim_set_fault_inject(&c, 0); + TEST_ASSERT_EQUAL_UINT(0u, c.fault_inject); + + int correct_rc = raft_sim_run(&c, RAFT_SIM_NEG_STEPS); + + /* The correct build PASSES the SAME scenario/seed with no invariant violation. */ + TEST_ASSERT_EQUAL_INT_MESSAGE(0, correct_rc, + "CORRECT config (fault off) surfaced a violation on the SAME (n, seed) — " + "the library is correct, so this must be clean"); + TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_SIM_INV_NONE, raft_sim_check_all(&c), + "CORRECT config final state violates an invariant on the SAME (n, seed)"); + } + + /* R7.4: BOTH directions asserted in this ONE test — the broken build is caught + * (broken_rc < 0, Log Matching) AND the correct build passes (correct_rc == 0). */ +} + #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) */ @@ -8935,6 +9030,7 @@ void test_raft(void) RUN_TEST(test_sim_5_4_figure8); RUN_TEST(test_sim_5_5_stale_rejoin); RUN_TEST(test_sim_7_1_fault_switch_mechanism); + RUN_TEST(test_sim_7_2_negative_both_directions); RUN_TEST(test_sim_6_1_seed_sweep); /* Group A: basic election correctness (task 5.1) */