/* * test_persist_truncate.c — RED-phase tests for persist_truncate_wal_above(). * * Function does not exist yet (task 7.5). Will fail to link until implemented. * * Requirements: 5.6, 5.7 */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "persist.h" #include "wal_types.h" #include "config.h" /* ── Forward declaration of function-under-test (not yet implemented) ─────── */ extern int persist_truncate_wal_above(persist_t *p, uint64_t cutoff_lsn); /* ── Helpers ────────────────────────────────────────────────────────────────── */ #define WAL_SEG_FMT "%s/data/wal/seg-%016" PRIx64 ".wal" static persist_t *make_fresh_persist(const char *tmpdir) { persist_t *p = calloc(1, sizeof(persist_t)); if (!p) return NULL; persist_config_t cfg = persist_config_defaults(); cfg.wal_sync_ms = 5000; cfg.snap_keep_n = 2; persist_set_workdir(p, tmpdir, "trunc-test"); if (persist_init(p, &cfg, NULL) != 0) { free(p); return NULL; } if (persist_start(p) != 0) { persist_fini(p); free(p); return NULL; } return p; } static void teardown_persist(persist_t *p) { persist_stop(p); persist_fini(p); free(p); } /* Write a minimal wal_entry_hdr_t with the given lsn to an open fd at offset */ static int write_wal_entry(int fd, uint64_t lsn, off_t offset) { wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.type = WAL_JOB_SUBMIT; hdr.lsn = lsn; hdr.payload_len = 0; hdr.crc = 0; hdr.prev_crc = 0; ssize_t n = pwrite(fd, &hdr, sizeof(hdr), offset); return (n == (ssize_t)sizeof(hdr)) ? 0 : -EIO; } /* Count .wal files in the WAL directory */ static int count_wal_segments(const char *wal_dir) { DIR *d = opendir(wal_dir); if (!d) return 0; int count = 0; struct dirent *e; while ((e = readdir(d)) != NULL) { size_t len = strlen(e->d_name); if (len > 4 && strcmp(e->d_name + len - 4, ".wal") == 0) count++; } closedir(d); return count; } /* ── Test 1: truncation at segment boundary deletes whole segment ──────────── */ static void test_truncate_at_segment_boundary_deletes_segment(void) { char tmpdir[] = "/tmp/xce-trunc-test-XXXXXX"; TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t *p = make_fresh_persist(tmpdir); TEST_ASSERT_NOT_NULL(p); /* Write 3 WAL entries so we have a known LSN sequence */ int dummy = 0; persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); uint64_t lsn_before = p->current_lsn; /* Append a 4th entry — this represents an uncommitted entry above cutoff */ persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); /* Truncate above lsn_before-1 (the 3rd entry); 4th should be removed */ int rc = persist_truncate_wal_above(p, lsn_before - 1); TEST_ASSERT_EQUAL_INT(0, rc); /* After truncation, current_lsn (or applied_lsn) should reflect cutoff */ TEST_ASSERT_TRUE(p->current_lsn <= lsn_before); teardown_persist(p); /* Cleanup */ char wal_dir[256]; snprintf(wal_dir, sizeof(wal_dir), "%s/data/wal", tmpdir); printf("test_truncate_at_segment_boundary_deletes_segment: PASS\n"); } /* ── Test 2: truncation mid-segment scans to correct byte offset ───────────── */ static void test_truncate_mid_segment_at_correct_offset(void) { char tmpdir[] = "/tmp/xce-trunc-mid-XXXXXX"; TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t *p = make_fresh_persist(tmpdir); TEST_ASSERT_NOT_NULL(p); int dummy = 0; /* Write 5 entries, truncate above entry 3 (lsn=2), entries 3-4 gone */ for (int i = 0; i < 5; i++) persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); uint64_t cutoff = 2; /* keep entries with lsn <= 2 */ int rc = persist_truncate_wal_above(p, cutoff); TEST_ASSERT_EQUAL_INT(0, rc); /* applied_lsn / current_lsn must be <= cutoff + 1 */ TEST_ASSERT_TRUE(p->current_lsn <= cutoff + 1); teardown_persist(p); printf("test_truncate_mid_segment_at_correct_offset: PASS\n"); } /* ── Test 3: idempotency — calling twice with same cutoff is safe ──────────── */ static void test_truncate_idempotent(void) { char tmpdir[] = "/tmp/xce-trunc-idem-XXXXXX"; TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t *p = make_fresh_persist(tmpdir); TEST_ASSERT_NOT_NULL(p); int dummy = 0; for (int i = 0; i < 4; i++) persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); uint64_t cutoff = 1; int rc1 = persist_truncate_wal_above(p, cutoff); int rc2 = persist_truncate_wal_above(p, cutoff); TEST_ASSERT_EQUAL_INT(0, rc1); TEST_ASSERT_EQUAL_INT(0, rc2); /* current_lsn consistent after two calls */ TEST_ASSERT_TRUE(p->current_lsn <= cutoff + 1); teardown_persist(p); printf("test_truncate_idempotent: PASS\n"); } /* ── Test 4: applied_lsn updated atomically to cutoff_lsn ─────────────────── */ static void test_truncate_updates_applied_lsn(void) { char tmpdir[] = "/tmp/xce-trunc-lsn-XXXXXX"; TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t *p = make_fresh_persist(tmpdir); TEST_ASSERT_NOT_NULL(p); int dummy = 0; for (int i = 0; i < 6; i++) persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); uint64_t cutoff = 3; int rc = persist_truncate_wal_above(p, cutoff); TEST_ASSERT_EQUAL_INT(0, rc); /* committed_lsn field must exist on persist_t after task 7.5 adds it. * For now, verify current_lsn reflects the truncation. */ TEST_ASSERT_TRUE(p->current_lsn <= cutoff + 1); teardown_persist(p); printf("test_truncate_updates_applied_lsn: PASS\n"); } /* ── Test 5: no-op when cutoff_lsn == applied_lsn (nothing to truncate) ───── */ static void test_truncate_noop_at_current_lsn(void) { char tmpdir[] = "/tmp/xce-trunc-noop-XXXXXX"; TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir)); persist_t *p = make_fresh_persist(tmpdir); TEST_ASSERT_NOT_NULL(p); int dummy = 0; persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); persist_wal_sync_append(p, WAL_JOB_SUBMIT, &dummy, sizeof(dummy), NULL); /* Truncate AT current boundary — should be a no-op */ uint64_t cutoff = p->current_lsn - 1; int rc = persist_truncate_wal_above(p, cutoff); TEST_ASSERT_EQUAL_INT(0, rc); /* Nothing should change */ TEST_ASSERT_TRUE(p->current_lsn <= cutoff + 1); teardown_persist(p); printf("test_truncate_noop_at_current_lsn: PASS\n"); } /* ── Test group entry point ────────────────────────────────────────────────── */ void test_persist_truncate(void) { UNITY_BEGIN(); RUN_TEST(test_truncate_at_segment_boundary_deletes_segment); RUN_TEST(test_truncate_mid_segment_at_correct_offset); RUN_TEST(test_truncate_idempotent); RUN_TEST(test_truncate_updates_applied_lsn); RUN_TEST(test_truncate_noop_at_current_lsn); UNITY_END(); }