feat(raft-library): task 3.2 — raft_start_election with single-node shortcircuit; 4 tests

This commit is contained in:
Rongsong Shen 2026-05-31 05:30:00 +00:00
parent 1aa7db4583
commit 4955fa3805
3 changed files with 318 additions and 3 deletions

View file

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

View file

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