feat(raft-log-replication): raft_on_append_entries_request UpdateTerm+logOk+accept (tasks 5.1-5.2, 6.1-6.3)
Complete HandleAppendEntriesRequest (raft.tla:327) + UpdateTerm (raft.tla:406), implemented as one function to avoid a misleading partial handler. Fixed step order: UpdateTerm FIRST on every RPC incl. heartbeat (G5, strictly-greater, clears voted_for, persists, leader step-down fires on_become_follower) -> stale reject -> candidate step-down -> logOk (G2) -> reject if !logOk -> accept (already-have / conflict-truncate-then-repersist (G3) / append). Never advances commit, never does transport I/O. Review-applied: logOk now includes the explicit prev_log_index <= log_len bound (raft.tla:330 / Req 4.1) instead of relying solely on the 0-absent sentinel; added a candidate-equal-term-logOk-fails step-ordering test. 15 tests, ASAN green.
This commit is contained in:
parent
a1aacc0389
commit
3c5185a5c3
3 changed files with 756 additions and 5 deletions
|
|
@ -81,14 +81,14 @@
|
|||
|
||||
## 5. UpdateTerm-on-AppendEntries (raft.tla:406, closes G5)
|
||||
|
||||
- [ ] 5.1 RED: higher-term AE/heartbeat adopts term before logOk
|
||||
- [x] 5.1 RED: higher-term AE/heartbeat adopts term before logOk
|
||||
- Test: a follower and a leader at term T receiving an AE (and separately a 0-entry heartbeat) at term T+1 adopt T+1, clear `voted_for`, call `on_persist(T+1,-1)`, and (was-leader case) fire `on_become_follower`; assert this happens even when the local log would fail `logOk`
|
||||
- Observable completion: tests compile and FAIL
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
- _Boundary: test_raft.c_
|
||||
- _Depends: 1.3_
|
||||
|
||||
- [ ] 5.2 GREEN: term-adoption block at the top of the request handler
|
||||
- [x] 5.2 GREEN: term-adoption block at the top of the request handler
|
||||
- Implement the `UpdateTerm` block as the FIRST step of `raft_on_append_entries_request`, before `logOk`; persist-before-process; fire `on_become_follower` if was leader; no change on equal/lower term
|
||||
- Observable completion: 5.1 tests pass; the block is unmistakably before `logOk` in `raft.c`
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
|
|
@ -97,21 +97,21 @@
|
|||
|
||||
## 6. HandleAppendEntriesRequest — logOk / reject / step-down / accept (raft.tla:327, closes G2/G3)
|
||||
|
||||
- [ ] 6.1 RED: logOk + reject + candidate step-down tests
|
||||
- [x] 6.1 RED: logOk + reject + candidate step-down tests
|
||||
- Test: `logOk` true when `prevLogIndex==0` or `on_get_entry_term(prevLogIndex)==prevLogTerm`; reject (success=0, matchIndex=0) on stale term and on `follower && !logOk`; candidate at equal term transitions to follower
|
||||
- Observable completion: tests compile and FAIL
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
- _Boundary: test_raft.c_
|
||||
- _Depends: 5.2_
|
||||
|
||||
- [ ] 6.2 RED: accept branch tests (already-have / conflict-truncate / append)
|
||||
- [x] 6.2 RED: accept branch tests (already-have / conflict-truncate / append)
|
||||
- Test: already-have (empty entries or matching term) → success, no log rewrite, `matchIndex = prevLogIndex + entries_len`; conflict (`local[index].term != entry.term`) → `on_truncate_from(index)` then `on_persist_entry(index, entry)`, success `matchIndex=index`; append (`log_len == prevLogIndex`) → `on_persist_entry(index)`, success
|
||||
- Observable completion: tests compile and FAIL
|
||||
- _Requirements: 4.4, 4.5, 4.6, 4.7_
|
||||
- _Boundary: test_raft.c_
|
||||
- _Depends: 6.1_
|
||||
|
||||
- [ ] 6.3 GREEN: implement `raft_on_append_entries_request` (logOk + all branches)
|
||||
- [x] 6.3 GREEN: implement `raft_on_append_entries_request` (logOk + all branches)
|
||||
- Implement (after the 5.2 UpdateTerm block): `logOk` via `on_get_entry_term`; reject/candidate-step-down/accept; the three accept sub-cases exactly per raft.tla:351-388; fill `raft_append_result_t`; never advance commit (`leader_commit` ignored); never perform transport I/O
|
||||
- Observable completion: 6.1 and 6.2 tests pass; `on_truncate_from` is always followed by re-persist of the leader entry on the conflict branch
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7_
|
||||
|
|
|
|||
|
|
@ -541,3 +541,139 @@ int raft_send_append_entries(raft_state_t *s, int peer_idx,
|
|||
|
||||
return RAFT_OK;
|
||||
}
|
||||
|
||||
/* Fill *out as a rejection (success=0, match_index=0) at the current term and
|
||||
* return RAFT_OK. Every reject branch shares this shape (raft.tla reject, 333). */
|
||||
static int raft_ae_reject(raft_state_t *s, raft_append_result_t *out,
|
||||
const char *trace_event)
|
||||
{
|
||||
out->reply_term = s->term;
|
||||
out->success = 0;
|
||||
out->match_index = 0;
|
||||
if (trace_event && s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, trace_event, NULL);
|
||||
return RAFT_OK;
|
||||
}
|
||||
|
||||
int raft_on_append_entries_request(raft_state_t *s, int leader_idx,
|
||||
uint64_t term, uint64_t prev_log_index,
|
||||
uint64_t prev_log_term, uint64_t leader_commit,
|
||||
const void *entry, size_t entry_len,
|
||||
uint64_t entry_term, int entries_len,
|
||||
raft_append_result_t *out)
|
||||
{
|
||||
/* leader_commit is accepted into the contract but NOT acted on in this spec
|
||||
* — commit-index advance is owned by raft-commit-safety (Req 3.4 / 5.4). */
|
||||
(void)leader_commit;
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "ae_recv", NULL);
|
||||
|
||||
/* ---- Step 1: UpdateTerm FIRST (raft.tla 406, Req 6.1–6.5 — closes G5) ----
|
||||
* Applied on EVERY incoming AppendEntries/heartbeat, BEFORE logOk, so a
|
||||
* strictly-higher-term RPC is never spuriously rejected for a log mismatch.
|
||||
* Strictly-greater-than only: equal/lower term never alters term/voted_for. */
|
||||
if (term > s->term) {
|
||||
int was_leader = (s->role == RAFT_LEADER);
|
||||
s->term = term;
|
||||
s->voted_for = -1;
|
||||
s->voted_for_term = 0;
|
||||
s->role = RAFT_FOLLOWER;
|
||||
/* Persist the adopted (term, voted_for) before processing the rest of
|
||||
* the RPC (durability ordering, Req 6.2). */
|
||||
s->cbs->on_persist(s->ctx, s->term, -1);
|
||||
/* A node that was leader must notify the HA FSM so it steps down
|
||||
* (Req 6.3), mirroring the vote-path higher-term step-down. */
|
||||
if (was_leader)
|
||||
s->cbs->on_become_follower(s->ctx, s->term, leader_idx);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "term_adopt_ae", NULL);
|
||||
}
|
||||
|
||||
/* ---- Step 2: stale-term reject (raft.tla 333) ----
|
||||
* After UpdateTerm, term < s->term can only happen for a strictly-lower
|
||||
* incoming term (the higher case adopted above). */
|
||||
if (term < s->term)
|
||||
return raft_ae_reject(s, out, "ae_reject_stale");
|
||||
|
||||
/* ---- Step 3: candidate step-down at equal term (raft.tla 346) ----
|
||||
* A candidate that sees an AppendEntries at its own term concedes that a
|
||||
* leader already exists for this term and returns to follower. */
|
||||
if (s->role == RAFT_CANDIDATE)
|
||||
s->role = RAFT_FOLLOWER;
|
||||
|
||||
/* ---- Step 4: logOk (raft.tla 328, Req 4.1 — closes G2) ----
|
||||
* logOk = prevLogIndex == 0
|
||||
* OR (prevLogIndex <= log_len AND on_get_entry_term(prevLogIndex) == prevLogTerm).
|
||||
* The explicit prevLogIndex <= log_len bound mirrors raft.tla:330
|
||||
* (prevLogIndex <= Len(log)). The on_get_entry_term 0 sentinel already
|
||||
* rejects absent entries for any non-zero prevLogTerm, but the bound also
|
||||
* defends against a malformed sender presenting prevLogTerm==0 with
|
||||
* prevLogIndex past the local log (0==0 would otherwise spuriously pass). */
|
||||
int log_ok = (prev_log_index == 0) ||
|
||||
(prev_log_index <= s->log_len &&
|
||||
s->cbs->on_get_entry_term(s->ctx, prev_log_index) == prev_log_term);
|
||||
|
||||
/* ---- Step 5: reject if !logOk (follower path, raft.tla 333, Req 4.2) ---- */
|
||||
if (!log_ok)
|
||||
return raft_ae_reject(s, out, "ae_reject_logmismatch");
|
||||
|
||||
/* ---- Step 6: accept (raft.tla 351), index = prevLogIndex + 1 ----
|
||||
* match_index reported on success is prevLogIndex + entries_len for every
|
||||
* accept sub-case (already-have / conflict-truncate / append). */
|
||||
uint64_t index = prev_log_index + 1;
|
||||
uint64_t match_index = prev_log_index + (uint64_t)entries_len;
|
||||
|
||||
/* already-have: a heartbeat (no entries), or the local entry at `index`
|
||||
* already carries the incoming entry's term → success, no log rewrite. */
|
||||
if (entries_len == 0 ||
|
||||
s->cbs->on_get_entry_term(s->ctx, index) == entry_term) {
|
||||
out->reply_term = s->term;
|
||||
out->success = 1;
|
||||
out->match_index = match_index;
|
||||
/* must not shrink the log (already-have keeps the existing suffix) */
|
||||
if (s->log_len < match_index)
|
||||
s->log_len = match_index;
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "ae_already_have", NULL);
|
||||
return RAFT_OK;
|
||||
}
|
||||
|
||||
/* conflict-truncate (raft.tla 375, Req 4.5 — closes G3): a local entry at
|
||||
* `index` exists with a DIFFERENT term. Delete the divergent suffix, then
|
||||
* (intentional safe collapse) re-persist the leader's entry in the same
|
||||
* handler invocation. The truncate is ALWAYS followed by the re-persist. */
|
||||
if (s->cbs->on_get_entry_term(s->ctx, index) != 0) {
|
||||
int rc = s->cbs->on_truncate_from(s->ctx, index);
|
||||
if (rc != 0)
|
||||
/* truncate failed → report a non-success so the leader retries; the
|
||||
* follower's log is not corrupted (it simply did not converge). */
|
||||
return raft_ae_reject(s, out, "ae_conflict_truncate_fail");
|
||||
rc = s->cbs->on_persist_entry(s->ctx, index, entry_term, entry, entry_len);
|
||||
if (rc != 0)
|
||||
return raft_ae_reject(s, out, "ae_conflict_persist_fail");
|
||||
out->reply_term = s->term;
|
||||
out->success = 1;
|
||||
out->match_index = match_index;
|
||||
s->log_len = match_index;
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "ae_conflict_truncate", NULL);
|
||||
return RAFT_OK;
|
||||
}
|
||||
|
||||
/* append (raft.tla 383, Req 4.6): the local log ends exactly at
|
||||
* prevLogIndex (no entry at `index`) → append the incoming entry. */
|
||||
{
|
||||
int rc = s->cbs->on_persist_entry(s->ctx, index, entry_term, entry, entry_len);
|
||||
if (rc != 0)
|
||||
return raft_ae_reject(s, out, "ae_append_persist_fail");
|
||||
out->reply_term = s->term;
|
||||
out->success = 1;
|
||||
out->match_index = match_index;
|
||||
if (s->log_len < match_index)
|
||||
s->log_len = match_index;
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "ae_append", NULL);
|
||||
return RAFT_OK;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,6 +1891,603 @@ static void test_send_ae_non_leader_emits_nothing(void)
|
|||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
}
|
||||
|
||||
/* ---------- raft_on_append_entries_request tests
|
||||
* (tasks 5.1+5.2 UpdateTerm-first / 6.1 logOk+reject+candidate-step-down /
|
||||
* 6.2+6.3 accept already-have/conflict-truncate/append;
|
||||
* raft.tla HandleAppendEntriesRequest 327 + UpdateTerm 406) ----------
|
||||
*
|
||||
* Reuses the existing spy_persist_entry/g_pe_spy, spy_get_entry_term/g_gt_spy,
|
||||
* and mock_persist/mock_become_follower (g_mock). Adds a truncate spy.
|
||||
*/
|
||||
|
||||
/* Spy on_truncate_from: records each call's index and a call count. */
|
||||
typedef struct {
|
||||
int call_count;
|
||||
uint64_t last_index;
|
||||
} truncate_spy_t;
|
||||
|
||||
static truncate_spy_t g_tr_spy;
|
||||
|
||||
static void reset_tr_spy(void)
|
||||
{
|
||||
memset(&g_tr_spy, 0, sizeof(g_tr_spy));
|
||||
}
|
||||
|
||||
static int spy_truncate_from(void *ctx, uint64_t index)
|
||||
{
|
||||
(void)ctx;
|
||||
g_tr_spy.call_count++;
|
||||
g_tr_spy.last_index = index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build callbacks wired to ALL the log-replication spies + g_mock for the
|
||||
* shared election-side callbacks (on_persist, on_become_follower). This lets
|
||||
* the AppendEntries-request tests observe term adoption (mock_persist /
|
||||
* mock_become_follower) and log mutation (pe/gt/tr spies) together. */
|
||||
static raft_callbacks_t make_ae_req_cbs(void)
|
||||
{
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
cbs.on_persist_entry = spy_persist_entry;
|
||||
cbs.on_get_entry_term = spy_get_entry_term;
|
||||
cbs.on_truncate_from = spy_truncate_from;
|
||||
return cbs;
|
||||
}
|
||||
|
||||
static void reset_ae_req_spies(void)
|
||||
{
|
||||
reset_mock();
|
||||
reset_pe_spy();
|
||||
reset_gt_spy();
|
||||
reset_tr_spy();
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_follower_adopts_higher_term (task 5.1/5.2, Req 6.1, 6.2, 6.5 — G5)
|
||||
*
|
||||
* A FOLLOWER at term T receiving an AppendEntries at term T+1 must adopt T+1,
|
||||
* clear voted_for (=-1), and persist (T+1, -1) via on_persist BEFORE the rest of
|
||||
* the RPC — even when the local log would FAIL logOk (prev_log_index/term that
|
||||
* mismatch). Term adoption happens regardless of logOk. reply_term carries the
|
||||
* adopted term. (on_become_follower NOT required for a non-leader adopter.)
|
||||
*/
|
||||
static void test_ae_req_follower_adopts_higher_term(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
/* restored log_len = 2; node starts FOLLOWER at term 5, voted_for=1 */
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 2));
|
||||
s.term = 5;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
s.voted_for = 1;
|
||||
s.voted_for_term = 5;
|
||||
/* logOk WILL fail: prev_log_index=2 expects term 9, but local entry 2 is term 5 */
|
||||
g_gt_spy.term_at[2] = 5;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0xEE, sizeof(out));
|
||||
const unsigned char e[] = { 0x7 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/6,
|
||||
/*prev_log_index=*/2, /*prev_log_term=*/9, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/6, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
/* term adopted even though logOk fails */
|
||||
TEST_ASSERT_EQUAL_UINT64(6, 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);
|
||||
/* persisted (term=6, voted_for=-1) at least once */
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(1, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(6, g_mock.last_persist_term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_persist_voted_for);
|
||||
/* reply carries the adopted term; the RPC is still rejected (logOk failed) */
|
||||
TEST_ASSERT_EQUAL_UINT64(6, out.reply_term);
|
||||
TEST_ASSERT_EQUAL_INT(0, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, out.match_index);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_leader_adopts_higher_term_steps_down (task 5.1/5.2, Req 6.1, 6.3)
|
||||
*
|
||||
* A LEADER at term T receiving an AppendEntries at term T+1 must adopt T+1,
|
||||
* clear vote, step down to FOLLOWER, persist (T+1,-1), AND fire on_become_follower
|
||||
* with (T+1, leader_idx). Applies before logOk.
|
||||
*/
|
||||
static void test_ae_req_leader_adopts_higher_term_steps_down(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 0));
|
||||
/* drive to LEADER at term 1, then a higher-term AE arrives */
|
||||
drive_to_leader(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
int bf_before = g_mock.become_follower_count;
|
||||
uint64_t higher = s.term + 1;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
/* heartbeat (entries_len=0, prev_log_index=0 → logOk trivially true) */
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/2, higher,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(higher, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
/* on_become_follower fired once for the step-down with (higher, leader_idx=2) */
|
||||
TEST_ASSERT_EQUAL_INT(bf_before + 1, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(higher, g_mock.last_become_follower_term);
|
||||
TEST_ASSERT_EQUAL_INT(2, g_mock.last_become_follower_leader_idx);
|
||||
/* persisted the adopted term */
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(1, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(higher, g_mock.last_persist_term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_persist_voted_for);
|
||||
/* the higher-term heartbeat is accepted (logOk trivially true) */
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(higher, out.reply_term);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_heartbeat_adopts_higher_term (task 5.1/5.2, Req 6.1, 6.4 — G5)
|
||||
*
|
||||
* A 0-entry heartbeat at a strictly-higher term still triggers UpdateTerm
|
||||
* (adoption applies on EVERY RPC incl. heartbeat), even when the heartbeat's
|
||||
* prev_log_index would fail logOk on the local log.
|
||||
*/
|
||||
static void test_ae_req_heartbeat_adopts_higher_term(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 1));
|
||||
s.term = 3;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
/* logOk fails: prev_log_index=1 expects term 8 but local entry 1 is term 3 */
|
||||
g_gt_spy.term_at[1] = 3;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/7,
|
||||
/*prev_log_index=*/1, /*prev_log_term=*/8, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
/* adopted despite logOk failure */
|
||||
TEST_ASSERT_EQUAL_UINT64(7, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_INT(1, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, g_mock.last_persist_term);
|
||||
/* heartbeat with a failing logOk is rejected, but at the adopted term */
|
||||
TEST_ASSERT_EQUAL_INT(0, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, out.reply_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, out.match_index);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_equal_term_no_term_change (Req 6.5)
|
||||
*
|
||||
* On an EQUAL-term AppendEntries, the handler must NOT change term or voted_for,
|
||||
* and must NOT persist a term adoption.
|
||||
*/
|
||||
static void test_ae_req_equal_term_no_term_change(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 0));
|
||||
s.term = 4;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
s.voted_for = 2;
|
||||
s.voted_for_term = 4;
|
||||
int persist_before = g_mock.persist_count;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
/* empty log, prev_log_index=0 → logOk true → append branch (heartbeat) */
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/4,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.voted_for); /* untouched */
|
||||
TEST_ASSERT_EQUAL_UINT64(4, s.voted_for_term);
|
||||
/* no on_persist for term adoption on equal term */
|
||||
TEST_ASSERT_EQUAL_INT(persist_before, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success); /* equal-term heartbeat accepted */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_stale_term_rejected (task 6.1, Req 4.2)
|
||||
*
|
||||
* term < currentTerm → reject (success=0, match_index=0, reply_term=currentTerm),
|
||||
* no term change, no log mutation.
|
||||
*/
|
||||
static void test_ae_req_stale_term_rejected(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 2));
|
||||
s.term = 9;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0x1 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/3,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/3, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, out.match_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(9, out.reply_term); /* current term */
|
||||
TEST_ASSERT_EQUAL_UINT64(9, s.term); /* unchanged */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_pe_spy.call_count); /* no log write */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count); /* no truncate */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_logok_prev_zero_accepts_append (task 6.1/6.3, Req 4.1, 4.6)
|
||||
*
|
||||
* prev_log_index == 0 → logOk true unconditionally; the local log ends at 0
|
||||
* (empty) so the one incoming entry is appended via on_persist_entry at index 1;
|
||||
* success, match_index = prev_log_index + entries_len = 1; log_len grows to 1.
|
||||
*/
|
||||
static void test_ae_req_logok_prev_zero_accepts_append(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 0));
|
||||
s.term = 2;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0xAA, 0xBB };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/2,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/2, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, out.match_index); /* prev(0) + entries_len(1) */
|
||||
/* appended at index 1 with the incoming entry term */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_pe_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_pe_spy.last_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, g_pe_spy.last_term);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(e, g_pe_spy.last_bytes, sizeof(e));
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count); /* append, no truncate */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.log_len); /* log extended */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_logok_match_accepts (task 6.1/6.3, Req 4.1)
|
||||
*
|
||||
* prev_log_index > 0 and on_get_entry_term(prev_log_index) == prev_log_term →
|
||||
* logOk true. Append at index prev+1 (local log ends at prev) → success,
|
||||
* match_index = prev + entries_len.
|
||||
*/
|
||||
static void test_ae_req_logok_match_accepts(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 3));
|
||||
s.term = 4;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
/* local entry 3 has term 4 == prev_log_term ⇒ logOk true; index 4 absent */
|
||||
g_gt_spy.term_at[3] = 4;
|
||||
/* g_gt_spy.term_at[4] == 0 → absent → append branch */
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0xC0 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/4,
|
||||
/*prev_log_index=*/3, /*prev_log_term=*/4, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/4, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, out.match_index); /* prev(3) + 1 */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_pe_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, g_pe_spy.last_index);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, s.log_len);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_logok_mismatch_rejected (task 6.1, Req 4.1, 4.2 — G2)
|
||||
*
|
||||
* prev_log_index > 0 and on_get_entry_term(prev_log_index) != prev_log_term →
|
||||
* logOk FALSE → follower at equal term rejects (success=0, match_index=0),
|
||||
* with no log mutation.
|
||||
*/
|
||||
static void test_ae_req_logok_mismatch_rejected(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 3));
|
||||
s.term = 4;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
/* local entry 3 has term 2, but prev_log_term says 4 → logOk false */
|
||||
g_gt_spy.term_at[3] = 2;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0xD0 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/4,
|
||||
/*prev_log_index=*/3, /*prev_log_term=*/4, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/4, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, out.match_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, out.reply_term);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_pe_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, s.log_len); /* unchanged */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_candidate_equal_term_steps_down (task 6.1, Req 4.3)
|
||||
*
|
||||
* A CANDIDATE at EQUAL term receiving a logOk AppendEntries transitions to
|
||||
* FOLLOWER (return-to-follower branch) and processes the accept.
|
||||
*/
|
||||
static void test_ae_req_candidate_equal_term_steps_down(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 0));
|
||||
/* become CANDIDATE at term 1 */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
uint64_t cand_term = s.term;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
/* equal-term heartbeat, prev_log_index=0 → logOk true */
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, cand_term,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* stepped down */
|
||||
TEST_ASSERT_EQUAL_UINT64(cand_term, s.term); /* no term change (equal) */
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success); /* heartbeat accepted */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_candidate_equal_term_steps_down_then_rejects (task 6.1, Req 4.2/4.3)
|
||||
*
|
||||
* A CANDIDATE at EQUAL term receiving an AppendEntries whose prev entry does
|
||||
* NOT match (logOk false) must still step down to FOLLOWER (step 3, before
|
||||
* logOk) AND then reject (step 5). This pins the step ordering: a mutation that
|
||||
* moved candidate step-down after the logOk reject would leave the node a
|
||||
* CANDIDATE here and be caught.
|
||||
*/
|
||||
static void test_ae_req_candidate_equal_term_steps_down_then_rejects(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 5));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, raft_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
uint64_t cand_term = s.term;
|
||||
|
||||
/* prev_log_index=3 within log, but local term there (4) != prev_log_term (9)
|
||||
* → logOk false → reject, yet the candidate must have stepped down first. */
|
||||
g_gt_spy.term_at[3] = 4;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, cand_term,
|
||||
/*prev_log_index=*/3, /*prev_log_term=*/9, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); /* stepped down BEFORE logOk */
|
||||
TEST_ASSERT_EQUAL_UINT64(cand_term, s.term); /* equal term, no change */
|
||||
TEST_ASSERT_EQUAL_INT(0, out.success); /* logOk failed → reject */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, out.match_index); /* reject → match_index 0 */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_pe_spy.call_count); /* no persist on reject */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count); /* no truncate on reject */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_already_have_heartbeat (task 6.2, Req 4.4)
|
||||
*
|
||||
* entries_len == 0 (heartbeat) with logOk → already-satisfied: success,
|
||||
* match_index = prev + 0 (= prev), NO log rewrite, NO truncate.
|
||||
*/
|
||||
static void test_ae_req_already_have_heartbeat(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 5));
|
||||
s.term = 6;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
g_gt_spy.term_at[5] = 6; /* prev_log_index=5 matches prev_log_term=6 */
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/6,
|
||||
/*prev_log_index=*/5, /*prev_log_term=*/6, /*leader_commit=*/0,
|
||||
NULL, 0, /*entry_term=*/0, /*entries_len=*/0, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, out.match_index); /* prev(5) + entries_len(0) */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_pe_spy.call_count); /* no rewrite */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count); /* no truncate */
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.log_len); /* heartbeat must NOT shrink log */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_already_have_matching_term (task 6.2, Req 4.4)
|
||||
*
|
||||
* entries_len == 1 but the local entry at prev+1 already has the SAME term as the
|
||||
* incoming entry → already-satisfied: success, match_index = prev + entries_len,
|
||||
* NO log rewrite, NO truncate.
|
||||
*/
|
||||
static void test_ae_req_already_have_matching_term(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 5));
|
||||
s.term = 6;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
g_gt_spy.term_at[3] = 6; /* prev_log_index=3 matches prev_log_term=6 (logOk) */
|
||||
g_gt_spy.term_at[4] = 6; /* local entry at index 4 already term 6 == entry_term */
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0x55 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/6,
|
||||
/*prev_log_index=*/3, /*prev_log_term=*/6, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/6, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, out.match_index); /* prev(3) + 1 */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_pe_spy.call_count); /* already have it — no rewrite */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.log_len); /* must NOT shrink */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_conflict_truncate_then_persist (task 6.2/6.3, Req 4.5 — G3)
|
||||
*
|
||||
* logOk holds, but the local entry at index = prev+1 exists with a DIFFERENT term
|
||||
* than the incoming entry → on_truncate_from(index) THEN on_persist_entry(index,
|
||||
* entry_term, entry); success, match_index = index (= prev + entries_len).
|
||||
* The truncate MUST be immediately followed by the re-persist of the leader entry.
|
||||
*/
|
||||
static void test_ae_req_conflict_truncate_then_persist(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 5));
|
||||
s.term = 7;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
g_gt_spy.term_at[3] = 7; /* prev_log_index=3, prev_log_term=7 → logOk */
|
||||
g_gt_spy.term_at[4] = 4; /* local entry 4 has term 4 (divergent) */
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0x99, 0xAA };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/7,
|
||||
/*prev_log_index=*/3, /*prev_log_term=*/7, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/7, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, out.match_index); /* index = prev+1 */
|
||||
/* truncate at the conflicting index, then re-persist the leader's entry there */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_tr_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, g_tr_spy.last_index);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_pe_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, g_pe_spy.last_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, g_pe_spy.last_term); /* the leader's entry_term */
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(e, g_pe_spy.last_bytes, sizeof(e));
|
||||
/* log_len is at least prev + entries_len after a conflict-and-append */
|
||||
TEST_ASSERT_EQUAL_UINT64(4, s.log_len);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_append_extends_log (task 6.3, Req 4.6)
|
||||
*
|
||||
* logOk holds and the local log ends exactly at prev_log_index (no entry at
|
||||
* prev+1) → append via on_persist_entry; success; match_index = prev + entries_len;
|
||||
* log_len extended to prev + entries_len.
|
||||
*/
|
||||
static void test_ae_req_append_extends_log(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 2));
|
||||
s.term = 5;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
g_gt_spy.term_at[2] = 5; /* prev_log_index=2, prev_log_term=5 → logOk */
|
||||
/* index 3 absent (term_at[3]==0), log_len==2 == prev_log_index → append */
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0x11, 0x22, 0x33 };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/5,
|
||||
/*prev_log_index=*/2, /*prev_log_term=*/5, /*leader_commit=*/0,
|
||||
e, sizeof(e), /*entry_term=*/5, /*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, out.match_index);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_tr_spy.call_count); /* no truncate on plain append */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_pe_spy.call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, g_pe_spy.last_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, g_pe_spy.last_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, s.log_len); /* extended */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_ae_req_leader_commit_not_acted (Req 4.x / R3.4 — commit opaque)
|
||||
*
|
||||
* A large leader_commit must NOT advance any commit index or otherwise leak into
|
||||
* library state — the handler only fills *out and mutates the log per the accept
|
||||
* branch. (No commit field exists in raft_state_t; this asserts the accept path
|
||||
* is unaffected by leader_commit value.)
|
||||
*/
|
||||
static void test_ae_req_leader_commit_not_acted(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_ae_req_cbs();
|
||||
|
||||
reset_ae_req_spies();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node_log_len(&s, &cbs, 3, 0, 0));
|
||||
s.term = 1;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
raft_append_result_t out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
const unsigned char e[] = { 0x7E };
|
||||
int rc = raft_on_append_entries_request(&s, /*leader_idx=*/1, /*term=*/1,
|
||||
/*prev_log_index=*/0, /*prev_log_term=*/0,
|
||||
/*leader_commit=*/999999, e, sizeof(e), /*entry_term=*/1,
|
||||
/*entries_len=*/1, &out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_OK, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, out.success);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, out.match_index); /* unaffected by leader_commit */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, s.log_len);
|
||||
}
|
||||
|
||||
/* ---------- raft_on_election_timeout tests (task 3.5) ---------- */
|
||||
|
||||
/*
|
||||
|
|
@ -4501,6 +5098,24 @@ void test_raft(void)
|
|||
RUN_TEST(test_send_ae_one_entry_when_next_within_log);
|
||||
RUN_TEST(test_send_ae_non_leader_emits_nothing);
|
||||
|
||||
/* raft_on_append_entries_request (tasks 5.1+5.2 UpdateTerm-first, 6.1 logOk
|
||||
* +reject+candidate-step-down, 6.2+6.3 accept already-have/conflict/append) */
|
||||
RUN_TEST(test_ae_req_follower_adopts_higher_term);
|
||||
RUN_TEST(test_ae_req_leader_adopts_higher_term_steps_down);
|
||||
RUN_TEST(test_ae_req_heartbeat_adopts_higher_term);
|
||||
RUN_TEST(test_ae_req_equal_term_no_term_change);
|
||||
RUN_TEST(test_ae_req_stale_term_rejected);
|
||||
RUN_TEST(test_ae_req_logok_prev_zero_accepts_append);
|
||||
RUN_TEST(test_ae_req_logok_match_accepts);
|
||||
RUN_TEST(test_ae_req_logok_mismatch_rejected);
|
||||
RUN_TEST(test_ae_req_candidate_equal_term_steps_down);
|
||||
RUN_TEST(test_ae_req_candidate_equal_term_steps_down_then_rejects);
|
||||
RUN_TEST(test_ae_req_already_have_heartbeat);
|
||||
RUN_TEST(test_ae_req_already_have_matching_term);
|
||||
RUN_TEST(test_ae_req_conflict_truncate_then_persist);
|
||||
RUN_TEST(test_ae_req_append_extends_log);
|
||||
RUN_TEST(test_ae_req_leader_commit_not_acted);
|
||||
|
||||
/* raft_on_vote_request (task 3.3) */
|
||||
RUN_TEST(test_vote_req_stale_term);
|
||||
RUN_TEST(test_vote_req_higher_term_updates_state);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue