feat(raft-library): task 5.2 — protocol Groups C+H, 11 test cases (eligible policy + persist)

This commit is contained in:
Rongsong Shen 2026-05-31 06:34:33 +00:00
parent 389047194f
commit 0aa4c84bfe
2 changed files with 350 additions and 1 deletions

View file

@ -102,7 +102,7 @@
- All 10 Unity test cases pass; `raft_sim_dump_log` confirms correct event sequence for A2
- _Requirements: 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 3.5, 8.2, 8.3_
- [ ] 5.2 Test Groups C and H: on_vote_eligible policy and persistence/restart (11 cases)
- [x] 5.2 Test Groups C and H: on_vote_eligible policy and persistence/restart (11 cases)
- C1: all Raft invariants pass, `on_vote_eligible` returns 0 → denied
- C2: all Raft invariants pass, `on_vote_eligible` returns 1 → granted
- C3: `on_vote_eligible` is NULL at init → eligible candidates granted when Raft invariants pass

View file

@ -1829,6 +1829,340 @@ static void test_proto_A5_leader_denies_vote_request(void)
TEST_ASSERT_EQUAL_INT(lc_before, c.leader_count);
}
/* ---------- Group C: on_vote_eligible policy callback (task 5.2) ---------- */
/*
* test_proto_C1_eligible_denies
*
* on_vote_eligible returns 0 → RAFT_DENY, voted_for unchanged (-1),
* vote_eligible_call_count == 1.
*/
static void test_proto_C1_eligible_denies(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
reset_mock();
g_mock.vote_eligible_return_value = 0; /* deny */
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
}
/*
* test_proto_C2_eligible_grants
*
* on_vote_eligible returns 1 → RAFT_GRANT, voted_for==1,
* vote_eligible_call_count == 1.
*/
static void test_proto_C2_eligible_grants(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
reset_mock();
g_mock.vote_eligible_return_value = 1; /* grant */
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
}
/*
* test_proto_C3_eligible_null_grants
*
* on_vote_eligible is NULL (default make_mock_cbs) → treated as eligible,
* grant succeeds (RAFT_GRANT).
*/
static void test_proto_C3_eligible_null_grants(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
/* on_vote_eligible left NULL in make_mock_cbs */
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
/* vote_eligible not called (NULL) */
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
}
/*
* test_proto_C4_stale_term_no_eligible_call
*
* Node at term=5, receives request at term=3 (stale).
* The stale-term check must return DENY before the policy gate is reached.
* vote_eligible_call_count must be 0.
*/
static void test_proto_C4_stale_term_no_eligible_call(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 5;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count);
}
/*
* test_proto_C5_already_voted_no_eligible_call
*
* voted_for=1, voted_for_term=3 at term=3.
* Receives request from idx=2 at term=3.
* The dup-vote check must fire before the policy gate.
* vote_eligible_call_count must be 0.
*/
static void test_proto_C5_already_voted_no_eligible_call(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_vote_eligible = mock_vote_eligible;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 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_INT(0, g_mock.vote_eligible_call_count);
}
/* ---------- Group H: Persistence and restart (task 5.2) ---------- */
/*
* test_proto_H1_init_with_persisted_state
*
* raft_config_t with initial_term=5, initial_voted_for=1.
* After raft_init: s.term==5, s.voted_for==1, s.role==RAFT_FOLLOWER.
*/
static void test_proto_H1_init_with_persisted_state(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.election_timeout_ms = 1000;
cfg.initial_term = 5;
cfg.initial_voted_for = 1;
cfg.cbs = &cbs;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
}
/*
* test_proto_H2_init_first_boot
*
* initial_term=0, initial_voted_for=-1 (first boot).
* After raft_init: s.term==0, s.voted_for==-1, s.role==RAFT_FOLLOWER.
*/
static void test_proto_H2_init_first_boot(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.election_timeout_ms = 1000;
cfg.initial_term = 0;
cfg.initial_voted_for = -1;
cfg.cbs = &cbs;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT64(0, s.term);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
}
/*
* test_proto_H3_persist_before_vote_recorded
*
* Verify that on_persist fires BEFORE voted_for is written to raft_state_t.
* A custom persist callback captures s->voted_for at call time; it must
* still be -1 (unset) when persist is invoked, and set to the candidate
* after raft_on_vote_request returns RAFT_GRANT.
*/
static raft_state_t *h3_state_ptr = NULL;
static int h3_voted_for_snapshot = 999; /* sentinel; updated by h3_persist */
static int h3_persist(void *ctx, uint64_t term, int voted_for)
{
(void)ctx; (void)term; (void)voted_for;
/* Capture voted_for from the live state object at persist call time */
if (h3_state_ptr)
h3_voted_for_snapshot = h3_state_ptr->voted_for;
g_mock.persist_count++;
return 0;
}
static void test_proto_H3_persist_before_vote_recorded(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
reset_mock();
h3_state_ptr = &s;
h3_voted_for_snapshot = 999; /* reset sentinel */
cbs.on_persist = h3_persist;
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
/* Must grant */
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
/* on_persist must have fired once */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
/* At persist call time, voted_for must still have been -1 (not yet written) */
TEST_ASSERT_EQUAL_INT(-1, h3_voted_for_snapshot);
/* After the call returns, voted_for must be set to from_idx=1 */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
h3_state_ptr = NULL;
}
/*
* test_proto_H4_persist_fail_denies
*
* g_mock.persist_return_value = -1.
* raft_on_vote_request must return RAFT_DENY and leave voted_for==-1.
*/
static void test_proto_H4_persist_fail_denies(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
reset_mock();
g_mock.persist_return_value = -1;
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
s.term = 1;
s.voted_for = -1;
int rc = raft_on_vote_request(&s, 1, 1, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
}
/*
* test_proto_H5_start_election_self_vote_persisted
*
* raft_start_election(s) for a 3-node cluster.
* on_persist must fire with (new_term=1, self_idx=0).
* g_mock.last_persist_voted_for == s.self_idx (0).
* g_mock.last_persist_term == 1.
*/
static void test_proto_H5_start_election_self_vote_persisted(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));
int rc = raft_start_election(&s);
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
/* on_persist fired with (term=1, self_idx=0) for the self-vote */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_persist_term);
TEST_ASSERT_EQUAL_INT(s.self_idx, g_mock.last_persist_voted_for);
TEST_ASSERT_EQUAL_INT(0, g_mock.last_persist_voted_for);
}
/*
* test_proto_Hnull_required_callback_einval
*
* raft_init with NULL on_become_follower → returns -EINVAL.
* State remains untouched (canary check).
*/
static void test_proto_Hnull_required_callback_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
raft_config_t cfg;
memset(&s, 0xAB, sizeof(s));
cbs.on_become_follower = NULL; /* knock out a required callback */
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.cbs = &cbs;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
/* s must be completely unchanged (canary check) */
{
unsigned char buf[sizeof(s)];
memset(buf, 0xAB, sizeof(buf));
TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s));
}
}
/* ---------- Group B: voted_for Invariant (task 5.1) ---------- */
/*
@ -2065,5 +2399,20 @@ void test_raft(void)
RUN_TEST(test_proto_B3_stale_term_denied);
RUN_TEST(test_proto_B4_different_candidate_denied);
RUN_TEST(test_proto_B5_idempotent_same_candidate);
/* Group C: on_vote_eligible policy callback (task 5.2) */
RUN_TEST(test_proto_C1_eligible_denies);
RUN_TEST(test_proto_C2_eligible_grants);
RUN_TEST(test_proto_C3_eligible_null_grants);
RUN_TEST(test_proto_C4_stale_term_no_eligible_call);
RUN_TEST(test_proto_C5_already_voted_no_eligible_call);
/* Group H: Persistence and restart (task 5.2) */
RUN_TEST(test_proto_H1_init_with_persisted_state);
RUN_TEST(test_proto_H2_init_first_boot);
RUN_TEST(test_proto_H3_persist_before_vote_recorded);
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);
#endif
}