#define _GNU_SOURCE #include #include #include #include #include #include #include "config.h" /* WAL_HDR_CRC_SIZE, WAL_CRC_NONE */ #include "controller.capnp.h" /* Job encode for the JOB_SUBMIT decode test */ #include "wal_types.h" #include "waldump.h" /* ------------------------------------------------------------------ * * Test harness: hand-write raw WAL segments so the test fully controls * LSNs (to inject gaps), the CRC chain (to inject a break), and a real * Cap'n Proto Job payload (to exercise the submit jobid decode). This * also pins the on-disk WAL format independently of persist.c. * ------------------------------------------------------------------ */ static char g_dir[256]; /* Append one entry to `fp`, chaining prev_crc from *last_crc and updating it. * Mirrors persist.c's append: CRC32 over WAL_HDR_CRC_SIZE header bytes + payload. */ static void put_entry(FILE* fp, uint64_t term, uint64_t lsn, uint32_t type, const void* payload, uint32_t plen, uint32_t* last_crc, int first_in_seg) { wal_entry_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.term = term; hdr.lsn = lsn; hdr.timestamp = 0; hdr.type = type; hdr.payload_len = plen; hdr.prev_crc = first_in_seg ? WAL_CRC_NONE : *last_crc; uint32_t c = crc32(0L, (const Bytef*)&hdr, WAL_HDR_CRC_SIZE); if (plen > 0) c = crc32(c, (const Bytef*)payload, plen); hdr.crc = c; *last_crc = c; fwrite(&hdr, sizeof(hdr), 1, fp); if (plen > 0) fwrite(payload, plen, 1, fp); } /* Encode a minimal Cap'n Proto Job carrying `jobid`+`subtime` (what a real * WAL_JOB_SUBMIT payload looks like). Returns malloc'd bytes; caller frees. * subtime lets a test make two payloads byte-identical (same subtime) or * distinct (different subtime) to exercise bug-845 dup detection. */ static uint8_t* encode_job(uint64_t jobid, uint64_t subtime, size_t* out_len) { struct capn c; capn_init_malloc(&c); capn_ptr root = capn_root(&c); Job_ptr jp = new_Job(root.seg); struct Job cj; memset(&cj, 0, sizeof(cj)); cj.jobid = jobid; cj.subtime = subtime; cj.state = 1; write_Job(&cj, jp); capn_setp(root, 0, jp.p); size_t sz = capn_size(&c) + 64; uint8_t* buf = malloc(sz); int64_t n = capn_write_mem(&c, buf, sz, 0); capn_free(&c); *out_len = (size_t)n; return buf; } static const char* seg_path(uint64_t start_lsn, char* out, size_t n) { snprintf(out, n, "%s/seg-%016lx.wal", g_dir, (unsigned long)start_lsn); return out; } /* setUp/tearDown are owned by the shared controller_test runner, so each * test mints its own fresh temp WAL dir here instead. */ static void fresh_dir(void) { strcpy(g_dir, "/tmp/xce_waldump_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(g_dir)); } /* ------------------------------------------------------------------ */ /* A healthy, contiguous log from LSN 1: no gaps, tip == last entry. */ /* ------------------------------------------------------------------ */ static void test_contiguous_log_has_no_gaps(void) { fresh_dir(); char path[512]; FILE* fp = fopen(seg_path(1, path, sizeof(path)), "wb"); TEST_ASSERT_NOT_NULL(fp); uint32_t last = WAL_CRC_NONE; put_entry(fp, 0, 1, WAL_NOOP, NULL, 0, &last, 1); size_t p2, p3; uint8_t* j2 = encode_job(1001, 100, &p2); /* distinct jobs → no dup */ uint8_t* j3 = encode_job(1002, 100, &p3); put_entry(fp, 0, 2, WAL_JOB_SUBMIT, j2, (uint32_t)p2, &last, 0); put_entry(fp, 0, 3, WAL_JOB_SUBMIT, j3, (uint32_t)p3, &last, 0); free(j2); free(j3); fclose(fp); waldump_stats_t st; int rc = waldump_path(g_dir, stdout, &st); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT(3, st.n_entries); TEST_ASSERT_EQUAL_UINT(2, st.n_submits); TEST_ASSERT_EQUAL_UINT(0, st.n_gaps); TEST_ASSERT_EQUAL_UINT(0, st.n_dup_submits); /* distinct jobids */ TEST_ASSERT_EQUAL_UINT64(1, st.first_lsn); TEST_ASSERT_EQUAL_UINT64(3, st.last_lsn); TEST_ASSERT_EQUAL_UINT64(3, st.recovered_tip); TEST_ASSERT_EQUAL_INT(0, st.crc_broken); } /* ------------------------------------------------------------------ */ /* The failover bug signature: log starts at a high LSN (empty-rejoin */ /* caught up from the watermark) so the committed prefix is missing. */ /* ------------------------------------------------------------------ */ static void test_missing_prefix_flagged_as_gap(void) { fresh_dir(); char path[512]; /* seg file named from its first LSN, 13 (like ctrl-3 in the live repro) */ FILE* fp = fopen(seg_path(13, path, sizeof(path)), "wb"); TEST_ASSERT_NOT_NULL(fp); uint32_t last = WAL_CRC_NONE; put_entry(fp, 0, 13, WAL_NOOP, NULL, 0, &last, 1); size_t p14, p15; uint8_t* j14 = encode_job(2001, 100, &p14); uint8_t* j15 = encode_job(2002, 100, &p15); put_entry(fp, 0, 14, WAL_JOB_SUBMIT, j14, (uint32_t)p14, &last, 0); put_entry(fp, 0, 15, WAL_JOB_SUBMIT, j15, (uint32_t)p15, &last, 0); free(j14); free(j15); fclose(fp); waldump_stats_t st; int rc = waldump_path(g_dir, stdout, &st); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT(3, st.n_entries); TEST_ASSERT_EQUAL_UINT64(13, st.first_lsn); /* prefix [1..12] absent → exactly one gap */ TEST_ASSERT_EQUAL_UINT(1, st.n_gaps); } /* ------------------------------------------------------------------ */ /* bug-845: each job double-appended (same jobid at adjacent LSN). The */ /* identical-payload case = a literal byte-for-byte double-write. */ /* ------------------------------------------------------------------ */ static void test_duplicate_submit_detection(void) { fresh_dir(); char path[512]; FILE* fp = fopen(seg_path(1, path, sizeof(path)), "wb"); TEST_ASSERT_NOT_NULL(fp); uint32_t last = WAL_CRC_NONE; put_entry(fp, 0, 1, WAL_NOOP, NULL, 0, &last, 1); /* Same jobid + same subtime → byte-identical payload (literal dup). */ size_t pa, pb; uint8_t* ja = encode_job(7001, 555, &pa); uint8_t* jb = encode_job(7001, 555, &pb); put_entry(fp, 0, 2, WAL_JOB_SUBMIT, ja, (uint32_t)pa, &last, 0); put_entry(fp, 0, 3, WAL_JOB_SUBMIT, jb, (uint32_t)pb, &last, 0); /* A second job, also doubled but with a DIFFERING subtime per copy. */ size_t pc, pd; uint8_t* jc = encode_job(7002, 600, &pc); uint8_t* jd = encode_job(7002, 601, &pd); put_entry(fp, 0, 4, WAL_JOB_SUBMIT, jc, (uint32_t)pc, &last, 0); put_entry(fp, 0, 5, WAL_JOB_SUBMIT, jd, (uint32_t)pd, &last, 0); free(ja); free(jb); free(jc); free(jd); fclose(fp); waldump_stats_t st; int rc = waldump_path(g_dir, stdout, &st); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT(4, st.n_submits); TEST_ASSERT_EQUAL_UINT(2, st.n_dup_submits); /* both pairs flagged */ TEST_ASSERT_EQUAL_UINT(1, st.n_dup_identical); /* only the first is byte-identical */ TEST_ASSERT_EQUAL_UINT(0, st.n_gaps); } /* ------------------------------------------------------------------ */ /* An interior hole (a skipped LSN between two present entries). */ /* ------------------------------------------------------------------ */ static void test_interior_gap_flagged(void) { fresh_dir(); /* Two segments: [1..2] then [5..6]; 3,4 are missing. */ char p1[512], p2[512]; FILE* f1 = fopen(seg_path(1, p1, sizeof(p1)), "wb"); uint32_t last = WAL_CRC_NONE; put_entry(f1, 0, 1, WAL_NOOP, NULL, 0, &last, 1); put_entry(f1, 0, 2, WAL_JOB_UPDATE, "x", 1, &last, 0); fclose(f1); FILE* f2 = fopen(seg_path(5, p2, sizeof(p2)), "wb"); uint32_t last2 = WAL_CRC_NONE; put_entry(f2, 0, 5, WAL_JOB_UPDATE, "y", 1, &last2, 1); put_entry(f2, 0, 6, WAL_JOB_UPDATE, "z", 1, &last2, 0); fclose(f2); waldump_stats_t st; int rc = waldump_path(g_dir, stdout, &st); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT(4, st.n_entries); TEST_ASSERT_EQUAL_UINT(2, st.n_segments); TEST_ASSERT_EQUAL_UINT64(1, st.first_lsn); TEST_ASSERT_EQUAL_UINT64(6, st.last_lsn); TEST_ASSERT_EQUAL_UINT(1, st.n_gaps); /* the 3..4 hole */ } /* ------------------------------------------------------------------ */ /* A CRC break truncates the recovered tip below the last physical LSN. */ /* ------------------------------------------------------------------ */ static void test_crc_break_sets_recovered_tip(void) { fresh_dir(); char path[512]; FILE* fp = fopen(seg_path(1, path, sizeof(path)), "wb"); uint32_t last = WAL_CRC_NONE; put_entry(fp, 0, 1, WAL_NOOP, NULL, 0, &last, 1); put_entry(fp, 0, 2, WAL_JOB_UPDATE, "ok", 2, &last, 0); fclose(fp); /* Corrupt the second entry's stored CRC in place. */ fp = fopen(path, "r+b"); wal_entry_hdr_t h; long off2 = sizeof(wal_entry_hdr_t) + 0; /* entry 1 has 0 payload */ fseek(fp, off2, SEEK_SET); fread(&h, sizeof(h), 1, fp); h.crc ^= 0xdeadbeefu; /* break it */ fseek(fp, off2, SEEK_SET); fwrite(&h, sizeof(h), 1, fp); fclose(fp); waldump_stats_t st; int rc = waldump_path(g_dir, stdout, &st); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT64(1, st.recovered_tip); /* stops before the bad LSN 2 */ TEST_ASSERT_NOT_EQUAL(0, st.crc_broken); } /* ------------------------------------------------------------------ */ /* Missing path is a hard error. */ /* ------------------------------------------------------------------ */ static void test_missing_path_errors(void) { waldump_stats_t st; int rc = waldump_path("/tmp/xce_waldump_does_not_exist_zzz", stdout, &st); TEST_ASSERT_LESS_THAN_INT(0, rc); } /* ------------------------------------------------------------------ */ /* A non-WAL file (e.g. a config blob) must ERROR, not report 0 entries.*/ /* ------------------------------------------------------------------ */ static void test_non_wal_file_errors(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/cluster.bin", g_dir); FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); const char junk[] = "not a wal segment — a config blob"; fwrite(junk, sizeof(junk), 1, fp); fclose(fp); /* Pointing at a non-seg-*.wal file must be a hard error (-EINVAL), not a * silent "0 entries" scan. */ waldump_stats_t st; int rc = waldump_path(path, stdout, &st); TEST_ASSERT_LESS_THAN_INT(0, rc); } void test_waldump(void) { RUN_TEST(test_contiguous_log_has_no_gaps); RUN_TEST(test_missing_prefix_flagged_as_gap); RUN_TEST(test_duplicate_submit_detection); RUN_TEST(test_interior_gap_flagged); RUN_TEST(test_crc_break_sets_recovered_tip); RUN_TEST(test_missing_path_errors); RUN_TEST(test_non_wal_file_errors); }