2418 lines
74 KiB
C
2418 lines
74 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];
|
|
|
|
/* on_become_follower mock */
|
|
int become_follower_count;
|
|
uint64_t last_become_follower_term;
|
|
int last_become_follower_leader_idx;
|
|
|
|
/* on_cancel_timeout mock */
|
|
int cancel_timeout_count;
|
|
|
|
/* on_start_election_after_delay mock (task 3.5) */
|
|
int start_election_after_delay_count;
|
|
uint32_t last_start_election_delay;
|
|
} 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 void mock_become_follower(void *ctx, uint64_t term, int leader_idx)
|
|
{
|
|
(void)ctx;
|
|
g_mock.become_follower_count++;
|
|
g_mock.last_become_follower_term = term;
|
|
g_mock.last_become_follower_leader_idx = leader_idx;
|
|
}
|
|
|
|
static void mock_cancel_timeout(void *ctx)
|
|
{
|
|
(void)ctx;
|
|
g_mock.cancel_timeout_count++;
|
|
}
|
|
|
|
static void mock_start_election_after_delay(void *ctx, uint32_t delay_ms)
|
|
{
|
|
(void)ctx;
|
|
g_mock.start_election_after_delay_count++;
|
|
g_mock.last_start_election_delay = delay_ms;
|
|
}
|
|
|
|
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 = mock_become_follower;
|
|
cbs.on_persist = mock_persist;
|
|
cbs.on_schedule_timeout = mock_schedule_timeout;
|
|
cbs.on_cancel_timeout = mock_cancel_timeout;
|
|
cbs.on_start_election_after_delay = mock_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_on_vote_reply tests (task 3.4) ---------- */
|
|
|
|
/*
|
|
* test_vote_reply_higher_term_steps_down
|
|
*
|
|
* Node is CANDIDATE at term=2. Receives reply with term=5 (higher).
|
|
* Expected: RAFT_BECAME_FOLLOWER, role=FOLLOWER, term=5, election_decided=1,
|
|
* on_become_follower fired with (5, -1), on_cancel_timeout fired once.
|
|
*/
|
|
static void test_vote_reply_higher_term_steps_down(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
/* Manually put node into CANDIDATE state at term=2 */
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 2;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 0;
|
|
|
|
int rc = raft_on_vote_reply(&s, 1, 0, 5);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
|
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
|
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
|
|
|
/* on_become_follower must fire once with (term=5, leader_idx=-1) */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
|
TEST_ASSERT_EQUAL_UINT64(5, g_mock.last_become_follower_term);
|
|
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
|
|
|
/* on_cancel_timeout must fire once */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
|
|
|
/* on_become_leader must NOT fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
|
}
|
|
|
|
/*
|
|
* test_vote_reply_noop_after_decided
|
|
*
|
|
* election_decided=1 already set. Call raft_on_vote_reply.
|
|
* Expected: RAFT_PENDING, no callbacks fire.
|
|
*/
|
|
static void test_vote_reply_noop_after_decided(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 2;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 1; /* already decided */
|
|
|
|
int rc = raft_on_vote_reply(&s, 1, 1, 2);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc);
|
|
|
|
/* No callbacks must fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
|
}
|
|
|
|
/*
|
|
* test_vote_reply_quorum_3node_1grant
|
|
*
|
|
* 3-node cluster (quorum=2). Node is CANDIDATE with votes_granted=1 (self-vote).
|
|
* One peer grant arrives → votes_granted=2 >= quorum=2.
|
|
* Expected: RAFT_BECAME_LEADER, role=LEADER, election_decided=1,
|
|
* on_become_leader fired, on_cancel_timeout fired.
|
|
*/
|
|
static void test_vote_reply_quorum_3node_1grant(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
/* Simulate state after raft_start_election: CANDIDATE, self-vote counted */
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 1;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 0;
|
|
|
|
/* One peer grants */
|
|
int rc = raft_on_vote_reply(&s, 1, 1, 1);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
|
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
|
TEST_ASSERT_EQUAL_INT(2, s.votes_granted);
|
|
|
|
/* on_become_leader must fire once with the current term */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
|
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_leader_term);
|
|
|
|
/* on_cancel_timeout must fire once */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
|
|
|
/* on_become_follower must NOT fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
|
}
|
|
|
|
/*
|
|
* test_vote_reply_no_quorum_all_replied_3node
|
|
*
|
|
* 3-node cluster. Node starts election (votes_granted=1, replies_received=0).
|
|
* Both peers deny. After second deny: replies_received=2 == n_peers-1=2,
|
|
* votes_granted=1 < quorum=2 → RAFT_BECAME_FOLLOWER.
|
|
*/
|
|
static void test_vote_reply_no_quorum_all_replied_3node(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 1;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 0;
|
|
|
|
/* First deny: replies_received=1, not yet all replied */
|
|
int rc1 = raft_on_vote_reply(&s, 1, 0, 1);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc1);
|
|
TEST_ASSERT_EQUAL_INT(1, s.replies_received);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
|
|
|
/* Second deny: replies_received=2 == n_peers-1=2 → step down */
|
|
int rc2 = raft_on_vote_reply(&s, 2, 0, 1);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc2);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
|
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
|
|
|
/* on_become_follower must fire once */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
|
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_follower_term);
|
|
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
|
|
|
/* on_become_leader must NOT fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
|
|
|
/* on_cancel_timeout must NOT fire for all-replied-no-quorum (only term-upgrade and quorum paths cancel) */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
|
}
|
|
|
|
/*
|
|
* test_vote_reply_quorum_5node
|
|
*
|
|
* 5-node cluster (quorum=3). Node starts election (votes_granted=1).
|
|
* Two grants arrive → votes_granted=3 = quorum=3 → RAFT_BECAME_LEADER.
|
|
*/
|
|
static void test_vote_reply_quorum_5node(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 5, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 1;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 0;
|
|
|
|
/* First grant: votes_granted=2, quorum=3 → still pending */
|
|
int rc1 = raft_on_vote_reply(&s, 1, 1, 1);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc1);
|
|
TEST_ASSERT_EQUAL_INT(2, s.votes_granted);
|
|
|
|
/* Second grant: votes_granted=3 = quorum → leader */
|
|
int rc2 = raft_on_vote_reply(&s, 2, 1, 1);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc2);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
|
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
|
TEST_ASSERT_EQUAL_INT(3, s.votes_granted);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
|
}
|
|
|
|
/*
|
|
* test_vote_reply_pending_waiting
|
|
*
|
|
* 3-node cluster. Node starts election (votes_granted=1, replies_received=0).
|
|
* One deny arrives: replies_received=1, n_peers-1=2 → not all replied yet.
|
|
* Expected: RAFT_PENDING, no role change, no callbacks.
|
|
*/
|
|
static void test_vote_reply_pending_waiting(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 1;
|
|
s.votes_granted = 1;
|
|
s.replies_received = 0;
|
|
s.election_decided = 0;
|
|
|
|
/* One deny: replies_received=1, n_peers-1=2 → still waiting */
|
|
int rc = raft_on_vote_reply(&s, 1, 0, 1);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
|
TEST_ASSERT_EQUAL_INT(0, s.election_decided);
|
|
TEST_ASSERT_EQUAL_INT(1, s.replies_received);
|
|
TEST_ASSERT_EQUAL_INT(1, s.votes_granted); /* unchanged */
|
|
|
|
/* No role-change callbacks */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
|
}
|
|
|
|
/* ---------- raft_on_election_timeout tests (task 3.5) ---------- */
|
|
|
|
/*
|
|
* test_election_timeout_undecided
|
|
*
|
|
* Node is CANDIDATE, election_decided=0.
|
|
* raft_on_election_timeout must:
|
|
* - set role=FOLLOWER
|
|
* - set election_decided=1
|
|
* - fire on_become_follower once
|
|
* - fire on_cancel_timeout once
|
|
* - trace "election_timeout"
|
|
*/
|
|
static void test_election_timeout_undecided(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 2;
|
|
s.election_decided = 0;
|
|
|
|
int become_follower_before = g_mock.become_follower_count;
|
|
|
|
raft_on_election_timeout(&s);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
|
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
|
|
|
/* on_become_follower fired once (with current term, leader_idx=-1) */
|
|
TEST_ASSERT_EQUAL_INT(become_follower_before + 1, g_mock.become_follower_count);
|
|
TEST_ASSERT_EQUAL_UINT64(2, g_mock.last_become_follower_term);
|
|
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
|
|
|
/* on_cancel_timeout fired once */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
|
|
|
/* trace fired with "election_timeout" */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("election_timeout", g_mock.last_trace_event);
|
|
}
|
|
|
|
/*
|
|
* test_election_timeout_already_decided
|
|
*
|
|
* election_decided=1 already set (election outcome already fired).
|
|
* raft_on_election_timeout must:
|
|
* - fire on_cancel_timeout once (stale timer cleanup)
|
|
* - NOT fire an additional on_become_follower
|
|
* - trace "election_timeout_noop"
|
|
*/
|
|
static void test_election_timeout_already_decided(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.role = RAFT_CANDIDATE;
|
|
s.term = 2;
|
|
s.election_decided = 1; /* already decided */
|
|
|
|
/* Snapshot become_follower_count before the call */
|
|
int become_follower_before = g_mock.become_follower_count;
|
|
|
|
raft_on_election_timeout(&s);
|
|
|
|
/* on_cancel_timeout must fire once (stale timer cleanup) */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
|
|
|
/* on_become_follower must NOT have fired an additional time */
|
|
TEST_ASSERT_EQUAL_INT(become_follower_before, g_mock.become_follower_count);
|
|
|
|
/* trace fired with "election_timeout_noop" */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("election_timeout_noop", g_mock.last_trace_event);
|
|
}
|
|
|
|
/* ---------- raft_on_leader_lost tests (task 3.5) ---------- */
|
|
|
|
/*
|
|
* test_leader_lost_clears_voted_for
|
|
*
|
|
* Node had voted_for=1. raft_on_leader_lost must:
|
|
* - set voted_for=-1
|
|
* - set role=FOLLOWER
|
|
* - fire on_start_election_after_delay with delay = my_priority * election_timeout_ms
|
|
* - trace "leader_lost"
|
|
*/
|
|
static void test_leader_lost_clears_voted_for(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
raft_config_t cfg;
|
|
memset(&cfg, 0, sizeof(cfg));
|
|
cfg.n_peers = 3;
|
|
cfg.self_idx = 0;
|
|
cfg.election_timeout_ms = 500;
|
|
cfg.my_priority = 2;
|
|
cfg.initial_voted_for = -1;
|
|
cfg.cbs = &cbs;
|
|
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
|
|
|
s.voted_for = 1;
|
|
s.role = RAFT_FOLLOWER;
|
|
|
|
raft_on_leader_lost(&s);
|
|
|
|
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
|
|
|
/* on_start_election_after_delay fired once with delay = priority * timeout */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.start_election_after_delay_count);
|
|
TEST_ASSERT_EQUAL_UINT32(2 * 500, g_mock.last_start_election_delay);
|
|
|
|
/* trace fired with "leader_lost" */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("leader_lost", g_mock.last_trace_event);
|
|
}
|
|
|
|
/* ---------- raft_check_and_start_election tests (task 3.5) ---------- */
|
|
|
|
/*
|
|
* test_check_and_start_voted_for_other
|
|
*
|
|
* voted_for=1, self_idx=0 (voted for a different candidate).
|
|
* Must return RAFT_SKIP_ELECTION, role stays FOLLOWER, no election started.
|
|
* Trace "jitter_skip" fires.
|
|
*/
|
|
static void test_check_and_start_voted_for_other(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
|
|
cbs.on_trace = mock_trace;
|
|
reset_mock();
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
|
s.voted_for = 1; /* voted for peer 1, not self (0) */
|
|
s.role = RAFT_FOLLOWER;
|
|
|
|
int rc = raft_check_and_start_election(&s);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
|
|
|
/* No election RPCs must have fired */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count);
|
|
|
|
/* trace fired with "jitter_skip" */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("jitter_skip", g_mock.last_trace_event);
|
|
}
|
|
|
|
/*
|
|
* test_check_and_start_not_voted
|
|
*
|
|
* voted_for==-1 (no vote cast yet).
|
|
* Must call raft_start_election internally and return RAFT_ELECTION_STARTED.
|
|
* role becomes CANDIDATE (3-node cluster).
|
|
*/
|
|
static void test_check_and_start_not_voted(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));
|
|
s.voted_for = -1;
|
|
s.role = RAFT_FOLLOWER;
|
|
|
|
int rc = raft_check_and_start_election(&s);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
|
|
|
/* raft_start_election must have fired vote requests */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.send_vote_request_count);
|
|
}
|
|
|
|
/*
|
|
* test_check_and_start_voted_for_self
|
|
*
|
|
* voted_for==self_idx (we previously voted for ourselves — this means
|
|
* we already started an election, so start again to re-enter candidacy).
|
|
* Must return RAFT_ELECTION_STARTED, not RAFT_SKIP_ELECTION.
|
|
*/
|
|
static void test_check_and_start_voted_for_self(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));
|
|
s.voted_for = 0; /* voted for self (self_idx=0) */
|
|
s.role = RAFT_FOLLOWER;
|
|
|
|
int rc = raft_check_and_start_election(&s);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc);
|
|
}
|
|
|
|
/* ---------- 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);
|
|
}
|
|
|
|
/* ---------- simulator tests (task 4.1) ---------- */
|
|
|
|
#ifdef XCE_TESTING
|
|
|
|
/*
|
|
* test_sim_init_3node
|
|
*
|
|
* raft_sim_init(c, 3, 2000) returns 0.
|
|
* All 3 nodes have: role=FOLLOWER, term=0, voted_for=-1.
|
|
* leader_count=0, pending_count=0.
|
|
*/
|
|
static void test_sim_init_3node(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0xFF, sizeof(c)); /* canary — force all fields to non-zero */
|
|
|
|
int rc = raft_sim_init(&c, 3, 2000);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(3, c.n);
|
|
TEST_ASSERT_EQUAL_INT(0, c.leader_count);
|
|
TEST_ASSERT_EQUAL_INT(0, c.pending_count);
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[i].state.role);
|
|
TEST_ASSERT_EQUAL_UINT64(0, c.nodes[i].state.term);
|
|
TEST_ASSERT_EQUAL_INT(-1, c.nodes[i].state.voted_for);
|
|
TEST_ASSERT_EQUAL_INT(i, c.nodes[i].idx);
|
|
TEST_ASSERT_EQUAL_INT(0, c.nodes[i].partition);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* test_sim_trace_buf
|
|
*
|
|
* After raft_sim_init, calling raft_start_election on node 0 (which has
|
|
* on_trace wired) must append at least one event to node 0's trace_buf.
|
|
*/
|
|
static void test_sim_trace_buf(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
int rc = raft_sim_init(&c, 3, 2000);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* trace_buf must start empty */
|
|
TEST_ASSERT_EQUAL_INT(0, c.nodes[0].trace_len);
|
|
|
|
/* Start election on node 0 — on_trace wired in raft_sim_init will write to trace_buf */
|
|
raft_start_election(&c.nodes[0].state);
|
|
|
|
/* At least one trace event must have been written */
|
|
TEST_ASSERT_GREATER_THAN_INT(0, c.nodes[0].trace_len);
|
|
}
|
|
|
|
/*
|
|
* test_sim_3node_election_one_leader (task 4.2)
|
|
*
|
|
* Node 0 starts the election alone (priority=0, highest priority).
|
|
* Nodes 1 and 2 have not yet voted, so they grant node 0's request.
|
|
* After delivering pending RPCs, node 0 reaches quorum and becomes LEADER.
|
|
*
|
|
* Steps:
|
|
* 1. raft_sim_init(c, 3, 2000)
|
|
* 2. raft_start_election only for node 0 → buffers 2 vote-request RPCs
|
|
* 3. raft_sim_deliver_pending(c) → nodes 1 and 2 grant; node 0 wins
|
|
* 4. Assert c->leader_count == 1 (exactly one LEADER, no split brain)
|
|
* 5. Assert node 0 is RAFT_LEADER, nodes 1 and 2 are RAFT_FOLLOWER
|
|
*/
|
|
static void test_sim_3node_election_one_leader(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
int rc = raft_sim_init(&c, 3, 2000);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Only node 0 starts an election (highest priority) */
|
|
raft_start_election(&c.nodes[0].state);
|
|
|
|
/* 2 RPCs buffered (to nodes 1 and 2) */
|
|
TEST_ASSERT_EQUAL_INT(2, c.pending_count);
|
|
|
|
/* Deliver: nodes 1 and 2 have not voted yet → both grant node 0 */
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
/* Exactly one leader must exist */
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
|
|
/* Node 0 must be the leader */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
|
|
/* Nodes 1 and 2 are followers */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[1].state.role);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[2].state.role);
|
|
}
|
|
|
|
/*
|
|
* test_sim_start_all_elections_no_winner (task 4.2)
|
|
*
|
|
* All 3 nodes start elections simultaneously at the same term.
|
|
* Each node votes for itself; all peer requests are denied (already voted).
|
|
* No node reaches quorum — leader_count remains 0.
|
|
*
|
|
* This demonstrates that simultaneous same-term elections produce no winner
|
|
* and require a timeout + retry cycle to resolve.
|
|
*/
|
|
static void test_sim_start_all_elections_no_winner(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
int rc = raft_sim_init(&c, 3, 2000);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* All nodes start elections at term 1 — each votes for itself */
|
|
raft_sim_start_all_elections(&c);
|
|
|
|
/* 6 RPCs buffered: each of 3 nodes sends to 2 peers */
|
|
TEST_ASSERT_EQUAL_INT(6, c.pending_count);
|
|
|
|
/* Deliver: every node has voted_for=self, so all peer requests are denied */
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
/* No leader — all denied each other */
|
|
TEST_ASSERT_EQUAL_INT(0, c.leader_count);
|
|
|
|
/* All nodes have stepped back to FOLLOWER after all-replied-no-quorum */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[0].state.role);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[1].state.role);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[2].state.role);
|
|
}
|
|
|
|
/*
|
|
* test_sim_partition_no_quorum (task 4.2)
|
|
*
|
|
* Node 2 is isolated before it starts an election.
|
|
* Its vote-request RPCs are dropped during delivery (partition flag set).
|
|
* Node 2 receives no grants and remains CANDIDATE; leader_count stays 0.
|
|
*
|
|
* Steps:
|
|
* 1. raft_sim_init(c, 3, 2000)
|
|
* 2. raft_sim_partition(c, 2, 1) — isolate node 2
|
|
* 3. raft_start_election(&c->nodes[2]) — node 2 starts election
|
|
* 4. raft_sim_deliver_pending(c) — RPCs to/from node 2 are dropped
|
|
* 5. Assert node 2 is still CANDIDATE (no quorum reached)
|
|
* 6. Assert c->leader_count == 0
|
|
*/
|
|
static void test_sim_partition_no_quorum(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
int rc = raft_sim_init(&c, 3, 2000);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Isolate node 2 */
|
|
raft_sim_partition(&c, 2, 1);
|
|
TEST_ASSERT_EQUAL_INT(1, c.nodes[2].partition);
|
|
|
|
/* Node 2 starts an election — becomes CANDIDATE and buffers RPCs */
|
|
raft_start_election(&c.nodes[2].state);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, c.nodes[2].state.role);
|
|
|
|
/* Deliver: node 2 is partitioned → all its RPCs are dropped */
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
/* Node 2 never received any grants — still CANDIDATE */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, c.nodes[2].state.role);
|
|
|
|
/* No leader elected */
|
|
TEST_ASSERT_EQUAL_INT(0, c.leader_count);
|
|
}
|
|
|
|
/*
|
|
* test_sim_fire_timeout_steps_down (task 4.2)
|
|
*
|
|
* Node 0 starts an election (becomes CANDIDATE).
|
|
* raft_sim_fire_timeout fires the election timeout for node 0.
|
|
* Node 0 must step down to FOLLOWER.
|
|
*/
|
|
static void test_sim_fire_timeout_steps_down(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
|
|
|
|
/* Start election — node 0 becomes CANDIDATE */
|
|
raft_start_election(&c.nodes[0].state);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, c.nodes[0].state.role);
|
|
|
|
/* Fire timeout — no quorum was reached */
|
|
raft_sim_fire_timeout(&c, 0);
|
|
|
|
/* Node 0 must step down */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[0].state.role);
|
|
}
|
|
|
|
/* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */
|
|
|
|
/*
|
|
* test_proto_A1_single_node_self_elects
|
|
*
|
|
* 1-node cluster: raft_start_election returns RAFT_ELECTED_IMMEDIATELY.
|
|
* role==LEADER, leader_count==1, no on_send_vote_request calls (no pending RPCs).
|
|
* on_become_leader fired once.
|
|
*/
|
|
static void test_proto_A1_single_node_self_elects(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 1, 2000));
|
|
|
|
int rc = raft_start_election(&c.nodes[0].state);
|
|
|
|
/* Single-node shortcircuit */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_ELECTED_IMMEDIATELY, rc);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
|
|
/* on_become_leader fired once → leader_count incremented */
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
|
|
/* No pending RPCs — no on_send_vote_request calls */
|
|
TEST_ASSERT_EQUAL_INT(0, c.pending_count);
|
|
}
|
|
|
|
/*
|
|
* test_proto_A2_3node_one_leader
|
|
*
|
|
* 3-node cluster: node 0 (highest priority, idx=0) wins the election.
|
|
* After deliver_pending: leader_count==1, node 0 is LEADER, others FOLLOWER.
|
|
*/
|
|
static void test_proto_A2_3node_one_leader(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
|
|
|
|
/* Only node 0 starts — it will ask nodes 1 and 2 for votes */
|
|
raft_start_election(&c.nodes[0].state);
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
/* Exactly one leader */
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[1].state.role);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[2].state.role);
|
|
}
|
|
|
|
/*
|
|
* test_proto_A3_late_election_denied
|
|
*
|
|
* Node 0 wins at term=1. Node 1 already voted_for=0 at term=1.
|
|
* Send a vote request from idx=2 to node 1 at term=1 directly.
|
|
* Node 1 must DENY (already voted this term), leader_count stays 1.
|
|
*/
|
|
static void test_proto_A3_late_election_denied(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
|
|
|
|
/* Node 0 wins at term=1 */
|
|
raft_start_election(&c.nodes[0].state);
|
|
raft_sim_deliver_pending(&c);
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
|
|
/* Node 1 granted node 0 at term=1: voted_for=0, voted_for_term=1 */
|
|
TEST_ASSERT_EQUAL_INT(0, c.nodes[1].state.voted_for);
|
|
TEST_ASSERT_EQUAL_UINT64(1, c.nodes[1].state.voted_for_term);
|
|
|
|
/* Now node 2 tries to request a vote from node 1 at the same term=1 */
|
|
uint64_t reply_term = 0;
|
|
int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, &reply_term);
|
|
|
|
/* Node 1 must deny — already voted for node 0 this term */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
|
|
|
|
/* leader_count still 1 (no new leader elected) */
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
}
|
|
|
|
/*
|
|
* test_proto_A4_5node_quorum_3
|
|
*
|
|
* 5-node cluster: node 0 starts the election.
|
|
* Quorum=3 (self + 2 peer grants). After deliver, node 0 is LEADER.
|
|
*/
|
|
static void test_proto_A4_5node_quorum_3(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 5, 2000));
|
|
|
|
raft_start_election(&c.nodes[0].state);
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, c.leader_count);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
/* Self vote + 4 peer grants → election_decided=1 after first quorum reached */
|
|
TEST_ASSERT_GREATER_OR_EQUAL(3, c.nodes[0].state.votes_granted);
|
|
}
|
|
|
|
/*
|
|
* test_proto_A5_leader_denies_vote_request
|
|
*
|
|
* Node 0 is LEADER at term=1. Receives a vote request from idx=1 at term=1.
|
|
* The role gate must deny (LEADER, same term → no step-down).
|
|
* Node 0 stays LEADER.
|
|
*/
|
|
static void test_proto_A5_leader_denies_vote_request(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
|
|
|
|
/* Make node 0 the leader */
|
|
raft_start_election(&c.nodes[0].state);
|
|
raft_sim_deliver_pending(&c);
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
|
|
uint64_t term_before = c.nodes[0].state.term; /* =1 */
|
|
int lc_before = c.leader_count;
|
|
|
|
/* Send a vote request to the leader at the same term */
|
|
uint64_t reply_term = 0;
|
|
int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, &reply_term);
|
|
|
|
/* Must be denied */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
|
|
|
|
/* Node 0 remains LEADER */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
|
|
|
|
/* No new leader elected */
|
|
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) ---------- */
|
|
|
|
/*
|
|
* test_proto_B1_one_vote_per_term
|
|
*
|
|
* Node 2 receives two RPCs in the same term — one from node 0, one from node 1.
|
|
* Both RPCs are buffered (nodes 0 and 1 both start elections simultaneously).
|
|
* After delivery node 2 grants only the first requester; the second is denied.
|
|
* voted_for is set to exactly one candidate.
|
|
*/
|
|
static void test_proto_B1_one_vote_per_term(void)
|
|
{
|
|
raft_sim_cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
|
|
|
|
/* Both node 0 and node 1 start elections — they each send to all peers */
|
|
raft_start_election(&c.nodes[0].state);
|
|
raft_start_election(&c.nodes[1].state);
|
|
|
|
/* 4 RPCs buffered: node0→1, node0→2, node1→0, node1→2 */
|
|
TEST_ASSERT_EQUAL_INT(4, c.pending_count);
|
|
|
|
/* Deliver all */
|
|
raft_sim_deliver_pending(&c);
|
|
|
|
/* Node 2 must have voted for exactly one candidate */
|
|
TEST_ASSERT_NOT_EQUAL(-1, c.nodes[2].state.voted_for);
|
|
|
|
/* voted_for must be 0 or 1 (one of the two candidates that requested) */
|
|
int vf = c.nodes[2].state.voted_for;
|
|
TEST_ASSERT_TRUE(vf == 0 || vf == 1);
|
|
}
|
|
|
|
/*
|
|
* test_proto_B2_higher_term_updates_state
|
|
*
|
|
* Single-node at term=2, role=FOLLOWER, voted_for=1 (old vote).
|
|
* Receives vote request at term=5 (higher).
|
|
* Expected: node.term==5, voted_for updated (grant because on_vote_eligible is NULL).
|
|
*/
|
|
static void test_proto_B2_higher_term_updates_state(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
uint64_t reply_term = 0;
|
|
|
|
reset_mock();
|
|
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 1, 2));
|
|
|
|
/* Simulate old vote from a previous term */
|
|
s.voted_for = 1; /* node voted for itself in an earlier election */
|
|
s.voted_for_term = 1; /* that vote was in term 1 */
|
|
|
|
int rc = raft_on_vote_request(&s, 0, 5, &reply_term);
|
|
|
|
/* Term must be upgraded to 5 */
|
|
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
|
|
|
/* After term upgrade, voted_for is cleared to -1 and then re-evaluated.
|
|
* With on_vote_eligible=NULL and persist succeeding, must grant. */
|
|
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
|
|
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
|
|
|
|
/* voted_for must now be set to from_idx=0 */
|
|
TEST_ASSERT_EQUAL_INT(0, s.voted_for);
|
|
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
|
|
}
|
|
|
|
/*
|
|
* test_proto_B3_stale_term_denied
|
|
*
|
|
* Node at term=5. Receives request at term=3 (stale).
|
|
* Must return RAFT_DENY, reply_term==5, node.term==5 (unchanged).
|
|
*/
|
|
static void test_proto_B3_stale_term_denied(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
uint64_t reply_term = 0;
|
|
|
|
reset_mock();
|
|
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
|
|
|
|
int rc = raft_on_vote_request(&s, 0, 3, &reply_term);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc);
|
|
TEST_ASSERT_EQUAL_UINT64(5, reply_term);
|
|
TEST_ASSERT_EQUAL_UINT64(5, s.term); /* term unchanged */
|
|
|
|
/* on_persist must NOT fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
|
}
|
|
|
|
/*
|
|
* test_proto_B4_different_candidate_denied
|
|
*
|
|
* voted_for=1, voted_for_term=3 at term=3.
|
|
* Receives request from idx=2 at term=3.
|
|
* Must DENY — already voted for 1 this term.
|
|
*/
|
|
static void test_proto_B4_different_candidate_denied(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
uint64_t reply_term = 0;
|
|
|
|
reset_mock();
|
|
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 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_UINT64(3, reply_term);
|
|
|
|
/* voted_for must still be 1 — unchanged */
|
|
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
|
|
TEST_ASSERT_EQUAL_UINT64(3, s.voted_for_term);
|
|
|
|
/* on_persist must NOT fire */
|
|
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
|
}
|
|
|
|
/*
|
|
* test_proto_B5_idempotent_same_candidate
|
|
*
|
|
* voted_for=1, voted_for_term=3 at term=3.
|
|
* Receives request from idx=1 at term=3 again (same candidate, same term).
|
|
* Must GRANT (idempotent re-grant).
|
|
*/
|
|
static void test_proto_B5_idempotent_same_candidate(void)
|
|
{
|
|
raft_state_t s;
|
|
raft_callbacks_t cbs = make_mock_cbs();
|
|
uint64_t reply_term = 0;
|
|
|
|
reset_mock();
|
|
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3));
|
|
s.voted_for = 1;
|
|
s.voted_for_term = 3;
|
|
|
|
int rc = raft_on_vote_request(&s, 1, 3, &reply_term);
|
|
|
|
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
|
|
TEST_ASSERT_EQUAL_UINT64(3, reply_term);
|
|
|
|
/* voted_for must still be 1 */
|
|
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
|
|
TEST_ASSERT_EQUAL_UINT64(3, s.voted_for_term);
|
|
|
|
/* on_persist fires once for the re-grant */
|
|
TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count);
|
|
}
|
|
|
|
#endif /* XCE_TESTING */
|
|
|
|
/* ---------- 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_reply (task 3.4) */
|
|
RUN_TEST(test_vote_reply_higher_term_steps_down);
|
|
RUN_TEST(test_vote_reply_noop_after_decided);
|
|
RUN_TEST(test_vote_reply_quorum_3node_1grant);
|
|
RUN_TEST(test_vote_reply_no_quorum_all_replied_3node);
|
|
RUN_TEST(test_vote_reply_quorum_5node);
|
|
RUN_TEST(test_vote_reply_pending_waiting);
|
|
|
|
/* 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);
|
|
|
|
/* raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election (task 3.5) */
|
|
RUN_TEST(test_election_timeout_undecided);
|
|
RUN_TEST(test_election_timeout_already_decided);
|
|
RUN_TEST(test_leader_lost_clears_voted_for);
|
|
RUN_TEST(test_check_and_start_voted_for_other);
|
|
RUN_TEST(test_check_and_start_not_voted);
|
|
RUN_TEST(test_check_and_start_voted_for_self);
|
|
|
|
/* simulator init (task 4.1) */
|
|
#ifdef XCE_TESTING
|
|
RUN_TEST(test_sim_init_3node);
|
|
RUN_TEST(test_sim_trace_buf);
|
|
|
|
/* simulator network operations (task 4.2) */
|
|
RUN_TEST(test_sim_3node_election_one_leader);
|
|
RUN_TEST(test_sim_start_all_elections_no_winner);
|
|
RUN_TEST(test_sim_partition_no_quorum);
|
|
RUN_TEST(test_sim_fire_timeout_steps_down);
|
|
|
|
/* Group A: basic election correctness (task 5.1) */
|
|
RUN_TEST(test_proto_A1_single_node_self_elects);
|
|
RUN_TEST(test_proto_A2_3node_one_leader);
|
|
RUN_TEST(test_proto_A3_late_election_denied);
|
|
RUN_TEST(test_proto_A4_5node_quorum_3);
|
|
RUN_TEST(test_proto_A5_leader_denies_vote_request);
|
|
|
|
/* Group B: voted_for invariant (task 5.1) */
|
|
RUN_TEST(test_proto_B1_one_vote_per_term);
|
|
RUN_TEST(test_proto_B2_higher_term_updates_state);
|
|
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
|
|
}
|