From 1aa7db4583a779b0cbcba957c7368074ae5c7ac3 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 31 May 2026 05:18:42 +0000 Subject: [PATCH] =?UTF-8?q?feat(raft-library):=20task=203.1=20=E2=80=94=20?= =?UTF-8?q?raft=5Finit=20validation,=20state=20init,=20raft=5Fsnapshot;=20?= =?UTF-8?q?12=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/specs/raft-library/tasks.md | 2 +- src/lib/raft/raft.c | 36 +++- src/lib/raft/raft.h | 8 +- src/lib/testsuite/test_raft.c | 330 +++++++++++++++++++++++++++++- 4 files changed, 368 insertions(+), 8 deletions(-) diff --git a/.kiro/specs/raft-library/tasks.md b/.kiro/specs/raft-library/tasks.md index 3565042..c4b2eb1 100644 --- a/.kiro/specs/raft-library/tasks.md +++ b/.kiro/specs/raft-library/tasks.md @@ -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) diff --git a/src/lib/raft/raft.c b/src/lib/raft/raft.c index 143ba60..5ddadde 100644 --- a/src/lib/raft/raft.c +++ b/src/lib/raft/raft.c @@ -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; } diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index b149497..4b15469 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -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); diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 7c47515..b4e3c1f 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -1,16 +1,338 @@ +#include +#include #include +#include #include -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); }