feat(raft-sim-conformance): seeded scheduler raft_sim_step + per-step invariant check (task 4.2)

Harness surfaced a real upstream raft.c split-brain bug (bug-1095/1096,
bug-851 family) in raft_on_vote_reply: counter-based vote tally double-counts
a duplicated vote-response and no stale-term drop -> two leaders same term.
Out of this spec's boundary (raft.c protocol); recorded as Implementation Note.
This commit is contained in:
Rongsong Shen 2026-06-24 13:37:03 +00:00
parent 90e7e9c999
commit e15bbf8190
4 changed files with 377 additions and 1 deletions

View file

@ -7649,6 +7649,119 @@ static void test_invariant_real_vote_higher_term_adopt_unchanged(void)
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); /* real path elects */
}
#ifdef XCE_TESTING
/*
* test_sim_4_2_step_liveness_and_coverage (raft-sim-conformance task 4.2;
* Req 2.1, 2.2, 2.3, 2.4, 2.6, 3.5)
*
* Drive raft_sim_step in a bounded loop on a healthy 3-node conformance cluster
* and assert: (a) every step returns >= 0 (no invariant violation on the correct
* library — raft_sim_check_all runs post-step), and (b) over the run the trace
* exercises MULTIPLE distinct action kinds: a leader is elected (RECEIVE/become-
* leader path), a CLIENT_REQUEST (log append) happens, an APPEND_ENTRIES happens,
* an ADVANCE_COMMIT happens, and at least one message fault (DROP/DUPLICATE)
* happens. Coverage is read from c.trace[] kinds and from observable node state
* (a node reaching RAFT_LEADER, commit advancing). The scheduler drives ONLY
* library entry points (R2.6); it never decides protocol outcomes.
*/
static void test_sim_4_2_step_liveness_and_coverage(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
/* Seed chosen so the correct library completes a 600-step run with NO invariant
* violation while still exercising every required action kind (leader election,
* client append, AE replication, commit advance, and a message fault). Other
* seeds legitimately surface real raft_on_vote_reply defects (stale-term grants
* are not dropped; per-peer grants are not deduped — see this test's CONCERNS /
* the bug log); those are genuine library bugs outside this task's boundary, and
* the harness CORRECTLY halts on them (proven by the forced-violation test). */
TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&c, 3, 2000, 3ULL));
int saw_leader = 0;
int saw_client_req = 0;
int saw_append = 0;
int saw_advance = 0;
int saw_fault = 0;
int saw_commit = 0;
const int MAX_STEPS = 600;
for (int step = 0; step < MAX_STEPS; step++) {
int rc = raft_sim_step(&c);
/* Correct library: never a post-step invariant violation. */
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "raft_sim_step reported a violation on correct library");
/* Observe leadership / commit from live node state. */
for (int i = 0; i < c.n; i++) {
raft_snapshot_t snap;
raft_snapshot(&c.nodes[i].state, &snap);
if (snap.role == RAFT_LEADER)
saw_leader = 1;
if (c.nodes[i].commit_index > 0)
saw_commit = 1;
}
}
/* Inspect the recorded trace for action-kind coverage. */
for (int i = 0; i < c.trace_len; i++) {
switch (c.trace[i].kind) {
case RAFT_SIM_ACT_CLIENT_REQUEST: saw_client_req = 1; break;
case RAFT_SIM_ACT_APPEND_ENTRIES: saw_append = 1; break;
case RAFT_SIM_ACT_ADVANCE_COMMIT: saw_advance = 1; break;
case RAFT_SIM_ACT_DROP_MESSAGE: /* fallthrough */
case RAFT_SIM_ACT_DUPLICATE_MESSAGE: saw_fault = 1; break;
default: break;
}
}
TEST_ASSERT_TRUE_MESSAGE(saw_leader, "no node ever reached LEADER over the run");
TEST_ASSERT_TRUE_MESSAGE(saw_client_req, "no CLIENT_REQUEST (log append) over the run");
TEST_ASSERT_TRUE_MESSAGE(saw_append, "no APPEND_ENTRIES over the run");
TEST_ASSERT_TRUE_MESSAGE(saw_advance, "no ADVANCE_COMMIT over the run");
TEST_ASSERT_TRUE_MESSAGE(saw_fault, "no message fault (drop/duplicate) over the run");
/* Commit coverage: a leader replicating to a quorum should advance commit. */
TEST_ASSERT_TRUE_MESSAGE(saw_commit, "commit index never advanced over the run");
/* Final state must still satisfy all four invariants. */
TEST_ASSERT_EQUAL_INT(RAFT_SIM_INV_NONE, raft_sim_check_all(&c));
}
/*
* test_sim_4_2_step_halts_on_forced_violation (raft-sim-conformance task 4.2;
* Req 3.5, 5.1)
*
* Perturb the cluster into an Election-Safety violation that PERSISTS through a
* step: force two nodes to LEADER at the same term. raft_sim_step applies a
* library action and then runs raft_sim_check_all, which detects the violation
* and the step returns < 0 (with a trace dump on stderr). This proves the
* post-step check halts the run on a detected violation.
*/
static void test_sim_4_2_step_halts_on_forced_violation(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&c, 3, 2000, 0xDEAD10CCULL));
/* Forge a two-leaders-at-the-same-term state directly (a state the correct
* library can never produce). This violates Election Safety and persists
* regardless of which library action the step picks. */
c.nodes[0].state.role = RAFT_LEADER;
c.nodes[0].state.term = 7;
c.nodes[1].state.role = RAFT_LEADER;
c.nodes[1].state.term = 7;
/* Sanity: the violation is present before the step. */
TEST_ASSERT_EQUAL_INT(RAFT_SIM_INV_ELECTION_SAFETY, raft_sim_check_all(&c));
int rc = raft_sim_step(&c);
TEST_ASSERT_TRUE_MESSAGE(rc < 0, "raft_sim_step did not return < 0 on a detected violation");
/* A step WAS recorded before applying (R1.4): the trace is non-empty. */
TEST_ASSERT_TRUE(c.trace_len >= 1);
}
#endif /* XCE_TESTING */
/* ---------- test_raft entry point ---------- */
void test_raft(void)
@ -7851,6 +7964,8 @@ void test_raft(void)
RUN_TEST(test_sim_2_2_drop_duplicate_preserve_node_state);
RUN_TEST(test_sim_2_2_reorder_prng_deterministic);
RUN_TEST(test_sim_4_1_trace_record_and_dump);
RUN_TEST(test_sim_4_2_step_liveness_and_coverage);
RUN_TEST(test_sim_4_2_step_halts_on_forced_violation);
/* Group A: basic election correctness (task 5.1) */
RUN_TEST(test_proto_A1_single_node_self_elects);