#include #include #include #include #include #include "../raft/raft.h" /* ---------- stub callbacks ---------- */ static int stub_send_vote_request(void *ctx, int peer_idx, uint64_t term) { (void)ctx; (void)peer_idx; (void)term; return 0; } static void stub_become_leader(void *ctx, uint64_t term) { (void)ctx; (void)term; } static void stub_become_follower(void *ctx, uint64_t term, int leader_idx) { (void)ctx; (void)term; (void)leader_idx; } static int stub_persist(void *ctx, uint64_t term, int voted_for) { (void)ctx; (void)term; (void)voted_for; return 0; } static void stub_schedule_timeout(void *ctx, uint32_t delay_ms) { (void)ctx; (void)delay_ms; } static void stub_cancel_timeout(void *ctx) { (void)ctx; } static void stub_start_election_after_delay(void *ctx, uint32_t delay_ms) { (void)ctx; (void)delay_ms; } /* Build a fully-populated callbacks struct */ static raft_callbacks_t make_full_cbs(void) { raft_callbacks_t cbs; memset(&cbs, 0, sizeof(cbs)); cbs.on_send_vote_request = stub_send_vote_request; cbs.on_become_leader = stub_become_leader; cbs.on_become_follower = stub_become_follower; cbs.on_persist = stub_persist; cbs.on_schedule_timeout = stub_schedule_timeout; cbs.on_cancel_timeout = stub_cancel_timeout; cbs.on_start_election_after_delay = stub_start_election_after_delay; /* on_vote_eligible and on_trace left NULL (optional) */ return cbs; } /* ---------- mock infrastructure for raft_start_election tests ---------- */ /* Per-test counters — reset by each test before calling start_election */ typedef struct { int send_vote_request_count; int last_send_peer_idx; uint64_t last_send_term; /* track each peer index sent (up to 8 for test purposes) */ int sent_peers[8]; int schedule_timeout_count; uint32_t last_schedule_delay; int persist_count; uint64_t last_persist_term; int last_persist_voted_for; int persist_return_value; /* configurable: 0=success, non-zero=fail */ int become_leader_count; uint64_t last_become_leader_term; /* on_vote_eligible mock */ int vote_eligible_return_value; /* configurable: 1=grant, 0=deny; default 1 */ int vote_eligible_call_count; /* on_trace mock */ int trace_call_count; char last_trace_event[64]; } mock_cbs_state_t; static mock_cbs_state_t g_mock; static void reset_mock(void) { memset(&g_mock, 0, sizeof(g_mock)); /* Defaults: persist succeeds, vote_eligible grants */ g_mock.persist_return_value = 0; g_mock.vote_eligible_return_value = 1; } static int mock_send_vote_request(void *ctx, int peer_idx, uint64_t term) { (void)ctx; if (g_mock.send_vote_request_count < 8) g_mock.sent_peers[g_mock.send_vote_request_count] = peer_idx; g_mock.last_send_peer_idx = peer_idx; g_mock.last_send_term = term; g_mock.send_vote_request_count++; return 0; } static void mock_become_leader(void *ctx, uint64_t term) { (void)ctx; g_mock.become_leader_count++; g_mock.last_become_leader_term = term; } static int mock_persist(void *ctx, uint64_t term, int voted_for) { (void)ctx; g_mock.persist_count++; g_mock.last_persist_term = term; g_mock.last_persist_voted_for = voted_for; return g_mock.persist_return_value; } static void mock_schedule_timeout(void *ctx, uint32_t delay_ms) { (void)ctx; g_mock.schedule_timeout_count++; g_mock.last_schedule_delay = delay_ms; } static int mock_vote_eligible(void *ctx, int from_idx, uint64_t term) { (void)ctx; (void)from_idx; (void)term; g_mock.vote_eligible_call_count++; return g_mock.vote_eligible_return_value; } static void mock_trace(void *ctx, const char *event, const char *detail) { (void)ctx; (void)detail; g_mock.trace_call_count++; if (event) { strncpy(g_mock.last_trace_event, event, sizeof(g_mock.last_trace_event) - 1); g_mock.last_trace_event[sizeof(g_mock.last_trace_event) - 1] = '\0'; } } /* Build callbacks that record invocations in g_mock */ static raft_callbacks_t make_mock_cbs(void) { raft_callbacks_t cbs; memset(&cbs, 0, sizeof(cbs)); cbs.on_send_vote_request = mock_send_vote_request; cbs.on_become_leader = mock_become_leader; cbs.on_become_follower = stub_become_follower; /* plain stub */ cbs.on_persist = mock_persist; cbs.on_schedule_timeout = mock_schedule_timeout; cbs.on_cancel_timeout = stub_cancel_timeout; cbs.on_start_election_after_delay = stub_start_election_after_delay; /* on_vote_eligible and on_trace left NULL — tests that need them set explicitly */ return cbs; } /* ---------- helpers ---------- */ /* Initialize a raft_state_t with mock callbacks; n_peers and self_idx configurable */ static int init_node(raft_state_t *s, raft_callbacks_t *cbs, int n_peers, int self_idx) { raft_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = n_peers; cfg.self_idx = self_idx; cfg.election_timeout_ms = 1000; cfg.cbs = cbs; return raft_init(s, &cfg); } /* ---------- raft_init tests ---------- */ /* raft_init with NULL required callback returns -EINVAL and does not modify s */ static void test_raft_init_null_send_vote_request_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; /* canary: fill s with a non-zero sentinel so we can verify it's untouched */ memset(&s, 0xAB, sizeof(s)); cbs.on_send_vote_request = NULL; /* knock out a required callback */ memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; cfg.ctx = NULL; int rc = raft_init(&s, &cfg); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); /* s must be completely unchanged (canary check — spot-check first byte) */ { unsigned char buf[sizeof(s)]; memset(buf, 0xAB, sizeof(buf)); TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s)); } } static void test_raft_init_null_become_leader_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_become_leader = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_become_follower_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_become_follower = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_persist_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_persist = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_schedule_timeout_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_schedule_timeout = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_cancel_timeout_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_cancel_timeout = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_start_election_after_delay_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_start_election_after_delay = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } /* n_peers == 0 → -EINVAL */ static void test_raft_init_zero_peers_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0xAB, sizeof(s)); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 0; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); /* s must be unchanged */ unsigned char buf[sizeof(s)]; memset(buf, 0xAB, sizeof(buf)); TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s)); } /* Successful init: check all fields */ static void test_raft_init_success_fields(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 1; cfg.election_timeout_ms = 1500; cfg.my_priority = 2; cfg.initial_term = 7; cfg.initial_voted_for = -1; cfg.cbs = &cbs; cfg.ctx = (void *)0xDEADBEEF; int rc = raft_init(&s, &cfg); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(3, s.n_peers); TEST_ASSERT_EQUAL_INT(1, s.self_idx); TEST_ASSERT_EQUAL_UINT32(1500, s.election_timeout_ms); TEST_ASSERT_EQUAL_INT(2, s.my_priority); TEST_ASSERT_EQUAL_UINT64(7, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(0, s.votes_granted); TEST_ASSERT_EQUAL_INT(0, s.replies_received); TEST_ASSERT_EQUAL_INT(0, s.election_decided); /* cbs/ctx must be stored in raft_state_t for the state machine to fire callbacks */ TEST_ASSERT_EQUAL_PTR(&cbs, s.cbs); TEST_ASSERT_EQUAL_PTR((void *)0xDEADBEEF, s.ctx); } /* election_timeout_ms == 0 → default 2000 */ static void test_raft_init_default_election_timeout(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.election_timeout_ms = 0; /* trigger default */ cfg.cbs = &cbs; int rc = raft_init(&s, &cfg); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT32(2000, s.election_timeout_ms); } /* optional callbacks (on_vote_eligible, on_trace) may be NULL — must succeed */ static void test_raft_init_optional_callbacks_null_succeeds(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); cbs.on_vote_eligible = NULL; cbs.on_trace = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); } /* ---------- raft_start_election tests (task 3.2) ---------- */ /* * test_start_election_from_follower_3node * * 3-node cluster (self=0). raft_start_election transitions to CANDIDATE, * increments term, records self-vote, fires on_persist(new_term, self_idx), * fires on_send_vote_request for each peer != self_idx (peers 1 and 2), * fires on_schedule_timeout once, returns RAFT_OK. */ static void test_start_election_from_follower_3node(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0)); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_UINT64(0, s.term); int rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_UINT64(1, s.term); TEST_ASSERT_EQUAL_INT(0, s.self_idx); TEST_ASSERT_EQUAL_INT(0, s.voted_for); /* voted_for == self_idx */ TEST_ASSERT_EQUAL_UINT64(1, s.voted_for_term); /* voted_for_term == new term */ TEST_ASSERT_EQUAL_INT(1, s.votes_granted); /* self-vote counted */ TEST_ASSERT_EQUAL_INT(0, s.replies_received); TEST_ASSERT_EQUAL_INT(0, s.election_decided); /* on_persist must fire once with (new_term=1, self_idx=0) */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_persist_term); TEST_ASSERT_EQUAL_INT(0, g_mock.last_persist_voted_for); /* on_send_vote_request must fire for peers 1 and 2 (not self=0) */ TEST_ASSERT_EQUAL_INT(2, g_mock.send_vote_request_count); /* Both sends must use the new term */ TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_send_term); /* Verify neither sent peer is self_idx=0 */ { int i; for (i = 0; i < g_mock.send_vote_request_count; i++) { TEST_ASSERT_NOT_EQUAL(0, g_mock.sent_peers[i]); } } /* on_schedule_timeout must fire exactly once */ TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count); TEST_ASSERT_EQUAL_UINT32(1000, g_mock.last_schedule_delay); /* on_become_leader must NOT fire (multi-node path) */ TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); } /* * test_start_election_single_node * * 1-node cluster (self=0). raft_start_election shortcircuits to LEADER, * fires on_persist then on_become_leader, returns RAFT_ELECTED_IMMEDIATELY. * No on_send_vote_request or on_schedule_timeout calls. */ static void test_start_election_single_node(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 1, 0)); int rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_ELECTED_IMMEDIATELY, rc); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); TEST_ASSERT_EQUAL_UINT64(1, s.term); TEST_ASSERT_EQUAL_INT(0, s.voted_for); /* self-vote */ TEST_ASSERT_EQUAL_INT(1, s.election_decided); /* on_persist fires once with (term=1, self_idx=0) */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_persist_term); TEST_ASSERT_EQUAL_INT(0, g_mock.last_persist_voted_for); /* on_become_leader fires once with term=1 */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_leader_term); /* on_send_vote_request must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count); /* on_schedule_timeout must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count); } /* * test_start_election_already_candidate * * Call raft_start_election once (become CANDIDATE), then call again. * Second call must return -EALREADY and leave state completely unchanged. */ static void test_start_election_already_candidate(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 1)); /* First election — becomes CANDIDATE */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); uint64_t term_after_first = s.term; /* Snapshot the state before the second call */ int saved_voted_for = s.voted_for; int saved_votes_granted = s.votes_granted; int saved_replies_recvd = s.replies_received; int saved_election_dec = s.election_decided; reset_mock(); /* Second call must be rejected */ int rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(-EALREADY, rc); /* State must be unchanged */ TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_UINT64(term_after_first, s.term); TEST_ASSERT_EQUAL_INT(saved_voted_for, s.voted_for); TEST_ASSERT_EQUAL_INT(saved_votes_granted, s.votes_granted); TEST_ASSERT_EQUAL_INT(saved_replies_recvd, s.replies_received); TEST_ASSERT_EQUAL_INT(saved_election_dec, s.election_decided); /* No callbacks must have fired on the second call */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count); TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); } /* * test_start_election_already_leader * * Manually set role=RAFT_LEADER, then call raft_start_election. * Must return -EALREADY without modifying any state or firing any callback. */ static void test_start_election_already_leader(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 2)); /* Manually elevate to LEADER */ s.role = RAFT_LEADER; s.term = 5; int rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(-EALREADY, rc); /* All state must be unchanged */ TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); TEST_ASSERT_EQUAL_UINT64(5, s.term); /* No callbacks must fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count); TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); } /* ---------- raft_on_vote_request tests (task 3.3) ---------- */ /* * Helper: init a 3-node node with mock callbacks (including trace). * term, role, voted_for can be set after init. */ static int init_vote_node(raft_state_t *s, raft_callbacks_t *cbs, int n_peers, int self_idx, uint64_t initial_term) { raft_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = n_peers; cfg.self_idx = self_idx; cfg.election_timeout_ms = 1000; cfg.initial_term = initial_term; cfg.initial_voted_for = -1; cfg.cbs = cbs; return raft_init(s, &cfg); } /* * test_vote_req_stale_term * * Node at term=5, receives request with term=3. * Expected: RAFT_DENY, *reply_term=5, state unchanged, * on_trace fired with "vote_denied_stale_term". */ static void test_vote_req_stale_term(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); /* Confirm starting state */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); int rc = raft_on_vote_request(&s, 1, 3, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* State must be unchanged */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); /* on_trace must fire with "vote_denied_stale_term" */ TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count); TEST_ASSERT_EQUAL_STRING("vote_denied_stale_term", g_mock.last_trace_event); /* on_persist must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_vote_req_higher_term_updates_state * * Node at term=2, receives request with term=5 (higher). * Expected: s->term updated to 5, voted_for cleared to -1, role=FOLLOWER. * Since voted_for is unset and persist succeeds: RAFT_GRANT. * reply_term = 5. */ static void test_vote_req_higher_term_updates_state(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 2)); /* Simulate node was CANDIDATE in term=2 */ s.role = RAFT_CANDIDATE; s.voted_for = 0; /* voted for self */ s.voted_for_term = 2; int rc = raft_on_vote_request(&s, 1, 5, &reply_term); /* Term upgrade must happen: term updated, role stepped down */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* Then grant because voted_for is cleared and persist succeeds */ TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* on_persist must fire once */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); TEST_ASSERT_EQUAL_UINT64(5, g_mock.last_persist_term); TEST_ASSERT_EQUAL_INT(1, g_mock.last_persist_voted_for); /* voted_for set on grant */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); } /* * test_vote_req_same_term_already_voted_different * * Node at term=5, voted_for=1, voted_for_term=5. * Receives request from idx=2 at term=5. * Expected: RAFT_DENY, "vote_denied_already_voted" trace. */ static void test_vote_req_same_term_already_voted_different(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.voted_for = 1; s.voted_for_term = 5; int rc = raft_on_vote_request(&s, 2, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* State: voted_for still 1 (not 2), term unchanged */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.term); /* on_trace fires with already_voted */ TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count); TEST_ASSERT_EQUAL_STRING("vote_denied_already_voted", g_mock.last_trace_event); /* on_persist must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_vote_req_same_term_idempotent_same_candidate * * Node at term=5, voted_for=1, voted_for_term=5. * Receives request from idx=1 at term=5 (same candidate, same term). * Expected: idempotent re-grant → RAFT_GRANT, voted_for still 1. */ static void test_vote_req_same_term_idempotent_same_candidate(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.voted_for = 1; s.voted_for_term = 5; int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* voted_for still 1 */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); /* on_persist must fire once for the re-grant */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); } /* * test_vote_req_leader_denies * * Node is RAFT_LEADER at term=5, receives request at term=5 (same term, no upgrade). * Expected: RAFT_DENY ("vote_denied_role" trace), role stays LEADER. */ static void test_vote_req_leader_denies(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.role = RAFT_LEADER; int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* Role gate fires: still LEADER (term==request term, no upgrade) */ TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); /* on_trace fires with "vote_denied_role" */ TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count); TEST_ASSERT_EQUAL_STRING("vote_denied_role", g_mock.last_trace_event); /* on_persist must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_vote_req_on_vote_eligible_denies * * on_vote_eligible returns 0 → RAFT_DENY, voted_for unchanged, * "vote_denied_freshness" trace fires. */ static void test_vote_req_on_vote_eligible_denies(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_vote_eligible = mock_vote_eligible; cbs.on_trace = mock_trace; reset_mock(); g_mock.vote_eligible_return_value = 0; /* deny */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* voted_for must be unchanged */ TEST_ASSERT_EQUAL_INT(-1, s.voted_for); /* on_vote_eligible must have been called */ TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); /* on_trace fires with "vote_denied_freshness" */ TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count); TEST_ASSERT_EQUAL_STRING("vote_denied_freshness", g_mock.last_trace_event); /* on_persist must NOT fire */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_vote_req_on_vote_eligible_grants * * on_vote_eligible returns 1 and persist succeeds → RAFT_GRANT, voted_for set. */ static void test_vote_req_on_vote_eligible_grants(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_vote_eligible = mock_vote_eligible; cbs.on_trace = mock_trace; reset_mock(); g_mock.vote_eligible_return_value = 1; /* grant */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* voted_for must be set */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); /* on_vote_eligible called once */ TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); /* on_persist called once */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); /* on_trace fires "vote_granted" */ TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count); TEST_ASSERT_EQUAL_STRING("vote_granted", g_mock.last_trace_event); } /* * test_vote_req_on_vote_eligible_null * * on_vote_eligible is NULL → treated as eligible, grant if persist ok. */ static void test_vote_req_on_vote_eligible_null(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; /* on_vote_eligible left NULL in make_mock_cbs */ cbs.on_trace = mock_trace; reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* voted_for must be set */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); /* on_persist called once */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); } /* * test_vote_req_persist_fails * * on_persist returns non-zero → RAFT_DENY, voted_for unchanged. */ static void test_vote_req_persist_fails(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; reset_mock(); g_mock.persist_return_value = -1; /* fail */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); /* voted_for must NOT be updated */ TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); /* on_persist called once (it ran, but failed) */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); } /* * test_vote_req_reply_term_always_set * * Verify that *reply_term == s->term on every return path by testing * several scenarios and checking reply_term each time. */ static void test_vote_req_reply_term_always_set(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term; /* Path 1: stale term deny */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 7)); reply_term = 0; raft_on_vote_request(&s, 1, 3, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal current term=7 */ /* Path 2: term upgrade + grant */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3)); reply_term = 0; raft_on_vote_request(&s, 1, 7, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal upgraded term=7 */ /* Path 3: already voted for different — deny */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.voted_for = 1; s.voted_for_term = 5; reply_term = 0; raft_on_vote_request(&s, 2, 5, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */ /* Path 4: persist fails — deny */ g_mock.persist_return_value = -1; reset_mock(); g_mock.persist_return_value = -1; TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); reply_term = 0; raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */ } /* ---------- raft_snapshot tests ---------- */ static void test_raft_snapshot_returns_current_state(void) { raft_state_t s; raft_snapshot_t snap; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0, sizeof(s)); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.initial_term = 5; cfg.initial_voted_for = 1; cfg.cbs = &cbs; int rc = raft_init(&s, &cfg); TEST_ASSERT_EQUAL_INT(0, rc); /* manually set a few fields to distinguish from zero defaults */ s.votes_granted = 2; s.election_decided = 1; s.voted_for_term = 5; memset(&snap, 0xFF, sizeof(snap)); /* canary */ raft_snapshot(&s, &snap); TEST_ASSERT_EQUAL_UINT64(5, snap.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, snap.role); TEST_ASSERT_EQUAL_INT(1, snap.voted_for); TEST_ASSERT_EQUAL_UINT64(5, snap.voted_for_term); TEST_ASSERT_EQUAL_INT(2, snap.votes_granted); TEST_ASSERT_EQUAL_INT(1, snap.election_decided); } /* ---------- test_raft entry point ---------- */ void test_raft(void) { printf("\n>> raft test\n\n"); RUN_TEST(test_raft_init_null_send_vote_request_returns_einval); RUN_TEST(test_raft_init_null_become_leader_returns_einval); RUN_TEST(test_raft_init_null_become_follower_returns_einval); RUN_TEST(test_raft_init_null_persist_returns_einval); RUN_TEST(test_raft_init_null_schedule_timeout_returns_einval); RUN_TEST(test_raft_init_null_cancel_timeout_returns_einval); RUN_TEST(test_raft_init_null_start_election_after_delay_returns_einval); RUN_TEST(test_raft_init_zero_peers_returns_einval); RUN_TEST(test_raft_init_success_fields); RUN_TEST(test_raft_init_default_election_timeout); RUN_TEST(test_raft_init_optional_callbacks_null_succeeds); RUN_TEST(test_raft_snapshot_returns_current_state); /* raft_start_election (task 3.2) */ RUN_TEST(test_start_election_from_follower_3node); RUN_TEST(test_start_election_single_node); RUN_TEST(test_start_election_already_candidate); RUN_TEST(test_start_election_already_leader); /* raft_on_vote_request (task 3.3) */ RUN_TEST(test_vote_req_stale_term); RUN_TEST(test_vote_req_higher_term_updates_state); RUN_TEST(test_vote_req_same_term_already_voted_different); RUN_TEST(test_vote_req_same_term_idempotent_same_candidate); RUN_TEST(test_vote_req_leader_denies); RUN_TEST(test_vote_req_on_vote_eligible_denies); RUN_TEST(test_vote_req_on_vote_eligible_grants); RUN_TEST(test_vote_req_on_vote_eligible_null); RUN_TEST(test_vote_req_persist_fails); RUN_TEST(test_vote_req_reply_term_always_set); }