feat(raft-library): task 3.2 — raft_start_election with single-node shortcircuit; 4 tests
This commit is contained in:
parent
1aa7db4583
commit
4955fa3805
3 changed files with 318 additions and 3 deletions
|
|
@ -51,8 +51,51 @@ int raft_init(raft_state_t *s, const raft_config_t *cfg)
|
|||
|
||||
int raft_start_election(raft_state_t *s)
|
||||
{
|
||||
(void)s;
|
||||
return 0;
|
||||
/* Guard: reject if already CANDIDATE or LEADER */
|
||||
if (s->role == RAFT_CANDIDATE || s->role == RAFT_LEADER)
|
||||
return -EALREADY;
|
||||
|
||||
/* Increment term and record self-vote before any RPCs (split-brain prevention) */
|
||||
s->term++;
|
||||
s->voted_for = s->self_idx;
|
||||
s->voted_for_term = s->term;
|
||||
s->votes_granted = 1; /* self-vote */
|
||||
s->replies_received = 0;
|
||||
s->election_decided = 0;
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "term_update", NULL);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "self_vote", NULL);
|
||||
|
||||
/* Persist new term and self-vote before sending any RPCs */
|
||||
s->cbs->on_persist(s->ctx, s->term, s->self_idx);
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "election_start", NULL);
|
||||
|
||||
/* Single-node shortcircuit: win immediately, no RPCs, no timeout */
|
||||
if (s->n_peers == 1) {
|
||||
s->role = RAFT_LEADER;
|
||||
s->election_decided = 1;
|
||||
s->cbs->on_become_leader(s->ctx, s->term);
|
||||
return RAFT_ELECTED_IMMEDIATELY;
|
||||
}
|
||||
|
||||
/* Multi-node: transition to CANDIDATE */
|
||||
s->role = RAFT_CANDIDATE;
|
||||
|
||||
/* Send RequestVote to all peers except self */
|
||||
for (int i = 0; i < s->n_peers; i++) {
|
||||
if (i == s->self_idx)
|
||||
continue;
|
||||
s->cbs->on_send_vote_request(s->ctx, i, s->term);
|
||||
}
|
||||
|
||||
/* Schedule election timeout watchdog */
|
||||
s->cbs->on_schedule_timeout(s->ctx, s->election_timeout_ms);
|
||||
|
||||
return RAFT_OK;
|
||||
}
|
||||
|
||||
int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,98 @@ static raft_callbacks_t make_full_cbs(void)
|
|||
return cbs;
|
||||
}
|
||||
|
||||
/* ---------- mock infrastructure for raft_start_election tests ---------- */
|
||||
|
||||
/* Per-test counters — reset by each test before calling start_election */
|
||||
typedef struct {
|
||||
int send_vote_request_count;
|
||||
int last_send_peer_idx;
|
||||
uint64_t last_send_term;
|
||||
/* track each peer index sent (up to 8 for test purposes) */
|
||||
int sent_peers[8];
|
||||
|
||||
int schedule_timeout_count;
|
||||
uint32_t last_schedule_delay;
|
||||
|
||||
int persist_count;
|
||||
uint64_t last_persist_term;
|
||||
int last_persist_voted_for;
|
||||
|
||||
int become_leader_count;
|
||||
uint64_t last_become_leader_term;
|
||||
} mock_cbs_state_t;
|
||||
|
||||
static mock_cbs_state_t g_mock;
|
||||
|
||||
static void reset_mock(void)
|
||||
{
|
||||
memset(&g_mock, 0, sizeof(g_mock));
|
||||
}
|
||||
|
||||
static int mock_send_vote_request(void *ctx, int peer_idx, uint64_t term)
|
||||
{
|
||||
(void)ctx;
|
||||
if (g_mock.send_vote_request_count < 8)
|
||||
g_mock.sent_peers[g_mock.send_vote_request_count] = peer_idx;
|
||||
g_mock.last_send_peer_idx = peer_idx;
|
||||
g_mock.last_send_term = term;
|
||||
g_mock.send_vote_request_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mock_become_leader(void *ctx, uint64_t term)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.become_leader_count++;
|
||||
g_mock.last_become_leader_term = term;
|
||||
}
|
||||
|
||||
static int mock_persist(void *ctx, uint64_t term, int voted_for)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.persist_count++;
|
||||
g_mock.last_persist_term = term;
|
||||
g_mock.last_persist_voted_for = voted_for;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mock_schedule_timeout(void *ctx, uint32_t delay_ms)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.schedule_timeout_count++;
|
||||
g_mock.last_schedule_delay = delay_ms;
|
||||
}
|
||||
|
||||
/* Build callbacks that record invocations in g_mock */
|
||||
static raft_callbacks_t make_mock_cbs(void)
|
||||
{
|
||||
raft_callbacks_t cbs;
|
||||
memset(&cbs, 0, sizeof(cbs));
|
||||
cbs.on_send_vote_request = mock_send_vote_request;
|
||||
cbs.on_become_leader = mock_become_leader;
|
||||
cbs.on_become_follower = stub_become_follower; /* plain stub */
|
||||
cbs.on_persist = mock_persist;
|
||||
cbs.on_schedule_timeout = mock_schedule_timeout;
|
||||
cbs.on_cancel_timeout = stub_cancel_timeout;
|
||||
cbs.on_start_election_after_delay = stub_start_election_after_delay;
|
||||
/* on_vote_eligible and on_trace left NULL */
|
||||
return cbs;
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
/* Initialize a raft_state_t with mock callbacks; n_peers and self_idx configurable */
|
||||
static int init_node(raft_state_t *s, raft_callbacks_t *cbs, int n_peers, int self_idx)
|
||||
{
|
||||
raft_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.n_peers = n_peers;
|
||||
cfg.self_idx = self_idx;
|
||||
cfg.election_timeout_ms = 1000;
|
||||
cfg.cbs = cbs;
|
||||
return raft_init(s, &cfg);
|
||||
}
|
||||
|
||||
/* ---------- raft_init tests ---------- */
|
||||
|
||||
/* raft_init with NULL required callback returns -EINVAL and does not modify s */
|
||||
|
|
@ -281,6 +373,180 @@ static void test_raft_init_optional_callbacks_null_succeeds(void)
|
|||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
}
|
||||
|
||||
/* ---------- raft_start_election tests (task 3.2) ---------- */
|
||||
|
||||
/*
|
||||
* test_start_election_from_follower_3node
|
||||
*
|
||||
* 3-node cluster (self=0). raft_start_election transitions to CANDIDATE,
|
||||
* increments term, records self-vote, fires on_persist(new_term, self_idx),
|
||||
* fires on_send_vote_request for each peer != self_idx (peers 1 and 2),
|
||||
* fires on_schedule_timeout once, returns RAFT_OK.
|
||||
*/
|
||||
static void test_start_election_from_follower_3node(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));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, s.term);
|
||||
|
||||
int rc = raft_start_election(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.self_idx);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.voted_for); /* voted_for == self_idx */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.voted_for_term); /* voted_for_term == new term */
|
||||
TEST_ASSERT_EQUAL_INT(1, s.votes_granted); /* self-vote counted */
|
||||
TEST_ASSERT_EQUAL_INT(0, s.replies_received);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.election_decided);
|
||||
|
||||
/* on_persist must fire once with (new_term=1, self_idx=0) */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_persist_term);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.last_persist_voted_for);
|
||||
|
||||
/* on_send_vote_request must fire for peers 1 and 2 (not self=0) */
|
||||
TEST_ASSERT_EQUAL_INT(2, g_mock.send_vote_request_count);
|
||||
/* Both sends must use the new term */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_send_term);
|
||||
/* Verify neither sent peer is self_idx=0 */
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < g_mock.send_vote_request_count; i++) {
|
||||
TEST_ASSERT_NOT_EQUAL(0, g_mock.sent_peers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* on_schedule_timeout must fire exactly once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count);
|
||||
TEST_ASSERT_EQUAL_UINT32(1000, g_mock.last_schedule_delay);
|
||||
|
||||
/* on_become_leader must NOT fire (multi-node path) */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_start_election_single_node
|
||||
*
|
||||
* 1-node cluster (self=0). raft_start_election shortcircuits to LEADER,
|
||||
* fires on_persist then on_become_leader, returns RAFT_ELECTED_IMMEDIATELY.
|
||||
* No on_send_vote_request or on_schedule_timeout calls.
|
||||
*/
|
||||
static void test_start_election_single_node(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 1, 0));
|
||||
|
||||
int rc = raft_start_election(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_ELECTED_IMMEDIATELY, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.voted_for); /* self-vote */
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_persist fires once with (term=1, self_idx=0) */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_persist_term);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.last_persist_voted_for);
|
||||
|
||||
/* on_become_leader fires once with term=1 */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_leader_term);
|
||||
|
||||
/* on_send_vote_request must NOT fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count);
|
||||
|
||||
/* on_schedule_timeout must NOT fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_start_election_already_candidate
|
||||
*
|
||||
* Call raft_start_election once (become CANDIDATE), then call again.
|
||||
* Second call must return -EALREADY and leave state completely unchanged.
|
||||
*/
|
||||
static void test_start_election_already_candidate(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 1));
|
||||
|
||||
/* First election — becomes CANDIDATE */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
uint64_t term_after_first = s.term;
|
||||
|
||||
/* Snapshot the state before the second call */
|
||||
int saved_voted_for = s.voted_for;
|
||||
int saved_votes_granted = s.votes_granted;
|
||||
int saved_replies_recvd = s.replies_received;
|
||||
int saved_election_dec = s.election_decided;
|
||||
|
||||
reset_mock();
|
||||
|
||||
/* Second call must be rejected */
|
||||
int rc = raft_start_election(&s);
|
||||
TEST_ASSERT_EQUAL_INT(-EALREADY, rc);
|
||||
|
||||
/* State must be unchanged */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(term_after_first, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(saved_voted_for, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_INT(saved_votes_granted, s.votes_granted);
|
||||
TEST_ASSERT_EQUAL_INT(saved_replies_recvd, s.replies_received);
|
||||
TEST_ASSERT_EQUAL_INT(saved_election_dec, s.election_decided);
|
||||
|
||||
/* No callbacks must have fired on the second call */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_start_election_already_leader
|
||||
*
|
||||
* Manually set role=RAFT_LEADER, then call raft_start_election.
|
||||
* Must return -EALREADY without modifying any state or firing any callback.
|
||||
*/
|
||||
static void test_start_election_already_leader(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 2));
|
||||
|
||||
/* Manually elevate to LEADER */
|
||||
s.role = RAFT_LEADER;
|
||||
s.term = 5;
|
||||
|
||||
int rc = raft_start_election(&s);
|
||||
TEST_ASSERT_EQUAL_INT(-EALREADY, rc);
|
||||
|
||||
/* All state must be unchanged */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
|
||||
/* No callbacks must fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/* ---------- raft_snapshot tests ---------- */
|
||||
|
||||
static void test_raft_snapshot_returns_current_state(void)
|
||||
|
|
@ -335,4 +601,10 @@ void test_raft(void)
|
|||
RUN_TEST(test_raft_init_default_election_timeout);
|
||||
RUN_TEST(test_raft_init_optional_callbacks_null_succeeds);
|
||||
RUN_TEST(test_raft_snapshot_returns_current_state);
|
||||
|
||||
/* raft_start_election (task 3.2) */
|
||||
RUN_TEST(test_start_election_from_follower_3node);
|
||||
RUN_TEST(test_start_election_single_node);
|
||||
RUN_TEST(test_start_election_already_candidate);
|
||||
RUN_TEST(test_start_election_already_leader);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue