#define _GNU_SOURCE #include #include #include #include #include #include #include #include #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) { UnitySetTestFile(__FILE__); 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); }