/* P0-15d: GC E2E — retention + transient job cleanup * * Exercises cross-phase interactions: * - GC retention predicates (P0-E) applied to concrete job/worker fixtures * - JOB_STAT_TRANSIENT timeout behaviour * - Checkpoint succeeds after WAL writes (P0-C integration smoke) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include "config.h" #include "persist.h" #include "xce/types.h" /* ------------------------------------------------------------------ */ /* Fixtures */ /* ------------------------------------------------------------------ */ static job_t make_gc_job(unsigned int state, time_t finish_time, uint64_t transient_since) { job_t j; memset(&j, 0, sizeof(j)); j.job.state = state; j.job.finish_time = finish_time; j.job.transient_since = (uint64_t)transient_since; return j; } static xce_worker_t make_gc_worker(uint32_t status, time_t dead_since) { xce_worker_t w; memset(&w, 0, sizeof(w)); w.status = status; w.dead_since = dead_since; return w; } /* ------------------------------------------------------------------ */ /* Test 1: running job must be retained */ /* ------------------------------------------------------------------ */ static void test_job_gc_retain_running_job(void) { persist_config_t cfg = persist_config_defaults(); job_t j = make_gc_job(JOB_STAT_RUN, 0, 0); TEST_ASSERT_TRUE(job_should_retain(&j, &cfg)); printf("test_job_gc_retain_running_job: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 2: expired finished job must not be retained */ /* ------------------------------------------------------------------ */ static void test_job_gc_cleanup_expired_finished_job(void) { time_t now = time(NULL); persist_config_t cfg = persist_config_defaults(); cfg.finished_retain_s = 3600; job_t j = make_gc_job(JOB_STAT_EXIT, now - 7200, 0); TEST_ASSERT_FALSE(job_should_retain(&j, &cfg)); printf("test_job_gc_cleanup_expired_finished_job: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 3: transient job times out */ /* ------------------------------------------------------------------ */ static void test_transient_job_times_out(void) { time_t now = time(NULL); persist_config_t cfg = persist_config_defaults(); cfg.transient_job_timeout_s = 300; /* transient_since = now - 400 seconds — past the 300s window */ job_t j = make_gc_job(JOB_STAT_TRANSIENT, 0, (uint64_t)(now - 400)); TEST_ASSERT_FALSE(job_should_retain(&j, &cfg)); printf("test_transient_job_times_out: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 4: expired unavailable worker must not be retained */ /* ------------------------------------------------------------------ */ static void test_worker_gc_cleanup_expired_unavailable(void) { time_t now = time(NULL); persist_config_t cfg = persist_config_defaults(); cfg.worker_retain_s = 86400; /* 24 h */ xce_worker_t w = make_gc_worker(XCE_WORKER_UNAVAIL, now - 100000); TEST_ASSERT_FALSE(worker_should_retain(&w, &cfg)); printf("test_worker_gc_cleanup_expired_unavailable: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 5: checkpoint succeeds and creates snapshot (integration smoke)*/ /* ------------------------------------------------------------------ */ static void test_checkpoint_gc_removes_expired_entity(void) { char tmpdir[256]; strcpy(tmpdir, "/tmp/xce_e2e_gc_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t* p = calloc(1, sizeof(persist_t)); TEST_ASSERT_NOT_NULL(p); persist_config_t cfg = persist_config_defaults(); cfg.wal_sync_ms = 5000; cfg.snap_keep_n = 2; persist_set_workdir(p, tmpdir, "e2e-gc"); TEST_ASSERT_EQUAL_INT(0, persist_init(p, &cfg, NULL)); TEST_ASSERT_EQUAL_INT(0, persist_start(p)); /* Write some WAL entries (representing state changes) */ for (int i = 0; i < 5; i++) TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "gc-test", 7)); /* No live apply pump runs in this cluster-less fixture, so * last_applied_lsn stays 0 unless simulated -- persist_checkpoint now * stamps checkpoint_lsn from last_applied_lsn, not current_lsn * (checkpoint_lsn-vs-last_applied_lsn fix); simulate apply having * caught up to what was appended before the checkpoint runs. */ atomic_store_explicit(&p->last_applied_lsn, p->current_lsn - 1, memory_order_relaxed); /* Trigger checkpoint — must succeed and leave a snapshot */ TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p)); TEST_ASSERT_TRUE(p->checkpoint_lsn > 0); char snap_path[512]; snprintf(snap_path, sizeof(snap_path), "%s/e2e-gc/data/snap", tmpdir); DIR* d = opendir(snap_path); TEST_ASSERT_NOT_NULL(d); int found = 0; struct dirent* e; while ((e = readdir(d)) != NULL) { size_t len = strlen(e->d_name); if (len > 6 && strcmp(e->d_name + len - 6, ".capnp") == 0) found++; } closedir(d); TEST_ASSERT_TRUE(found >= 1); persist_stop(p); persist_fini(p); free(p); printf("test_checkpoint_gc_removes_expired_entity: PASS\n"); } /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ void test_e2e_gc(void) { UnitySetTestFile(__FILE__); printf("\n>> P0-15d: GC E2E — retention + transient job cleanup\n\n"); RUN_TEST(test_job_gc_retain_running_job); RUN_TEST(test_job_gc_cleanup_expired_finished_job); RUN_TEST(test_transient_job_times_out); RUN_TEST(test_worker_gc_cleanup_expired_unavailable); RUN_TEST(test_checkpoint_gc_removes_expired_entity); }