forked from lei.chen/myxce
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.
217 lines
7.6 KiB
C
217 lines
7.6 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "configcompare.h"
|
|
|
|
static char g_dir[256];
|
|
|
|
static void fresh_dir(void)
|
|
{
|
|
strcpy(g_dir, "/tmp/xce_configcompare_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);
|
|
}
|
|
|
|
/* Creates <root>/<name>/conf/ (a member's `workdir/name` directory, mirroring
|
|
* conf_dir = "%s/%s", myconf->workdir, myconf->name in persist.c /
|
|
* config_repl.c) and writes the resulting path into out_path. Does NOT touch
|
|
* config_repl.c or persist.c -- these are hand-written fixture files so the
|
|
* test exercises configcompare.c's OWN resolve-then-read logic, not a
|
|
* different code path's. */
|
|
static void make_member_dir(const char* root, const char* name,
|
|
char out_path[512])
|
|
{
|
|
snprintf(out_path, 512, "%s/%s", root, name);
|
|
TEST_ASSERT_EQUAL_INT(0, mkdir(out_path, 0700));
|
|
char conf_dir[560];
|
|
snprintf(conf_dir, sizeof(conf_dir), "%s/conf", out_path);
|
|
TEST_ASSERT_EQUAL_INT(0, mkdir(conf_dir, 0700));
|
|
}
|
|
|
|
static void write_conf_current(const char* member_path, const char* hexsuffix)
|
|
{
|
|
char ptr_path[600];
|
|
snprintf(ptr_path, sizeof(ptr_path), "%s/conf/current", member_path);
|
|
FILE* f = fopen(ptr_path, "w");
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
fprintf(f, "cluster.bin.%s\n", hexsuffix);
|
|
fclose(f);
|
|
}
|
|
|
|
static void write_conf_blob(const char* member_path, const char* hexsuffix,
|
|
const uint8_t* bytes, size_t len)
|
|
{
|
|
char blob_path[600];
|
|
snprintf(blob_path, sizeof(blob_path), "%s/conf/cluster.bin.%s",
|
|
member_path, hexsuffix);
|
|
FILE* f = fopen(blob_path, "wb");
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
if (len > 0)
|
|
TEST_ASSERT_EQUAL_INT((int)len, (int)fwrite(bytes, 1, len, f));
|
|
fclose(f);
|
|
}
|
|
|
|
/* Full happy-path member: dir + conf/current + the versioned blob it
|
|
* points at. */
|
|
static void setup_member_ok(const char* root, const char* name,
|
|
const char* hexsuffix, const uint8_t* bytes,
|
|
size_t len, char out_path[512])
|
|
{
|
|
make_member_dir(root, name, out_path);
|
|
write_conf_current(out_path, hexsuffix);
|
|
write_conf_blob(out_path, hexsuffix, bytes, len);
|
|
}
|
|
|
|
static void test_two_members_identical_config_reports_identical(void)
|
|
{
|
|
fresh_dir();
|
|
const uint8_t data[] = "identical config bytes for both members";
|
|
|
|
char m0[512], m1[512];
|
|
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
|
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data, sizeof(data) - 1, m1);
|
|
|
|
const char* paths[2] = { m0, m1 };
|
|
configcompare_report_t rep;
|
|
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(1, rep.all_present);
|
|
TEST_ASSERT_EQUAL_INT(1, rep.all_identical);
|
|
TEST_ASSERT_EQUAL_INT(-1, rep.first_divergent_index);
|
|
cleanup_dir();
|
|
}
|
|
|
|
static void test_two_members_differing_config_reports_divergent(void)
|
|
{
|
|
fresh_dir();
|
|
const uint8_t data0[] = "config bytes version A";
|
|
const uint8_t data1[] = "config bytes version B, quite different";
|
|
|
|
char m0[512], m1[512];
|
|
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data0, sizeof(data0) - 1, m0);
|
|
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data1, sizeof(data1) - 1, m1);
|
|
|
|
const char* paths[2] = { m0, m1 };
|
|
configcompare_report_t rep;
|
|
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_EQUAL_INT(1, rep.all_present);
|
|
TEST_ASSERT_EQUAL_INT(0, rep.all_identical);
|
|
TEST_ASSERT_EQUAL_INT(1, rep.first_divergent_index);
|
|
cleanup_dir();
|
|
}
|
|
|
|
/* A member with no conf/current pointer file at all must be reported as
|
|
* unreadable (read_rc != 0), never silently folded into "identical" or
|
|
* "divergent" against the two members that DO agree. */
|
|
static void test_missing_conf_current_reported_unreadable_not_divergent(void)
|
|
{
|
|
fresh_dir();
|
|
const uint8_t data[] = "shared config bytes across the healthy members";
|
|
|
|
char m0[512], m1[512], m2[512];
|
|
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
|
/* m1: conf/ dir exists but conf/current was never written. */
|
|
make_member_dir(g_dir, "m1", m1);
|
|
setup_member_ok(g_dir, "m2", "cccccccccccc", data, sizeof(data) - 1, m2);
|
|
|
|
const char* paths[3] = { m0, m1, m2 };
|
|
configcompare_report_t rep;
|
|
int rc = configcompare_paths(paths, 3, stdout, &rep);
|
|
|
|
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(0, rep.read_rc[0]);
|
|
TEST_ASSERT_NOT_EQUAL(0, rep.read_rc[1]);
|
|
TEST_ASSERT_EQUAL_INT(0, rep.all_present);
|
|
cleanup_dir();
|
|
}
|
|
|
|
/* A member whose conf/current points at a versioned file that does not
|
|
* exist on disk gets the same "unreadable" treatment as a missing pointer
|
|
* file -- neither is evidence of matching or mismatching configuration. */
|
|
static void test_current_points_to_missing_versioned_file_reported_unreadable(void)
|
|
{
|
|
fresh_dir();
|
|
const uint8_t data[] = "shared config bytes, dangling pointer case";
|
|
|
|
char m0[512], m1[512];
|
|
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
|
/* m1: conf/current points at cluster.bin.dddddddddddd, but that
|
|
* versioned file was never written. */
|
|
make_member_dir(g_dir, "m1", m1);
|
|
write_conf_current(m1, "dddddddddddd");
|
|
|
|
const char* paths[2] = { m0, m1 };
|
|
configcompare_report_t rep;
|
|
int rc = configcompare_paths(paths, 2, stdout, &rep);
|
|
|
|
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(0, rep.read_rc[0]);
|
|
TEST_ASSERT_NOT_EQUAL(0, rep.read_rc[1]);
|
|
cleanup_dir();
|
|
}
|
|
|
|
/* Basic determinism sanity check: comparing the same two byte-identical
|
|
* files twice in a row yields the same hash both times. */
|
|
static void test_repeated_comparison_of_identical_files_is_deterministic(void)
|
|
{
|
|
fresh_dir();
|
|
const uint8_t data[] = "deterministic hash check bytes, fixed content";
|
|
|
|
char m0[512], m1[512];
|
|
setup_member_ok(g_dir, "m0", "aaaaaaaaaaaa", data, sizeof(data) - 1, m0);
|
|
setup_member_ok(g_dir, "m1", "bbbbbbbbbbbb", data, sizeof(data) - 1, m1);
|
|
|
|
const char* paths[2] = { m0, m1 };
|
|
configcompare_report_t rep1, rep2;
|
|
int rc1 = configcompare_paths(paths, 2, NULL, &rep1);
|
|
int rc2 = configcompare_paths(paths, 2, NULL, &rep2);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc1);
|
|
TEST_ASSERT_EQUAL_INT(0, rc2);
|
|
TEST_ASSERT_EQUAL_INT(0, memcmp(rep1.sha256[0], rep2.sha256[0], 32));
|
|
TEST_ASSERT_EQUAL_INT(0, memcmp(rep1.sha256[1], rep2.sha256[1], 32));
|
|
cleanup_dir();
|
|
}
|
|
|
|
/* Fewer than two members means there is nothing to compare -- a negative
|
|
* errno, not a degenerate "identical" verdict. */
|
|
static void test_n_less_than_2_returns_negative_errno(void)
|
|
{
|
|
const char* one_path[1] = { "/tmp/xce_configcompare_irrelevant" };
|
|
configcompare_report_t rep;
|
|
|
|
int rc0 = configcompare_paths(NULL, 0, NULL, &rep);
|
|
TEST_ASSERT_LESS_THAN_INT(0, rc0);
|
|
|
|
int rc1 = configcompare_paths(one_path, 1, NULL, &rep);
|
|
TEST_ASSERT_LESS_THAN_INT(0, rc1);
|
|
}
|
|
|
|
void test_configcompare(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
RUN_TEST(test_two_members_identical_config_reports_identical);
|
|
RUN_TEST(test_two_members_differing_config_reports_divergent);
|
|
RUN_TEST(test_missing_conf_current_reported_unreadable_not_divergent);
|
|
RUN_TEST(test_current_points_to_missing_versioned_file_reported_unreadable);
|
|
RUN_TEST(test_repeated_comparison_of_identical_files_is_deterministic);
|
|
RUN_TEST(test_n_less_than_2_returns_negative_errno);
|
|
}
|