- 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
140 lines
4.1 KiB
C
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);
|
|
}
|