feat(raft-library): task 3.1 — raft_init validation, state init, raft_snapshot; 12 tests

This commit is contained in:
Rongsong Shen 2026-05-31 05:18:42 +00:00
parent d109155a21
commit 1aa7db4583
4 changed files with 368 additions and 8 deletions

View file

@ -24,7 +24,7 @@
- _Requirements: 1.2, 7.1, 7.2, 7.3_
- [ ] 3. Core state machine implementation (raft.c)
- [ ] 3.1 Implement raft_init and raft_snapshot
- [x] 3.1 Implement raft_init and raft_snapshot
- `raft_init` validates all required callback pointers are non-NULL (all except `on_vote_eligible` and `on_trace`); returns `-EINVAL` without modifying state if any required callback is NULL or `n_peers < 1`
- Initializes from `raft_config_t`: copies `n_peers`, `self_idx`, `election_timeout_ms` (apply 2000ms default if 0), `my_priority`, `initial_term`, `initial_voted_for`; sets `role=FOLLOWER`, `votes_granted=0`, `replies_received=0`, `election_decided=0`
- `raft_snapshot()` fills snap with current field values (called while caller holds lock)

View file

@ -12,8 +12,40 @@ void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap)
int raft_init(raft_state_t *s, const raft_config_t *cfg)
{
(void)s;
(void)cfg;
/* Validate required callbacks and n_peers before touching *s */
if (cfg->n_peers < 1)
return -EINVAL;
if (!cfg->cbs)
return -EINVAL;
if (!cfg->cbs->on_send_vote_request)
return -EINVAL;
if (!cfg->cbs->on_become_leader)
return -EINVAL;
if (!cfg->cbs->on_become_follower)
return -EINVAL;
if (!cfg->cbs->on_persist)
return -EINVAL;
if (!cfg->cbs->on_schedule_timeout)
return -EINVAL;
if (!cfg->cbs->on_cancel_timeout)
return -EINVAL;
if (!cfg->cbs->on_start_election_after_delay)
return -EINVAL;
s->term = cfg->initial_term;
s->role = RAFT_FOLLOWER;
s->voted_for = cfg->initial_voted_for;
s->voted_for_term = 0;
s->n_peers = cfg->n_peers;
s->self_idx = cfg->self_idx;
s->votes_granted = 0;
s->replies_received = 0;
s->election_decided = 0;
s->election_timeout_ms = cfg->election_timeout_ms ? cfg->election_timeout_ms : 2000;
s->my_priority = cfg->my_priority;
s->cbs = cfg->cbs;
s->ctx = cfg->ctx;
return 0;
}

View file

@ -12,6 +12,9 @@ typedef enum {
RAFT_LEADER = 2,
} raft_role_t;
/* Forward declaration — full definition follows raft_state_t */
typedef struct raft_callbacks_t raft_callbacks_t;
/* raft_state_t: pure Raft consensus state for one node.
*
* Contains only the Raft invariant state. Application-specific fields
@ -29,6 +32,9 @@ typedef struct {
int election_decided; /* 1 once on_become_leader/follower fired */
uint32_t election_timeout_ms;
int my_priority; /* priority index for jitter (0 = highest) */
/* set by raft_init — needed by the state machine to fire callbacks */
const raft_callbacks_t *cbs;
void *ctx;
} raft_state_t;
/* raft_snapshot_t: point-in-time read of all Raft state for logging/debugging */
@ -57,7 +63,7 @@ typedef struct {
* on_vote_eligible — if NULL, all Raft-eligible candidates are granted
* on_trace — if NULL, tracing is suppressed
*/
typedef struct {
typedef struct raft_callbacks_t {
/* REQUIRED: Send a RequestVote RPC to peer peer_idx for the given term.
* The caller encodes config_tag, log_lsn, and any other fields in the wire RPC. */
int (*on_send_vote_request)(void *ctx, int peer_idx, uint64_t term);

View file

@ -1,16 +1,338 @@
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
static void test_raft_placeholder(void)
#include "../raft/raft.h"
/* ---------- stub callbacks ---------- */
static int stub_send_vote_request(void *ctx, int peer_idx, uint64_t term)
{
/* placeholder — real tests added in task 2 */
TEST_PASS();
(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;
}
/* ---------- 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_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_placeholder);
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);
}