forked from lei.chen/myxce
1039 lines
32 KiB
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);
|
|
}
|