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.
650 lines
25 KiB
C
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);
|
|
}
|