test(ha-config-bootstrap): add RED-phase tests for persist_truncate_wal_above (task 7.4)

This commit is contained in:
Rongsong Shen 2026-05-28 14:00:54 +00:00
parent 30bcb74a67
commit e7da075315
4 changed files with 248 additions and 1 deletions

View file

@ -168,7 +168,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A);
- _Requirements: 5.1, 5.5, 5.8, 10.3_
- _Boundary: Replication Layer (repl.c)_
- [ ] 7.4 Write failing tests for persist_truncate_wal_above()
- [x] 7.4 Write failing tests for persist_truncate_wal_above()
- In `src/controller/testsuite/test_persist_truncate.c`: test truncation at segment boundary deletes whole segment; test truncation mid-segment scans sequentially to correct byte offset; test idempotency (calling twice with same `cutoff_lsn` is safe); test `applied_lsn` updated atomically; test no-op when `cutoff_lsn == applied_lsn`
- `make test` fails
- _Requirements: 5.6, 5.7_

View file

@ -107,6 +107,7 @@ controller_unit_test_src = [
'testsuite/test_ha_bootstrap_support.c',
'testsuite/test_ha_durability.c',
'testsuite/test_repl_majority.c',
'testsuite/test_persist_truncate.c',
'startup_validation.c',
'worker.c',
'persist.c',

View file

@ -39,6 +39,7 @@ extern void test_ha_election(void);
extern void test_ha_bootstrap(void);
extern void test_ha_durability(void);
extern void test_repl_majority(void);
extern void test_persist_truncate(void);
void setUp(void) { return; }
@ -147,6 +148,7 @@ int main(int argc, char* argv[])
TEST(test_ha_bootstrap);
TEST(test_ha_durability);
TEST(test_repl_majority);
TEST(test_persist_truncate);
return UNITY_END();
}

View file

@ -0,0 +1,244 @@
/*
* 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 <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
#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();
}