185 lines
5.8 KiB
C
185 lines
5.8 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");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Schedule-checkpoint and WAL record-count threshold tests */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* test: schedule_checkpoint CAS guard is idempotent */
|
|
static void test_schedule_checkpoint_cas_idempotent(void)
|
|
{
|
|
/* Simulate what schedule_checkpoint does: CAS 0→1 twice.
|
|
* Second must fail (idempotency guard). */
|
|
atomic_int flag = 0;
|
|
int expected = 0;
|
|
int swapped = atomic_compare_exchange_strong(&flag, &expected, 1);
|
|
TEST_ASSERT_EQUAL_INT(1, swapped); /* first CAS succeeds */
|
|
TEST_ASSERT_EQUAL_INT(1, atomic_load(&flag));
|
|
|
|
expected = 0;
|
|
swapped = atomic_compare_exchange_strong(&flag, &expected, 1);
|
|
TEST_ASSERT_EQUAL_INT(0, swapped); /* second CAS fails — idempotent */
|
|
atomic_store(&flag, 0); /* cleanup */
|
|
}
|
|
|
|
/* test: wal_record_count increments and triggers at threshold */
|
|
static void test_wal_record_count_threshold(void)
|
|
{
|
|
persist_config_t cfg = persist_config_defaults();
|
|
cfg.max_wal_records = 5;
|
|
|
|
atomic_uint count = 0;
|
|
for (int i = 0; i < 5; i++)
|
|
atomic_fetch_add(&count, 1);
|
|
|
|
TEST_ASSERT_TRUE(atomic_load(&count) >= cfg.max_wal_records);
|
|
}
|
|
|
|
/* test: persist_config_defaults has max_wal_records set */
|
|
static void test_persist_config_defaults_max_wal_records(void)
|
|
{
|
|
persist_config_t cfg = persist_config_defaults();
|
|
TEST_ASSERT_EQUAL_UINT(DEFAULT_MAX_WAL_RECORDS, cfg.max_wal_records);
|
|
TEST_ASSERT_TRUE(cfg.max_wal_records >= 100);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 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);
|
|
RUN_TEST(test_schedule_checkpoint_cas_idempotent);
|
|
RUN_TEST(test_wal_record_count_threshold);
|
|
RUN_TEST(test_persist_config_defaults_max_wal_records);
|
|
}
|