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:
Rongsong Shen 2026-05-31 06:58:02 +00:00
parent 0aa4c84bfe
commit 5c677bc52d
3 changed files with 279 additions and 3 deletions

View file

@ -117,7 +117,7 @@
- All 11 Unity test cases pass
- _Requirements: 1.2, 3.4, 3.5, 7.2, 7.3, 8.3_
- [ ] 5.3 Test Groups D and E: quorum evaluation and election timeout (7 cases)
- [x] 5.3 Test Groups D and E: quorum evaluation and election timeout (7 cases)
- D1: 3-node cluster, 1 peer grant received (self + 1 = quorum 2) → `role==RAFT_LEADER`; `on_become_leader` fired once
- D2: 3-node, both peers deny → `election_decided==1`, `role==RAFT_FOLLOWER`; `on_become_follower` fired
- D3: `on_become_leader` fired; second vote reply arrives → returns `RAFT_PENDING`; `leader_count` stays 1

View file

@ -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);

View file

@ -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
}