feat(raft-sim-conformance): Leader Completeness invariant checker (task 3.3)

This commit is contained in:
Rongsong Shen 2026-06-24 12:44:18 +00:00
parent d9b5fd51e0
commit 63a9740c85
4 changed files with 143 additions and 1 deletions

View file

@ -3750,6 +3750,80 @@ static void test_sim_check_election_safety_pass_on_single_leader(void)
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_election_safety(&c));
}
/*
* test_sim_check_leader_completeness_fail_on_missing_committed (task 3.3)
*
* Leader Completeness: an entry committed in term t must be present (same index,
* same term) in the log of every leader of a higher term. Hand-build a cluster
* where node B has committed (index=2, term=3) but the current higher-term leader
* (node A, LEADER at term 5) is MISSING that index ⇒ the checker must FAIL (0).
* (Hand-set role/term/log/commit_index directly — this is a checker unit test over
* a hand-built cluster, not a protocol run.)
*/
static void test_sim_check_leader_completeness_fail_on_missing_committed(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
/* Node B (idx 1): committed prefix 1..2, with entry 2 carrying term 3. */
c.nodes[1].log[0].term = 1;
c.nodes[1].log[1].term = 3;
c.nodes[1].log_count = 2;
c.nodes[1].commit_index = 2; /* (index=2, term=3) is committed */
/* Node A (idx 0): current LEADER at the HIGHER term 5, but its log is SHORT
* (only index 1) ⇒ it is MISSING the committed index 2 ⇒ violation. */
c.nodes[0].state.role = RAFT_LEADER;
c.nodes[0].state.term = 5;
c.nodes[0].log[0].term = 1;
c.nodes[0].log_count = 1;
c.nodes[2].state.role = RAFT_FOLLOWER;
c.nodes[2].state.term = 5;
TEST_ASSERT_EQUAL_INT(0, raft_sim_check_leader_completeness(&c));
}
/*
* test_sim_check_leader_completeness_pass_when_present (task 3.3)
*
* Leader Completeness holds when every higher-term leader's log contains every
* lower-term committed entry at the same (index, term). Two scenarios pass:
* (a) the committed (index=2, term=3) entry IS present in higher-term leader A;
* (b) a clean cluster with no current leaders trivially holds.
*/
static void test_sim_check_leader_completeness_pass_when_present(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
/* Node B (idx 1): committed prefix 1..2, entry 2 carries term 3. */
c.nodes[1].log[0].term = 1;
c.nodes[1].log[1].term = 3;
c.nodes[1].log_count = 2;
c.nodes[1].commit_index = 2;
/* (a) Node A (idx 0): LEADER at higher term 5 and DOES contain index 2 at
* term 3 (and index 1) ⇒ Leader Completeness holds. */
c.nodes[0].state.role = RAFT_LEADER;
c.nodes[0].state.term = 5;
c.nodes[0].log[0].term = 1;
c.nodes[0].log[1].term = 3;
c.nodes[0].log_count = 2;
c.nodes[2].state.role = RAFT_FOLLOWER;
c.nodes[2].state.term = 5;
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_leader_completeness(&c));
/* (b) no current leaders at all ⇒ trivially holds (nothing to bind). */
c.nodes[0].state.role = RAFT_FOLLOWER;
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_leader_completeness(&c));
}
/* ---------- Group: Simulator invariant tests (tasks 9.1-9.4) ----------
*
* These are observation-based invariant tests. They drive the COMPLETE
@ -7485,6 +7559,8 @@ void test_raft(void)
RUN_TEST(test_sim_3_2_log_matching_shared_idx_term_requires_identical_prefix);
RUN_TEST(test_sim_check_election_safety_fail_on_two_leaders);
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_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);