myxce/src/lib/testsuite/test_raft.c

1039 lines
32 KiB
C

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
#include "../raft/raft.h"
/* ---------- stub callbacks ---------- */
static int stub_send_vote_request(void *ctx, int peer_idx, uint64_t term)
{
(void)ctx; (void)peer_idx; (void)term;
return 0;
}
static void stub_become_leader(void *ctx, uint64_t term)
{
(void)ctx; (void)term;
}
static void stub_become_follower(void *ctx, uint64_t term, int leader_idx)
{
(void)ctx; (void)term; (void)leader_idx;
}
static int stub_persist(void *ctx, uint64_t term, int voted_for)
{
(void)ctx; (void)term; (void)voted_for;
return 0;
}
static void stub_schedule_timeout(void *ctx, uint32_t delay_ms)
{
(void)ctx; (void)delay_ms;
}
static void stub_cancel_timeout(void *ctx)
{
(void)ctx;
}
static void stub_start_election_after_delay(void *ctx, uint32_t delay_ms)
{
(void)ctx; (void)delay_ms;
}
/* Build a fully-populated callbacks struct */
static raft_callbacks_t make_full_cbs(void)
{
raft_callbacks_t cbs;
memset(&cbs, 0, sizeof(cbs));
cbs.on_send_vote_request = stub_send_vote_request;
cbs.on_become_leader = stub_become_leader;
cbs.on_become_follower = stub_become_follower;
cbs.on_persist = stub_persist;
cbs.on_schedule_timeout = stub_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 (optional) */
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 persist_return_value; /* configurable: 0=success, non-zero=fail */
int become_leader_count;
uint64_t last_become_leader_term;
/* on_vote_eligible mock */
int vote_eligible_return_value; /* configurable: 1=grant, 0=deny; default 1 */
int vote_eligible_call_count;
/* on_trace mock */
int trace_call_count;
char last_trace_event[64];
} mock_cbs_state_t;
static mock_cbs_state_t g_mock;
static void reset_mock(void)
{
memset(&g_mock, 0, sizeof(g_mock));
/* Defaults: persist succeeds, vote_eligible grants */
g_mock.persist_return_value = 0;
g_mock.vote_eligible_return_value = 1;
}
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 g_mock.persist_return_value;
}
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;
}
static int mock_vote_eligible(void *ctx, int from_idx, uint64_t term)
{
(void)ctx; (void)from_idx; (void)term;
g_mock.vote_eligible_call_count++;
return g_mock.vote_eligible_return_value;
}
static void mock_trace(void *ctx, const char *event, const char *detail)
{
(void)ctx; (void)detail;
g_mock.trace_call_count++;
if (event) {
strncpy(g_mock.last_trace_event, event,
sizeof(g_mock.last_trace_event) - 1);
g_mock.last_trace_event[sizeof(g_mock.last_trace_event) - 1] = '\0';
}
}
/* 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 — tests that need them set explicitly */
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 */
static void test_raft_init_null_send_vote_request_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
/* canary: fill s with a non-zero sentinel so we can verify it's untouched */
memset(&s, 0xAB, sizeof(s));
cbs.on_send_vote_request = NULL; /* knock out a required callback */
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.cbs = &cbs;
cfg.ctx = NULL;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
/* s must be completely unchanged (canary check — spot-check first byte) */
{
unsigned char buf[sizeof(s)];
memset(buf, 0xAB, sizeof(buf));
TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s));
}
}
static void test_raft_init_null_become_leader_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_become_leader = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
static void test_raft_init_null_become_follower_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_become_follower = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
static void test_raft_init_null_persist_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_persist = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
static void test_raft_init_null_schedule_timeout_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_schedule_timeout = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
static void test_raft_init_null_cancel_timeout_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_cancel_timeout = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
static void test_raft_init_null_start_election_after_delay_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_start_election_after_delay = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
}
/* n_peers == 0 → -EINVAL */
static void test_raft_init_zero_peers_returns_einval(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0xAB, sizeof(s));
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 0;
cfg.self_idx = 0;
cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg));
/* s must be unchanged */
unsigned char buf[sizeof(s)];
memset(buf, 0xAB, sizeof(buf));
TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s));
}
/* Successful init: check all fields */
static void test_raft_init_success_fields(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 1;
cfg.election_timeout_ms = 1500;
cfg.my_priority = 2;
cfg.initial_term = 7;
cfg.initial_voted_for = -1;
cfg.cbs = &cbs;
cfg.ctx = (void *)0xDEADBEEF;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_INT(3, s.n_peers);
TEST_ASSERT_EQUAL_INT(1, s.self_idx);
TEST_ASSERT_EQUAL_UINT32(1500, s.election_timeout_ms);
TEST_ASSERT_EQUAL_INT(2, s.my_priority);
TEST_ASSERT_EQUAL_UINT64(7, s.term);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term);
TEST_ASSERT_EQUAL_INT(0, s.votes_granted);
TEST_ASSERT_EQUAL_INT(0, s.replies_received);
TEST_ASSERT_EQUAL_INT(0, s.election_decided);
/* cbs/ctx must be stored in raft_state_t for the state machine to fire callbacks */
TEST_ASSERT_EQUAL_PTR(&cbs, s.cbs);
TEST_ASSERT_EQUAL_PTR((void *)0xDEADBEEF, s.ctx);
}
/* election_timeout_ms == 0 → default 2000 */
static void test_raft_init_default_election_timeout(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_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 = 0; /* trigger default */
cfg.cbs = &cbs;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT32(2000, s.election_timeout_ms);
}
/* optional callbacks (on_vote_eligible, on_trace) may be NULL — must succeed */
static void test_raft_init_optional_callbacks_null_succeeds(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
cbs.on_vote_eligible = NULL;
cbs.on_trace = NULL;
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.cbs = &cbs;
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
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_on_vote_request tests (task 3.3) ---------- */
/*
* Helper: init a 3-node node with mock callbacks (including trace).
* term, role, voted_for can be set after init.
*/
static int init_vote_node(raft_state_t *s, raft_callbacks_t *cbs,
int n_peers, int self_idx, uint64_t initial_term)
{
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.initial_term = initial_term;
cfg.initial_voted_for = -1;
cfg.cbs = cbs;
return raft_init(s, &cfg);
}
/*
* test_vote_req_stale_term
*
* Node at term=5, receives request with term=3.
* Expected: RAFT_DENY, *reply_term=5, state unchanged,
* on_trace fired with "vote_denied_stale_term".
*/
static void test_vote_req_stale_term(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
/* Confirm starting state */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* State must be unchanged */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
/* on_trace must fire with "vote_denied_stale_term" */
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
TEST_ASSERT_EQUAL_STRING("vote_denied_stale_term", g_mock.last_trace_event);
/* on_persist must NOT fire */
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_vote_req_higher_term_updates_state
*
* Node at term=2, receives request with term=5 (higher).
* Expected: s->term updated to 5, voted_for cleared to -1, role=FOLLOWER.
* Since voted_for is unset and persist succeeds: RAFT_GRANT.
* reply_term = 5.
*/
static void test_vote_req_higher_term_updates_state(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 2));
/* Simulate node was CANDIDATE in term=2 */
s.role = RAFT_CANDIDATE;
s.voted_for = 0; /* voted for self */
s.voted_for_term = 2;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
/* Term upgrade must happen: term updated, role stepped down */
TEST_ASSERT_EQUAL_UINT64(5, s.term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
/* Then grant because voted_for is cleared and persist succeeds */
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* on_persist must fire once */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
TEST_ASSERT_EQUAL_UINT64(5, g_mock.last_persist_term);
TEST_ASSERT_EQUAL_INT(1, g_mock.last_persist_voted_for);
/* voted_for set on grant */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
}
/*
* test_vote_req_same_term_already_voted_different
*
* Node at term=5, voted_for=1, voted_for_term=5.
* Receives request from idx=2 at term=5.
* Expected: RAFT_DENY, "vote_denied_already_voted" trace.
*/
static void test_vote_req_same_term_already_voted_different(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.voted_for = 1;
s.voted_for_term = 5;
int rc = raft_on_vote_request(&s, 2, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* State: voted_for still 1 (not 2), term unchanged */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.term);
/* on_trace fires with already_voted */
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
TEST_ASSERT_EQUAL_STRING("vote_denied_already_voted", g_mock.last_trace_event);
/* on_persist must NOT fire */
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_vote_req_same_term_idempotent_same_candidate
*
* Node at term=5, voted_for=1, voted_for_term=5.
* Receives request from idx=1 at term=5 (same candidate, same term).
* Expected: idempotent re-grant → RAFT_GRANT, voted_for still 1.
*/
static void test_vote_req_same_term_idempotent_same_candidate(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.voted_for = 1;
s.voted_for_term = 5;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* voted_for still 1 */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
/* on_persist must fire once for the re-grant */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
}
/*
* test_vote_req_leader_denies
*
* Node is RAFT_LEADER at term=5, receives request at term=5 (same term, no upgrade).
* Expected: RAFT_DENY ("vote_denied_role" trace), role stays LEADER.
*/
static void test_vote_req_leader_denies(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.role = RAFT_LEADER;
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* Role gate fires: still LEADER (term==request term, no upgrade) */
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
/* on_trace fires with "vote_denied_role" */
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
TEST_ASSERT_EQUAL_STRING("vote_denied_role", g_mock.last_trace_event);
/* on_persist must NOT fire */
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_vote_req_on_vote_eligible_denies
*
* on_vote_eligible returns 0 → RAFT_DENY, voted_for unchanged,
* "vote_denied_freshness" trace fires.
*/
static void test_vote_req_on_vote_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;
cbs.on_trace = mock_trace;
reset_mock();
g_mock.vote_eligible_return_value = 0; /* deny */
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* voted_for must be unchanged */
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
/* on_vote_eligible must have been called */
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
/* on_trace fires with "vote_denied_freshness" */
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
TEST_ASSERT_EQUAL_STRING("vote_denied_freshness", g_mock.last_trace_event);
/* on_persist must NOT fire */
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
}
/*
* test_vote_req_on_vote_eligible_grants
*
* on_vote_eligible returns 1 and persist succeeds → RAFT_GRANT, voted_for set.
*/
static void test_vote_req_on_vote_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;
cbs.on_trace = mock_trace;
reset_mock();
g_mock.vote_eligible_return_value = 1; /* grant */
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* voted_for must be set */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
/* on_vote_eligible called once */
TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count);
/* on_persist called once */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
/* on_trace fires "vote_granted" */
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
TEST_ASSERT_EQUAL_STRING("vote_granted", g_mock.last_trace_event);
}
/*
* test_vote_req_on_vote_eligible_null
*
* on_vote_eligible is NULL → treated as eligible, grant if persist ok.
*/
static void test_vote_req_on_vote_eligible_null(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 */
cbs.on_trace = mock_trace;
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* voted_for must be set */
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
/* on_persist called once */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
}
/*
* test_vote_req_persist_fails
*
* on_persist returns non-zero → RAFT_DENY, voted_for unchanged.
*/
static void test_vote_req_persist_fails(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term = 0;
cbs.on_trace = mock_trace;
reset_mock();
g_mock.persist_return_value = -1; /* fail */
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
int rc = raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
/* voted_for must NOT be updated */
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term);
/* on_persist called once (it ran, but failed) */
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
}
/*
* test_vote_req_reply_term_always_set
*
* Verify that *reply_term == s->term on every return path by testing
* several scenarios and checking reply_term each time.
*/
static void test_vote_req_reply_term_always_set(void)
{
raft_state_t s;
raft_callbacks_t cbs = make_mock_cbs();
uint64_t reply_term;
/* Path 1: stale term deny */
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 7));
reply_term = 0;
raft_on_vote_request(&s, 1, 3, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal current term=7 */
/* Path 2: term upgrade + grant */
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3));
reply_term = 0;
raft_on_vote_request(&s, 1, 7, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal upgraded term=7 */
/* Path 3: already voted for different — deny */
reset_mock();
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
s.voted_for = 1; s.voted_for_term = 5;
reply_term = 0;
raft_on_vote_request(&s, 2, 5, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */
/* Path 4: persist fails — deny */
g_mock.persist_return_value = -1;
reset_mock();
g_mock.persist_return_value = -1;
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
reply_term = 0;
raft_on_vote_request(&s, 1, 5, &reply_term);
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */
}
/* ---------- raft_snapshot tests ---------- */
static void test_raft_snapshot_returns_current_state(void)
{
raft_state_t s;
raft_snapshot_t snap;
raft_callbacks_t cbs = make_full_cbs();
raft_config_t cfg;
memset(&s, 0, sizeof(s));
memset(&cfg, 0, sizeof(cfg));
cfg.n_peers = 3;
cfg.self_idx = 0;
cfg.initial_term = 5;
cfg.initial_voted_for = 1;
cfg.cbs = &cbs;
int rc = raft_init(&s, &cfg);
TEST_ASSERT_EQUAL_INT(0, rc);
/* manually set a few fields to distinguish from zero defaults */
s.votes_granted = 2;
s.election_decided = 1;
s.voted_for_term = 5;
memset(&snap, 0xFF, sizeof(snap)); /* canary */
raft_snapshot(&s, &snap);
TEST_ASSERT_EQUAL_UINT64(5, snap.term);
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, snap.role);
TEST_ASSERT_EQUAL_INT(1, snap.voted_for);
TEST_ASSERT_EQUAL_UINT64(5, snap.voted_for_term);
TEST_ASSERT_EQUAL_INT(2, snap.votes_granted);
TEST_ASSERT_EQUAL_INT(1, snap.election_decided);
}
/* ---------- test_raft entry point ---------- */
void test_raft(void)
{
printf("\n>> raft test\n\n");
RUN_TEST(test_raft_init_null_send_vote_request_returns_einval);
RUN_TEST(test_raft_init_null_become_leader_returns_einval);
RUN_TEST(test_raft_init_null_become_follower_returns_einval);
RUN_TEST(test_raft_init_null_persist_returns_einval);
RUN_TEST(test_raft_init_null_schedule_timeout_returns_einval);
RUN_TEST(test_raft_init_null_cancel_timeout_returns_einval);
RUN_TEST(test_raft_init_null_start_election_after_delay_returns_einval);
RUN_TEST(test_raft_init_zero_peers_returns_einval);
RUN_TEST(test_raft_init_success_fields);
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);
/* raft_on_vote_request (task 3.3) */
RUN_TEST(test_vote_req_stale_term);
RUN_TEST(test_vote_req_higher_term_updates_state);
RUN_TEST(test_vote_req_same_term_already_voted_different);
RUN_TEST(test_vote_req_same_term_idempotent_same_candidate);
RUN_TEST(test_vote_req_leader_denies);
RUN_TEST(test_vote_req_on_vote_eligible_denies);
RUN_TEST(test_vote_req_on_vote_eligible_grants);
RUN_TEST(test_vote_req_on_vote_eligible_null);
RUN_TEST(test_vote_req_persist_fails);
RUN_TEST(test_vote_req_reply_term_always_set);
}