feat(raft-library): task 5.1 — protocol Groups A+B, 10 test cases (election + voted_for)
This commit is contained in:
parent
c1b07392df
commit
389047194f
2 changed files with 317 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue