feat(raft-sim-conformance): partition/heal adversarial scenario (task 5.1)

This commit is contained in:
Rongsong Shen 2026-06-24 14:34:22 +00:00
parent b844e75f3b
commit 7d0964dd16
4 changed files with 364 additions and 1 deletions

View file

@ -8080,6 +8080,83 @@ static void test_sim_4_4_run_deterministic_replay(void)
TEST_ASSERT_TRUE(a.trace_len > 0);
}
}
/*
* test_sim_5_1_partition_heal (raft-sim-conformance task 5.1; Req 4.1)
*
* Network-partition scenario: drive a cluster to a leader + committed entries,
* isolate a MINORITY of nodes during a phase of client appends/replication, keep
* the MAJORITY making progress, then HEAL and drain the bag so the formerly-isolated
* nodes catch up. raft_sim_scenario_partition_heal asserts all four invariants
* THROUGHOUT (after every action it applies — not only at the end) and after
* convergence; it returns 0 on zero violations and < 0 (the negated first failing
* invariant) on any violation.
*
* The library is correct (bug-1095/1096 fixed), so a faithful partition/heal MUST
* complete with zero violations: a return < 0 here is a REAL divergence the scenario
* surfaced (the step already dumped the replayable trace to stderr) — NOT something
* to mask by hand-picking a seed. Several (n, seed) combinations are exercised
* (minority isolation in both 3-node and 5-node clusters) so the property is not a
* single-seed accident.
*/
static void test_sim_5_1_partition_heal(void)
{
/* (n, seed) pairs: 3-node clusters isolate 1 node; 5-node clusters isolate 2. The
* scenario inits the caller's cluster internally from (n, seed). */
struct { int n; uint64_t seed; } cases[] = {
{ 3, 3ULL }, { 3, 0x4A33ULL }, { 3, 1ULL },
{ 5, 7ULL }, { 5, 0xBEEFULL },
};
for (size_t k = 0; k < sizeof(cases) / sizeof(cases[0]); k++) {
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
int rc = raft_sim_scenario_partition_heal(&c, cases[k].n, cases[k].seed);
/* Zero invariant violations throughout the partition + heal + convergence. */
TEST_ASSERT_TRUE_MESSAGE(rc == 0,
"partition/heal scenario surfaced an invariant violation on the correct library");
/* Post-convergence: the cluster still satisfies all four invariants, has a
* single leader, and the formerly-isolated minority caught up to the leader's
* committed prefix (Log Matching + Leader Completeness over a converged log). */
TEST_ASSERT_EQUAL_INT(RAFT_SIM_INV_NONE, raft_sim_check_all(&c));
int leaders = 0;
int leader_idx = -1;
for (int i = 0; i < c.n; i++) {
raft_snapshot_t snap;
raft_snapshot(&c.nodes[i].state, &snap);
if (snap.role == RAFT_LEADER) {
leaders++;
leader_idx = i;
}
}
TEST_ASSERT_EQUAL_INT_MESSAGE(1, leaders,
"post-heal cluster does not have exactly one leader");
/* Convergence is observable: the leader committed a non-trivial prefix, and
* every (formerly-isolated) node's committed entries match the leader's at the
* same index/term over their shared committed prefix. */
TEST_ASSERT_TRUE_MESSAGE(c.nodes[leader_idx].commit_index > 0,
"leader never committed any entry across the scenario");
for (int i = 0; i < c.n; i++) {
if (i == leader_idx)
continue;
uint64_t shared = c.nodes[i].commit_index;
if (shared > c.nodes[leader_idx].commit_index)
shared = c.nodes[leader_idx].commit_index;
for (uint64_t e = 1; e <= shared; e++) {
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
c.nodes[leader_idx].log[e - 1].term,
c.nodes[i].log[e - 1].term,
"formerly-isolated node diverges from leader committed prefix");
}
}
}
}
#endif /* XCE_TESTING */
/* ---------- test_raft entry point ---------- */
@ -8290,6 +8367,7 @@ void test_raft(void)
RUN_TEST(test_sim_4_2_step_halts_on_forced_violation);
RUN_TEST(test_sim_4_3_restart_preserves_persistence);
RUN_TEST(test_sim_4_4_run_deterministic_replay);
RUN_TEST(test_sim_5_1_partition_heal);
/* Group A: basic election correctness (task 5.1) */
RUN_TEST(test_proto_A1_single_node_self_elects);