From 389047194f6be83b6abd1704fbfa1fb3cc2cde63 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 31 May 2026 06:26:52 +0000 Subject: [PATCH] =?UTF-8?q?feat(raft-library):=20task=205.1=20=E2=80=94=20?= =?UTF-8?q?protocol=20Groups=20A+B,=2010=20test=20cases=20(election=20+=20?= =?UTF-8?q?voted=5Ffor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/specs/raft-library/tasks.md | 2 +- src/lib/testsuite/test_raft.c | 316 ++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/raft-library/tasks.md b/.kiro/specs/raft-library/tasks.md index 1709b28..abdfcfb 100644 --- a/.kiro/specs/raft-library/tasks.md +++ b/.kiro/specs/raft-library/tasks.md @@ -88,7 +88,7 @@ - [ ] 5. Protocol-level test suite (test_raft.c) _Depends: 4_ -- [ ] 5.1 Test Groups A and B: basic election correctness and voted_for invariant (10 cases) +- [x] 5.1 Test Groups A and B: basic election correctness and voted_for invariant (10 cases) - A1: single-node cluster → LEADER immediately; `on_become_leader` fires once; no `on_send_vote_request` calls - A2: 3-node simultaneous election → `leader_count == 1` after `raft_sim_deliver_pending` - A3: node 0 wins, node 1 starts late election same term → node 1 denied; total `leader_count` stays 1 diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index c3d6231..c264ed3 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -1684,6 +1684,308 @@ static void test_sim_fire_timeout_steps_down(void) TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[0].state.role); } +/* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */ + +/* + * test_proto_A1_single_node_self_elects + * + * 1-node cluster: raft_start_election returns RAFT_ELECTED_IMMEDIATELY. + * role==LEADER, leader_count==1, no on_send_vote_request calls (no pending RPCs). + * on_become_leader fired once. + */ +static void test_proto_A1_single_node_self_elects(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 1, 2000)); + + int rc = raft_start_election(&c.nodes[0].state); + + /* Single-node shortcircuit */ + TEST_ASSERT_EQUAL_INT(RAFT_ELECTED_IMMEDIATELY, rc); + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + + /* on_become_leader fired once → leader_count incremented */ + TEST_ASSERT_EQUAL_INT(1, c.leader_count); + + /* No pending RPCs — no on_send_vote_request calls */ + TEST_ASSERT_EQUAL_INT(0, c.pending_count); +} + +/* + * test_proto_A2_3node_one_leader + * + * 3-node cluster: node 0 (highest priority, idx=0) wins the election. + * After deliver_pending: leader_count==1, node 0 is LEADER, others FOLLOWER. + */ +static void test_proto_A2_3node_one_leader(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); + + /* Only node 0 starts — it will ask nodes 1 and 2 for votes */ + raft_start_election(&c.nodes[0].state); + raft_sim_deliver_pending(&c); + + /* Exactly one leader */ + TEST_ASSERT_EQUAL_INT(1, c.leader_count); + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[1].state.role); + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[2].state.role); +} + +/* + * test_proto_A3_late_election_denied + * + * Node 0 wins at term=1. Node 1 already voted_for=0 at term=1. + * Send a vote request from idx=2 to node 1 at term=1 directly. + * Node 1 must DENY (already voted this term), leader_count stays 1. + */ +static void test_proto_A3_late_election_denied(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); + + /* Node 0 wins at term=1 */ + raft_start_election(&c.nodes[0].state); + raft_sim_deliver_pending(&c); + TEST_ASSERT_EQUAL_INT(1, c.leader_count); + + /* Node 1 granted node 0 at term=1: voted_for=0, voted_for_term=1 */ + TEST_ASSERT_EQUAL_INT(0, c.nodes[1].state.voted_for); + TEST_ASSERT_EQUAL_UINT64(1, c.nodes[1].state.voted_for_term); + + /* Now node 2 tries to request a vote from node 1 at the same term=1 */ + uint64_t reply_term = 0; + int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, &reply_term); + + /* Node 1 must deny — already voted for node 0 this term */ + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + + /* leader_count still 1 (no new leader elected) */ + TEST_ASSERT_EQUAL_INT(1, c.leader_count); +} + +/* + * test_proto_A4_5node_quorum_3 + * + * 5-node cluster: node 0 starts the election. + * Quorum=3 (self + 2 peer grants). After deliver, node 0 is LEADER. + */ +static void test_proto_A4_5node_quorum_3(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 5, 2000)); + + raft_start_election(&c.nodes[0].state); + raft_sim_deliver_pending(&c); + + TEST_ASSERT_EQUAL_INT(1, c.leader_count); + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + /* Self vote + 4 peer grants → election_decided=1 after first quorum reached */ + TEST_ASSERT_GREATER_OR_EQUAL(3, c.nodes[0].state.votes_granted); +} + +/* + * test_proto_A5_leader_denies_vote_request + * + * Node 0 is LEADER at term=1. Receives a vote request from idx=1 at term=1. + * The role gate must deny (LEADER, same term → no step-down). + * Node 0 stays LEADER. + */ +static void test_proto_A5_leader_denies_vote_request(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); + + /* Make node 0 the leader */ + raft_start_election(&c.nodes[0].state); + raft_sim_deliver_pending(&c); + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + + uint64_t term_before = c.nodes[0].state.term; /* =1 */ + int lc_before = c.leader_count; + + /* Send a vote request to the leader at the same term */ + uint64_t reply_term = 0; + int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, &reply_term); + + /* Must be denied */ + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + + /* Node 0 remains LEADER */ + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + + /* No new leader elected */ + TEST_ASSERT_EQUAL_INT(lc_before, c.leader_count); +} + +/* ---------- Group B: voted_for Invariant (task 5.1) ---------- */ + +/* + * test_proto_B1_one_vote_per_term + * + * Node 2 receives two RPCs in the same term — one from node 0, one from node 1. + * Both RPCs are buffered (nodes 0 and 1 both start elections simultaneously). + * After delivery node 2 grants only the first requester; the second is denied. + * voted_for is set to exactly one candidate. + */ +static void test_proto_B1_one_vote_per_term(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 start elections — they each send to all peers */ + raft_start_election(&c.nodes[0].state); + raft_start_election(&c.nodes[1].state); + + /* 4 RPCs buffered: node0→1, node0→2, node1→0, node1→2 */ + TEST_ASSERT_EQUAL_INT(4, c.pending_count); + + /* Deliver all */ + raft_sim_deliver_pending(&c); + + /* Node 2 must have voted for exactly one candidate */ + TEST_ASSERT_NOT_EQUAL(-1, c.nodes[2].state.voted_for); + + /* voted_for must be 0 or 1 (one of the two candidates that requested) */ + int vf = c.nodes[2].state.voted_for; + TEST_ASSERT_TRUE(vf == 0 || vf == 1); +} + +/* + * test_proto_B2_higher_term_updates_state + * + * Single-node at term=2, role=FOLLOWER, voted_for=1 (old vote). + * Receives vote request at term=5 (higher). + * Expected: node.term==5, voted_for updated (grant because on_vote_eligible is NULL). + */ +static void test_proto_B2_higher_term_updates_state(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + reset_mock(); + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 1, 2)); + + /* Simulate old vote from a previous term */ + s.voted_for = 1; /* node voted for itself in an earlier election */ + s.voted_for_term = 1; /* that vote was in term 1 */ + + int rc = raft_on_vote_request(&s, 0, 5, &reply_term); + + /* Term must be upgraded to 5 */ + TEST_ASSERT_EQUAL_UINT64(5, s.term); + + /* After term upgrade, voted_for is cleared to -1 and then re-evaluated. + * With on_vote_eligible=NULL and persist succeeding, must grant. */ + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_UINT64(5, reply_term); + + /* voted_for must now be set to from_idx=0 */ + TEST_ASSERT_EQUAL_INT(0, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); +} + +/* + * test_proto_B3_stale_term_denied + * + * Node at term=5. Receives request at term=3 (stale). + * Must return RAFT_DENY, reply_term==5, node.term==5 (unchanged). + */ +static void test_proto_B3_stale_term_denied(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + reset_mock(); + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + + int rc = raft_on_vote_request(&s, 0, 3, &reply_term); + + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_UINT64(5, reply_term); + TEST_ASSERT_EQUAL_UINT64(5, s.term); /* term unchanged */ + + /* on_persist must NOT fire */ + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + +/* + * test_proto_B4_different_candidate_denied + * + * voted_for=1, voted_for_term=3 at term=3. + * Receives request from idx=2 at term=3. + * Must DENY — already voted for 1 this term. + */ +static void test_proto_B4_different_candidate_denied(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + reset_mock(); + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3)); + s.voted_for = 1; + s.voted_for_term = 3; + + int rc = raft_on_vote_request(&s, 2, 3, &reply_term); + + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_UINT64(3, reply_term); + + /* voted_for must still be 1 — unchanged */ + TEST_ASSERT_EQUAL_INT(1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(3, s.voted_for_term); + + /* on_persist must NOT fire */ + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + +/* + * test_proto_B5_idempotent_same_candidate + * + * voted_for=1, voted_for_term=3 at term=3. + * Receives request from idx=1 at term=3 again (same candidate, same term). + * Must GRANT (idempotent re-grant). + */ +static void test_proto_B5_idempotent_same_candidate(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + reset_mock(); + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3)); + s.voted_for = 1; + s.voted_for_term = 3; + + int rc = raft_on_vote_request(&s, 1, 3, &reply_term); + + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_UINT64(3, reply_term); + + /* voted_for must still be 1 */ + TEST_ASSERT_EQUAL_INT(1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(3, s.voted_for_term); + + /* on_persist fires once for the re-grant */ + TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); +} + #endif /* XCE_TESTING */ /* ---------- test_raft entry point ---------- */ @@ -1749,5 +2051,19 @@ void test_raft(void) RUN_TEST(test_sim_start_all_elections_no_winner); RUN_TEST(test_sim_partition_no_quorum); RUN_TEST(test_sim_fire_timeout_steps_down); + + /* Group A: basic election correctness (task 5.1) */ + RUN_TEST(test_proto_A1_single_node_self_elects); + RUN_TEST(test_proto_A2_3node_one_leader); + RUN_TEST(test_proto_A3_late_election_denied); + RUN_TEST(test_proto_A4_5node_quorum_3); + RUN_TEST(test_proto_A5_leader_denies_vote_request); + + /* Group B: voted_for invariant (task 5.1) */ + RUN_TEST(test_proto_B1_one_vote_per_term); + RUN_TEST(test_proto_B2_higher_term_updates_state); + RUN_TEST(test_proto_B3_stale_term_denied); + RUN_TEST(test_proto_B4_different_candidate_denied); + RUN_TEST(test_proto_B5_idempotent_same_candidate); #endif }