myxce/src/controller/testsuite/test_walcompare.c
Rongsong Shen a166ecb45b test(ha-term-safety): cross-member walcompare divergence tool (task 5.3)
Diffs member WALs by (lsn -> term, pcrc) over the committed prefix, exits non-zero
on any committed-prefix divergence with the diverging LSN + per-member entry
identities. Reuses the canonical waldump decoder via a new per-entry visitor seam
(no second WAL parser). Detects same-LSN/diff-term and same-LSN/diff-content
divergences (the bug-851 class) that an LSN-range check would mask.
2026-06-22 13:05:23 +00:00

370 lines
14 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <zlib.h>
#include <unity/unity.h>
#include "config.h" /* WAL_HDR_CRC_SIZE, WAL_CRC_NONE */
#include "persist.h" /* WAL_LEGACY_TERM */
#include "walcompare.h"
#include "wal_types.h"
/* ------------------------------------------------------------------ *
* Harness: hand-write raw WAL segments (LSN, term, payload all under
* test control) into per-member temp dirs, then drive walcompare over
* them. Mirrors test_waldump's writer so the on-disk format is pinned
* independently of persist.c — and exercises the REAL waldump decoder
* that walcompare reuses (no second parser).
* ------------------------------------------------------------------ */
/* Append one entry to `fp`, chaining prev_crc from *last_crc. CRC32 over
* WAL_HDR_CRC_SIZE header bytes + payload (identical to persist.c append). */
static void put_entry(FILE* fp, uint64_t term, uint64_t lsn, uint32_t type,
const void* payload, uint32_t plen, uint32_t* last_crc,
int first_in_seg)
{
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.term = term;
hdr.lsn = lsn;
hdr.timestamp = 0;
hdr.type = type;
hdr.payload_len = plen;
hdr.prev_crc = first_in_seg ? WAL_CRC_NONE : *last_crc;
uint32_t c = crc32(0L, (const Bytef*)&hdr, WAL_HDR_CRC_SIZE);
if (plen > 0)
c = crc32(c, (const Bytef*)payload, plen);
hdr.crc = c;
*last_crc = c;
fwrite(&hdr, sizeof(hdr), 1, fp);
if (plen > 0)
fwrite(payload, plen, 1, fp);
}
/* A single hand-built WAL entry spec for a member. */
typedef struct {
uint64_t term;
uint32_t type;
const char* payload; /* NUL-terminated; length is strlen (0 => no payload) */
} wal_spec_t;
/* Mint a fresh temp dir and write a single seg-0001.wal holding `specs`
* (LSNs 1..n contiguous). Returns the dir path in `dir_out`. */
static void build_member(char* dir_out, size_t dirsz, const wal_spec_t* specs,
int n)
{
snprintf(dir_out, dirsz, "/tmp/xce_walcmp_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(dir_out));
char path[512];
snprintf(path, sizeof(path), "%s/seg-%016lx.wal", dir_out, 1UL);
FILE* fp = fopen(path, "wb");
TEST_ASSERT_NOT_NULL(fp);
uint32_t last = WAL_CRC_NONE;
for (int i = 0; i < n; i++) {
uint32_t plen =
specs[i].payload ? (uint32_t)strlen(specs[i].payload) : 0;
put_entry(fp, specs[i].term, (uint64_t)(i + 1), specs[i].type,
specs[i].payload, plen, &last, i == 0);
}
fclose(fp);
}
/* ------------------------------------------------------------------ */
/* Identical member WALs → success (exit 0). */
/* ------------------------------------------------------------------ */
static void test_identical_members_ok(void)
{
wal_spec_t specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 2, WAL_JOB_SUBMIT, "jobB" },
};
char a[256], b[256];
build_member(a, sizeof(a), specs, 3);
build_member(b, sizeof(b), specs, 3);
walcompare_member_t m[2] = {
{ a, 0 },
{ b, 0 },
};
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
}
/* ------------------------------------------------------------------ */
/* Hand-divergent pair: SAME LSN, DIFFERENT term within the committed */
/* prefix → FAIL with the diverging LSN (R6.2/R6.3). */
/* ------------------------------------------------------------------ */
static void test_same_lsn_diff_term_divergent(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 2, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 2 */
};
wal_spec_t b_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 3, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 3 — divergent */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 3);
build_member(b, sizeof(b), b_specs, 3);
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_EQUAL_UINT64(3, div.lsn); /* the diverging LSN */
TEST_ASSERT_EQUAL_UINT64(2, div.member_term[0]);
TEST_ASSERT_EQUAL_UINT64(3, div.member_term[1]);
}
/* ------------------------------------------------------------------ */
/* SAME LSN, SAME term, DIFFERENT payload (pcrc) → FAIL (R6.2: a content */
/* divergence is not masked by equal term + LSN range). */
/* ------------------------------------------------------------------ */
static void test_same_lsn_diff_payload_divergent(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* LSN 2 */
};
wal_spec_t b_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobZ" }, /* LSN 2, same term, different bytes */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 2);
build_member(b, sizeof(b), b_specs, 2);
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_EQUAL_UINT64(2, div.lsn);
TEST_ASSERT_EQUAL_UINT64(1, div.member_term[0]);
TEST_ASSERT_EQUAL_UINT64(1, div.member_term[1]);
TEST_ASSERT_NOT_EQUAL(div.member_pcrc[0], div.member_pcrc[1]);
}
/* ------------------------------------------------------------------ */
/* A member that is AHEAD (longer log) with a DIFFERENT uncommitted tail */
/* is NOT a divergence — the shared committed prefix matches (§6.1). */
/* ------------------------------------------------------------------ */
static void test_uncommitted_tail_not_flagged(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* shared committed prefix: LSN 1..2 */
};
wal_spec_t b_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 2, WAL_JOB_SUBMIT, "tailX" }, /* uncommitted tail — beyond shared tip */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 2); /* tip 2 */
build_member(b, sizeof(b), b_specs, 3); /* tip 3 */
/* Shared boundary = min recovered tip = 2; the LSN-3 tail is not compared. */
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
}
/* ------------------------------------------------------------------ */
/* An explicit committed boundary SHORTER than a member's log: a */
/* divergence ABOVE the boundary must NOT be flagged (§6.1 committed */
/* prefix only). */
/* ------------------------------------------------------------------ */
static void test_divergence_above_committed_boundary_not_flagged(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* LSN 2 committed */
{ 2, WAL_JOB_SUBMIT, "uncA" }, /* LSN 3 uncommitted, divergent */
};
wal_spec_t b_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 9, WAL_JOB_SUBMIT, "uncB" }, /* LSN 3, different term+bytes */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 3);
build_member(b, sizeof(b), b_specs, 3);
/* committed boundary pinned at 2 → the LSN-3 divergence is below scope. */
walcompare_member_t m[2] = { { a, 2 }, { b, 2 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
/* Same data, but raise the boundary to 3 → now it IS a divergence. */
walcompare_member_t m3[2] = { { a, 3 }, { b, 3 } };
rc = walcompare(m3, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
}
/* ------------------------------------------------------------------ */
/* A member MISSING a committed entry (a hole inside the committed */
/* prefix the boundary covers) is a divergence (R4.2/R6.4). */
/* ------------------------------------------------------------------ */
static void test_missing_committed_entry_divergent(void)
{
/* Member A holds 1..3; member B holds only 1..2 but is told its
* committed boundary is 3 — it is MISSING the committed entry at LSN 3. */
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 2, WAL_JOB_SUBMIT, "jobB" },
};
wal_spec_t b_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 3);
build_member(b, sizeof(b), b_specs, 2);
walcompare_member_t m[2] = { { a, 3 }, { b, 3 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
TEST_ASSERT_EQUAL_INT(1, div.member_present[0]); /* A holds it */
TEST_ASSERT_EQUAL_INT(0, div.member_present[1]); /* B is missing it */
}
/* ------------------------------------------------------------------ */
/* Two legacy-0-term entries at the same LSN are NOT divergent: legacy 0 */
/* maps to WAL_LEGACY_TERM for both (R1.8). */
/* ------------------------------------------------------------------ */
static void test_legacy_zero_term_not_divergent(void)
{
wal_spec_t specs[] = {
{ 0, WAL_NOOP, NULL }, /* legacy-0 term */
{ 0, WAL_JOB_SUBMIT, "jobA" }, /* legacy-0 term, same payload */
};
char a[256], b[256];
build_member(a, sizeof(a), specs, 2);
build_member(b, sizeof(b), specs, 2);
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
}
/* ------------------------------------------------------------------ */
/* Three members, all identical → success (the live regression shape). */
/* ------------------------------------------------------------------ */
static void test_three_members_identical_ok(void)
{
wal_spec_t specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "j1" },
{ 1, WAL_CONFIG_MARKER, "cfgref" }, /* a config marker in the log */
{ 2, WAL_JOB_SUBMIT, "j2" },
};
char a[256], b[256], c[256];
build_member(a, sizeof(a), specs, 4);
build_member(b, sizeof(b), specs, 4);
build_member(c, sizeof(c), specs, 4);
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 3, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
}
/* ------------------------------------------------------------------ */
/* Three members, one with a divergent committed config marker → FAIL */
/* (R6.6: config markers reconcile by (term,lsn) like job entries). */
/* ------------------------------------------------------------------ */
static void test_three_members_divergent_config_marker(void)
{
wal_spec_t ok_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "j1" },
{ 1, WAL_CONFIG_MARKER, "cfgX" }, /* LSN 3 config ref */
};
wal_spec_t bad_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "j1" },
{ 1, WAL_CONFIG_MARKER, "cfgY" }, /* LSN 3, different config ref */
};
char a[256], b[256], c[256];
build_member(a, sizeof(a), ok_specs, 3);
build_member(b, sizeof(b), ok_specs, 3);
build_member(c, sizeof(c), bad_specs, 3);
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 3, stdout, &div);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
/* A and B agree; C diverges. */
TEST_ASSERT_EQUAL_UINT32(div.member_pcrc[0], div.member_pcrc[1]);
TEST_ASSERT_NOT_EQUAL(div.member_pcrc[0], div.member_pcrc[2]);
}
/* ------------------------------------------------------------------ */
/* A missing WAL source is a hard (negative) error, not a false OK. */
/* ------------------------------------------------------------------ */
static void test_missing_member_errors(void)
{
wal_spec_t specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
};
char a[256];
build_member(a, sizeof(a), specs, 2);
walcompare_member_t m[2] = {
{ a, 0 },
{ "/tmp/xce_walcmp_does_not_exist_zzz", 0 },
};
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_LESS_THAN_INT(0, rc); /* negative errno, not OK/DIVERGENT */
}
/* ------------------------------------------------------------------ */
/* Fewer than two members is a usage error. */
/* ------------------------------------------------------------------ */
static void test_too_few_members_errors(void)
{
wal_spec_t specs[] = { { 1, WAL_NOOP, NULL } };
char a[256];
build_member(a, sizeof(a), specs, 1);
walcompare_member_t m[1] = { { a, 0 } };
int rc = walcompare(m, 1, stdout, NULL);
TEST_ASSERT_LESS_THAN_INT(0, rc);
}
void test_walcompare(void)
{
RUN_TEST(test_identical_members_ok);
RUN_TEST(test_same_lsn_diff_term_divergent);
RUN_TEST(test_same_lsn_diff_payload_divergent);
RUN_TEST(test_uncommitted_tail_not_flagged);
RUN_TEST(test_divergence_above_committed_boundary_not_flagged);
RUN_TEST(test_missing_committed_entry_divergent);
RUN_TEST(test_legacy_zero_term_not_divergent);
RUN_TEST(test_three_members_identical_ok);
RUN_TEST(test_three_members_divergent_config_marker);
RUN_TEST(test_missing_member_errors);
RUN_TEST(test_too_few_members_errors);
}