myxce/src/controller/testsuite/test_walcompare.c
Rongsong Shen a81f2a249c fix(controller-tests): make Unity test-file attribution and counters consistent suite-wide
Only 21 of 86 test_*.c group functions called their own nested
UNITY_BEGIN()/UNITY_END(). Each nested call reset Unity's global
NumberOfTests/TestFailures counters, so controller_test.c's final
`return UNITY_END();` -- the value meson test actually gates on --
only ever reflected whichever small window of tests ran since the
LAST nested reset, silently discarding pass/fail evidence for
everything before it (measured: 16 of 972 total tests in one run).
The same nested-BEGIN calls also caused PASS/FAIL banners to show
the wrong source file for tests in files that don't reset it
themselves (Unity.TestFile just carries over from whichever group
last called UNITY_BEGIN()).

Fix: replace every nested UNITY_BEGIN() with UnitySetTestFile(__FILE__)
(sets file attribution only, no counter reset) and drop the matching
UNITY_END(); add the same UnitySetTestFile(__FILE__) as the first
statement to the other group functions that had neither. The 2
genuinely standalone binaries (test_job_array_sched_push.c,
test_job_array_apiserver_push.c -- each has its own int main(), not
registered via controller_test.c's TEST() macro) are untouched.

Verified: single consolidated summary now reads
"972 Tests 0 Failures 0 Ignored OK", exactly matching the
ground-truth RUN_TEST call-site count across all 83 registered
group files (976 total minus 4 belonging to the 2 standalone
binaries). Every PASS/FAIL banner now names its own true source
file. Full xce-build.sh all regression pass clean (Ok: 9, Fail: 0).

No production code touched -- test-harness plumbing only.
2026-07-17 14:49:12 +00:00

650 lines
25 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.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);
}
/* ------------------------------------------------------------------ *
* Harness for the per-member checkpoint-floor tests (raft-ha-core-safety
* task 5.10a / ha-entity-snapshots task 7.1): builds `<dir_out>/wal/` (real
* segs, LSNs starting at `start_lsn`, contiguous CRC chain) and, when
* `floor_lsn > 0`, an empty `<dir_out>/snap/snap-<floor_lsn:016x>.capnp`
* marker (walcompare only parses the FILENAME's embedded LSN, never the
* content) — the sibling `wal/`+`snap/` layout `member_snapshot_floor`'s
* "Candidate 1" path expects, matching the real deployed layout
* (`<data>/wal/`, `<data>/snap/`).
* ------------------------------------------------------------------ */
static void build_member_floored(char* dir_out, size_t dirsz,
const wal_spec_t* specs, int n,
uint64_t start_lsn, uint64_t floor_lsn)
{
snprintf(dir_out, dirsz, "/tmp/xce_walcmp_fl_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(dir_out));
char waldir[288];
snprintf(waldir, sizeof(waldir), "%s/wal", dir_out);
TEST_ASSERT_EQUAL_INT(0, mkdir(waldir, 0755));
char path[512];
snprintf(path, sizeof(path), "%s/seg-%016lx.wal", waldir,
(unsigned long)start_lsn);
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, start_lsn + (uint64_t)i, specs[i].type,
specs[i].payload, plen, &last, i == 0);
}
fclose(fp);
if (floor_lsn > 0) {
char snapdir[288];
snprintf(snapdir, sizeof(snapdir), "%s/snap", dir_out);
TEST_ASSERT_EQUAL_INT(0, mkdir(snapdir, 0755));
char snapfile[512];
snprintf(snapfile, sizeof(snapfile), "%s/snap-%016lx.capnp", snapdir,
(unsigned long)floor_lsn);
FILE* sf = fopen(snapfile, "wb");
TEST_ASSERT_NOT_NULL(sf);
fclose(sf); /* content is irrelevant — only the filename is parsed */
}
}
/* ------------------------------------------------------------------ */
/* A member that caught up via a full-checkpoint snapshot legitimately */
/* holds NO raw WAL below its own installed checkpoint LSN — that range */
/* must be excluded from comparison, never flagged MISSING/divergent */
/* (raft-ha-core-safety task 5.10a / ha-entity-snapshots task 7.1). */
/* ------------------------------------------------------------------ */
static void test_checkpoint_floor_excludes_compacted_range(void)
{
/* Member A: full history, LSNs 1..5, uniform term 1. */
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 1, WAL_JOB_SUBMIT, "jobB" },
{ 1, WAL_JOB_SUBMIT, "jobC" },
{ 1, WAL_JOB_SUBMIT, "jobD" },
};
/* Member B: caught up via a checkpoint at LSN 3 — its raw WAL only
* physically holds LSNs 4..5 (matching A's LSN 4..5 exactly), and it
* carries a snap-...0003.capnp marker declaring that floor. */
wal_spec_t b_specs[] = {
{ 1, WAL_JOB_SUBMIT, "jobC" }, /* LSN 4 */
{ 1, WAL_JOB_SUBMIT, "jobD" }, /* LSN 5 */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 5);
build_member_floored(b, sizeof(b), b_specs, 2, /*start_lsn=*/4,
/*floor_lsn=*/3);
walcompare_member_t m[2] = { { a, 5 }, { b, 5 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT_MESSAGE(WALCMP_OK, rc,
"a member's own checkpoint floor must suppress the compacted range "
"from comparison, not flag it MISSING");
}
/* ------------------------------------------------------------------ */
/* The checkpoint floor must NOT mask a genuine divergence within the */
/* range the compacted member DOES claim to hold (past its own floor). */
/* ------------------------------------------------------------------ */
static void test_checkpoint_floor_does_not_mask_real_divergence(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 1, WAL_JOB_SUBMIT, "jobB" },
{ 1, WAL_JOB_SUBMIT, "jobC" },
{ 1, WAL_JOB_SUBMIT, "jobD" },
};
/* Member B's post-floor LSN 4 entry DISAGREES with A's (different
* term+payload) — this is a real divergence and must still be caught. */
wal_spec_t b_specs[] = {
{ 9, WAL_JOB_SUBMIT, "DIFFERENT" }, /* LSN 4 — diverges from A */
{ 1, WAL_JOB_SUBMIT, "jobD" }, /* LSN 5 */
};
char a[256], b[256];
build_member(a, sizeof(a), a_specs, 5);
build_member_floored(b, sizeof(b), b_specs, 2, /*start_lsn=*/4,
/*floor_lsn=*/3);
walcompare_member_t m[2] = { { a, 5 }, { b, 5 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, stdout, &div);
TEST_ASSERT_EQUAL_INT_MESSAGE(WALCMP_DIVERGENT, rc,
"a checkpoint floor excludes the compacted range only -- a real "
"divergence within the member's post-floor range must still be caught");
TEST_ASSERT_EQUAL_UINT64(4, 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);
}
/* ------------------------------------------------------------------ */
/* Machine-checkable report: a committed-prefix divergence must emit a */
/* parseable per-member line set and a committed-vs-uncommitted */
/* classification (task 1.1, R7.1/R7.5). Captured via open_memstream so */
/* the exact greppable tokens are asserted, not just the verdict. */
/* ------------------------------------------------------------------ */
static void test_machine_report_committed_divergence(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);
char* buf = NULL;
size_t len = 0;
FILE* out = open_memstream(&buf, &len);
TEST_ASSERT_NOT_NULL(out);
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
walcompare_divergence_t div;
int rc = walcompare(m, 2, out, &div);
fclose(out);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
/* committed-vs-uncommitted classification line. */
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP_DIVERGENCE"));
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
/* parseable per-member lines with fixed field order. */
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=3"));
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=1 lsn=3"));
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
/* divergence struct carries the committed classification flag. */
TEST_ASSERT_EQUAL_INT(1, div.committed);
free(buf);
}
/* ------------------------------------------------------------------ */
/* walcompare_report emits a full per-member, per-committed-LSN table */
/* (lsn, term, pcrc, status) for identical members and a clean verdict. */
/* ------------------------------------------------------------------ */
static void test_report_full_member_table(void)
{
wal_spec_t specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" },
{ 2, WAL_JOB_SUBMIT, "jobB" },
};
char a[256], b[256], c[256];
build_member(a, sizeof(a), specs, 3);
build_member(b, sizeof(b), specs, 3);
build_member(c, sizeof(c), specs, 3);
char* buf = NULL;
size_t len = 0;
FILE* out = open_memstream(&buf, &len);
TEST_ASSERT_NOT_NULL(out);
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
walcompare_divergence_t div;
int rc = walcompare_report(m, 3, out, &div);
fclose(out);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
/* every member appears at every committed LSN with committed=1. */
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=1"));
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=2 lsn=3"));
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=1"));
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
free(buf);
}
/* ------------------------------------------------------------------ */
/* A MISSING committed entry is tagged status=MISSING in the report, */
/* and classified class=committed (a committed-prefix hole, R4.2/R6.4). */
/* ------------------------------------------------------------------ */
static void test_report_missing_tagged(void)
{
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);
char* buf = NULL;
size_t len = 0;
FILE* out = open_memstream(&buf, &len);
TEST_ASSERT_NOT_NULL(out);
walcompare_member_t m[2] = { { a, 3 }, { b, 3 } };
walcompare_divergence_t div;
int rc = walcompare_report(m, 2, out, &div);
fclose(out);
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
TEST_ASSERT_NOT_NULL(strstr(buf, "status=MISSING"));
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
TEST_ASSERT_EQUAL_INT(1, div.committed);
free(buf);
}
/* ------------------------------------------------------------------ */
/* A divergence in the UNCOMMITTED tail (above the committed boundary) is */
/* reported class=uncommitted and is NON-fatal: the committed verdict is */
/* still WALCMP_OK (task 1.1 classification; §6.1 committed-prefix only). */
/* ------------------------------------------------------------------ */
static void test_report_uncommitted_tail_classified(void)
{
wal_spec_t a_specs[] = {
{ 1, WAL_NOOP, NULL },
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* committed LSN 2 */
{ 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);
char* buf = NULL;
size_t len = 0;
FILE* out = open_memstream(&buf, &len);
TEST_ASSERT_NOT_NULL(out);
/* committed boundary pinned at 2 → LSN-3 divergence is the tail. */
walcompare_member_t m[2] = { { a, 2 }, { b, 2 } };
walcompare_divergence_t div;
int rc = walcompare_report(m, 2, out, &div);
fclose(out);
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc); /* tail divergence is non-fatal */
TEST_ASSERT_NOT_NULL(strstr(buf, "class=uncommitted"));
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=0"));
free(buf);
}
void test_walcompare(void)
{
UnitySetTestFile(__FILE__);
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_checkpoint_floor_excludes_compacted_range);
RUN_TEST(test_checkpoint_floor_does_not_mask_real_divergence);
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);
RUN_TEST(test_machine_report_committed_divergence);
RUN_TEST(test_report_full_member_table);
RUN_TEST(test_report_missing_tagged);
RUN_TEST(test_report_uncommitted_tail_classified);
}