#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #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 //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); }