feat(raft-log-replication): Log Matching assertion helper (task 8.3)

raft_sim_check_log_matching: for every node pair and every shared index i, if
both entries at i have equal term, assert the whole prefix 1..i is term-and-byte
identical (Req 8.4, raft.tla Log Matching). Read-only, framework-agnostic (returns
1/0), bounded. Self-tests: pass on a converged cluster, fail on a hand-divergent
pair sharing (i=2,term=7) but differing at the index-1 prefix. ASAN green.
This commit is contained in:
Rongsong Shen 2026-06-21 16:47:49 +00:00
parent 0e8c6ed05a
commit 6ff07e5843
4 changed files with 152 additions and 1 deletions

View file

@ -3100,6 +3100,100 @@ static void test_sim_deliver_append_entries_replicates(void)
TEST_ASSERT_EQUAL_INT(0, c.nodes[2].log_count);
}
/*
* test_sim_check_log_matching_pass_on_converged (task 8.3)
*
* Log Matching helper must return PASS (1) when two nodes hold identical logs.
* Build a 3-node cluster, elect node 0, append two entries, replicate both to
* node 1; assert raft_sim_check_log_matching == 1 and node 1's prefix matches
* node 0's byte-for-byte.
*/
static void test_sim_check_log_matching_pass_on_converged(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
raft_start_election(&c.nodes[0].state);
raft_sim_deliver_pending(&c);
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
/* Append two client entries on the leader and replicate each to node 1.
* One AppendEntries carries at most one entry, so replicate index by index. */
unsigned char p1[3] = { 0x01, 0x02, 0x03 };
unsigned char p2[3] = { 0x04, 0x05, 0x06 };
uint64_t out_index = 0;
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_log_append(&c.nodes[0].state, p1, sizeof(p1), &out_index));
TEST_ASSERT_EQUAL_UINT64(1, out_index);
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_send_append_entries(&c.nodes[0].state, 1, /*leader_commit=*/0));
raft_sim_deliver_append_entries(&c);
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_log_append(&c.nodes[0].state, p2, sizeof(p2), &out_index));
TEST_ASSERT_EQUAL_UINT64(2, out_index);
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_send_append_entries(&c.nodes[0].state, 1, /*leader_commit=*/0));
raft_sim_deliver_append_entries(&c);
/* Node 1 holds the leader's prefix byte-for-byte. */
TEST_ASSERT_EQUAL_INT(2, c.nodes[0].log_count);
TEST_ASSERT_EQUAL_INT(2, c.nodes[1].log_count);
/* Log Matching holds across the cluster. */
TEST_ASSERT_EQUAL_INT(1, raft_sim_check_log_matching(&c));
}
/*
* test_sim_check_log_matching_fail_on_divergence (task 8.3)
*
* Log Matching helper must return FAIL (0) when two nodes share the same
* (index, term) at some i but their prefixes differ. Hand-craft node 0 and
* node 1 so that index 2 carries the SAME term but index 1 differs in bytes —
* a Log Matching violation (equal (i,term) but non-identical prefix through i).
*/
static void test_sim_check_log_matching_fail_on_divergence(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
TEST_ASSERT_EQUAL_INT(0, raft_sim_init(&c, 3, 2000));
/* Hand-craft node 0: index1=(term 5, "AAA"), index2=(term 7, "XXX"). */
c.nodes[0].log[0].term = 5;
c.nodes[0].log[0].bytes[0] = 'A';
c.nodes[0].log[0].bytes[1] = 'A';
c.nodes[0].log[0].bytes[2] = 'A';
c.nodes[0].log[0].len = 3;
c.nodes[0].log[1].term = 7;
c.nodes[0].log[1].bytes[0] = 'X';
c.nodes[0].log[1].bytes[1] = 'X';
c.nodes[0].log[1].bytes[2] = 'X';
c.nodes[0].log[1].len = 3;
c.nodes[0].log_count = 2;
/* Hand-craft node 1: SAME (index2, term 7) but a DIFFERENT index1 prefix
* (term 5 but bytes "BBB"). This must be flagged: equal (i=2, term=7) yet
* the prefix through 2 is not byte-identical. */
c.nodes[1].log[0].term = 5;
c.nodes[1].log[0].bytes[0] = 'B';
c.nodes[1].log[0].bytes[1] = 'B';
c.nodes[1].log[0].bytes[2] = 'B';
c.nodes[1].log[0].len = 3;
c.nodes[1].log[1].term = 7;
c.nodes[1].log[1].bytes[0] = 'X';
c.nodes[1].log[1].bytes[1] = 'X';
c.nodes[1].log[1].bytes[2] = 'X';
c.nodes[1].log[1].len = 3;
c.nodes[1].log_count = 2;
/* Same (index2, term7) with divergent index1 ⇒ Log Matching violated. */
TEST_ASSERT_EQUAL_INT(0, raft_sim_check_log_matching(&c));
}
/* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */
/*
@ -5348,6 +5442,8 @@ void test_raft(void)
RUN_TEST(test_sim_partition_no_quorum);
RUN_TEST(test_sim_fire_timeout_steps_down);
RUN_TEST(test_sim_deliver_append_entries_replicates);
RUN_TEST(test_sim_check_log_matching_pass_on_converged);
RUN_TEST(test_sim_check_log_matching_fail_on_divergence);
/* Group A: basic election correctness (task 5.1) */
RUN_TEST(test_proto_A1_single_node_self_elects);