#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; } /* ---- log-replication callback stubs (raft-log-replication) ---- */ static int stub_persist_entry(void *ctx, uint64_t index, uint64_t term, const void *entry, size_t len) { (void)ctx; (void)index; (void)term; (void)entry; (void)len; return 0; } static uint64_t stub_get_entry_term(void *ctx, uint64_t index) { (void)ctx; (void)index; return 0; } static int stub_send_append_entries(void *ctx, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, uint64_t leader_commit, int n_entries) { (void)ctx; (void)peer_idx; (void)prev_log_index; (void)prev_log_term; (void)leader_commit; (void)n_entries; return 0; } static int stub_truncate_from(void *ctx, uint64_t index) { (void)ctx; (void)index; return 0; } /* 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; cbs.on_persist_entry = stub_persist_entry; cbs.on_get_entry_term = stub_get_entry_term; cbs.on_send_append_entries = stub_send_append_entries; cbs.on_truncate_from = stub_truncate_from; /* 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; /* on_send_pre_vote_request mock (task 2.2) */ int send_pre_vote_request_count; int last_pre_vote_peer_idx; uint64_t last_pre_vote_term; /* per-peer prospective term recorded by index (up to 8) */ uint64_t pre_vote_sent_term[8]; /* term sent to peer i; 0 = not sent */ int pre_vote_sent_peers[8]; /* ordered list of peer indices sent to */ } 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_send_pre_vote_request(void *ctx, int peer_idx, uint64_t prospective_term) { (void)ctx; if (g_mock.send_pre_vote_request_count < 8) g_mock.pre_vote_sent_peers[g_mock.send_pre_vote_request_count] = peer_idx; if (peer_idx >= 0 && peer_idx < 8) g_mock.pre_vote_sent_term[peer_idx] = prospective_term; g_mock.last_pre_vote_peer_idx = peer_idx; g_mock.last_pre_vote_term = prospective_term; g_mock.send_pre_vote_request_count++; return 0; } 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; cbs.on_send_pre_vote_request = mock_send_pre_vote_request; /* Log-replication callbacks: stubs (task 2.1 only needs them non-NULL for * raft_init validation; behaviour is exercised by later tasks). */ cbs.on_persist_entry = stub_persist_entry; cbs.on_get_entry_term = stub_get_entry_term; cbs.on_send_append_entries = stub_send_append_entries; cbs.on_truncate_from = stub_truncate_from; /* 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 = 3; 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 = 3; 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 = 3; 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 = 3; 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 = 3; 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 = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } /* ---- log-replication required callbacks (Req 7.1–7.4): each NULL → -EINVAL ---- */ static void test_raft_init_null_persist_entry_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_entry = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_get_entry_term_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_get_entry_term = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_send_append_entries_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_send_append_entries = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } static void test_raft_init_null_truncate_from_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_truncate_from = NULL; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } /* All four log callbacks present → succeeds; initial_log_len restored to log_len (Req 1.1) */ static void test_raft_init_log_callbacks_present_succeeds(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.initial_log_len = 5; /* WAL length restored from disk */ cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); TEST_ASSERT_EQUAL_UINT64(5, s.log_len); } /* n_peers even (>1) → -EINVAL: even-sized clusters have no fault-tolerance * advantage over the next-smaller odd cluster; reject to prevent misconfiguration. */ static void test_raft_init_even_peers_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0xAB, sizeof(s)); /* n_peers=2: even, >1 → must fail */ 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=4: even, >1 → must fail */ cfg.n_peers = 4; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); /* n_peers=1: single-node is always valid (odd by convention) */ cfg.n_peers = 1; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); /* n_peers=3: odd, >1 → must succeed */ 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)); /* s must NOT be unchanged after valid inits above */ TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); } /* n_peers > RAFT_MAX_PEERS → -EINVAL: the fixed-size next_index[]/match_index[]/ * pre_vote_replies[] arrays are indexed by peer, so an oversize cluster must be * rejected before any leader-progress/pre-vote loop can OOB-write. */ static void test_raft_init_oversize_peers_returns_einval(void) { raft_state_t s; raft_callbacks_t cbs = make_full_cbs(); raft_config_t cfg; memset(&s, 0xAB, sizeof(s)); /* n_peers = RAFT_MAX_PEERS + 1 → must fail (would OOB next_index[]) */ memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = RAFT_MAX_PEERS + 1; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); /* s must be untouched on rejection (canary) */ { unsigned char buf[sizeof(s)]; memset(buf, 0xAB, sizeof(buf)); TEST_ASSERT_EQUAL_MEMORY(buf, &s, sizeof(s)); } /* n_peers = RAFT_MAX_PEERS (odd? RAFT_MAX_PEERS=8 is even → also rejected by * the even-peers rule, so use the largest valid odd size at the bound) */ cfg.n_peers = RAFT_MAX_PEERS - 1; /* 7: odd, within bound → must succeed */ TEST_ASSERT_EQUAL_INT(0, 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); } /* ---------- leader progress init on becoming leader (task 2.2) ---------- */ /* Init a node with a non-zero restored log length (initial_log_len). */ static int init_node_log_len(raft_state_t *s, raft_callbacks_t *cbs, int n_peers, int self_idx, uint64_t initial_log_len) { 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_log_len = initial_log_len; cfg.cbs = cbs; return raft_init(s, &cfg); } /* * test_leader_progress_init_single_node (raft.tla BecomeLeader, Req 3.1) * * Single-node immediate-win path (raft_start_election → RAFT_ELECTED_IMMEDIATELY). * Non-zero restored log (log_len=4): on becoming leader, next_index[j] must equal * log_len+1 (=5) and match_index[j] must equal 0 for EVERY peer j in [0, n_peers). */ static void test_leader_progress_init_single_node(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 1, 0, 4)); TEST_ASSERT_EQUAL_UINT64(4, s.log_len); int rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_ELECTED_IMMEDIATELY, rc); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); for (int j = 0; j < s.n_peers; j++) { TEST_ASSERT_EQUAL_UINT64(s.log_len + 1, s.next_index[j]); TEST_ASSERT_EQUAL_UINT64(0, s.match_index[j]); } } /* * test_leader_progress_init_multi_node (raft.tla BecomeLeader, Req 3.1) * * Multi-node become-leader path (raft_on_vote_reply → RAFT_BECAME_LEADER). * Non-zero restored log (log_len=7): on reaching quorum, next_index[j] must equal * log_len+1 (=8) and match_index[j] must equal 0 for EVERY peer j in [0, n_peers). */ static void test_leader_progress_init_multi_node(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 7)); TEST_ASSERT_EQUAL_UINT64(7, s.log_len); /* Drive to CANDIDATE then reach quorum with one peer grant. */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); int rc = raft_on_vote_reply(&s, 1, 1, s.term); TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); for (int j = 0; j < s.n_peers; j++) { TEST_ASSERT_EQUAL_UINT64(s.log_len + 1, s.next_index[j]); TEST_ASSERT_EQUAL_UINT64(0, s.match_index[j]); } } /* ---------- 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, Req 6.3) * - 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, Req 6.3) */ 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); /* The restored vote belongs to the restored term — without this, the * duplicate-vote check cannot match after restart (see H6). */ TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); 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_H6_restored_vote_blocks_second_vote * * Election Safety across restart: a node that voted for candidate 1 in * term 5, then restarted (raft_init with initial_term=5, initial_voted_for=1), * must DENY a vote request from a DIFFERENT candidate (idx=2) in the same * term 5. Without restoring voted_for_term alongside voted_for, the * duplicate-vote check cannot match the restored term and the node would * grant a second vote in term 5 → two leaders in one term. */ static void test_proto_H6_restored_vote_blocks_second_vote(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; reset_mock(); 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; /* persisted vote for candidate 1, term 5 */ cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); /* Different candidate (idx=2) requests a vote in the SAME 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); /* The restored vote must be untouched */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); /* Same candidate (idx=1) retransmitting must still be re-granted */ rc = raft_on_vote_request(&s, 1, 5, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); } /* * 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); } /* ---------- Group D: Quorum and Result (task 5.3) ---------- */ /* * test_proto_D1_quorum_1grant_of_2_needed * * 3-node cluster, node 0 starts election (votes_granted=1, n_peers=3, quorum=2). * Feed 1 grant reply from peer 1: votes_granted becomes 2 == quorum. * Expected: RAFT_BECAME_LEADER, role==RAFT_LEADER, election_decided==1, * on_become_leader fired exactly once. */ static void test_proto_D1_quorum_1grant_of_2_needed(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)); /* Start election: votes_granted=1 (self), role=CANDIDATE */ int start_rc = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, start_rc); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_INT(1, s.votes_granted); TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count); /* Feed 1 grant from peer 1: votes_granted=2 == quorum=2 */ 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); /* on_become_leader must have fired exactly once */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); } /* * test_proto_D2_both_peers_deny_becomes_follower * * 3-node cluster, node 0 starts election (votes_granted=1, replies_received=0). * Feed 2 deny replies from peers 1 and 2. * After second deny: replies_received==2 == n_peers-1=2, votes_granted=1 < quorum=2. * Expected: RAFT_BECAME_FOLLOWER, role==RAFT_FOLLOWER, election_decided==1, * on_become_follower fired. */ static void test_proto_D2_both_peers_deny_becomes_follower(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)); /* Start election */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_INT(0, s.replies_received); /* First deny from peer 1: replies_received=1, still pending */ 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, s.election_decided); /* Second deny from peer 2: 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 have fired once */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count); } /* * test_proto_D3_second_vote_reply_noop * * 3-node cluster, start election. * Feed 1 grant → RAFT_BECAME_LEADER (election_decided=1). * Feed a second grant from peer 2 → RAFT_PENDING (noop, election_decided guard). * Expected: become_leader_count still 1 (on_become_leader fired exactly once). */ static void test_proto_D3_second_vote_reply_noop(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)); /* Start election */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); /* First grant: wins election */ int rc1 = raft_on_vote_reply(&s, 1, 1, 1); TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc1); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); TEST_ASSERT_EQUAL_INT(1, s.election_decided); TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); /* Second grant from peer 2: election already decided → noop */ int rc2 = raft_on_vote_reply(&s, 2, 1, 1); TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc2); /* on_become_leader must NOT have fired again */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); } /* * test_proto_D4_reply_higher_term_steps_down * * 3-node cluster, start election (term=1). * Feed reply with term=3 (granted=0) → higher term → step down. * Expected: RAFT_BECAME_FOLLOWER, role==RAFT_FOLLOWER, s.term==3, * election_decided==1, on_become_follower fired. */ static void test_proto_D4_reply_higher_term_steps_down(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)); /* Start election: term becomes 1 */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(1, (int)s.term); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); /* Reply with higher term=3, not granted */ int rc = raft_on_vote_reply(&s, 1, 0, 3); TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_UINT64(3, s.term); TEST_ASSERT_EQUAL_INT(1, s.election_decided); /* on_become_follower must have fired once */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count); } /* ---------- Group E: Election Timeout (task 5.3) ---------- */ /* * test_proto_E1_timeout_before_quorum * * 3-node cluster, start election (on_schedule_timeout fires → schedule_timeout_count==1). * Call raft_on_election_timeout before any replies. * Expected: role==RAFT_FOLLOWER, election_decided==1, on_become_follower fired, * on_cancel_timeout fired. */ static void test_proto_E1_timeout_before_quorum(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)); /* Start election: schedule_timeout fires */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count); /* Fire timeout before any replies */ raft_on_election_timeout(&s); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(1, s.election_decided); /* on_become_follower must have fired */ TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count); /* on_cancel_timeout must have fired */ TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count); } /* * test_proto_E2_timeout_after_quorum_noop * * 3-node cluster, start election, feed 1 grant → RAFT_BECAME_LEADER. * Call raft_on_election_timeout. * Expected: role still RAFT_LEADER (already decided, timeout is noop), * on_cancel_timeout fired (stale timer cleanup), * on_become_follower NOT additionally fired. */ static void test_proto_E2_timeout_after_quorum_noop(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)); /* Start election */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); /* Feed 1 grant → LEADER */ 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); /* Snapshot counters before stale timeout delivery */ int follower_before = g_mock.become_follower_count; int cancel_before = g_mock.cancel_timeout_count; /* Fire timeout: already decided → fires on_cancel_timeout, no other effects (Req 6.3) */ raft_on_election_timeout(&s); /* Role must still be LEADER */ TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); /* on_cancel_timeout fires exactly once for the stale delivery (Req 6.3) */ TEST_ASSERT_EQUAL_INT(cancel_before + 1, g_mock.cancel_timeout_count); /* on_become_follower must NOT have fired additionally */ TEST_ASSERT_EQUAL_INT(follower_before, g_mock.become_follower_count); } /* * test_proto_E3_new_election_after_timeout * * 3-node cluster, start election (term=1). * Timeout fires → becomes FOLLOWER (election_decided=1). * Start new election: raft_start_election again. * Expected: term==2 (incremented), on_schedule_timeout fired again * (schedule_timeout_count==2 total). */ static void test_proto_E3_new_election_after_timeout(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)); /* First election: term=1 */ TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_UINT64(1, s.term); TEST_ASSERT_EQUAL_INT(1, g_mock.schedule_timeout_count); /* Timeout: step down to FOLLOWER */ raft_on_election_timeout(&s); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(1, s.election_decided); /* Start new election: election_decided=1 means old state decided — need to * reset election state so raft_start_election allows another attempt. * Per spec: a new election restarts the state machine; reset election_decided. */ s.election_decided = 0; s.role = RAFT_FOLLOWER; int rc2 = raft_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc2); /* Term must have incremented to 2 */ TEST_ASSERT_EQUAL_UINT64(2, s.term); /* on_schedule_timeout must have fired again (total 2) */ TEST_ASSERT_EQUAL_INT(2, g_mock.schedule_timeout_count); } /* ---------- Group F: RDHUP Failover (task 5.4) ---------- */ /* * test_proto_F1_leader_lost_priority_delay * * Verify that on_start_election_after_delay fires with delay = my_priority * election_timeout_ms. * Node 1 (priority=1, timeout=1000) → delay=1000ms. * Node 2 (priority=2, timeout=1000) → delay=2000ms. */ static void test_proto_F1_leader_lost_priority_delay(void) { /* --- Node 1: priority=1, election_timeout_ms=1000 → expected delay=1000 --- */ { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); raft_config_t cfg; reset_mock(); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 1; cfg.my_priority = 1; cfg.election_timeout_ms = 1000; cfg.initial_voted_for = -1; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); raft_on_leader_lost(&s); TEST_ASSERT_EQUAL_INT(1, g_mock.start_election_after_delay_count); TEST_ASSERT_EQUAL_UINT32(1000, g_mock.last_start_election_delay); } /* --- Node 2: priority=2, election_timeout_ms=1000 → expected delay=2000 --- */ { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); raft_config_t cfg; reset_mock(); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 2; cfg.my_priority = 2; cfg.election_timeout_ms = 1000; cfg.initial_voted_for = -1; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); raft_on_leader_lost(&s); TEST_ASSERT_EQUAL_INT(1, g_mock.start_election_after_delay_count); TEST_ASSERT_EQUAL_UINT32(2000, g_mock.last_start_election_delay); } } /* * test_proto_F2_voted_during_delay_skip * * Node calls raft_on_leader_lost (voted_for=-1). * During the jitter delay, a vote request arrives from node 0 at term=1. * Node grants the vote (voted_for=0). * Then raft_check_and_start_election is called. * Since voted_for==0 != self_idx==1, must return RAFT_SKIP_ELECTION. */ static void test_proto_F2_voted_during_delay_skip(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); raft_config_t cfg; uint64_t reply_term = 0; reset_mock(); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 1; cfg.my_priority = 1; cfg.election_timeout_ms = 1000; cfg.initial_voted_for = -1; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); /* Leader lost: clears voted_for to -1, schedules delayed election */ raft_on_leader_lost(&s); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); /* During the delay, a vote request arrives from node 0 at term=1 */ int rc = raft_on_vote_request(&s, 0, 1, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_INT(0, s.voted_for); /* Timer fires: check-and-start sees voted_for=0 != self_idx=1 → skip */ int check_rc = raft_check_and_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, check_rc); /* Role must stay FOLLOWER — no election started */ TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); } /* * test_proto_F3_no_vote_during_delay_starts_election * * Node 0 (priority=0, self_idx=0) calls raft_on_leader_lost. * No vote arrives during the delay (voted_for stays -1). * raft_check_and_start_election must return RAFT_ELECTION_STARTED and role==CANDIDATE. */ static void test_proto_F3_no_vote_during_delay_starts_election(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); raft_config_t cfg; reset_mock(); memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = 3; cfg.self_idx = 0; cfg.my_priority = 0; cfg.election_timeout_ms = 1000; cfg.initial_voted_for = -1; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); /* Leader lost: voted_for cleared to -1 */ raft_on_leader_lost(&s); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); /* No vote arrived during the delay */ int rc = raft_check_and_start_election(&s); TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); } /* * test_proto_F4_simultaneous_rdhup_one_leader * * Nodes 1 and 2 both receive RDHUP (leader/node 0 lost). * Both call raft_on_leader_lost → voted_for=-1 for both. * Node 1 (lower idx, lower priority) calls check_and_start first → ELECTION_STARTED. * Node 1 sends vote requests; deliver them to node 2. * Node 2 grants node 1 (voted_for=1). * Node 2 then calls check_and_start → RAFT_SKIP_ELECTION (voted_for=1 != self_idx=2). * Result: leader_count==1. */ static void test_proto_F4_simultaneous_rdhup_one_leader(void) { raft_sim_cluster_t c; memset(&c, 0, sizeof(c)); TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); /* Simulate: node 0 was the leader, now lost. Nodes 1 and 2 see RDHUP. */ raft_on_leader_lost(&c.nodes[1].state); raft_on_leader_lost(&c.nodes[2].state); /* Both have voted_for=-1 now */ TEST_ASSERT_EQUAL_INT(-1, c.nodes[1].state.voted_for); TEST_ASSERT_EQUAL_INT(-1, c.nodes[2].state.voted_for); /* Node 1 fires its delayed timer first (lower priority = smaller delay) */ int rc1 = raft_check_and_start_election(&c.nodes[1].state); TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc1); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, c.nodes[1].state.role); /* Deliver node 1's vote requests to the cluster (node 2 gets one, node 0 is gone) */ /* Only deliver the RPC intended for node 2; skip node 0 (it's gone/isolated) */ raft_sim_partition(&c, 0, 1); /* node 0 is isolated (lost leader) */ raft_sim_deliver_pending(&c); /* Node 2 received node 1's vote request and granted it */ TEST_ASSERT_EQUAL_INT(1, c.nodes[2].state.voted_for); /* Node 2's timer fires: check_and_start sees voted_for=1 != self_idx=2 → skip */ int rc2 = raft_check_and_start_election(&c.nodes[2].state); TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, rc2); /* Exactly one leader (node 1) elected */ TEST_ASSERT_EQUAL_INT(1, c.leader_count); } /* ---------- Group G: Split-Brain Prevention (task 5.4) ---------- */ /* * test_proto_G1_no_split_brain_100_rounds * * Run 100 independent election rounds on a fresh 3-node cluster each time. * Node 0 always starts first. After delivering pending RPCs, leader_count must * equal exactly 1 (no split brain, no starvation). */ static void test_proto_G1_no_split_brain_100_rounds(void) { int total_leaders = 0; for (int round = 0; round < 100; round++) { raft_sim_cluster_t c; memset(&c, 0, sizeof(c)); TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); /* Node 0 always starts first (highest priority, idx=0) */ raft_start_election(&c.nodes[0].state); raft_sim_deliver_pending(&c); /* Each round must produce exactly one leader — no split brain */ TEST_ASSERT_EQUAL_INT(1, c.leader_count); total_leaders += c.leader_count; } /* Across 100 rounds, exactly 100 leaders must have been elected */ TEST_ASSERT_EQUAL_INT(100, total_leaders); } /* * test_proto_G2_partition_minority_no_quorum * * 3-node cluster. Node 2 is isolated (partition of 1). * Nodes 0+1 form the majority partition (size=2 = quorum). * Node 0 starts election in the majority partition → wins (leader_count=1). * Node 2 tries to start election in isolation → RPCs dropped → cannot win. * leader_count remains 1. */ static void test_proto_G2_partition_minority_no_quorum(void) { raft_sim_cluster_t c; memset(&c, 0, sizeof(c)); TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000)); /* Isolate node 2 (minority partition of 1) */ raft_sim_partition(&c, 2, 1); /* Node 0 starts election in the majority partition {0, 1} */ raft_start_election(&c.nodes[0].state); raft_sim_deliver_pending(&c); /* Nodes 0+1 have quorum=2; node 0 must be LEADER */ TEST_ASSERT_EQUAL_INT(1, c.leader_count); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); /* Node 2 attempts to start an election in isolation */ raft_start_election(&c.nodes[2].state); raft_sim_deliver_pending(&c); /* RPCs to/from node 2 are dropped */ /* Node 2 cannot win — still CANDIDATE (no grants received) */ TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, c.nodes[2].state.role); /* leader_count still 1 — no split brain */ TEST_ASSERT_EQUAL_INT(1, c.leader_count); } #endif /* XCE_TESTING */ /* ---------- raft_on_pre_vote_request tests (task 2.1) ---------- */ /* * raft_on_pre_vote_request is the voter-side PreVote decision. It mirrors * raft_on_vote_request's Step 1 (stale-term deny) and Step 5 (on_vote_eligible * gate) but strips ALL persistent mutation: it must never touch term, * voted_for, voted_for_term, role, and must never call on_persist. Freshness is * applied by the dispatcher BEFORE this call (same contract as the real vote). */ /* * test_pre_vote_req_grant_higher_term_eligible * * Node at term=5 (FOLLOWER, no vote), receives PreVote for cand_term=6 with the * eligibility callback granting (return 1). Expected: grant (1), and term / * voted_for / voted_for_term / role all unchanged, on_persist NOT called. */ static void test_pre_vote_req_grant_higher_term_eligible(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; cbs.on_vote_eligible = mock_vote_eligible; reset_mock(); g_mock.vote_eligible_return_value = 1; /* eligibility grants */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_request(&s, 1, 6); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); /* PURITY: nothing persistent may change on a granted PreVote */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); /* eligibility gate was consulted */ TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); } /* * test_pre_vote_req_grant_equal_term_eligible * * Equal-term candidate (cand_term == s->term) must NOT be denied by the * stale-term gate (deny is only for cand_term < term), and is granted when * eligibility allows. Purity invariant still holds. */ static void test_pre_vote_req_grant_equal_term_eligible(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; cbs.on_vote_eligible = mock_vote_eligible; reset_mock(); g_mock.vote_eligible_return_value = 1; TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_request(&s, 1, 5); /* equal term */ TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_pre_vote_req_deny_stale_term * * Node at term=5, receives PreVote for cand_term=4 (< term). Expected: deny (0) * WITHOUT consulting on_vote_eligible, no term-adoption, no persist. */ static void test_pre_vote_req_deny_stale_term(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; cbs.on_vote_eligible = mock_vote_eligible; reset_mock(); g_mock.vote_eligible_return_value = 1; /* would grant if consulted */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_request(&s, 1, 4); /* stale */ TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); /* Stale-term denial short-circuits BEFORE the eligibility gate. */ TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count); /* PURITY: term must NOT be adopted, no persist. */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_pre_vote_req_deny_eligibility_rejects * * Fresh, higher-term candidate but the eligibility callback rejects (returns 0) * — e.g. primary-deny / fresh-heartbeat stickiness / catch-up. Expected: deny, * with the eligibility gate consulted, and the purity invariant preserved. */ static void test_pre_vote_req_deny_eligibility_rejects(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; cbs.on_vote_eligible = mock_vote_eligible; reset_mock(); g_mock.vote_eligible_return_value = 0; /* eligibility denies */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_request(&s, 1, 6); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_pre_vote_req_null_eligible_grants * * When on_vote_eligible is NULL, a fresh higher-or-equal-term candidate is * granted (all Raft-eligible candidates granted), with no mutation/persist. */ static void test_pre_vote_req_null_eligible_grants(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; /* on_vote_eligible deliberately left NULL */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_request(&s, 1, 6); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_pre_vote_req_grant_preserves_existing_vote (Req 2.4) * * A node that has already voted for someone in the current term may still GRANT * a PreVote (PreVote does not consume / change the recorded vote). Confirms the * existing voted_for / voted_for_term are untouched by a granted PreVote. */ static void test_pre_vote_req_grant_preserves_existing_vote(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_trace = mock_trace; cbs.on_vote_eligible = mock_vote_eligible; reset_mock(); g_mock.vote_eligible_return_value = 1; TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); /* Node already cast its real vote for peer 1 in term 5. */ s.voted_for = 1; s.voted_for_term = 5; /* A different candidate (peer 2) probes a PreVote for term 6. */ int rc = raft_on_pre_vote_request(&s, 2, 6); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); /* The recorded real vote must be untouched (no consume, no overwrite). */ TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* ---------- raft_begin_pre_vote tests (task 2.2) ---------- */ /* * raft_begin_pre_vote is the candidate-side PreVote start. It is the pre-image * of raft_start_election: it sends a probe to each peer and tallies a self-grant, * but STRIPS the term++/persist/role-change/timeout. Per Req 1.2/1.3/6.2 it must * NOT modify s->term, s->voted_for, s->voted_for_term, s->role, and must NOT call * on_persist. The prospective term (term+1) is a LOCAL value only. */ /* * test_begin_pre_vote_uses_prospective_term_not_persisted * * 3-node FOLLOWER at term=5. raft_begin_pre_vote must send PreVote requests * carrying prospective_term = term+1 = 6 to each peer, while s->term itself * stays 5 and on_persist never fires. */ static void test_begin_pre_vote_uses_prospective_term_not_persisted(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_vote_node(&s, &cbs, 3, 0, 5)); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); int rc = raft_begin_pre_vote(&s); /* Round started (multi-node, not single) */ TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); /* Each peer probe carried prospective term = term+1 = 6 */ TEST_ASSERT_EQUAL_UINT64(6, g_mock.last_pre_vote_term); /* PURITY: persistent term is unchanged (still 5) and never persisted */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_begin_pre_vote_sends_once_per_non_self_peer * * 3-node FOLLOWER, self=1. The send-pre-vote callback must fire exactly once * for each peer != self (peers 0 and 2), never for self. */ static void test_begin_pre_vote_sends_once_per_non_self_peer(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 1, 5)); int rc = raft_begin_pre_vote(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); /* Exactly n_peers-1 = 2 sends */ TEST_ASSERT_EQUAL_INT(2, g_mock.send_pre_vote_request_count); /* Self (idx 1) must NOT be probed; peers 0 and 2 must each be probed once. */ TEST_ASSERT_EQUAL_UINT64(0, g_mock.pre_vote_sent_term[1]); /* self untouched */ TEST_ASSERT_EQUAL_UINT64(6, g_mock.pre_vote_sent_term[0]); TEST_ASSERT_EQUAL_UINT64(6, g_mock.pre_vote_sent_term[2]); /* None of the ordered sent peers is self_idx=1 */ for (int i = 0; i < g_mock.send_pre_vote_request_count; i++) TEST_ASSERT_NOT_EQUAL(1, g_mock.pre_vote_sent_peers[i]); } /* * test_begin_pre_vote_self_grant_and_in_progress * * After begin, the PreVote tally must show in_progress=1 and a single self-grant * (pre_votes_granted=1). No real election timeout is scheduled here (the * controller arms the round timeout separately). */ static void test_begin_pre_vote_self_grant_and_in_progress(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_begin_pre_vote(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self-grant only */ /* No election-timeout scheduled by begin_pre_vote (controller owns it). */ TEST_ASSERT_EQUAL_INT(0, g_mock.schedule_timeout_count); } /* * test_begin_pre_vote_purity_no_persistent_mutation * * Byte-for-byte: term, voted_for, voted_for_term, role unchanged; no persist, * no role transition to CANDIDATE, no become_leader/become_follower. */ static void test_begin_pre_vote_purity_no_persistent_mutation(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); /* Node already cast a real vote for peer 2 in term 5 — must survive. */ s.voted_for = 2; s.voted_for_term = 5; int rc = raft_begin_pre_vote(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); /* PURITY: every persistent field untouched */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(2, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* stays FOLLOWER, not CANDIDATE */ /* No persistence / no leadership-transition side effects */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_begin_pre_vote_single_node_wins * * 1-node cluster: no peers, the self-grant is already a majority, so the round * is won immediately. Returns RAFT_PRE_VOTE_WON, sends nothing, and still * mutates no persistent state. */ static void test_begin_pre_vote_single_node_wins(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 1, 0, 5)); int rc = raft_begin_pre_vote(&s); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc); /* No peer probes for a single-node cluster */ TEST_ASSERT_EQUAL_INT(0, g_mock.send_pre_vote_request_count); /* Self-grant recorded as a majority */ TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* PURITY even on the WON short-circuit */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); } /* * test_begin_pre_vote_null_send_callback_suppressed * * If on_send_pre_vote_request is NULL, begin must still run (the caller owns * transport) without crashing, recording the self-grant, and emitting no sends. */ static void test_begin_pre_vote_null_send_callback_suppressed(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); cbs.on_send_pre_vote_request = NULL; /* suppressed */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); int rc = raft_begin_pre_vote(&s); TEST_ASSERT_EQUAL_INT(RAFT_OK, rc); TEST_ASSERT_EQUAL_INT(0, g_mock.send_pre_vote_request_count); TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); TEST_ASSERT_EQUAL_UINT64(5, s.term); } /* ---------- raft_on_pre_vote_reply tally (task 2.3) ---------- */ /* * Helper: arm a pre-vote round on an n-node cluster at the given term. * Mirrors what raft_begin_pre_vote sets up (self-grant, in-progress), so the * tally tests can feed replies without depending on the send transport. */ static int arm_pre_vote_round(raft_state_t *s, raft_callbacks_t *cbs, int n_peers, int self_idx, uint64_t term) { int rc = init_vote_node(s, cbs, n_peers, self_idx, term); if (rc != 0) return rc; /* Suppress sends; we drive replies directly. */ raft_begin_pre_vote(s); return 0; } /* * test_pre_vote_reply_won_at_quorum * * 3-node cluster (quorum=2), self-grant already counted. One peer grants → * the round is WON. Tally lives on the raft state; in-progress is cleared. */ static void test_pre_vote_reply_won_at_quorum(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); /* begin sent to peers; clear send counter so it does not confuse asserts. */ int rc = raft_on_pre_vote_reply(&s, 1, 1 /* granted */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc); TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* self + peer 1 */ TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress); /* round closed */ /* CRITICAL: a win must NEVER fire leadership callbacks. */ TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_pre_vote_reply_lost_when_all_reply_no_quorum * * 3-node cluster. Both peers deny. After all non-self peers reply without * quorum, the round is LOST. */ static void test_pre_vote_reply_lost_when_all_reply_no_quorum(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); int rc1 = raft_on_pre_vote_reply(&s, 1, 0 /* deny */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc1); /* one peer still out */ int rc2 = raft_on_pre_vote_reply(&s, 2, 0 /* deny */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_LOST, rc2); /* all replied, no quorum */ TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only */ TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_pre_vote_reply_pending_when_undecided * * 3-node cluster. One peer denies, the other has not replied yet → PENDING * (quorum still reachable). */ static void test_pre_vote_reply_pending_when_undecided(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); int rc = raft_on_pre_vote_reply(&s, 1, 0 /* deny */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc); TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only */ TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); /* still open */ } /* * test_pre_vote_reply_duplicate_ignored * * A duplicate reply from the same peer must not double-count. A grant from * peer 1, then a second grant from peer 1, leaves the tally at 2 (self + one * grant). In a 5-node cluster (quorum=3) that keeps the round PENDING. */ static void test_pre_vote_reply_duplicate_ignored(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 5, 0, 5)); int rc1 = raft_on_pre_vote_reply(&s, 1, 1 /* grant */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc1); TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* self + peer 1 */ /* Duplicate grant from peer 1 — must be ignored. */ int rc2 = raft_on_pre_vote_reply(&s, 1, 1 /* grant again */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc2); TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* unchanged: no double-count */ TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); } /* * test_pre_vote_reply_won_purity_state_unchanged * * On WON: term, voted_for, voted_for_term, role are all unchanged and no * persist / leadership callback fires. */ static void test_pre_vote_reply_won_purity_state_unchanged(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); /* A real vote cast earlier in this term must survive the tally. */ s.voted_for = 2; s.voted_for_term = 5; int rc = raft_on_pre_vote_reply(&s, 1, 1 /* grant */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc); TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(2, s.voted_for); TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_pre_vote_reply_lost_purity_state_unchanged * * On LOST: term / voted_for / role unchanged; no leadership callback. */ static void test_pre_vote_reply_lost_purity_state_unchanged(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 7)); s.voted_for = 1; s.voted_for_term = 7; raft_on_pre_vote_reply(&s, 1, 0 /* deny */); int rc = raft_on_pre_vote_reply(&s, 2, 0 /* deny */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_LOST, rc); TEST_ASSERT_EQUAL_UINT64(7, s.term); TEST_ASSERT_EQUAL_INT(1, s.voted_for); TEST_ASSERT_EQUAL_UINT64(7, s.voted_for_term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_pre_vote_reply_out_of_range_peer_ignored * * A peer_idx outside [0, n_peers) (or >= RAFT_MAX_PEERS) is ignored: no tally * change, round stays in progress, returns PENDING. */ static void test_pre_vote_reply_out_of_range_peer_ignored(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); int rc_neg = raft_on_pre_vote_reply(&s, -1, 1 /* grant */); int rc_over = raft_on_pre_vote_reply(&s, 3, 1 /* grant */); /* == n_peers */ int rc_max = raft_on_pre_vote_reply(&s, RAFT_MAX_PEERS, 1); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_neg); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_over); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_max); TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only, no spurious count */ TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); } /* * test_pre_vote_reply_5node_won_at_three * * 5-node cluster (quorum=3): self + two granting peers → WON. */ static void test_pre_vote_reply_5node_won_at_three(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 5, 0, 5)); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, raft_on_pre_vote_reply(&s, 1, 1)); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, raft_on_pre_vote_reply(&s, 2, 1)); TEST_ASSERT_EQUAL_INT(3, s.pre_votes_granted); TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* ====================================================================== * * Task 6.1 — Raft safety-invariant assertions for the PreVote API * * The per-function tests above (tasks 2.1/2.2/2.3) check individual cases. * The tests below assert the SAFETY INVARIANTS as *properties* that hold * across the ENTIRE PreVote API, mechanically backing the RN-1 TLA+/ * verification-checklist consistency argument recorded in * .kiro/specs/ha-prevote/research.md (§"RN-1 resolved"). * * ---------------------------------------------------------------------- * * RN-1 CONSISTENCY ARGUMENT (mechanically evidenced below) * ---------------------------------------------------------------------- * * Claim: adding the PreVote phase preserves every Raft safety invariant * the repository's `raft/raft.tla` reference and the controller * verification checklist require — Election Safety (at most one leader per * term), Leader Completeness, term monotonicity, and a monotonic commit * index — so the system stays consistent after the change. * * Proof structure (purity ⇒ no new transitions): * * 1. The three PreVote functions — raft_begin_pre_vote, * raft_on_pre_vote_request, raft_on_pre_vote_reply — are PURE with * respect to the persistent Raft state. Across an arbitrary sequence of * begin / request / reply calls (mixed grant+deny, duplicates, * out-of-range peers, and both WON and LOST terminal outcomes), none of * them ever: * - changes s->term, s->voted_for, s->voted_for_term, or s->role, or * - invokes on_persist, or * - invokes on_become_leader / on_become_follower. * (Asserted by test_invariant_prevote_sequence_pure_no_persist and * test_invariant_prevote_won_grants_no_leadership.) * * 2. Because the PreVote path mutates no term, it cannot violate term * monotonicity: the only writer of s->term remains raft_start_election * (real term bump) and raft_on_vote_request Step 2 (adopt a strictly * higher term) — both UNCHANGED by this feature. A PreVote can only * *gate* whether raft_start_election is later called; it never advances * a term itself. (Asserted by the term-constant checks below.) * * 3. Because the PreVote path creates no leader (no on_become_leader, no * role := RAFT_LEADER), it adds no new way to satisfy the antecedent of * Election Safety. The sole leadership-creating transitions remain * raft_start_election's single-node short-circuit and * raft_on_vote_reply's quorum branch — again UNCHANGED. A PreVote round * reaching RAFT_PRE_VOTE_WON returns a STATUS only; the node stays * RAFT_FOLLOWER until the separate, unchanged real-election path runs. * (Asserted by test_invariant_prevote_won_grants_no_leadership.) * * 4. The legitimate higher-term-adopt failover path (the bug-339/bug-340 * fix) lives entirely in raft_on_vote_request / raft_on_vote_reply, * which this feature does not touch. PreVote sits strictly *before* that * path and, by (1), leaves its inputs (term/voted_for/role) untouched. * test_invariant_real_vote_higher_term_adopt_unchanged demonstrates the * real-vote path still adopts a higher term and fires become_follower / * become_leader exactly as before, even after a full PreVote round has * run on the same node — so PreVote did not alter the legitimate * failover semantics bug-339/bug-340 depend on. * * Conclusion: PreVote introduces no transition that writes a term or * creates a leader. Every term-writing / leadership-creating transition in * the model is byte-for-byte the pre-PreVote transition. Therefore the set * of reachable (term, leader) configurations is unchanged, and the TLA+ * invariants (Election Safety, Leader Completeness, term monotonicity, * monotonic commit index) and the controller verification checklist * continue to hold. The tests below are the mechanical evidence for * premises (1)–(4). * ====================================================================== */ /* * test_invariant_prevote_sequence_pure_no_persist (Req 6.2, 2.3, 1.2) * * PROPERTY (term monotonicity / no persistent mutation): drive a long, * mixed sequence of PreVote operations through a node and assert that across * the WHOLE sequence the persistent Raft state (term, voted_for, * voted_for_term, role) never changes and on_persist is NEVER called. * * The sequence deliberately exercises: begin → answer an incoming request * (grant) → answer a stale-term request (deny) → reply tally with a grant, * a duplicate of that grant, an out-of-range peer, and a deny — i.e. every * PreVote entry point and both grant/deny outcomes, including a WON * terminal. If ANY PreVote code path leaked a term/vote/role write or a * persist, this property fails — which would be a real Election-Safety / * term-monotonicity finding, not a test bug. */ static void test_invariant_prevote_sequence_pure_no_persist(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); cbs.on_vote_eligible = mock_vote_eligible; cbs.on_trace = mock_trace; reset_mock(); g_mock.vote_eligible_return_value = 1; /* grant when consulted */ /* 3-node FOLLOWER at term=5, having voted for peer 1 in an earlier term. * These persistent values are the canary: nothing below may change them. */ TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.voted_for = 1; s.voted_for_term = 4; const uint64_t term0 = s.term; const int voted_for0 = s.voted_for; const uint64_t voted_for_term0 = s.voted_for_term; const raft_role_t role0 = s.role; /* (a) begin a round — self-grant + prospective term, no persist */ TEST_ASSERT_EQUAL_INT(0, raft_begin_pre_vote(&s)); /* (b) answer an incoming PreVote request that PASSES eligibility — grant */ TEST_ASSERT_EQUAL_INT(1, raft_on_pre_vote_request(&s, 2, 6 /* higher */)); /* (c) answer a STALE-term incoming request — deny */ TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 3 /* stale */)); /* (d) answer an incoming request the eligibility gate REJECTS — deny */ g_mock.vote_eligible_return_value = 0; TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 6)); g_mock.vote_eligible_return_value = 1; /* (e) tally replies: a grant (drives to WON quorum on 3-node), then a * duplicate of the same grant, an out-of-range peer, and a deny. */ TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, raft_on_pre_vote_reply(&s, 1, 1 /* grant */)); /* round closed after WON; further replies are no-ops on persistent state */ (void)raft_on_pre_vote_reply(&s, 1, 1 /* duplicate grant */); (void)raft_on_pre_vote_reply(&s, 99, 1 /* out-of-range */); (void)raft_on_pre_vote_reply(&s, 2, 0 /* late deny */); /* INVARIANT: persistent Raft state byte-for-byte unchanged. */ TEST_ASSERT_EQUAL_UINT64(term0, s.term); TEST_ASSERT_EQUAL_INT(voted_for0, s.voted_for); TEST_ASSERT_EQUAL_UINT64(voted_for_term0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(role0, s.role); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* never promoted */ /* INVARIANT: PreVote never persists and never fires leadership callbacks. */ TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_invariant_prevote_lost_sequence_pure_no_persist (Req 6.2, 1.4) * * Same purity property as above, but driving the round to the LOST terminal * outcome (all peers deny). A WON path and a LOST path are the two ways a * round can end; both must be side-effect-free. This guards the case where * a frozen-then-resumed standby gets no PreVote majority (Req 4) — the round * must leave term/vote/role untouched so the node simply re-attaches. */ static void test_invariant_prevote_lost_sequence_pure_no_persist(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_vote_node(&s, &cbs, 3, 0, 9)); s.voted_for = 0; s.voted_for_term = 9; const uint64_t term0 = s.term; const int voted_for0 = s.voted_for; const uint64_t voted_for_term0 = s.voted_for_term; const raft_role_t role0 = s.role; TEST_ASSERT_EQUAL_INT(0, raft_begin_pre_vote(&s)); /* both peers deny → LOST, no majority */ TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, raft_on_pre_vote_reply(&s, 1, 0)); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_LOST, raft_on_pre_vote_reply(&s, 2, 0)); /* INVARIANT: persistent state unchanged across a LOST round. */ TEST_ASSERT_EQUAL_UINT64(term0, s.term); TEST_ASSERT_EQUAL_INT(voted_for0, s.voted_for); TEST_ASSERT_EQUAL_UINT64(voted_for_term0, s.voted_for_term); TEST_ASSERT_EQUAL_INT(role0, s.role); TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count); TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count); } /* * test_invariant_prevote_won_grants_no_leadership (Req 6.1) * * ELECTION SAFETY property: a PreVote round that reaches RAFT_PRE_VOTE_WON * grants NO leadership. The node must remain RAFT_FOLLOWER and no * on_become_leader / on_become_follower callback may fire — a PreVote WON is * a STATUS the controller acts on, not a leadership transition. Only the * separate, unchanged raft_start_election path creates a leader. * * The test then demonstrates the contrast: invoking the real * raft_start_election (the only leadership-creating path) from the same * still-FOLLOWER node bumps the term and self-votes — proving the leader is * created exclusively by the real path, never by the PreVote WON itself. */ static void test_invariant_prevote_won_grants_no_leadership(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); reset_mock(); /* arm a round on a 3-node FOLLOWER at term=5 (self-grant in progress) */ TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5)); const uint64_t term_before_won = s.term; /* one peer grant → WON on a 3-node cluster (quorum=2) */ int rc = raft_on_pre_vote_reply(&s, 1, 1 /* grant */); TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc); /* INVARIANT (Election Safety): WON created NO leader. */ TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* still a follower */ TEST_ASSERT_EQUAL_UINT64(term_before_won, s.term); /* term not advanced */ 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.persist_count); /* CONTRAST: leadership is created ONLY by the unchanged real path. * Starting the real election now bumps the term + records the self-vote * (this is the sole term-writing / leadership-creating transition). */ reset_mock(); TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s)); TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role); TEST_ASSERT_EQUAL_UINT64(term_before_won + 1, s.term); /* real bump here */ TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); /* persisted here */ } /* * test_invariant_real_vote_higher_term_adopt_unchanged (Req 5.2, 5.3) * * REAL-VOTE-PATH-UNCHANGED property: PreVote must not have altered the * legitimate higher-term-adopt failover path that the bug-339/bug-340 fixes * depend on. After running a full PreVote round on a node, the real * raft_on_vote_request still adopts a strictly higher term and steps a * leader down (fires on_become_follower), and raft_on_vote_reply still wins * at quorum (fires on_become_leader) — exactly as before PreVote existed. * * This is the library-level guard for bug-339 (higher-term adopt on * RDHUP failover) and bug-340 (rejoin), confirming PreVote sits beside the * real-vote path without modifying it. */ static void test_invariant_real_vote_higher_term_adopt_unchanged(void) { raft_state_t s; raft_callbacks_t cbs = make_mock_cbs(); uint64_t reply_term = 0; cbs.on_trace = mock_trace; /* --- Part 1: higher-term real RequestVote adopts term + steps down a * leader, even after a PreVote round ran on this node. --- */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 2)); /* Run a (lost) PreVote round first to prove it leaves the real-vote * adopt path untouched. */ TEST_ASSERT_EQUAL_INT(0, raft_begin_pre_vote(&s)); (void)raft_on_pre_vote_reply(&s, 1, 0); (void)raft_on_pre_vote_reply(&s, 2, 0); TEST_ASSERT_EQUAL_UINT64(2, s.term); /* PreVote did not move the term */ /* Now this node is the leader in term 2 and a real higher-term vote * request arrives — the unchanged Step 2 must adopt term 5 and step down. */ s.role = RAFT_LEADER; s.voted_for = 0; s.voted_for_term = 2; reset_mock(); int rc = raft_on_vote_request(&s, 1, 5, &reply_term); /* Higher-term adopt + leader step-down, exactly as bug-339/340 require. */ TEST_ASSERT_EQUAL_UINT64(5, s.term); TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_UINT64(5, reply_term); TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count); /* stepped down */ TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); /* then grants */ /* --- Part 2: real raft_on_vote_reply still elects a leader at quorum, * unaffected by PreVote. --- */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0)); s.role = RAFT_CANDIDATE; s.term = 1; s.votes_granted = 1; /* self-vote */ s.replies_received = 0; s.election_decided = 0; int rcr = raft_on_vote_reply(&s, 1, 1 /* grant */, 1); TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rcr); TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role); TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count); /* real path elects */ } /* ---------- 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_null_persist_entry_returns_einval); RUN_TEST(test_raft_init_null_get_entry_term_returns_einval); RUN_TEST(test_raft_init_null_send_append_entries_returns_einval); RUN_TEST(test_raft_init_null_truncate_from_returns_einval); RUN_TEST(test_raft_init_log_callbacks_present_succeeds); RUN_TEST(test_raft_init_even_peers_returns_einval); RUN_TEST(test_raft_init_oversize_peers_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); RUN_TEST(test_leader_progress_init_single_node); RUN_TEST(test_leader_progress_init_multi_node); /* 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); /* raft_on_pre_vote_request (task 2.1) */ RUN_TEST(test_pre_vote_req_grant_higher_term_eligible); RUN_TEST(test_pre_vote_req_grant_equal_term_eligible); RUN_TEST(test_pre_vote_req_deny_stale_term); RUN_TEST(test_pre_vote_req_deny_eligibility_rejects); RUN_TEST(test_pre_vote_req_null_eligible_grants); RUN_TEST(test_pre_vote_req_grant_preserves_existing_vote); /* raft_begin_pre_vote (task 2.2) */ RUN_TEST(test_begin_pre_vote_uses_prospective_term_not_persisted); RUN_TEST(test_begin_pre_vote_sends_once_per_non_self_peer); RUN_TEST(test_begin_pre_vote_self_grant_and_in_progress); RUN_TEST(test_begin_pre_vote_purity_no_persistent_mutation); RUN_TEST(test_begin_pre_vote_single_node_wins); RUN_TEST(test_begin_pre_vote_null_send_callback_suppressed); /* raft_on_pre_vote_reply tally (task 2.3) */ RUN_TEST(test_pre_vote_reply_won_at_quorum); RUN_TEST(test_pre_vote_reply_lost_when_all_reply_no_quorum); RUN_TEST(test_pre_vote_reply_pending_when_undecided); RUN_TEST(test_pre_vote_reply_duplicate_ignored); RUN_TEST(test_pre_vote_reply_won_purity_state_unchanged); RUN_TEST(test_pre_vote_reply_lost_purity_state_unchanged); RUN_TEST(test_pre_vote_reply_out_of_range_peer_ignored); RUN_TEST(test_pre_vote_reply_5node_won_at_three); /* Raft safety-invariant assertions for the PreVote API (task 6.1, RN-1) */ RUN_TEST(test_invariant_prevote_sequence_pure_no_persist); RUN_TEST(test_invariant_prevote_lost_sequence_pure_no_persist); RUN_TEST(test_invariant_prevote_won_grants_no_leadership); RUN_TEST(test_invariant_real_vote_higher_term_adopt_unchanged); /* 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_H6_restored_vote_blocks_second_vote); RUN_TEST(test_proto_Hnull_required_callback_einval); /* Group D: Quorum and Result (task 5.3) */ RUN_TEST(test_proto_D1_quorum_1grant_of_2_needed); RUN_TEST(test_proto_D2_both_peers_deny_becomes_follower); RUN_TEST(test_proto_D3_second_vote_reply_noop); RUN_TEST(test_proto_D4_reply_higher_term_steps_down); /* Group E: Election Timeout (task 5.3) */ RUN_TEST(test_proto_E1_timeout_before_quorum); RUN_TEST(test_proto_E2_timeout_after_quorum_noop); RUN_TEST(test_proto_E3_new_election_after_timeout); /* Group F: RDHUP Failover (task 5.4) */ RUN_TEST(test_proto_F1_leader_lost_priority_delay); RUN_TEST(test_proto_F2_voted_during_delay_skip); RUN_TEST(test_proto_F3_no_vote_during_delay_starts_election); RUN_TEST(test_proto_F4_simultaneous_rdhup_one_leader); /* Group G: Split-Brain Prevention (task 5.4) */ RUN_TEST(test_proto_G1_no_split_brain_100_rounds); RUN_TEST(test_proto_G2_partition_minority_no_quorum); #endif }