#define _GNU_SOURCE #include #include #include #include #include #include #include #include "snapverify.h" static char g_dir[256]; static void fresh_dir(void) { strcpy(g_dir, "/tmp/xce_snapverify_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(g_dir)); } static void cleanup_dir(void) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir); system(cmd); } /* Writes a checkpoint file matching persist.c write_snapshot()'s exact * on-disk layout for SNAPSHOT_FORMAT_VERSION=2 (magic+lsn+cfgref(41) + fmt + * job_count + per-job[payload_len+buf] + queue_count + per-queue * [payload_len+opaque_bytes] + host_count + per-host[payload_len+buf] + * cfg_bytes_len + cfg_bytes). * * Queue/host frame CONTENT is irrelevant here -- snapverify validates * structure only (length-prefix walk), never capnp-decodes a payload -- so * queue frames are written as an opaque [payload_len][name_len+name+status] * blob (convenient filler reusing the queue_names/queue_status test * parameters, NOT a real capnp QueueAdminState encoding) and host frames as * an opaque [payload_len][0xCD-filled bytes] blob. * * job_lens/job_count, queue_names/queue_status/queue_count, and * host_lens/host_count are all caller-controlled so tests can inject a * specific defect or size at a specific section. cfg_bytes_len controls the * trailing config-bytes section's declared length (content is 0xEF filler); * pass 0 for "section present but empty" -- the section's 4-byte length * field is ALWAYS written by this helper (mirrors persist.c writing it on * every checkpoint, even when there are no local config bytes yet). Tests * that need to inject a real header cfgref, or a file with NO config-bytes * length field at all, use write_snap_full()/a hand-written file instead * (see below). * * `truncate_last_bytes` drops that many bytes off the END of the fully- * written file (0 = write it whole). */ static void write_snap_full(const char* path, uint64_t lsn, int cfgref_present, uint64_t cfgref_tag, const uint8_t* cfgref_sha /* 32 bytes, or NULL * for all-zero */, uint32_t fmt, const uint32_t* job_lens, uint32_t job_count, const char** queue_names, const uint32_t* queue_status, uint32_t queue_count, const uint32_t* host_lens, uint32_t host_count, uint32_t cfg_bytes_len, int truncate_last_bytes) { FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); char header[16 + 1 + 8 + 32]; memset(header, 0, sizeof(header)); memcpy(header, "XCESNAP\0", 8); memcpy(header + 8, &lsn, 8); header[16] = (char)(cfgref_present ? 1 : 0); memcpy(header + 17, &cfgref_tag, 8); if (cfgref_sha) memcpy(header + 25, cfgref_sha, 32); fwrite(header, 1, sizeof(header), fp); fwrite(&fmt, sizeof(fmt), 1, fp); fwrite(&job_count, sizeof(job_count), 1, fp); for (uint32_t i = 0; i < job_count; i++) { fwrite(&job_lens[i], sizeof(job_lens[i]), 1, fp); uint8_t* buf = malloc(job_lens[i]); memset(buf, 0xAB, job_lens[i]); fwrite(buf, 1, job_lens[i], fp); free(buf); } fwrite(&queue_count, sizeof(queue_count), 1, fp); for (uint32_t i = 0; i < queue_count; i++) { uint32_t nlen = (uint32_t)strlen(queue_names[i]); uint32_t payload_len = (uint32_t)(sizeof(nlen) + nlen + sizeof(queue_status[i])); fwrite(&payload_len, sizeof(payload_len), 1, fp); fwrite(&nlen, sizeof(nlen), 1, fp); fwrite(queue_names[i], 1, nlen, fp); fwrite(&queue_status[i], sizeof(queue_status[i]), 1, fp); } fwrite(&host_count, sizeof(host_count), 1, fp); for (uint32_t i = 0; i < host_count; i++) { fwrite(&host_lens[i], sizeof(host_lens[i]), 1, fp); uint8_t* buf = malloc(host_lens[i]); memset(buf, 0xCD, host_lens[i]); fwrite(buf, 1, host_lens[i], fp); free(buf); } fwrite(&cfg_bytes_len, sizeof(cfg_bytes_len), 1, fp); if (cfg_bytes_len > 0) { uint8_t* buf = malloc(cfg_bytes_len); memset(buf, 0xEF, cfg_bytes_len); fwrite(buf, 1, cfg_bytes_len, fp); free(buf); } fclose(fp); if (truncate_last_bytes > 0) { long sz = 0; FILE* f = fopen(path, "rb"); fseek(f, 0, SEEK_END); sz = ftell(f); fclose(f); TEST_ASSERT_TRUE(truncate(path, sz - truncate_last_bytes) == 0); } } /* Convenience wrapper for the (common) case of no header cfgref -- mirrors * every pre-task-8.4 call site's shape, just with the new host_lens/ * host_count/cfg_bytes_len parameters appended. */ static void write_snap(const char* path, uint64_t lsn, uint32_t fmt, const uint32_t* job_lens, uint32_t job_count, const char** queue_names, const uint32_t* queue_status, uint32_t queue_count, const uint32_t* host_lens, uint32_t host_count, uint32_t cfg_bytes_len, int truncate_last_bytes) { write_snap_full(path, lsn, 0, 0, NULL, fmt, job_lens, job_count, queue_names, queue_status, queue_count, host_lens, host_count, cfg_bytes_len, truncate_last_bytes); } static void test_legacy_position_only_file_is_ok(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-000000000000000a.capnp", g_dir); char header[16]; memcpy(header, "XCESNAP\0", 8); uint64_t lsn = 10; memcpy(header + 8, &lsn, 8); FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); fwrite(header, 1, sizeof(header), fp); fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(0, rep.has_entity_section); TEST_ASSERT_EQUAL_UINT64(10, rep.checkpoint_lsn); cleanup_dir(); } static void test_entity_bearing_well_formed_file_is_ok(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-0000000000000064.capnp", g_dir); uint32_t job_lens[3] = { 100, 200, 50 }; const char* qnames[2] = { "gpu", "cpu" }; uint32_t qstatus[2] = { 1, 0 }; /* fmt=2 (SNAPSHOT_FORMAT_VERSION), no hosts, cfg-bytes section present * but empty -- must still be fully consumed to EOF by the fmt-2-aware * parser (task 8.4 fix: this file shape is what a real checkpoint * written by the current codebase looks like). */ write_snap(path, 100, 2, job_lens, 3, qnames, qstatus, 2, NULL, 0, 0, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(1, rep.has_entity_section); TEST_ASSERT_EQUAL_UINT32(2, rep.format_version); TEST_ASSERT_EQUAL_UINT32(3, rep.job_count); TEST_ASSERT_EQUAL_UINT32(3, rep.jobs_ok); TEST_ASSERT_EQUAL_UINT32(2, rep.queue_count); TEST_ASSERT_EQUAL_UINT32(2, rep.queues_ok); TEST_ASSERT_EQUAL_UINT32(0, rep.host_count); TEST_ASSERT_EQUAL_UINT32(0, rep.hosts_ok); TEST_ASSERT_EQUAL_INT(1, rep.has_config_bytes_section); TEST_ASSERT_EQUAL_UINT32(0, rep.config_bytes_len); TEST_ASSERT_EQUAL_INT(-1, (int)rep.fail_offset); cleanup_dir(); } static void test_bad_magic_is_einval(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-bad.capnp", g_dir); FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); fwrite("NOTASNAP", 1, 8, fp); char zero[8] = {0}; fwrite(zero, 1, 8, fp); fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); TEST_ASSERT_EQUAL_INT(0, rep.ok); cleanup_dir(); } static void test_truncated_header_is_eio(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-short.capnp", g_dir); FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); fwrite("XCE", 1, 3, fp); /* far short of the 16-byte minimum */ fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(-EIO, rc); TEST_ASSERT_EQUAL_INT(0, rep.ok); cleanup_dir(); } static void test_unrecognized_format_version_is_einval(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-0000000000000001.capnp", g_dir); uint32_t job_lens[1] = { 10 }; write_snap(path, 1, 99 /* bogus version */, job_lens, 1, NULL, NULL, 0, NULL, 0, 0, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); TEST_ASSERT_EQUAL_INT(0, rep.ok); TEST_ASSERT_EQUAL_UINT32(99, rep.format_version); cleanup_dir(); } /* bug-1389 regression shape: a job frame's length field decodes to garbage * (e.g. transport corrupted a chunk) mid-stream. snapverify must stop at * exactly the bad job, report how many parsed cleanly before it, and pinpoint * the byte offset of the bad length field — the diagnostic this tool exists * to produce instead of a bare "persist_load_snapshot failed rc=-5". */ static void test_corrupt_job_payload_len_reports_exact_failure_point(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-0000000000000002.capnp", g_dir); uint32_t job_lens[4] = { 50, 60, 70, 80 }; write_snap(path, 2, 2, job_lens, 4, NULL, NULL, 0, NULL, 0, 0, 0); /* Corrupt job 2's length field (3rd job, after header(57)+fmt(4)+ * job_count(4)+job0(4+50)+job1(4+60) = 57+4+4+54+64 = 183) to a value * that cannot possibly fit in the remaining file. */ FILE* fp = fopen(path, "r+b"); TEST_ASSERT_NOT_NULL(fp); long job2_len_off = 57 + 4 + 4 + (4 + 50) + (4 + 60); fseek(fp, job2_len_off, SEEK_SET); uint32_t garbage = 0x7FFFFFFF; fwrite(&garbage, sizeof(garbage), 1, fp); fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(-EIO, rc); TEST_ASSERT_EQUAL_INT(0, rep.ok); TEST_ASSERT_EQUAL_UINT32(2, rep.jobs_ok); /* job 0, job 1 parsed fine */ TEST_ASSERT_EQUAL_INT(job2_len_off, (int)rep.fail_offset); cleanup_dir(); } static void test_missing_path_errors(void) { snapverify_report_t rep; int rc = snapverify_path("/tmp/xce_snapverify_does_not_exist_zzz", stdout, &rep); TEST_ASSERT_LESS_THAN_INT(0, rc); } /* task 8.4: the header's cfgref tag/sha must be DECODED (not merely * reported present) -- a genuine round-trip assertion against concrete * injected values, not just "non-zero". */ static void test_cfgref_tag_and_sha_decode_roundtrip(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000c8.capnp", g_dir); uint64_t tag = 0x1122334455667788ULL; uint8_t sha[32]; memset(sha, 0xAA, 16); memset(sha + 16, 0xBB, 16); write_snap_full(path, 200, 1 /* cfgref_present */, tag, sha, 2, NULL, 0, NULL, NULL, 0, NULL, 0, 0, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(1, rep.has_cfgref); TEST_ASSERT_EQUAL_UINT64(tag, rep.cfgref_tag); TEST_ASSERT_EQUAL_MEMORY(sha, rep.cfgref_sha, 32); cleanup_dir(); } /* task 8.4: host frames get their own structural walk + counts, same shape * as the job/queue loops. */ static void test_host_frames_are_walked_and_counted(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000c9.capnp", g_dir); uint32_t host_lens[3] = { 30, 45, 12 }; write_snap(path, 201, 2, NULL, 0, NULL, NULL, 0, host_lens, 3, 0, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_UINT32(3, rep.host_count); TEST_ASSERT_EQUAL_UINT32(3, rep.hosts_ok); cleanup_dir(); } /* task 8.4: a non-empty trailing config-bytes section is reported with its * concrete declared length. */ static void test_nonempty_config_bytes_section_is_reported(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000ca.capnp", g_dir); write_snap(path, 202, 2, NULL, 0, NULL, NULL, 0, NULL, 0, 42, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(1, rep.has_config_bytes_section); TEST_ASSERT_EQUAL_UINT32(42, rep.config_bytes_len); cleanup_dir(); } /* task 8.4: an EXPLICIT zero-length config-bytes section (the 4-byte length * field IS present and reads 0, immediately followed by EOF) must be * distinguishable from a file that has no config-bytes section at all -- * both are ok==1, but only the explicit-zero case has * has_config_bytes_section==1. */ static void test_explicit_zero_length_config_bytes_section_is_ok(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000cb.capnp", g_dir); write_snap(path, 203, 2, NULL, 0, NULL, NULL, 0, NULL, 0, 0, 0); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(1, rep.has_config_bytes_section); TEST_ASSERT_EQUAL_UINT32(0, rep.config_bytes_len); cleanup_dir(); } /* task 8.4: a file that simply ENDS right after the host section (no * config_bytes_len field at all -- a slightly-older-but-still-fmt-2- * consistent shape) is still ok==1, but has_config_bytes_section==0 -- * the counterpart to the explicit-zero-length case above, proving the two * are genuinely distinguishable rather than collapsed into one "empty" * bucket. Hand-written (not via write_snap_full, which always writes the * length field) to produce exactly this shape. */ static void test_no_config_bytes_section_at_all_is_still_ok(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000cc.capnp", g_dir); FILE* fp = fopen(path, "wb"); TEST_ASSERT_NOT_NULL(fp); char header[16 + 1 + 8 + 32]; memset(header, 0, sizeof(header)); memcpy(header, "XCESNAP\0", 8); uint64_t lsn = 204; memcpy(header + 8, &lsn, 8); fwrite(header, 1, sizeof(header), fp); uint32_t fmt = 2; uint32_t zero = 0; fwrite(&fmt, sizeof(fmt), 1, fp); /* format_version */ fwrite(&zero, sizeof(zero), 1, fp); /* job_count = 0 */ fwrite(&zero, sizeof(zero), 1, fp); /* queue_count = 0 */ fwrite(&zero, sizeof(zero), 1, fp); /* host_count = 0 */ /* No config_bytes_len field at all -- file ends right here. */ fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, rep.ok); TEST_ASSERT_EQUAL_INT(0, rep.has_config_bytes_section); cleanup_dir(); } /* task 8.4: same corrupt-length-field diagnostic as * test_corrupt_job_payload_len_reports_exact_failure_point, in the host * section instead of the job section. */ static void test_corrupt_host_payload_len_reports_exact_failure_point(void) { fresh_dir(); char path[512]; snprintf(path, sizeof(path), "%s/snap-00000000000000cd.capnp", g_dir); uint32_t host_lens[3] = { 20, 30, 40 }; write_snap(path, 205, 2, NULL, 0, NULL, NULL, 0, host_lens, 3, 0, 0); /* header(57) + fmt(4) + job_count(4) + [0 jobs] + queue_count(4) + * [0 queues] + host_count(4) + host0(4+20) + host1(4+30) = offset of * host2's payload_len field. */ long host2_len_off = 57 + 4 + 4 + 4 + 4 + (4 + 20) + (4 + 30); FILE* fp = fopen(path, "r+b"); TEST_ASSERT_NOT_NULL(fp); fseek(fp, host2_len_off, SEEK_SET); uint32_t garbage = 0x7FFFFFFF; fwrite(&garbage, sizeof(garbage), 1, fp); fclose(fp); snapverify_report_t rep; int rc = snapverify_path(path, stdout, &rep); TEST_ASSERT_EQUAL_INT(-EIO, rc); TEST_ASSERT_EQUAL_INT(0, rep.ok); TEST_ASSERT_EQUAL_UINT32(2, rep.hosts_ok); /* host 0, host 1 parsed fine */ TEST_ASSERT_EQUAL_INT(host2_len_off, (int)rep.fail_offset); cleanup_dir(); } void test_snapverify(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_legacy_position_only_file_is_ok); RUN_TEST(test_entity_bearing_well_formed_file_is_ok); RUN_TEST(test_bad_magic_is_einval); RUN_TEST(test_truncated_header_is_eio); RUN_TEST(test_unrecognized_format_version_is_einval); RUN_TEST(test_corrupt_job_payload_len_reports_exact_failure_point); RUN_TEST(test_missing_path_errors); RUN_TEST(test_cfgref_tag_and_sha_decode_roundtrip); RUN_TEST(test_host_frames_are_walked_and_counted); RUN_TEST(test_nonempty_config_bytes_section_is_reported); RUN_TEST(test_explicit_zero_length_config_bytes_section_is_ok); RUN_TEST(test_no_config_bytes_section_at_all_is_still_ok); RUN_TEST(test_corrupt_host_payload_len_reports_exact_failure_point); }