feat(raft-library): task 5.3 — Groups D+E 7 test cases; fix Req 6.3 on_cancel_timeout in decided path
This commit is contained in:
parent
0aa4c84bfe
commit
5c677bc52d
3 changed files with 279 additions and 3 deletions
|
|
@ -225,6 +225,7 @@ int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term
|
|||
void raft_on_election_timeout(raft_state_t *s)
|
||||
{
|
||||
if (s->election_decided) {
|
||||
/* Req 6.3: cancel any pending timer and be a no-op */
|
||||
s->cbs->on_cancel_timeout(s->ctx);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "election_timeout_noop", NULL);
|
||||
|
|
|
|||
|
|
@ -1287,7 +1287,7 @@ static void test_election_timeout_undecided(void)
|
|||
*
|
||||
* election_decided=1 already set (election outcome already fired).
|
||||
* raft_on_election_timeout must:
|
||||
* - fire on_cancel_timeout once (stale timer cleanup)
|
||||
* - fire on_cancel_timeout once (stale timer cleanup, Req 6.3)
|
||||
* - NOT fire an additional on_become_follower
|
||||
* - trace "election_timeout_noop"
|
||||
*/
|
||||
|
|
@ -1309,7 +1309,7 @@ static void test_election_timeout_already_decided(void)
|
|||
|
||||
raft_on_election_timeout(&s);
|
||||
|
||||
/* on_cancel_timeout must fire once (stale timer cleanup) */
|
||||
/* on_cancel_timeout must fire once (stale timer cleanup, Req 6.3) */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* on_become_follower must NOT have fired an additional time */
|
||||
|
|
@ -2320,6 +2320,270 @@ static void test_proto_B5_idempotent_same_candidate(void)
|
|||
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
|
||||
}
|
||||
|
||||
/* ---------- Group D: Quorum and Result (task 5.3) ---------- */
|
||||
|
||||
/*
|
||||
* test_proto_D1_quorum_1grant_of_2_needed
|
||||
*
|
||||
* 3-node cluster, node 0 starts election (votes_granted=1, n_peers=3, quorum=2).
|
||||
* Feed 1 grant reply from peer 1: votes_granted becomes 2 == quorum.
|
||||
* Expected: RAFT_BECAME_LEADER, role==RAFT_LEADER, election_decided==1,
|
||||
* on_become_leader fired exactly once.
|
||||
*/
|
||||
static void test_proto_D1_quorum_1grant_of_2_needed(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election: votes_granted=1 (self), role=CANDIDATE */
|
||||
int start_rc = raft_start_election(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, start_rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.votes_granted);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count);
|
||||
|
||||
/* Feed 1 grant from peer 1: votes_granted=2 == quorum=2 */
|
||||
int rc = raft_on_vote_reply(&s, 1, 1, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_leader must have fired exactly once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_proto_D2_both_peers_deny_becomes_follower
|
||||
*
|
||||
* 3-node cluster, node 0 starts election (votes_granted=1, replies_received=0).
|
||||
* Feed 2 deny replies from peers 1 and 2.
|
||||
* After second deny: replies_received==2 == n_peers-1=2, votes_granted=1 < quorum=2.
|
||||
* Expected: RAFT_BECAME_FOLLOWER, role==RAFT_FOLLOWER, election_decided==1,
|
||||
* on_become_follower fired.
|
||||
*/
|
||||
static void test_proto_D2_both_peers_deny_becomes_follower(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.replies_received);
|
||||
|
||||
/* First deny from peer 1: replies_received=1, still pending */
|
||||
int rc1 = raft_on_vote_reply(&s, 1, 0, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.replies_received);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.election_decided);
|
||||
|
||||
/* Second deny from peer 2: replies_received=2 == n_peers-1=2 → step down */
|
||||
int rc2 = raft_on_vote_reply(&s, 2, 0, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower must have fired once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_proto_D3_second_vote_reply_noop
|
||||
*
|
||||
* 3-node cluster, start election.
|
||||
* Feed 1 grant → RAFT_BECAME_LEADER (election_decided=1).
|
||||
* Feed a second grant from peer 2 → RAFT_PENDING (noop, election_decided guard).
|
||||
* Expected: become_leader_count still 1 (on_become_leader fired exactly once).
|
||||
*/
|
||||
static void test_proto_D3_second_vote_reply_noop(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
|
||||
/* First grant: wins election */
|
||||
int rc1 = raft_on_vote_reply(&s, 1, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
|
||||
/* Second grant from peer 2: election already decided → noop */
|
||||
int rc2 = raft_on_vote_reply(&s, 2, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc2);
|
||||
|
||||
/* on_become_leader must NOT have fired again */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_proto_D4_reply_higher_term_steps_down
|
||||
*
|
||||
* 3-node cluster, start election (term=1).
|
||||
* Feed reply with term=3 (granted=0) → higher term → step down.
|
||||
* Expected: RAFT_BECAME_FOLLOWER, role==RAFT_FOLLOWER, s.term==3,
|
||||
* election_decided==1, on_become_follower fired.
|
||||
*/
|
||||
static void test_proto_D4_reply_higher_term_steps_down(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election: term becomes 1 */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(1, (int)s.term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
|
||||
/* Reply with higher term=3, not granted */
|
||||
int rc = raft_on_vote_reply(&s, 1, 0, 3);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower must have fired once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/* ---------- Group E: Election Timeout (task 5.3) ---------- */
|
||||
|
||||
/*
|
||||
* test_proto_E1_timeout_before_quorum
|
||||
*
|
||||
* 3-node cluster, start election (on_schedule_timeout fires → schedule_timeout_count==1).
|
||||
* Call raft_on_election_timeout before any replies.
|
||||
* Expected: role==RAFT_FOLLOWER, election_decided==1, on_become_follower fired,
|
||||
* on_cancel_timeout fired.
|
||||
*/
|
||||
static void test_proto_E1_timeout_before_quorum(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election: schedule_timeout fires */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count);
|
||||
|
||||
/* Fire timeout before any replies */
|
||||
raft_on_election_timeout(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower must have fired */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
||||
|
||||
/* on_cancel_timeout must have fired */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_proto_E2_timeout_after_quorum_noop
|
||||
*
|
||||
* 3-node cluster, start election, feed 1 grant → RAFT_BECAME_LEADER.
|
||||
* Call raft_on_election_timeout.
|
||||
* Expected: role still RAFT_LEADER (already decided, timeout is noop),
|
||||
* on_cancel_timeout fired (stale timer cleanup),
|
||||
* on_become_follower NOT additionally fired.
|
||||
*/
|
||||
static void test_proto_E2_timeout_after_quorum_noop(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* Start election */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
|
||||
/* Feed 1 grant → LEADER */
|
||||
int rc = raft_on_vote_reply(&s, 1, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* Snapshot counters before stale timeout delivery */
|
||||
int follower_before = g_mock.become_follower_count;
|
||||
int cancel_before = g_mock.cancel_timeout_count;
|
||||
|
||||
/* Fire timeout: already decided → fires on_cancel_timeout, no other effects (Req 6.3) */
|
||||
raft_on_election_timeout(&s);
|
||||
|
||||
/* Role must still be LEADER */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
|
||||
/* on_cancel_timeout fires exactly once for the stale delivery (Req 6.3) */
|
||||
TEST_ASSERT_EQUAL_INT(cancel_before + 1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* on_become_follower must NOT have fired additionally */
|
||||
TEST_ASSERT_EQUAL_INT(follower_before, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_proto_E3_new_election_after_timeout
|
||||
*
|
||||
* 3-node cluster, start election (term=1).
|
||||
* Timeout fires → becomes FOLLOWER (election_decided=1).
|
||||
* Start new election: raft_start_election again.
|
||||
* Expected: term==2 (incremented), on_schedule_timeout fired again
|
||||
* (schedule_timeout_count==2 total).
|
||||
*/
|
||||
static void test_proto_E3_new_election_after_timeout(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
|
||||
/* First election: term=1 */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count);
|
||||
|
||||
/* Timeout: step down to FOLLOWER */
|
||||
raft_on_election_timeout(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* Start new election: election_decided=1 means old state decided — need to
|
||||
* reset election state so raft_start_election allows another attempt.
|
||||
* Per spec: a new election restarts the state machine; reset election_decided. */
|
||||
s.election_decided = 0;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
int rc2 = raft_start_election(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc2);
|
||||
|
||||
/* Term must have incremented to 2 */
|
||||
TEST_ASSERT_EQUAL_UINT64(2, s.term);
|
||||
|
||||
/* on_schedule_timeout must have fired again (total 2) */
|
||||
TEST_ASSERT_EQUAL_INT(2, g_mock.schedule_timeout_count);
|
||||
}
|
||||
|
||||
#endif /* XCE_TESTING */
|
||||
|
||||
/* ---------- test_raft entry point ---------- */
|
||||
|
|
@ -2414,5 +2678,16 @@ void test_raft(void)
|
|||
RUN_TEST(test_proto_H4_persist_fail_denies);
|
||||
RUN_TEST(test_proto_H5_start_election_self_vote_persisted);
|
||||
RUN_TEST(test_proto_Hnull_required_callback_einval);
|
||||
|
||||
/* Group D: Quorum and Result (task 5.3) */
|
||||
RUN_TEST(test_proto_D1_quorum_1grant_of_2_needed);
|
||||
RUN_TEST(test_proto_D2_both_peers_deny_becomes_follower);
|
||||
RUN_TEST(test_proto_D3_second_vote_reply_noop);
|
||||
RUN_TEST(test_proto_D4_reply_higher_term_steps_down);
|
||||
|
||||
/* Group E: Election Timeout (task 5.3) */
|
||||
RUN_TEST(test_proto_E1_timeout_before_quorum);
|
||||
RUN_TEST(test_proto_E2_timeout_after_quorum_noop);
|
||||
RUN_TEST(test_proto_E3_new_election_after_timeout);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue