feat(config-upgrade-snapshot): add configcompare tool, extend snapverify (task 8.4)
Build configcompare_paths(), an offline sibling to snapdiff/walcompare that recomputes each member's active-config sha256 fresh from its own on-disk workdir/name/conf bytes and reports identical vs. divergent (Req 4.2). Wire it as a cmd_configcompare CLI subcommand in test.c, matching its siblings, after review caught it shipped as an unreachable library function in round 1. Extend snapverify to match the current on-disk checkpoint format: bump SNAP_FORMAT_VERSION to 2, replace the stale raw queue-frame decode with the capnp length-prefixed walk, add host-frame and config-bytes-section structural walks, and decode (not just presence-check) the header's cfgref tag/sha. Without this fix snapverify rejected every checkpoint written by the current codebase.
This commit is contained in:
parent
c5d8ecbd7d
commit
1d09631df0
10 changed files with 997 additions and 47 deletions
|
|
@ -26,15 +26,41 @@ static void cleanup_dir(void)
|
|||
}
|
||||
|
||||
/* Writes a checkpoint file matching persist.c write_snapshot()'s exact
|
||||
* on-disk layout (magic+lsn+cfgref(41, all-zero/absent) + fmt + job_count +
|
||||
* per-job[len+buf] + queue_count + per-queue[namelen+name+status]).
|
||||
* job_lens/job_count and queue_names/queue_count are caller-controlled so
|
||||
* tests can inject a specific defect. `truncate_last_bytes` drops that many
|
||||
* bytes off the END of the fully-written file (0 = write it whole). */
|
||||
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, int truncate_last_bytes)
|
||||
* 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);
|
||||
|
|
@ -43,6 +69,10 @@ static void write_snap(const char* path, uint64_t lsn, uint32_t fmt,
|
|||
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);
|
||||
|
|
@ -57,11 +87,31 @@ static void write_snap(const char* path, uint64_t lsn, uint32_t fmt,
|
|||
|
||||
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 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) {
|
||||
|
|
@ -74,6 +124,21 @@ static void write_snap(const char* path, uint64_t lsn, uint32_t fmt,
|
|||
}
|
||||
}
|
||||
|
||||
/* 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();
|
||||
|
|
@ -104,21 +169,29 @@ static void test_entity_bearing_well_formed_file_is_ok(void)
|
|||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/snap-0000000000000064.capnp", g_dir);
|
||||
|
||||
uint32_t job_lens[3] = { 100, 200, 50 };
|
||||
uint32_t job_lens[3] = { 100, 200, 50 };
|
||||
const char* qnames[2] = { "gpu", "cpu" };
|
||||
uint32_t qstatus[2] = { 1, 0 };
|
||||
write_snap(path, 100, 1, job_lens, 3, qnames, qstatus, 2, 0);
|
||||
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(1, rep.format_version);
|
||||
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();
|
||||
}
|
||||
|
|
@ -165,7 +238,8 @@ static void test_unrecognized_format_version_is_einval(void)
|
|||
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, 0);
|
||||
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);
|
||||
|
|
@ -187,7 +261,7 @@ static void test_corrupt_job_payload_len_reports_exact_failure_point(void)
|
|||
snprintf(path, sizeof(path), "%s/snap-0000000000000002.capnp", g_dir);
|
||||
|
||||
uint32_t job_lens[4] = { 50, 60, 70, 80 };
|
||||
write_snap(path, 2, 1, job_lens, 4, NULL, NULL, 0, 0);
|
||||
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
|
||||
|
|
@ -217,6 +291,166 @@ static void test_missing_path_errors(void)
|
|||
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__);
|
||||
|
|
@ -227,4 +461,10 @@ void test_snapverify(void)
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue