myxce/src/controller/testsuite/test_e2e_wal.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

270 lines
8.7 KiB
C

/* P0-15a: WAL integrity end-to-end smoke tests
*
* Exercises cross-phase interactions:
* - WAL writer (P0-B) produces segment files that can be read back
* - Torn-write simulation (ftruncate) + persist_recover() (P0-C)
* - Chained-CRC corruption detection during recovery replay
*/
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#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"
#include "persist.h"
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static char g_e2e_wal_tmpdir[256];
static persist_t* make_e2e_wal_persist(void)
{
strcpy(g_e2e_wal_tmpdir, "/tmp/xce_e2e_wal_XXXXXX");
if (!mkdtemp(g_e2e_wal_tmpdir)) return NULL;
persist_t* p = calloc(1, sizeof(persist_t));
if (!p) return NULL;
persist_config_t cfg = persist_config_defaults();
cfg.wal_sync_ms = 5000; /* suppress background fsync races */
persist_set_workdir(p, g_e2e_wal_tmpdir, "e2e-wal");
if (persist_init(p, &cfg, NULL) != 0) { free(p); return NULL; }
if (persist_start(p) != 0) { persist_fini(p); free(p); return NULL; }
return p;
}
static void teardown_e2e_wal(persist_t* p)
{
persist_stop(p);
persist_fini(p);
free(p);
}
/* Return the path to the first .wal segment in the WAL directory. */
static int find_first_wal_segment(const char* wal_dir, char* out, size_t outsz)
{
DIR* d = opendir(wal_dir);
if (!d) return -1;
struct dirent* e;
while ((e = readdir(d)) != NULL) {
size_t len = strlen(e->d_name);
if (len > 4 && strcmp(e->d_name + len - 4, ".wal") == 0) {
snprintf(out, outsz, "%s/%s", wal_dir, e->d_name);
closedir(d);
return 0;
}
}
closedir(d);
return -1;
}
/* Count valid wal_entry_hdr_t records in a segment file by reading
* sequentially and checking header CRC (first 32 bytes of header). */
static int count_valid_wal_entries(const char* seg_path)
{
int fd = open(seg_path, O_RDONLY);
if (fd < 0) return -1;
int count = 0;
off_t offset = 0;
for (;;) {
wal_entry_hdr_t hdr;
ssize_t n = pread(fd, &hdr, sizeof(hdr), offset);
if (n != (ssize_t)sizeof(hdr)) break;
/* Verify header CRC (first WAL_HDR_CRC_SIZE bytes) */
uint32_t expected_crc = (uint32_t)crc32(0L,
(const unsigned char*)&hdr, WAL_HDR_CRC_SIZE);
if (hdr.payload_len > 0) {
unsigned char* payload = malloc(hdr.payload_len);
if (!payload) break;
ssize_t pn = pread(fd, payload, hdr.payload_len,
offset + (off_t)sizeof(hdr));
if (pn == (ssize_t)hdr.payload_len)
expected_crc = (uint32_t)crc32(expected_crc,
payload, hdr.payload_len);
free(payload);
}
if (expected_crc != hdr.crc) break;
count++;
offset += (off_t)(sizeof(hdr) + hdr.payload_len);
}
close(fd);
return count;
}
/* ------------------------------------------------------------------ */
/* Test 1: write 10 WAL entries and verify they appear in segment file */
/* ------------------------------------------------------------------ */
static void test_wal_write_and_verify_entry_count(void)
{
persist_t* p = make_e2e_wal_persist();
TEST_ASSERT_NOT_NULL(p);
const char payload[] = "e2e-entry";
for (int i = 0; i < 10; i++) {
int rc = persist_wal_async(p, WAL_JOB_UPDATE, payload,
(uint32_t)strlen(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
}
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
char seg_path[512];
TEST_ASSERT_EQUAL_INT(0,
find_first_wal_segment(p->wal_path, seg_path, sizeof(seg_path)));
int count = count_valid_wal_entries(seg_path);
TEST_ASSERT_EQUAL_INT(10, count);
teardown_e2e_wal(p);
printf("test_wal_write_and_verify_entry_count: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Test 2: torn-write recovery — truncate last entry, recover, check */
/* ------------------------------------------------------------------ */
static void test_wal_torn_write_recovery(void)
{
/* Phase 1: write 5 entries and flush */
persist_t* p = make_e2e_wal_persist();
TEST_ASSERT_NOT_NULL(p);
const char payload[] = "torn";
for (int i = 0; i < 5; i++) {
TEST_ASSERT_EQUAL_INT(0,
persist_wal_async(p, WAL_JOB_UPDATE, payload,
(uint32_t)strlen(payload)));
}
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
/* Locate segment file and determine size of 4 complete entries */
char seg_path[512];
TEST_ASSERT_EQUAL_INT(0,
find_first_wal_segment(p->wal_path, seg_path, sizeof(seg_path)));
/* One entry = sizeof(wal_entry_hdr_t) + payload_len */
off_t entry_size = (off_t)(sizeof(wal_entry_hdr_t) + strlen(payload));
off_t trunc_offset = entry_size * 4; /* keep only 4 complete entries */
/* Truncate: simulate torn write on entry 5 */
int fd = open(seg_path, O_RDWR);
TEST_ASSERT_TRUE(fd >= 0);
TEST_ASSERT_EQUAL_INT(0, ftruncate(fd, trunc_offset));
close(fd);
uint64_t lsn_before = p->current_lsn;
teardown_e2e_wal(p); /* stop / destroy the first instance */
/* Phase 2: fresh persist_t pointing at the same directory */
persist_t* p2 = calloc(1, sizeof(persist_t));
TEST_ASSERT_NOT_NULL(p2);
persist_config_t cfg = persist_config_defaults();
cfg.wal_sync_ms = 5000;
persist_set_workdir(p2, g_e2e_wal_tmpdir, "e2e-wal");
TEST_ASSERT_EQUAL_INT(0, persist_init(p2, &cfg, NULL));
TEST_ASSERT_EQUAL_INT(0, persist_start(p2));
int rc = persist_recover(p2);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Recovery must have replayed at most 4 entries (not 5 — torn write) */
TEST_ASSERT_TRUE(p2->current_lsn < lsn_before);
teardown_e2e_wal(p2);
printf("test_wal_torn_write_recovery: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Test 3: chained CRC corruption stops replay at last valid entry */
/* ------------------------------------------------------------------ */
static void test_wal_chained_crc_detects_corruption(void)
{
/* Phase 1: write 5 entries and flush */
persist_t* p = make_e2e_wal_persist();
TEST_ASSERT_NOT_NULL(p);
const char payload[] = "crc-chain";
for (int i = 0; i < 5; i++) {
TEST_ASSERT_EQUAL_INT(0,
persist_wal_async(p, WAL_JOB_UPDATE, payload,
(uint32_t)strlen(payload)));
}
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
char seg_path[512];
TEST_ASSERT_EQUAL_INT(0,
find_first_wal_segment(p->wal_path, seg_path, sizeof(seg_path)));
/* Corrupt payload of entry 3 (zero-indexed: entries 0,1 valid; entry 2
* corrupted; entries 3,4 unreachable via CRC chain). */
off_t entry_size = (off_t)(sizeof(wal_entry_hdr_t) + strlen(payload));
off_t corrupt_offset = entry_size * 2 + (off_t)sizeof(wal_entry_hdr_t);
/* Write garbage over the payload of entry 2 */
int fd = open(seg_path, O_RDWR);
TEST_ASSERT_TRUE(fd >= 0);
const char garbage[] = "\xFF\xFF\xFF\xFF";
pwrite(fd, garbage, sizeof(garbage) - 1, corrupt_offset);
close(fd);
uint64_t lsn_before = p->current_lsn;
teardown_e2e_wal(p);
/* Phase 2: fresh persist_t — recover must stop before the corrupt entry */
persist_t* p2 = calloc(1, sizeof(persist_t));
TEST_ASSERT_NOT_NULL(p2);
persist_config_t cfg = persist_config_defaults();
cfg.wal_sync_ms = 5000;
persist_set_workdir(p2, g_e2e_wal_tmpdir, "e2e-wal");
TEST_ASSERT_EQUAL_INT(0, persist_init(p2, &cfg, NULL));
TEST_ASSERT_EQUAL_INT(0, persist_start(p2));
int rc = persist_recover(p2);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Must have replayed fewer than 5 entries — stopped at corruption */
TEST_ASSERT_TRUE(p2->current_lsn < lsn_before);
teardown_e2e_wal(p2);
printf("test_wal_chained_crc_detects_corruption: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point */
/* ------------------------------------------------------------------ */
void test_e2e_wal(void)
{
UnitySetTestFile(__FILE__);
printf("\n>> P0-15a: WAL E2E — torn write + CRC corruption recovery\n\n");
RUN_TEST(test_wal_write_and_verify_entry_count);
RUN_TEST(test_wal_torn_write_recovery);
RUN_TEST(test_wal_chained_crc_detects_corruption);
}