feat(raft-sim-conformance): State Machine Safety invariant checker (task 3.4)

This commit is contained in:
Rongsong Shen 2026-06-24 12:51:58 +00:00
parent 63a9740c85
commit a38d9a7271
4 changed files with 119 additions and 1 deletions

View file

@ -3824,6 +3824,75 @@ static void test_sim_check_leader_completeness_pass_when_present(void)
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_leader_completeness(&c));
}
/*
* test_sim_check_state_machine_safety_fail_on_divergence (task 3.4; Req 3.4, 3.6)
*
* State Machine Safety: no two nodes may have applied DIFFERENT values at the same
* (1-based) log index within their common applied prefix (1..min(commit_index)).
* Hand-build two nodes that both committed index 2 but applied DIFFERENT values at
* index 2 ⇒ the checker must FAIL (0). The applied extent per node is commit_index
* (sim_conf_on_commit_advanced fills applied[1..commit_index]); applied[k-1] is the
* value applied at 1-based index k. (Hand-set applied[]/commit_index directly — a
* checker unit test over a hand-built cluster, not a protocol run.)
*/
static void test_sim_check_state_machine_safety_fail_on_divergence(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
/* Both node 0 and node 1 committed (applied) indices 1..2. They AGREE at
* index 1 (value 10) but DISAGREE at index 2 (30 vs 99) ⇒ violation at a
* shared applied index. */
c.nodes[0].commit_index = 2;
c.nodes[0].applied[0] = 10;
c.nodes[0].applied[1] = 30;
c.nodes[1].commit_index = 2;
c.nodes[1].applied[0] = 10;
c.nodes[1].applied[1] = 99; /* differs from node 0 at index 2 */
TEST_ASSERT_EQUAL_INT(0, raft_sim_check_state_machine_safety(&c));
}
/*
* test_sim_check_state_machine_safety_pass_on_agreement (task 3.4; Req 3.4, 3.6)
*
* State Machine Safety holds when every pair of nodes agrees on applied values
* across their COMMON applied prefix. Two scenarios pass:
* (a) two nodes with identical applied[] over equal commit extents (both [10,20]);
* (b) nodes with DIFFERENT commit extents but identical values over the SHARED
* prefix (node0 [10,20,30] @commit 3 vs node1 [10,20] @commit 2 ⇒ shared
* prefix 1..2 is identical ⇒ holds; index 3 is not applied by node1).
*/
static void test_sim_check_state_machine_safety_pass_on_agreement(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
/* (a) Identical applied[] over equal commit extents. */
c.nodes[0].commit_index = 2;
c.nodes[0].applied[0] = 10;
c.nodes[0].applied[1] = 20;
c.nodes[1].commit_index = 2;
c.nodes[1].applied[0] = 10;
c.nodes[1].applied[1] = 20;
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_state_machine_safety(&c));
/* (b) Different commit extents, identical over the shared prefix 1..2. Node 0
* has applied one extra index (3 -> 30) that node 1 has not committed; the
* shared prefix still agrees ⇒ holds. */
c.nodes[0].commit_index = 3;
c.nodes[0].applied[2] = 30;
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_state_machine_safety(&c));
}
/* ---------- Group: Simulator invariant tests (tasks 9.1-9.4) ----------
*
* These are observation-based invariant tests. They drive the COMPLETE
@ -7561,6 +7630,8 @@ void test_raft(void)
RUN_TEST(test_sim_check_election_safety_pass_on_single_leader);
RUN_TEST(test_sim_check_leader_completeness_fail_on_missing_committed);
RUN_TEST(test_sim_check_leader_completeness_pass_when_present);
RUN_TEST(test_sim_check_state_machine_safety_fail_on_divergence);
RUN_TEST(test_sim_check_state_machine_safety_pass_on_agreement);
RUN_TEST(test_sim_9_1_replication_and_partition_catch_up);
RUN_TEST(test_sim_9_2_divergent_suffix_truncation);
RUN_TEST(test_sim_9_3_drop_dup_reorder_convergence);