forked from lei.chen/myxce
feat(raft-library): task 5.2 — protocol Groups C+H, 11 test cases (eligible policy + persist)
This commit is contained in:
parent
389047194f
commit
0aa4c84bfe
2 changed files with 350 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue