test(raft-ha-core-safety): payload-aware persist-guard RED + pins (task 2.1)
Case (a) empty-then-full = genuine RED (payload-blind guard at persist.c:822 no-ops the corrective write, Expected 64 Was 0). Cases (b) full-then-empty and (c) identical-rewrite = baseline-passing pins that constrain the 2.2 fix asymmetry (empty must never overwrite stored full; identical re-stream stays idempotent). 7 tests/1 RED/no ASAN.
This commit is contained in:
parent
07f08e58f5
commit
3b5eda1429
2 changed files with 119 additions and 2 deletions
|
|
@ -276,6 +276,118 @@ static void test_smoke_red_payload_blind_guard_drops_corrective_write(void)
|
|||
printf("test_smoke_red_payload_blind_guard_drops_corrective_write: done\n");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* TASK 2.1 — payload-aware persistence guard cases (a)/(b)/(c)
|
||||
*
|
||||
* These three cases pin the post-fix contract task 2.2 must satisfy when it
|
||||
* makes the LSN-only idempotency guard (persist.c:822) (term, lsn, payload-crc)
|
||||
* -aware. They read as one coherent group:
|
||||
*
|
||||
* (a) empty-then-full at the SAME (term, lsn) → must end FULL.
|
||||
* This is the RED case, owned by the smoke-RED above
|
||||
* (test_smoke_red_payload_blind_guard_drops_corrective_write): the
|
||||
* payload-blind guard no-ops the corrective write and keeps the empty
|
||||
* body — FAILS on the baseline, goes GREEN with 2.2.
|
||||
*
|
||||
* (b) full-then-empty at the SAME (term, lsn) → must NOT degrade to empty.
|
||||
* PIN: the baseline already satisfies this — once the full body advances
|
||||
* current_lsn, the same LSN-only idempotency guard no-ops the later empty
|
||||
* write, so the stored entry stays full. This case is therefore NOT a
|
||||
* baseline RED; it is a guard that 2.2's new payload-crc-aware branch does
|
||||
* not over-correct an empty (PLEN=0) write into a destructive
|
||||
* overwrite/truncate of a known-non-empty committed entry.
|
||||
*
|
||||
* (c) identical (term, payload-crc) re-write → safe idempotent no-op.
|
||||
* PIN: the baseline already satisfies this (the LSN-only guard no-ops it);
|
||||
* it guards 2.2 against treating an identical re-stream as a "conflict"
|
||||
* and over-truncating/duplicating a correct committed entry.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Case (b): a stray empty (PLEN=0) write at an existing committed (term, lsn)
|
||||
* must NEVER degrade the stored full entry to empty. Baseline-passing PIN
|
||||
* (the LSN-only idempotency guard already no-ops the empty write). */
|
||||
static void test_2_1_full_then_empty_must_not_degrade(void)
|
||||
{
|
||||
hcs_member_t m;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_member_up(&m, "fte"));
|
||||
|
||||
uint8_t full[HCS_FULL_LEN];
|
||||
hcs_fill_body(full);
|
||||
|
||||
/* (1) the CORRECT full body lands first at committed LSN 1. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
|
||||
"the full replicated write must persist (needs a real fs for the inline "
|
||||
"fdatasync; -EIO here means overlayfs — bug-924)");
|
||||
|
||||
/* (2) a stray empty (PLEN=0) body arrives for the SAME (term, lsn). */
|
||||
int rc_empty = hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT,
|
||||
/*payload=*/NULL, /*len=*/0);
|
||||
TEST_ASSERT_TRUE_MESSAGE(rc_empty == 0,
|
||||
"the stray empty write returns 0 (baseline: an idempotent no-op; "
|
||||
"fixed: a rejected/repaired conflict) — either way rc==0");
|
||||
|
||||
/* (3) OBSERVE: on disk the entry must STILL hold the FULL payload — an
|
||||
* empty body must never become the durable form of a known-non-empty
|
||||
* committed entry (the inverse of case (a)). */
|
||||
hcs_probe_t pr;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &pr));
|
||||
TEST_ASSERT_TRUE_MESSAGE(pr.present,
|
||||
"committed LSN 1 must remain present after the stray empty write");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(HCS_FULL_LEN, pr.len,
|
||||
"PIN: an empty (PLEN=0) write must NOT degrade the stored full entry to "
|
||||
"empty (guards 2.2 against over-correcting into a destructive overwrite)");
|
||||
TEST_ASSERT_NOT_EQUAL_MESSAGE(0u, pr.pcrc,
|
||||
"PIN: the committed entry must keep its real payload CRC, not drop to 0");
|
||||
|
||||
hcs_member_down(&m);
|
||||
printf("test_2_1_full_then_empty_must_not_degrade: PASS (pin)\n");
|
||||
}
|
||||
|
||||
/* Case (c): re-streaming the IDENTICAL (term, payload-crc) entry at the same
|
||||
* committed LSN is a safe idempotent no-op — no degrade, no duplicate, no
|
||||
* CRC-chain break. Baseline-passing PIN. */
|
||||
static void test_2_1_identical_rewrite_is_idempotent_noop(void)
|
||||
{
|
||||
hcs_member_t m;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_member_up(&m, "idem"));
|
||||
|
||||
uint8_t full[HCS_FULL_LEN];
|
||||
hcs_fill_body(full);
|
||||
|
||||
/* (1) the full body lands at committed LSN 1. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
|
||||
"the initial full write must persist (real fs required — bug-924)");
|
||||
hcs_probe_t before;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &before));
|
||||
TEST_ASSERT_TRUE(before.present);
|
||||
TEST_ASSERT_EQUAL_UINT32(HCS_FULL_LEN, before.len);
|
||||
TEST_ASSERT_NOT_EQUAL(0u, before.pcrc);
|
||||
|
||||
/* (2) the IDENTICAL (term, payload-crc) entry is re-streamed at the same
|
||||
* (term, lsn) — must be accepted as a no-op, never a conflict. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
hcs_wal_put(&m, /*lsn=*/1, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN),
|
||||
"an identical (term, payload-crc) re-write must be a safe idempotent no-op");
|
||||
|
||||
/* (3) OBSERVE: the stored entry is unchanged — same length and same
|
||||
* (term, payload-crc) fingerprint as before the re-write. */
|
||||
hcs_probe_t after;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, 1, &after));
|
||||
TEST_ASSERT_TRUE(after.present);
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(HCS_FULL_LEN, after.len,
|
||||
"PIN: an identical re-write must leave the full payload length intact");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(before.term, after.term,
|
||||
"PIN: an identical re-write must leave the stored term unchanged");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(before.pcrc, after.pcrc,
|
||||
"PIN: an identical re-write must leave the stored payload CRC unchanged "
|
||||
"(no degrade, no over-truncate)");
|
||||
|
||||
hcs_member_down(&m);
|
||||
printf("test_2_1_identical_rewrite_is_idempotent_noop: PASS (pin)\n");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* SCAFFOLDING (PASS) — prove each shared seam is usable by later phases.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -400,8 +512,13 @@ static void test_seam_election_injection(void)
|
|||
void test_ha_core_safety(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
/* The ONE smoke-RED: payload-blind guard drops the corrective write. */
|
||||
/* Task 2.1 payload-aware guard set:
|
||||
* (a) empty-then-full — RED (the smoke-RED below): FAILs on the baseline.
|
||||
* (b) full-then-empty — PIN: must not degrade (baseline-passing).
|
||||
* (c) identical re-write — PIN: idempotent no-op (baseline-passing). */
|
||||
RUN_TEST(test_smoke_red_payload_blind_guard_drops_corrective_write);
|
||||
RUN_TEST(test_2_1_full_then_empty_must_not_degrade);
|
||||
RUN_TEST(test_2_1_identical_rewrite_is_idempotent_noop);
|
||||
/* Scaffolding: each shared seam is usable by 2.3 / 4.1 / 5.1. */
|
||||
RUN_TEST(test_seam_wal_construct_and_probe);
|
||||
RUN_TEST(test_seam_divergence_is_observable);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue