test(raft-sim-conformance): negative test proving oracle teeth, both directions (task 7.2)

This commit is contained in:
Rongsong Shen 2026-06-24 16:30:20 +00:00
parent 62523892d9
commit 90197f8595
2 changed files with 97 additions and 1 deletions

View file

@ -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) */