/* * test_repl_apply.c — P2-2: Tests for apply_wal_entry() standby WAL replay. * * TDD RED phase: written BEFORE the implementation. * These tests verify: * - LSN advances past replayed entry * - WAL_CHECKPOINT entries are skipped (rc=0, lsn unchanged) * - Oversized payloads are rejected (rc != 0) * - Unknown types (> WAL_ENTRY_TYPE_MAX) are skipped with rc=0 */ #include #include #include #include #include "config.h" #include "persist.h" /* ------------------------------------------------------------------ */ /* Weak stubs for P0 entity-restore functions (job.c / worker.c). */ /* The unit test binary does not link those translation units — they */ /* pull in the full cluster.h / netio.h chain. apply_wal_entry() only */ /* calls these when p->cluster != NULL; tests use NULL cluster, so the */ /* weak stubs are never reached. The linker needs the symbols to link. */ /* ------------------------------------------------------------------ */ typedef struct cluster_s cluster_t; __attribute__((weak)) int restore_job(cluster_t* c, uint64_t jobid, const void* capnp_payload, size_t len) { (void)c; (void)jobid; (void)capnp_payload; (void)len; return 0; } __attribute__((weak)) int worker_from_capnp(cluster_t* c, const void* capnp_payload, size_t len) { (void)c; (void)capnp_payload; (void)len; return 0; } __attribute__((weak)) int restore_job_state(cluster_t* c, uint64_t jobid, uint32_t state) { (void)c; (void)jobid; (void)state; return 0; } __attribute__((weak)) int restore_queue_state(cluster_t* c, const char* name, uint32_t status) { (void)c; (void)name; (void)status; return 0; } /* ------------------------------------------------------------------ */ /* Test helpers */ /* ------------------------------------------------------------------ */ static persist_t* make_test_persist_for_apply(void) { persist_t* p = calloc(1, sizeof(persist_t)); if (!p) { fprintf(stderr, "calloc failed\n"); exit(1); } persist_config_t cfg = persist_config_defaults(); cfg.wal_sync_ms = 5000; /* slow fsync — we don't need it in unit tests */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/xce_apply_XXXXXX"); if (!mkdtemp(tmpdir)) { fprintf(stderr, "mkdtemp failed\n"); free(p); exit(1); } persist_set_workdir(p, tmpdir, "ctrl-apply"); persist_init(p, &cfg, NULL); /* NULL cluster — entity stubs gate on p->cluster */ persist_start(p); return p; } static void destroy_test_persist(persist_t* p) { persist_stop(p); persist_fini(p); free(p); } /* ------------------------------------------------------------------ */ /* Tests */ /* ------------------------------------------------------------------ */ static void test_apply_wal_entry_advances_lsn(void) { persist_t* p = make_test_persist_for_apply(); wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.type = WAL_JOB_UPDATE; hdr.lsn = 42; hdr.term = 1; hdr.payload_len = 4; hdr.crc = 0xDEADBEEF; hdr.prev_crc = WAL_CRC_NONE; uint8_t payload[4] = {1, 2, 3, 4}; int rc = apply_wal_entry(p, &hdr, payload); TEST_ASSERT_EQUAL_INT(0, rc); /* current_lsn must have advanced to at least lsn+1 */ TEST_ASSERT_TRUE(p->current_lsn > 42); /* last_crc must reflect the entry's crc */ TEST_ASSERT_EQUAL_UINT32(0xDEADBEEF, p->last_crc); destroy_test_persist(p); printf("test_apply_wal_entry_advances_lsn: PASS\n"); } static void test_apply_wal_entry_skips_checkpoint(void) { persist_t* p = make_test_persist_for_apply(); uint64_t lsn_before = p->current_lsn; wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.type = WAL_CHECKPOINT; hdr.lsn = 100; hdr.term = 1; hdr.payload_len = 0; int rc = apply_wal_entry(p, &hdr, NULL); TEST_ASSERT_EQUAL_INT(0, rc); /* skip = success, not error */ /* Checkpoint entries are local-only markers — standbys never receive them. * current_lsn must NOT advance: skip means no-op. */ TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn); destroy_test_persist(p); printf("test_apply_wal_entry_skips_checkpoint: PASS\n"); } static void test_apply_wal_entry_rejects_oversized(void) { persist_t* p = make_test_persist_for_apply(); wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.type = WAL_JOB_SUBMIT; hdr.lsn = 1; hdr.term = 1; hdr.payload_len = MAX_WAL_PAYLOAD_BYTES + 1; /* too large */ int rc = apply_wal_entry(p, &hdr, NULL); TEST_ASSERT_NOT_EQUAL(0, rc); /* must be rejected */ destroy_test_persist(p); printf("test_apply_wal_entry_rejects_oversized: PASS\n"); } static void test_apply_wal_entry_skips_unknown_type(void) { persist_t* p = make_test_persist_for_apply(); uint64_t lsn_before = p->current_lsn; /* * Type 200 is above WAL_ENTRY_TYPE_MAX (100). * Spec: log WARN and skip with rc=0 (forward compatibility). */ wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.type = 200; hdr.lsn = 5; hdr.term = 1; hdr.payload_len = 0; int rc = apply_wal_entry(p, &hdr, NULL); TEST_ASSERT_EQUAL_INT(0, rc); /* unknown type: skip not error */ /* LSN must NOT advance for a skipped-unknown type */ TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn); destroy_test_persist(p); printf("test_apply_wal_entry_skips_unknown_type: PASS\n"); } static void test_apply_wal_entry_null_guard(void) { /* Both NULL inputs must return non-zero (EINVAL) */ int rc = apply_wal_entry(NULL, NULL, NULL); TEST_ASSERT_NOT_EQUAL(0, rc); printf("test_apply_wal_entry_null_guard: PASS\n"); } /* ------------------------------------------------------------------ */ /* Suite entry point (registered in controller_test.c) */ /* ------------------------------------------------------------------ */ void test_repl_apply(void) { RUN_TEST(test_apply_wal_entry_advances_lsn); RUN_TEST(test_apply_wal_entry_skips_checkpoint); RUN_TEST(test_apply_wal_entry_rejects_oversized); RUN_TEST(test_apply_wal_entry_skips_unknown_type); RUN_TEST(test_apply_wal_entry_null_guard); }