myxce/src/controller/testsuite/test_persist_wal.c
Rongsong Shen b0a5fbb4e9 feat: persist.c WAL writer with chained CRC32 and dedicated fsync pthread (P0-1)
- Expose persist_t struct concretely in persist.h (opaque pattern deferred)
- persist_set_workdir() derives wal/snap paths from workdir+name
- persist_init() creates WAL/snap directories, rejects NFS mounts
- persist_wal_async() writes WAL entries with chained CRC32 via zlib
- Dedicated fsync pthread fires every wal_sync_ms milliseconds
- persist_flush() forces fdatasync for tests and clean shutdown
- WAL_CHECKPOINT type rejected with -EINVAL (never written via async path)
- Stubs: persist_recover, persist_wal_sync_append, persist_checkpoint
- Add fsync_thread_running flag to guard persist_stop when thread not started
- Add zlib_dep to controller and unit test meson targets
- 3 new TDD tests: init_creates_directories, wal_async_writes_entry,
  checkpoint_type_rejected — all GREEN
2026-05-07 10:00:10 +00:00

140 lines
4.1 KiB
C

#define _GNU_SOURCE
#include <assert.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
#include "config.h"
#include "persist.h"
/* ------------------------------------------------------------------ */
/* Test-local helpers */
/* ------------------------------------------------------------------ */
static char g_tmpdir[256];
/* Count *.wal files in a directory */
static int count_wal_files(const char* dir_path)
{
DIR* dir = opendir(dir_path);
if (!dir) return 0;
int count = 0;
struct dirent* ent;
while ((ent = readdir(dir)) != NULL) {
size_t len = strlen(ent->d_name);
if (len > 4 && strcmp(ent->d_name + len - 4, ".wal") == 0)
count++;
}
closedir(dir);
return count;
}
/* Allocate and initialise a persist_t for testing */
static persist_t* make_test_persist(void)
{
strcpy(g_tmpdir, "/tmp/xce_test_XXXXXX");
if (!mkdtemp(g_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; /* slow background fsync — don't want it firing during tests */
persist_set_workdir(p, g_tmpdir, "ctrl-test");
if (persist_init(p, &cfg, NULL) != 0) {
free(p);
return NULL;
}
return p;
}
static void cleanup_test_persist(persist_t* p)
{
persist_stop(p);
persist_fini(p);
free(p);
/* tmpdir is left on disk — /tmp is ephemeral */
}
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
static void test_persist_init_creates_directories(void)
{
persist_t* p = make_test_persist();
TEST_ASSERT_NOT_NULL(p);
char wal_path[512], snap_path[512];
snprintf(wal_path, sizeof(wal_path), "%s/ctrl-test/data/wal", g_tmpdir);
snprintf(snap_path, sizeof(snap_path), "%s/ctrl-test/data/snap", g_tmpdir);
struct stat st;
TEST_ASSERT_EQUAL_INT(0, stat(wal_path, &st));
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
TEST_ASSERT_EQUAL_INT(0, stat(snap_path, &st));
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
cleanup_test_persist(p);
printf("test_persist_init_creates_directories: PASS\n");
}
static void test_persist_wal_async_writes_entry(void)
{
persist_t* p = make_test_persist();
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_INT(0, persist_start(p));
const char payload[] = "test_job_submit_payload";
int rc = persist_wal_async(p, WAL_JOB_UPDATE, payload,
(uint32_t)strlen(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
/* Force sync so WAL is on disk */
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
/* WAL directory must contain at least one .wal segment file */
char wal_path[512];
snprintf(wal_path, sizeof(wal_path), "%s/ctrl-test/data/wal", g_tmpdir);
int found = count_wal_files(wal_path);
TEST_ASSERT_TRUE(found >= 1);
cleanup_test_persist(p);
printf("test_persist_wal_async_writes_entry: PASS\n");
}
static void test_wal_checkpoint_type_rejected(void)
{
persist_t* p = make_test_persist();
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_INT(0, persist_start(p));
/* WAL_CHECKPOINT must never be passed to persist_wal_async —
* it must return a non-zero error code (not abort/assert). */
int rc = persist_wal_async(p, WAL_CHECKPOINT, NULL, 0);
TEST_ASSERT_TRUE(rc != 0);
cleanup_test_persist(p);
printf("test_wal_checkpoint_type_rejected: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point (called from controller_test.c) */
/* ------------------------------------------------------------------ */
void test_persist_wal(void)
{
printf("\n>> persist WAL writer test\n\n");
RUN_TEST(test_persist_init_creates_directories);
RUN_TEST(test_persist_wal_async_writes_entry);
RUN_TEST(test_wal_checkpoint_type_rejected);
}