feat(xce-harness-component): consolidate harness_unit_test binary
Relocate the 13 test files that lived inside controller_unit_test into src/harness/testsuite/, add a new Unity runner (harness_test.c, modeled on controller_test.c) carrying their extern/RUN_TEST entries, and register the new harness_unit_test executable + suite in src/harness/meson.build with an explicit build-order dependency on xce_test (two of its tests spawn that binary over a pty). Finish clearing the controller side: remove test_test_cli.c/ test_xcetest_process_entry.c from controller_unit_test_src and their extern/RUN_TEST entries from controller_test.c, and remove the temporary xce_test_controller_link symlink bridge task 1.1 added (no longer needed now that both binaries build as siblings under harness/). Verified: harness suite 132 Tests 0 Failures 0 Ignored; controller suite 1199 Tests 0 Failures 0 Ignored. 1199+132=1331 matches the pre-move controller_unit_test baseline exactly (cross-checked against buglog bug-1798/bug-1805). Task 2.1 of xce-harness-component. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
79628f7b2b
commit
b5873a67aa
18 changed files with 192 additions and 29 deletions
|
|
@ -146,8 +146,6 @@ controller_unit_test_src = [
|
|||
'../harness/walcompare.c',
|
||||
'../harness/snapverify.c',
|
||||
'../harness/snapdiff.c',
|
||||
'testsuite/test_xcetest_process_entry.c',
|
||||
'testsuite/test_test_cli.c',
|
||||
'testsuite/test_persist_snapshot_entities.c',
|
||||
'testsuite/test_apply_seam.c',
|
||||
'testsuite/test_apply_committed_entry.c',
|
||||
|
|
@ -221,31 +219,9 @@ controller_unit_test = executable('controller_unit_test',
|
|||
c_args: ['-DXCE_TESTING'],
|
||||
install: false)
|
||||
|
||||
# TEMPORARY (xce-harness-component task 1.1): test_test_cli.c and
|
||||
# test_xcetest_process_entry.c stay registered in controller_unit_test_src
|
||||
# above until task 2.1 relocates them into src/harness/testsuite/. Both
|
||||
# spawn the built xce_test binary as a subprocess via resolve_xce_test_path()
|
||||
# (test_xcetest_process_entry.c), which finds it by looking in the SAME
|
||||
# build directory as the running test binary (dirname(/proc/self/exe) +
|
||||
# "/xce_test") -- an assumption that held when xce_test built into this
|
||||
# same controller/ build subdir, and breaks now that xce_test builds into
|
||||
# harness/ instead (task 1.1's whole point). This symlink keeps that
|
||||
# assumption satisfied without touching either test file, both of which
|
||||
# are out of this task's boundary -- remove this custom_target and the
|
||||
# test()'s `depends:` below once task 2.1 physically relocates both files,
|
||||
# at which point they become genuine build-dir siblings of xce_test again
|
||||
# (mirroring harness_unit_test's own explicit dependency on the xce_test
|
||||
# target, per the design's System Flows).
|
||||
xce_test_controller_link = custom_target('xce_test_controller_link',
|
||||
output: 'xce_test',
|
||||
input: xce_test,
|
||||
command: ['ln', '-sf', '../harness/xce_test', '@OUTPUT0@'],
|
||||
)
|
||||
|
||||
test('controller test', controller_unit_test,
|
||||
suite: 'controller',
|
||||
timeout: 300,
|
||||
depends: [xce_test_controller_link],
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
||||
# job-array-single-wal-record task 5.1: sched.c's push logic (sched_job_new_fun /
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ extern void test_ctrlstatus(void);
|
|||
extern void test_ha_heartbeat_committed(void);
|
||||
extern void test_ha_vote_floor_heartbeat(void);
|
||||
extern void test_term_safety_adapter(void);
|
||||
extern void test_xcetest_process_entry(void);
|
||||
extern void test_test_cli(void);
|
||||
extern void test_drive_async_config(void);
|
||||
extern void test_slowlane(void);
|
||||
extern void test_commit_admission_pool(void);
|
||||
|
|
@ -241,8 +239,6 @@ int main(int argc, char* argv[])
|
|||
TEST(test_ha_heartbeat_committed);
|
||||
TEST(test_ha_vote_floor_heartbeat);
|
||||
TEST(test_term_safety_adapter);
|
||||
TEST(test_xcetest_process_entry);
|
||||
TEST(test_test_cli);
|
||||
TEST(test_drive_async_config);
|
||||
TEST(test_slowlane);
|
||||
TEST(test_commit_admission_pool);
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
#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);
|
||||
}
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
/*
|
||||
* test_confirm.c -- proves confirm.c's destructive-command confirmation
|
||||
* gate (xce-test-cli-ux spec, task 2.1; Requirements 9.1-9.4, 9.6-9.9):
|
||||
*
|
||||
* 1. A non-interactive session (xcetest_is_interactive() false) makes
|
||||
* confirm_destructive() proceed immediately WITHOUT ever reading
|
||||
* stdin (9.4).
|
||||
* 2. A declining answer ("n", or anything not affirmative, or EOF)
|
||||
* makes it return "not run" (0), not "failed" (9.2).
|
||||
* 3. A REAL SIGINT delivered while confirm_destructive() is blocked in
|
||||
* its prompt's read makes it return "not run" (0), identically to a
|
||||
* decline -- not a crash, not a hang, not a distinct outcome (9.9).
|
||||
* 4. The confirm_set_skip()/confirm_is_skip_set() toggle genuinely
|
||||
* switches whether the read ever happens: with the toggle ON,
|
||||
* confirm_destructive() returns "proceed" (1) even when stdin holds
|
||||
* a decline answer that would otherwise cause "not run" (9.6/9.7).
|
||||
*
|
||||
* xcetest_is_interactive() (declared in confirm.h) is normally implemented
|
||||
* in test.c, over test.c's own private g_interactive flag -- but test.c
|
||||
* itself is never linked into controller_unit_test (it pulls in libedit/
|
||||
* cJSON/libxce/Cap'n Proto/OpenSSL and defines its own main(); see
|
||||
* xcetest_interrupt.h's header comment for the identical reasoning
|
||||
* already applied to xcetest_interrupted()). So this file provides its
|
||||
* OWN local fake definition of xcetest_is_interactive(), over its own
|
||||
* local flag -- the same "local fake standing in for a real dependency
|
||||
* this binary never links" pattern test_interruptcall.c already uses for
|
||||
* xcetest_interrupted().
|
||||
*
|
||||
* STDIN REDIRECTION TECHNIQUE (scenarios 1, 2, 4 -- all in-process, not
|
||||
* forked): rather than dup2()-ing a new fd onto the persistent
|
||||
* STDIN_FILENO while reusing the single process-wide `stdin` FILE*
|
||||
* object (which risks stale buffered bytes surviving from a previous
|
||||
* redirection), each test opens its fixture as a fresh fd and swaps the
|
||||
* global `stdin` pointer itself to a brand-new fdopen()'d FILE* -- glibc
|
||||
* declares `stdin` as a plain `extern FILE *stdin;`, so this reassignment
|
||||
* is well-defined. A fresh fdopen() guarantees a clean buffer with no
|
||||
* carry-over from any earlier test in this same combined Unity binary.
|
||||
* Each test restores the original `stdin` pointer before returning.
|
||||
*
|
||||
* Scenario 3 (the real-SIGINT case) is instead a fork()-based test, NOT
|
||||
* an in-process one -- mirroring test_xcetest_interrupt.c's and
|
||||
* test_interruptcall.c's existing convention in this exact directory.
|
||||
* Forking (rather than juggling stdin/alarms in-process) means a
|
||||
* regression that makes the prompt's read silently retry on EINTR (the
|
||||
* exact bug Requirement 9.9 exists to prevent) only hangs a short-lived
|
||||
* CHILD process, which a 2-second alarm() safety net self-terminates --
|
||||
* it can never hang this whole Unity binary or the rest of the suite.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "confirm.h"
|
||||
#include "xcetest_interrupt.h"
|
||||
|
||||
/* --- local fake for xcetest_is_interactive() (see file header) --- */
|
||||
static volatile int g_test_interactive = 1;
|
||||
|
||||
int xcetest_is_interactive(void) { return g_test_interactive; }
|
||||
|
||||
/* ---- test-local temp dir + fixture-file helpers (mirrors
|
||||
* test_rcfile.c's/test_configcompare.c's fresh_dir()/cleanup_dir()
|
||||
* convention) ---- */
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_confirm_test_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);
|
||||
}
|
||||
|
||||
static void write_file(const char* path, const char* content)
|
||||
{
|
||||
FILE* f = fopen(path, "wb");
|
||||
size_t len = strlen(content);
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
if (len > 0)
|
||||
TEST_ASSERT_EQUAL_INT((int)len, (int)fwrite(content, 1, len, f));
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ---- in-process stdin redirection (scenarios 1, 2, 4) ---- */
|
||||
static FILE* g_saved_stdin = NULL;
|
||||
|
||||
static void redirect_stdin_from_fd(int fd)
|
||||
{
|
||||
TEST_ASSERT_NULL(g_saved_stdin); /* catch a leaked prior redirection */
|
||||
g_saved_stdin = stdin;
|
||||
stdin = fdopen(fd, "r"); /* fresh FILE*, fresh buffer, owns fd */
|
||||
TEST_ASSERT_NOT_NULL(stdin);
|
||||
setvbuf(stdin, NULL, _IONBF, 0); /* belt-and-suspenders: no buffering
|
||||
* to ever carry residue at all */
|
||||
}
|
||||
|
||||
static void redirect_stdin_from_file(const char* path)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
TEST_ASSERT_TRUE(fd >= 0);
|
||||
redirect_stdin_from_fd(fd);
|
||||
}
|
||||
|
||||
static void restore_stdin(void)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL(g_saved_stdin);
|
||||
fclose(stdin); /* closes the fixture fd */
|
||||
stdin = g_saved_stdin;
|
||||
g_saved_stdin = NULL;
|
||||
}
|
||||
|
||||
/* ---- Scenario 1: non-interactive session (Requirement 9.4) ---- */
|
||||
|
||||
/* The fixture answer is "n\n" -- a DECLINE -- specifically so that if
|
||||
* confirm_destructive() incorrectly attempted to read it despite being
|
||||
* told the session isn't interactive, the test would observe a 0
|
||||
* (not-run) instead of the expected 1 (proceed), catching the bug via
|
||||
* the WRONG return value rather than merely "didn't crash". */
|
||||
static void test_non_interactive_session_proceeds_without_reading_stdin(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, "n\n");
|
||||
|
||||
g_test_interactive = 0;
|
||||
confirm_set_skip(0);
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
|
||||
g_test_interactive = 1; /* restore default for later tests */
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* ---- Scenario 2: decline / EOF (Requirement 9.2) ---- */
|
||||
|
||||
static void test_decline_answer_returns_not_run(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, "n\n");
|
||||
|
||||
g_test_interactive = 1;
|
||||
confirm_set_skip(0);
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_garbage_answer_returns_not_run(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, "maybe later\n");
|
||||
|
||||
g_test_interactive = 1;
|
||||
confirm_set_skip(0);
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* EOF (an empty fixture, no bytes at all) is distinguished internally by
|
||||
* confirm.c from a genuine decline (fgets() returns NULL with errno NOT
|
||||
* EINTR, vs. returning a real "n\n" string) but converges on the exact
|
||||
* same two-valued contract confirm_destructive() documents: 0/not-run.
|
||||
* design.md's Service Interface defines no third "error" outcome for
|
||||
* this function, so EOF is treated as the safer of the two available
|
||||
* answers rather than inventing a new return value. */
|
||||
static void test_eof_on_prompt_returns_not_run(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, "");
|
||||
|
||||
g_test_interactive = 1;
|
||||
confirm_set_skip(0);
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* Affirmative spelling coverage: "y"/"yes", case-insensitive, tolerant of
|
||||
* incidental surrounding whitespace -- the exact accepted spellings
|
||||
* confirm.h documents. */
|
||||
static void test_affirmative_answers_return_proceed(void)
|
||||
{
|
||||
static const char* answers[] = { "y\n", "Y\n", "yes\n", "YES\n",
|
||||
" yes \n", "Yes" };
|
||||
|
||||
for (size_t i = 0; i < sizeof(answers) / sizeof(answers[0]); i++) {
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, answers[i]);
|
||||
|
||||
g_test_interactive = 1;
|
||||
confirm_set_skip(0);
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, rc, answers[i]);
|
||||
cleanup_dir();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Scenario 4: the session-scoped skip toggle (Requirements 9.6/9.7) ---- */
|
||||
|
||||
/* Proves the toggle is a genuine short-circuit, not just a return-value
|
||||
* override: with the SAME declining fixture content, toggling skip ON
|
||||
* proceeds (never consults the decline) and toggling it back OFF makes
|
||||
* the identical content decline again. */
|
||||
static void test_skip_toggle_bypasses_and_restores_prompting(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/answer", g_dir);
|
||||
write_file(path, "n\n"); /* would decline if actually read */
|
||||
|
||||
g_test_interactive = 1;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, confirm_is_skip_set());
|
||||
confirm_set_skip(1);
|
||||
TEST_ASSERT_EQUAL_INT(1, confirm_is_skip_set());
|
||||
|
||||
redirect_stdin_from_file(path);
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
TEST_ASSERT_EQUAL_INT(1, rc); /* proceeded despite the decline fixture */
|
||||
|
||||
confirm_set_skip(0);
|
||||
TEST_ASSERT_EQUAL_INT(0, confirm_is_skip_set());
|
||||
|
||||
redirect_stdin_from_file(path); /* same fixture content, fresh fd */
|
||||
rc = confirm_destructive("do something destructive");
|
||||
restore_stdin();
|
||||
TEST_ASSERT_EQUAL_INT(0, rc); /* toggle off: prompting is back */
|
||||
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* ---- Scenario 3: a real SIGINT during the blocking prompt read
|
||||
* (Requirement 9.9) -- fork()-based, see file header. ---- */
|
||||
|
||||
#define CHILD_EXIT_NOT_RUN 4 /* confirm_destructive() returned 0 -- expected */
|
||||
#define CHILD_EXIT_PROCEED 5 /* returned 1 -- wrong: SIGINT was ignored */
|
||||
#define CHILD_EXIT_TIMED_OUT 6 /* alarm fired first -- EINTR was silently
|
||||
* retried instead of propagating, exactly
|
||||
* the bug Requirement 9.9 exists to catch */
|
||||
|
||||
static void child_alarm_handler(int signo)
|
||||
{
|
||||
(void)signo;
|
||||
_exit(CHILD_EXIT_TIMED_OUT);
|
||||
}
|
||||
|
||||
/* Stands in for test.c's sigbreak(): any real, non-default SIGINT handler
|
||||
* -- only its existence (not SIG_DFL/SIG_IGN) matters for whether the
|
||||
* blocked read() restarts or returns EINTR once xcetest_install_sigint()
|
||||
* (task 1.3) has cleared SA_RESTART. */
|
||||
static void child_sigint_noop_handler(int signo) { (void)signo; }
|
||||
|
||||
static void test_sigint_during_prompt_returns_not_run_like_decline(void)
|
||||
{
|
||||
int pipefd[2];
|
||||
TEST_ASSERT_EQUAL_INT(0, pipe(pipefd));
|
||||
|
||||
pid_t pid = fork();
|
||||
TEST_ASSERT_TRUE(pid >= 0);
|
||||
|
||||
if (pid == 0) {
|
||||
/* child: never writes to the pipe, so confirm_destructive()'s
|
||||
* fgets() can only unblock via the SIGINT the parent sends below,
|
||||
* or this alarm safety net if that fix regresses. */
|
||||
close(pipefd[1]);
|
||||
|
||||
signal(SIGALRM, child_alarm_handler);
|
||||
alarm(2);
|
||||
|
||||
xcetest_install_sigint(child_sigint_noop_handler); /* task 1.3's
|
||||
* installer --
|
||||
* the exact
|
||||
* one test.c's
|
||||
* real main()
|
||||
* uses */
|
||||
g_test_interactive = 1;
|
||||
confirm_set_skip(0);
|
||||
redirect_stdin_from_fd(pipefd[0]);
|
||||
|
||||
int rc = confirm_destructive("do something destructive");
|
||||
_exit(rc == 0 ? CHILD_EXIT_NOT_RUN : CHILD_EXIT_PROCEED);
|
||||
}
|
||||
|
||||
/* parent */
|
||||
close(pipefd[0]);
|
||||
|
||||
usleep(200000); /* give the child time to install its handler and
|
||||
* reach the blocking fgets() before we signal it */
|
||||
TEST_ASSERT_EQUAL_INT(0, kill(pid, SIGINT));
|
||||
|
||||
int status = 0;
|
||||
TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0));
|
||||
close(pipefd[1]);
|
||||
|
||||
TEST_ASSERT_TRUE(WIFEXITED(status));
|
||||
TEST_ASSERT_EQUAL_INT(CHILD_EXIT_NOT_RUN, WEXITSTATUS(status));
|
||||
}
|
||||
|
||||
void test_confirm(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_non_interactive_session_proceeds_without_reading_stdin);
|
||||
RUN_TEST(test_decline_answer_returns_not_run);
|
||||
RUN_TEST(test_garbage_answer_returns_not_run);
|
||||
RUN_TEST(test_eof_on_prompt_returns_not_run);
|
||||
RUN_TEST(test_affirmative_answers_return_proceed);
|
||||
RUN_TEST(test_skip_toggle_bypasses_and_restores_prompting);
|
||||
RUN_TEST(test_sigint_during_prompt_returns_not_run_like_decline);
|
||||
}
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "flagspec.h"
|
||||
|
||||
/* ---- fixture: a made-up two-flag test command -------------------------
|
||||
* "testcmd -h <host> [-c <cmdline>]" -- one required flag with a short
|
||||
* and long form, one optional flag with a short and long form. Used by
|
||||
* every test below so the same descriptor table exercises both
|
||||
* flagspec_build_opts() and flagspec_reject(), and so a rejection's usage
|
||||
* summary is provably the SAME string no matter which of the two flags is
|
||||
* the malformed one (the task's stated observable-completion criterion).
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
static const flagspec_t g_testcmd_spec[2] = {
|
||||
{ 'h', "host", "host", required_argument, 1, NULL },
|
||||
{ 'c', "command", "command-line", no_argument, 0, "off" },
|
||||
};
|
||||
|
||||
static const char g_testcmd_usage[] = "testcmd -h <host> [-c]";
|
||||
|
||||
/* ---- stream-capture helpers --------------------------------------------
|
||||
* Mirrors test_rcfile.c's dup/dup2/freopen technique for proving what a
|
||||
* function wrote to stderr, extended to capture stdout AND stderr at the
|
||||
* same time so a test can prove Requirement 3.1: a rejection message
|
||||
* lands on stderr only, never stdout. Uses a fresh temp dir per test
|
||||
* (mkdtemp, like test_rcfile.c/test_configcompare.c's fresh_dir()) so
|
||||
* capture files never collide with anything else in this shared /tmp. */
|
||||
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_flagspec_test_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);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int saved_stdout_fd;
|
||||
int saved_stderr_fd;
|
||||
char stdout_path[300];
|
||||
char stderr_path[300];
|
||||
} capture_t;
|
||||
|
||||
static void capture_begin(capture_t* cap)
|
||||
{
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
cap->saved_stdout_fd = dup(fileno(stdout));
|
||||
cap->saved_stderr_fd = dup(fileno(stderr));
|
||||
TEST_ASSERT_TRUE(cap->saved_stdout_fd >= 0);
|
||||
TEST_ASSERT_TRUE(cap->saved_stderr_fd >= 0);
|
||||
|
||||
snprintf(cap->stdout_path, sizeof(cap->stdout_path), "%s/out", g_dir);
|
||||
snprintf(cap->stderr_path, sizeof(cap->stderr_path), "%s/err", g_dir);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(freopen(cap->stdout_path, "w", stdout));
|
||||
TEST_ASSERT_NOT_NULL(freopen(cap->stderr_path, "w", stderr));
|
||||
}
|
||||
|
||||
/* Restores the real stdout/stderr fds and reads both captured files back
|
||||
* into caller-provided, NUL-terminated buffers. */
|
||||
static void capture_end(capture_t* cap, char* out_stdout,
|
||||
size_t out_stdout_cap, char* out_stderr,
|
||||
size_t out_stderr_cap)
|
||||
{
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
TEST_ASSERT_TRUE(dup2(cap->saved_stdout_fd, fileno(stdout)) >= 0);
|
||||
TEST_ASSERT_TRUE(dup2(cap->saved_stderr_fd, fileno(stderr)) >= 0);
|
||||
close(cap->saved_stdout_fd);
|
||||
close(cap->saved_stderr_fd);
|
||||
clearerr(stdout);
|
||||
clearerr(stderr);
|
||||
|
||||
FILE* f;
|
||||
size_t n;
|
||||
|
||||
f = fopen(cap->stdout_path, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
n = fread(out_stdout, 1, out_stdout_cap - 1, f);
|
||||
out_stdout[n] = '\0';
|
||||
fclose(f);
|
||||
|
||||
f = fopen(cap->stderr_path, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
n = fread(out_stderr, 1, out_stderr_cap - 1, f);
|
||||
out_stderr[n] = '\0';
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ---- flagspec_build_opts() tests --------------------------------------- */
|
||||
|
||||
static void test_build_opts_produces_correct_table_for_two_flag_command(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
|
||||
int rc = flagspec_build_opts(g_testcmd_spec, 2, opts, optstring,
|
||||
sizeof(optstring));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("host", opts[0].name);
|
||||
TEST_ASSERT_EQUAL_INT(required_argument, opts[0].has_arg);
|
||||
TEST_ASSERT_NULL(opts[0].flag);
|
||||
TEST_ASSERT_EQUAL_INT('h', opts[0].val);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("command", opts[1].name);
|
||||
TEST_ASSERT_EQUAL_INT(no_argument, opts[1].has_arg);
|
||||
TEST_ASSERT_NULL(opts[1].flag);
|
||||
TEST_ASSERT_EQUAL_INT('c', opts[1].val);
|
||||
|
||||
/* trailing all-zero sentinel getopt_long() requires */
|
||||
TEST_ASSERT_NULL(opts[2].name);
|
||||
TEST_ASSERT_EQUAL_INT(0, opts[2].has_arg);
|
||||
TEST_ASSERT_NULL(opts[2].flag);
|
||||
TEST_ASSERT_EQUAL_INT(0, opts[2].val);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("h:c", optstring);
|
||||
}
|
||||
|
||||
static void test_build_opts_assigns_synthetic_val_to_long_only_flag(void)
|
||||
{
|
||||
flagspec_t spec[3];
|
||||
memcpy(spec, g_testcmd_spec, sizeof(g_testcmd_spec));
|
||||
spec[2] = (flagspec_t){ 0, "json", "output-format", no_argument, 0,
|
||||
"table" };
|
||||
|
||||
struct option opts[4];
|
||||
char optstring[8];
|
||||
|
||||
int rc = flagspec_build_opts(spec, 3, opts, optstring,
|
||||
sizeof(optstring));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_STRING("json", opts[2].name);
|
||||
TEST_ASSERT_EQUAL_INT(no_argument, opts[2].has_arg);
|
||||
TEST_ASSERT_EQUAL_INT(FLAGSPEC_LONG_ONLY_BASE + 2, opts[2].val);
|
||||
/* a long-only flag contributes nothing to the short optstring */
|
||||
TEST_ASSERT_EQUAL_STRING("h:c", optstring);
|
||||
|
||||
/* still terminated correctly at the new, later index */
|
||||
TEST_ASSERT_NULL(opts[3].name);
|
||||
TEST_ASSERT_EQUAL_INT(0, opts[3].val);
|
||||
}
|
||||
|
||||
static void test_build_opts_rejects_null_spec(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
||||
flagspec_build_opts(NULL, 2, opts, optstring,
|
||||
sizeof(optstring)));
|
||||
}
|
||||
|
||||
static void test_build_opts_rejects_zero_count(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
-EINVAL, flagspec_build_opts(g_testcmd_spec, 0, opts, optstring,
|
||||
sizeof(optstring)));
|
||||
}
|
||||
|
||||
static void test_build_opts_rejects_entry_with_null_long_opt(void)
|
||||
{
|
||||
flagspec_t bad_spec[1] = {
|
||||
{ 'x', NULL, "x", no_argument, 0, NULL },
|
||||
};
|
||||
struct option opts[2];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
-EINVAL,
|
||||
flagspec_build_opts(bad_spec, 1, opts, optstring, sizeof(optstring)));
|
||||
}
|
||||
|
||||
static void test_build_opts_rejects_too_small_optstring_buffer(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
/* g_testcmd_spec builds "h:c" (h requires a value, c does not) --
|
||||
* exactly 4 bytes including the terminating NUL. A 3-byte buffer is
|
||||
* one short. */
|
||||
char optstring[3];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-ENOSPC,
|
||||
flagspec_build_opts(g_testcmd_spec, 2, opts,
|
||||
optstring, sizeof(optstring)));
|
||||
}
|
||||
|
||||
/* ---- flagspec_reject() tests: real getopt_long() pipeline -------------
|
||||
* Each case builds the real option table via flagspec_build_opts(), then
|
||||
* drives an actual getopt_long() call over a fabricated argv so the
|
||||
* bad_flag handed to flagspec_reject() is exactly what a real cmd_*
|
||||
* caller would see from its own `case '?':` arm -- not a hand-picked
|
||||
* string. */
|
||||
|
||||
static int run_getopt_long_expect_error(int argc, char* argv[],
|
||||
const struct option* opts,
|
||||
const char* optstring)
|
||||
{
|
||||
optind = 1;
|
||||
opterr = 0;
|
||||
int opt = getopt_long(argc, argv, optstring, opts, NULL);
|
||||
TEST_ASSERT_EQUAL_INT('?', opt);
|
||||
return optind;
|
||||
}
|
||||
|
||||
static void test_reject_rejects_unrecognized_long_flag(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts,
|
||||
optstring,
|
||||
sizeof(optstring)));
|
||||
|
||||
char* argv[] = { "testcmd", "--bogus", NULL };
|
||||
int after = run_getopt_long_expect_error(2, argv, opts, optstring);
|
||||
const char* bad_flag = argv[after - 1];
|
||||
TEST_ASSERT_EQUAL_STRING("--bogus", bad_flag);
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag,
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--bogus'"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h <host> [-c]"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_rejects_unrecognized_short_flag(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts,
|
||||
optstring,
|
||||
sizeof(optstring)));
|
||||
|
||||
char* argv[] = { "testcmd", "-z", NULL };
|
||||
int after = run_getopt_long_expect_error(2, argv, opts, optstring);
|
||||
const char* bad_flag = argv[after - 1];
|
||||
TEST_ASSERT_EQUAL_STRING("-z", bad_flag);
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag,
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '-z'"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h <host> [-c]"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_rejects_missing_value_long_flag(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts,
|
||||
optstring,
|
||||
sizeof(optstring)));
|
||||
|
||||
/* "--host" is the last argv element -- getopt_long() has nothing to
|
||||
* consume as its required value. */
|
||||
char* argv[] = { "testcmd", "--host", NULL };
|
||||
int after = run_getopt_long_expect_error(2, argv, opts, optstring);
|
||||
const char* bad_flag = argv[after - 1];
|
||||
TEST_ASSERT_EQUAL_STRING("--host", bad_flag);
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag,
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '--host' requires a value"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h <host> [-c]"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_rejects_missing_value_short_flag(void)
|
||||
{
|
||||
struct option opts[3];
|
||||
char optstring[8];
|
||||
TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts,
|
||||
optstring,
|
||||
sizeof(optstring)));
|
||||
|
||||
char* argv[] = { "testcmd", "-h", NULL };
|
||||
int after = run_getopt_long_expect_error(2, argv, opts, optstring);
|
||||
const char* bad_flag = argv[after - 1];
|
||||
TEST_ASSERT_EQUAL_STRING("-h", bad_flag);
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag,
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '-h' requires a value"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h <host> [-c]"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* Direct-call (non-getopt_long) tests: exercises flagspec_reject()'s own
|
||||
* argument handling in isolation, including NULL inputs it must survive
|
||||
* without crashing. */
|
||||
|
||||
static void test_reject_handles_null_bad_flag_and_null_usage_gracefully(void)
|
||||
{
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, NULL, NULL);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_no_argument_flag_given_a_value_reports_distinctly(void)
|
||||
{
|
||||
/* "-c" never requires a value; a caller that somehow reaches
|
||||
* flagspec_reject() with the bare "-c" token (matches an entry, but
|
||||
* that entry takes no_argument) must be told a different thing than
|
||||
* either the unrecognized-flag or missing-value message. */
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "-c",
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '-c' does not take a value"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h <host> [-c]"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* ---- flagspec_reject() typo-suggestion tests (Requirement 10) ---------
|
||||
* A second fixture, g_ambiguous_spec, is purpose-built for the
|
||||
* ambiguous-match case below so its edit distances are deliberate rather
|
||||
* than incidental to g_testcmd_spec's two very dissimilar flag names. */
|
||||
|
||||
static const flagspec_t g_ambiguous_spec[2] = {
|
||||
{ 0, "targeta", "target-a", no_argument, 0, NULL },
|
||||
{ 0, "targetb", "target-b", no_argument, 0, NULL },
|
||||
};
|
||||
|
||||
static void test_reject_suggests_single_close_match_flag_typo(void)
|
||||
{
|
||||
/* "--hst" is "--host" with the 'o' deleted -- one edit, well inside
|
||||
* the closeness threshold for a 6-character long-form spelling -- and
|
||||
* is nowhere near "--command". Requirement 10.1: name the
|
||||
* likely-intended flag in the rejection message. */
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--hst",
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--hst'"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "did you mean"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "--host"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_gives_no_suggestion_for_a_distant_typo(void)
|
||||
{
|
||||
/* "--wombat" does not closely resemble "--host" or "--command" by any
|
||||
* reasonable edit-distance threshold. Requirement 10.2: report
|
||||
* unrecognized without guessing. */
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--wombat",
|
||||
g_testcmd_usage);
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--wombat'"));
|
||||
TEST_ASSERT_NULL(strstr(err_buf, "did you mean"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_reject_gives_no_suggestion_when_typo_is_ambiguous(void)
|
||||
{
|
||||
/* "--targetc" is exactly one substitution away from BOTH "--targeta"
|
||||
* and "--targetb". Requirement 10.1 only promises a guess when
|
||||
* exactly one flag qualifies -- two equally-plausible candidates must
|
||||
* produce no suggestion at all, the same as a typo that matches
|
||||
* nothing. */
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
int rc = flagspec_reject("testcmd", g_ambiguous_spec, 2, "--targetc",
|
||||
"testcmd --targeta|--targetb");
|
||||
char out_buf[512], err_buf[512];
|
||||
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
|
||||
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_EQUAL_STRING("", out_buf);
|
||||
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--targetc'"));
|
||||
TEST_ASSERT_NULL(strstr(err_buf, "did you mean"));
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* ---- flagspec_render_help() tests (Requirement 8.2) -------------------- */
|
||||
|
||||
static void test_render_help_formats_multi_flag_table(void)
|
||||
{
|
||||
flagspec_t spec[3];
|
||||
memcpy(spec, g_testcmd_spec, sizeof(g_testcmd_spec));
|
||||
spec[2] = (flagspec_t){ 0, "json", "output-format", no_argument, 0,
|
||||
"table" };
|
||||
|
||||
char* help = flagspec_render_help(spec, 3);
|
||||
TEST_ASSERT_NOT_NULL(help);
|
||||
|
||||
/* required flag: short + long form, marked required */
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "-h"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "--host"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "required"));
|
||||
|
||||
/* optional flag: short + long form, marked optional, default shown */
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "-c"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "--command"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "optional"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "off"));
|
||||
|
||||
/* long-only flag: no short form, still shows long form + default */
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "--json"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(help, "table"));
|
||||
|
||||
free(help);
|
||||
}
|
||||
|
||||
static void test_render_help_rejects_null_and_empty_inputs(void)
|
||||
{
|
||||
TEST_ASSERT_NULL(flagspec_render_help(NULL, 2));
|
||||
TEST_ASSERT_NULL(flagspec_render_help(g_testcmd_spec, 0));
|
||||
}
|
||||
|
||||
void test_flagspec(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_build_opts_produces_correct_table_for_two_flag_command);
|
||||
RUN_TEST(test_build_opts_assigns_synthetic_val_to_long_only_flag);
|
||||
RUN_TEST(test_build_opts_rejects_null_spec);
|
||||
RUN_TEST(test_build_opts_rejects_zero_count);
|
||||
RUN_TEST(test_build_opts_rejects_entry_with_null_long_opt);
|
||||
RUN_TEST(test_build_opts_rejects_too_small_optstring_buffer);
|
||||
RUN_TEST(test_reject_rejects_unrecognized_long_flag);
|
||||
RUN_TEST(test_reject_rejects_unrecognized_short_flag);
|
||||
RUN_TEST(test_reject_rejects_missing_value_long_flag);
|
||||
RUN_TEST(test_reject_rejects_missing_value_short_flag);
|
||||
RUN_TEST(test_reject_handles_null_bad_flag_and_null_usage_gracefully);
|
||||
RUN_TEST(test_reject_no_argument_flag_given_a_value_reports_distinctly);
|
||||
RUN_TEST(test_reject_suggests_single_close_match_flag_typo);
|
||||
RUN_TEST(test_reject_gives_no_suggestion_for_a_distant_typo);
|
||||
RUN_TEST(test_reject_gives_no_suggestion_when_typo_is_ambiguous);
|
||||
RUN_TEST(test_render_help_formats_multi_flag_table);
|
||||
RUN_TEST(test_render_help_rejects_null_and_empty_inputs);
|
||||
}
|
||||
|
|
@ -1,582 +0,0 @@
|
|||
/*
|
||||
* test_interruptcall.c -- proves xcetest_run_interruptible()'s (task 1.4,
|
||||
* Requirements 1.1, 1.2, 1.3, 1.4) heap-block CAS ownership handoff: a
|
||||
* normal/fast call returns its real result and frees cleanly, and a
|
||||
* forced-abandonment call returns -EINTR immediately with no double-free
|
||||
* and no leak -- the abandoned worker thread finishes in the background
|
||||
* and frees the block itself, proven via a side channel rather than
|
||||
* assumed from a fixed sleep.
|
||||
*
|
||||
* ALSO proves (task 1.5, Requirements 1.1, 1.5) the per-connection lock
|
||||
* registry added on top of that same call: two calls sharing a
|
||||
* connection identity (set via xcetest_set_active_connection()) never
|
||||
* run their fn bodies concurrently -- including a call issued right
|
||||
* after a prior one on that same connection was abandoned, which must
|
||||
* wait (interruptibly) for the abandoned worker to finish and release
|
||||
* the lock -- while two calls against DIFFERENT connection identities
|
||||
* run independently/concurrently. A third scenario proves that a call
|
||||
* interrupted WHILE waiting for a lock still held by a prior abandoned
|
||||
* call returns -EINTR without ever acquiring the lock or spawning a
|
||||
* worker for itself.
|
||||
*
|
||||
* xcetest_interrupted() (declared in xcetest_interrupt.h) is normally
|
||||
* defined in test.c, over test.c's own private break_out flag -- but
|
||||
* test.c itself is never linked into controller_unit_test (see
|
||||
* xcetest_interrupt.h's own header comment: test.c pulls in libedit/
|
||||
* cJSON/libxce/Cap'n Proto/OpenSSL and defines its own main(), both of
|
||||
* which make it impractical to link into a small Unity binary). So this
|
||||
* file provides its OWN local fake definition of xcetest_interrupted(),
|
||||
* over its own local flag -- the same "local fake standing in for a
|
||||
* real dependency this binary never links" pattern already used
|
||||
* elsewhere in this directory (e.g. test_commit_barrier_reply.c's/
|
||||
* test_worker_join.c's own fakes for on_scheduler_join()/notify()/
|
||||
* collect_confvars(), per src/controller/meson.build's comment on
|
||||
* sched_push_unit_test_src). interruptcall.c only ever calls
|
||||
* xcetest_interrupted() through its documented pure-read contract, so a
|
||||
* fake reading a test-local flag exercises interruptcall.c's real CAS
|
||||
* state machine exactly as production would.
|
||||
*
|
||||
* The forced-abandonment test triggers a REAL, OS-delivered SIGINT (via
|
||||
* kill(getpid(), SIGINT), sent from a small helper thread once the
|
||||
* worker has had time to start) rather than flipping the fake flag
|
||||
* directly by hand, and installs its handler via xcetest_install_sigint()
|
||||
* (task 1.3) -- the exact installer test.c's own main() uses -- so this
|
||||
* test also doubles as a real, signal-level exercise of the same
|
||||
* interrupt path a live xce_test process would take, not a purely
|
||||
* hand-simulated one. This is choice (ii) from the task brief (a real
|
||||
* SIGINT), preferred over choice (i) (a direct break_out setter) because
|
||||
* no such setter exists on the REAL break_out anyway (xcetest_interrupt.h
|
||||
* deliberately exposes none) -- and a real signal exercises the actual
|
||||
* delivery path end-to-end, the same thing test_xcetest_interrupt.c
|
||||
* already does for xcetest_install_sigint() itself.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "interruptcall.h"
|
||||
#include "xcetest_interrupt.h"
|
||||
|
||||
/* --- local fake for xcetest_interrupted() (see file header) --- */
|
||||
static volatile sig_atomic_t g_test_break_out = 0;
|
||||
|
||||
int xcetest_interrupted(void) { return g_test_break_out; }
|
||||
|
||||
static void test_sigint_handler(int signo)
|
||||
{
|
||||
(void)signo;
|
||||
g_test_break_out = 1;
|
||||
}
|
||||
|
||||
/* ---- Scenario (a): normal, uninterrupted completion ---- */
|
||||
|
||||
#define NORMAL_FN_RC 7
|
||||
|
||||
static int fast_fn(void* ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return NORMAL_FN_RC;
|
||||
}
|
||||
|
||||
static void test_normal_completion_returns_real_result(void)
|
||||
{
|
||||
static int conn_normal; /* private fake connection key for this test */
|
||||
|
||||
g_test_break_out = 0;
|
||||
xcetest_set_active_connection(&conn_normal);
|
||||
|
||||
int rc = xcetest_run_interruptible(fast_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(NORMAL_FN_RC, rc);
|
||||
/* No side channel to poll here: on this path the CALLING thread's own
|
||||
* poll loop reads block->rc and frees the block synchronously, before
|
||||
* xcetest_run_interruptible() ever returns -- so by the time the
|
||||
* TEST_ASSERT_EQUAL_INT above runs, cleanup has unconditionally
|
||||
* already happened. The remaining proof is this whole suite running
|
||||
* clean under ASAN (no leak, no double-free) in the validating
|
||||
* build+test run. */
|
||||
}
|
||||
|
||||
/* ---- Scenario (b): forced abandonment via a real SIGINT ---- */
|
||||
|
||||
#define SLOW_FN_RC 99
|
||||
#define SLOW_FN_SLEEP_MS 900
|
||||
#define SIGNAL_DELAY_MS 150
|
||||
#define WAIT_FOR_FREE_MS 3000
|
||||
#define POLL_STEP_MS 20
|
||||
|
||||
static int slow_fn(void* ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = (long)SLOW_FN_SLEEP_MS * 1000L * 1000L;
|
||||
nanosleep(&ts, NULL);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
/* Runs on its own helper thread so it can deliver SIGINT to the process
|
||||
* WHILE the main test thread is blocked inside
|
||||
* xcetest_run_interruptible()'s poll loop below. */
|
||||
static void* send_sigint_after_delay(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = (long)SIGNAL_DELAY_MS * 1000L * 1000L;
|
||||
nanosleep(&ts, NULL);
|
||||
kill(getpid(), SIGINT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Polls the XCE_TESTING-only xcetest_test_abandoned_free_count side
|
||||
* channel (NOT part of the production API) until it reaches `expected`
|
||||
* or a generous timeout elapses, then asserts it landed exactly on
|
||||
* `expected` -- so a test proves an abandoned worker's asynchronous
|
||||
* cleanup (free + lock release, see interruptcall.c's abandoned path)
|
||||
* actually completed, rather than merely hoping enough wall-clock time
|
||||
* passed before the process moves on. Shared by every scenario below
|
||||
* that produces an abandoned-but-still-running worker. */
|
||||
static void wait_for_abandoned_free_count(int expected)
|
||||
{
|
||||
int waited_ms = 0;
|
||||
while (atomic_load(&xcetest_test_abandoned_free_count) < expected
|
||||
&& waited_ms < WAIT_FOR_FREE_MS) {
|
||||
struct timespec poll;
|
||||
poll.tv_sec = 0;
|
||||
poll.tv_nsec = (long)POLL_STEP_MS * 1000L * 1000L;
|
||||
nanosleep(&poll, NULL);
|
||||
waited_ms += POLL_STEP_MS;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(expected, atomic_load(&xcetest_test_abandoned_free_count));
|
||||
}
|
||||
|
||||
static void test_forced_abandonment_returns_eintr_and_worker_cleans_up(void)
|
||||
{
|
||||
static int conn_abandon; /* private fake connection key for this test */
|
||||
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
xcetest_set_active_connection(&conn_abandon);
|
||||
|
||||
pthread_t signaler;
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
/* SLOW_FN_SLEEP_MS (900ms) comfortably exceeds SIGNAL_DELAY_MS (150ms)
|
||||
* plus several 50ms poll intervals, so the interrupt is reliably
|
||||
* noticed well before slow_fn() itself would have returned -- this
|
||||
* call is expected to return -EINTR, not SLOW_FN_RC. */
|
||||
int rc = xcetest_run_interruptible(slow_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler, NULL));
|
||||
|
||||
/* The abandoned worker is still running slow_fn() in the background
|
||||
* at this point; wait_for_abandoned_free_count() proves its
|
||||
* asynchronous cleanup actually happened -- confirming no leak --
|
||||
* rather than merely hoping enough wall-clock time passed before the
|
||||
* process exits. */
|
||||
wait_for_abandoned_free_count(1);
|
||||
}
|
||||
|
||||
/* ---- Scenario (c): per-connection lock registry (task 1.5) ----
|
||||
*
|
||||
* Requirements 1.1, 1.5. Three scenarios, per the task's Observable
|
||||
* Completion line:
|
||||
* (i) two overlapping calls sharing the SAME fake connection key
|
||||
* (one abandoned, one issued immediately after) never run their
|
||||
* fn bodies concurrently -- the second waits for the first's
|
||||
* still-running abandoned worker to release the lock;
|
||||
* (ii) two calls against DIFFERENT fake connection keys run
|
||||
* independently -- one's abandonment does not block the other;
|
||||
* (iii) a call interrupted WHILE waiting for a lock still held by a
|
||||
* prior abandoned call returns -EINTR without ever acquiring
|
||||
* the lock or spawning a worker for itself.
|
||||
*
|
||||
* All three reuse the same real-SIGINT-delivery pattern as scenario (b)
|
||||
* above (xcetest_install_sigint() + a helper thread's kill(getpid(),
|
||||
* SIGINT)) rather than flipping g_test_break_out by hand, for the same
|
||||
* reason given in this file's header comment. g_test_break_out is
|
||||
* intentionally reset to 0 between two calls in the same scenario
|
||||
* whenever the SECOND call is expected to complete normally (not be
|
||||
* interrupted itself) -- xcetest_interrupted() is a pure, non-clearing
|
||||
* read (see xcetest_interrupt.h's CONTRACT), so a stale "interrupted"
|
||||
* flag left set by an earlier call in the same test would otherwise be
|
||||
* observed by the second call's own lock-wait or completion-wait poll
|
||||
* and spuriously abandon it too.
|
||||
*/
|
||||
|
||||
/* Shared probe: counts how many fn instances are concurrently "inside"
|
||||
* their body right now, and how many times an instance started while
|
||||
* that count was already nonzero. Used two ways below: scenario (i)
|
||||
* asserts overlap_count stays 0 (same key must serialize); scenario
|
||||
* (ii) asserts it becomes nonzero (different keys must NOT serialize). */
|
||||
typedef struct {
|
||||
_Atomic int running;
|
||||
_Atomic int overlap_count;
|
||||
_Atomic int ran;
|
||||
} overlap_probe_t;
|
||||
|
||||
static int probe_enter(overlap_probe_t* p)
|
||||
{
|
||||
int prev = atomic_fetch_add(&p->running, 1);
|
||||
if (prev != 0)
|
||||
atomic_fetch_add(&p->overlap_count, 1);
|
||||
atomic_store(&p->ran, 1);
|
||||
return prev;
|
||||
}
|
||||
|
||||
static void probe_leave(overlap_probe_t* p)
|
||||
{
|
||||
atomic_fetch_sub(&p->running, 1);
|
||||
}
|
||||
|
||||
static void sleep_ms(long ms)
|
||||
{
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000L * 1000L;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
static long elapsed_ms_since(const struct timespec* start)
|
||||
{
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (now.tv_sec - start->tv_sec) * 1000L
|
||||
+ (now.tv_nsec - start->tv_nsec) / 1000000L;
|
||||
}
|
||||
|
||||
/* -- (i) same connection key serializes -- */
|
||||
|
||||
#define SAME_KEY_SLOW_SLEEP_MS 900
|
||||
#define SAME_KEY_FAST_SLEEP_MS 50
|
||||
#define SAME_KEY_FAST_RC 42
|
||||
#define SAME_KEY_MIN_WAIT_MS 300 /* well below the ~700ms this call
|
||||
* should actually wait for the
|
||||
* first call's abandoned worker,
|
||||
* comfortably above scheduling
|
||||
* noise if the lock were NOT
|
||||
* enforced (that failure mode
|
||||
* returns in tens of ms). */
|
||||
|
||||
static int same_key_slow_fn(void* arg)
|
||||
{
|
||||
overlap_probe_t* p = arg;
|
||||
probe_enter(p);
|
||||
sleep_ms(SAME_KEY_SLOW_SLEEP_MS);
|
||||
probe_leave(p);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
static int same_key_fast_fn(void* arg)
|
||||
{
|
||||
overlap_probe_t* p = arg;
|
||||
probe_enter(p);
|
||||
sleep_ms(SAME_KEY_FAST_SLEEP_MS);
|
||||
probe_leave(p);
|
||||
return SAME_KEY_FAST_RC;
|
||||
}
|
||||
|
||||
static void test_same_connection_key_serializes_overlapping_calls(void)
|
||||
{
|
||||
static int conn_a; /* address used purely as an opaque fake key */
|
||||
overlap_probe_t probe = {0};
|
||||
pthread_t signaler;
|
||||
struct timespec wait_start;
|
||||
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
xcetest_set_active_connection(&conn_a);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
int rc1 = xcetest_run_interruptible(same_key_slow_fn, &probe);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler, NULL));
|
||||
TEST_ASSERT_TRUE_MESSAGE(atomic_load(&probe.ran),
|
||||
"the slow call must have started running "
|
||||
"before it was abandoned");
|
||||
|
||||
/* conn_a's lock is now held by the still-running abandoned worker.
|
||||
* Reset the fake interrupt flag -- this second call must NOT be
|
||||
* treated as interrupted itself; it is expected to wait for, then
|
||||
* acquire, the same lock once the first worker releases it. */
|
||||
g_test_break_out = 0;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &wait_start);
|
||||
int rc2 = xcetest_run_interruptible(same_key_fast_fn, &probe);
|
||||
long waited_ms = elapsed_ms_since(&wait_start);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(SAME_KEY_FAST_RC, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(0, atomic_load(&probe.overlap_count));
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
waited_ms >= SAME_KEY_MIN_WAIT_MS,
|
||||
"second call on the same connection returned too quickly -- the "
|
||||
"per-connection lock did not make it wait for the first call's "
|
||||
"abandoned worker to finish and release it");
|
||||
|
||||
/* By the time rc2's call could acquire the lock, the abandoned
|
||||
* worker's cleanup (free + counter increment, THEN lock release --
|
||||
* see interruptcall.c's abandoned path) must already have happened. */
|
||||
TEST_ASSERT_EQUAL_INT(1, atomic_load(&xcetest_test_abandoned_free_count));
|
||||
}
|
||||
|
||||
/* -- (ii) different connection keys run independently -- */
|
||||
|
||||
#define DIFF_KEY_SLOW_SLEEP_MS 900
|
||||
#define DIFF_KEY_FAST_SLEEP_MS 20
|
||||
#define DIFF_KEY_FAST_RC 77
|
||||
|
||||
static int diff_key_slow_fn(void* arg)
|
||||
{
|
||||
overlap_probe_t* p = arg;
|
||||
probe_enter(p);
|
||||
sleep_ms(DIFF_KEY_SLOW_SLEEP_MS);
|
||||
probe_leave(p);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
static int diff_key_fast_fn(void* arg)
|
||||
{
|
||||
overlap_probe_t* p = arg;
|
||||
probe_enter(p);
|
||||
sleep_ms(DIFF_KEY_FAST_SLEEP_MS);
|
||||
probe_leave(p);
|
||||
return DIFF_KEY_FAST_RC;
|
||||
}
|
||||
|
||||
static void test_different_connection_keys_run_independently(void)
|
||||
{
|
||||
static int conn_b, conn_c; /* two distinct opaque fake keys */
|
||||
overlap_probe_t probe = {0};
|
||||
pthread_t signaler;
|
||||
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
xcetest_set_active_connection(&conn_b);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
int rc1 = xcetest_run_interruptible(diff_key_slow_fn, &probe);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler, NULL));
|
||||
|
||||
/* Reset the fake flag before the second call, same reasoning as
|
||||
* scenario (i) -- this call is on a DIFFERENT connection and must
|
||||
* complete normally. */
|
||||
g_test_break_out = 0;
|
||||
xcetest_set_active_connection(&conn_c);
|
||||
|
||||
int rc2 = xcetest_run_interruptible(diff_key_fast_fn, &probe);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(DIFF_KEY_FAST_RC, rc2);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
atomic_load(&probe.overlap_count) >= 1,
|
||||
"different connection keys must not serialize against each "
|
||||
"other -- expected the second call to run concurrently with the "
|
||||
"first call's still-running abandoned worker");
|
||||
|
||||
wait_for_abandoned_free_count(1);
|
||||
}
|
||||
|
||||
/* -- (iii) interrupted while waiting for a held lock: no worker spawned -- */
|
||||
|
||||
#define WAIT_KEY_SLOW_SLEEP_MS 900
|
||||
|
||||
static int wait_key_slow_fn(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
sleep_ms(WAIT_KEY_SLOW_SLEEP_MS);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
static _Atomic int g_wait_key_fn_invoked = 0;
|
||||
|
||||
static int wait_key_never_should_run_fn(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
atomic_store(&g_wait_key_fn_invoked, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_interrupted_while_waiting_for_lock_never_spawns_worker(void)
|
||||
{
|
||||
static int conn_d;
|
||||
pthread_t signaler1, signaler2;
|
||||
struct timespec t0;
|
||||
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
atomic_store(&g_wait_key_fn_invoked, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
xcetest_set_active_connection(&conn_d);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler1, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
int rc1 = xcetest_run_interruptible(wait_key_slow_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler1, NULL));
|
||||
|
||||
/* conn_d's lock is now held by the still-running abandoned worker
|
||||
* (up to ~750ms left of its 900ms sleep). Reset the fake flag, then
|
||||
* arm a SECOND delayed SIGINT that fires while THIS next call is
|
||||
* still waiting to acquire that same lock -- well before the
|
||||
* abandoned worker could ever release it. */
|
||||
g_test_break_out = 0;
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler2, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
int rc2 = xcetest_run_interruptible(wait_key_never_should_run_fn, NULL);
|
||||
long elapsed_ms = elapsed_ms_since(&t0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler2, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc2);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, atomic_load(&g_wait_key_fn_invoked),
|
||||
"a call interrupted while waiting for a held connection lock "
|
||||
"must never spawn a worker / invoke fn for itself");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
elapsed_ms < (WAIT_KEY_SLOW_SLEEP_MS - SIGNAL_DELAY_MS),
|
||||
"the interrupted lock-wait should return promptly, well before "
|
||||
"the held lock would ever naturally free");
|
||||
|
||||
/* The first call's abandoned worker is still running in the
|
||||
* background; wait for it to finish so this test leaves no live
|
||||
* thread behind when it returns. */
|
||||
wait_for_abandoned_free_count(1);
|
||||
}
|
||||
|
||||
/* -- (iv) xcetest_wait_connection_idle() (task 3.2 remediation round 1,
|
||||
* bug-1795) -- deterministic proof of the exact mechanism leave_ctx() now
|
||||
* relies on to close the connection-teardown use-after-free.
|
||||
*
|
||||
* test_test_cli.c's black-box test_interrupt_then_immediate_teardown_no_uaf
|
||||
* exercises the SAME fix end-to-end through a real xce_test subprocess, but
|
||||
* the actual race window there (between an abandoned worker being woken by
|
||||
* xcecli_wakeup() and leave_ctx()'s teardown proceeding) is sub-millisecond
|
||||
* and timing-dependent -- it did not reliably reproduce as a failing test
|
||||
* even with the fix disabled in this environment (matching the original
|
||||
* round-1 review's own observation that a clean ASAN run is evidence the
|
||||
* race didn't fire on THAT run's timing, not evidence the gap doesn't
|
||||
* exist). This test instead proves xcetest_wait_connection_idle() itself,
|
||||
* in isolation, with a WIDE, deterministic timing margin (a 700ms abandoned
|
||||
* worker, not a sub-millisecond one) -- a direct, reliable RED (this
|
||||
* function returning early would fail it) / GREEN (the shipped
|
||||
* implementation passes it) demonstration of the invariant leave_ctx()
|
||||
* depends on. */
|
||||
|
||||
#define WCI_SLOW_SLEEP_MS 700
|
||||
#define WCI_MIN_WAIT_MS 400 /* comfortably below the ~700ms - \
|
||||
* SIGNAL_DELAY_MS remaining sleep once the \
|
||||
* call is abandoned, comfortably above \
|
||||
* scheduling noise if \
|
||||
* xcetest_wait_connection_idle() did NOT \
|
||||
* actually wait (that failure mode returns \
|
||||
* in ~0ms). */
|
||||
|
||||
static int wci_slow_fn(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
sleep_ms(WCI_SLOW_SLEEP_MS);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
static void test_wait_connection_idle_blocks_until_abandoned_worker_finishes(
|
||||
void)
|
||||
{
|
||||
static int conn_e; /* private fake connection key for this test */
|
||||
pthread_t signaler;
|
||||
struct timespec wait_start;
|
||||
long waited_ms;
|
||||
int rc2;
|
||||
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
xcetest_set_active_connection(&conn_e);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
int rc = xcetest_run_interruptible(wci_slow_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler, NULL));
|
||||
|
||||
/* At this instant the abandoned worker is still running wci_slow_fn()
|
||||
* -- it has NOT yet freed its call block or released conn_e's lock
|
||||
* (interruptcall.c's abandoned path). This is exactly the production
|
||||
* race leave_ctx() closes: xcetest_wait_connection_idle() below must
|
||||
* genuinely block until that worker finishes, not return immediately. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, atomic_load(&xcetest_test_abandoned_free_count),
|
||||
"the abandoned worker must still be running at this point for "
|
||||
"this test to actually exercise the wait -- if this fires, the "
|
||||
"worker finished implausibly fast and the timing margin above "
|
||||
"needs to grow, not shrink");
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &wait_start);
|
||||
xcetest_wait_connection_idle(&conn_e);
|
||||
waited_ms = elapsed_ms_since(&wait_start);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
waited_ms >= WCI_MIN_WAIT_MS,
|
||||
"xcetest_wait_connection_idle() returned too quickly -- it must "
|
||||
"genuinely block until the abandoned worker finishes, not return "
|
||||
"immediately (this is precisely the bug-1795 use-after-free gap: "
|
||||
"a caller proceeding to tear down the connection before this "
|
||||
"point would race the still-running worker)");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, atomic_load(&xcetest_test_abandoned_free_count),
|
||||
"the abandoned worker's cleanup must have genuinely completed by "
|
||||
"the time xcetest_wait_connection_idle() returns");
|
||||
|
||||
/* The semaphore must be left available (posted back), not held --
|
||||
* proven by a normal subsequent call on the SAME connection key
|
||||
* completing without waiting at all. Reset the fake interrupt flag
|
||||
* first -- xcetest_interrupted() is a pure, non-clearing read (see
|
||||
* xcetest_interrupt.h's CONTRACT), so the flag the earlier SIGINT set
|
||||
* is still "on" here and would otherwise spuriously abandon this call
|
||||
* too, same reasoning as every other multi-call scenario in this
|
||||
* file. */
|
||||
g_test_break_out = 0;
|
||||
clock_gettime(CLOCK_MONOTONIC, &wait_start);
|
||||
rc2 = xcetest_run_interruptible(fast_fn, NULL);
|
||||
waited_ms = elapsed_ms_since(&wait_start);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(NORMAL_FN_RC, rc2);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
waited_ms < WCI_MIN_WAIT_MS,
|
||||
"xcetest_wait_connection_idle() must release the semaphore back "
|
||||
"immediately after proving idleness, not hold it -- a subsequent "
|
||||
"call on the same connection should not itself have to wait");
|
||||
}
|
||||
|
||||
void test_interruptcall(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_normal_completion_returns_real_result);
|
||||
RUN_TEST(test_forced_abandonment_returns_eintr_and_worker_cleans_up);
|
||||
RUN_TEST(test_same_connection_key_serializes_overlapping_calls);
|
||||
RUN_TEST(test_different_connection_keys_run_independently);
|
||||
RUN_TEST(test_interrupted_while_waiting_for_lock_never_spawns_worker);
|
||||
RUN_TEST(test_wait_connection_idle_blocks_until_abandoned_worker_finishes);
|
||||
}
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "listfmt_json.h"
|
||||
|
||||
/* ---- stream-capture helpers --------------------------------------------
|
||||
* Mirrors test_flagspec.c's dup/dup2/freopen stdout+stderr capture
|
||||
* technique, so this test can prove Requirement 3's routing: JSON output
|
||||
* is command-RESULT data and must land on stdout only, never stderr. Uses
|
||||
* a fresh temp dir per test (mkdtemp) so capture files never collide with
|
||||
* anything else in this shared /tmp. */
|
||||
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_listfmt_json_test_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);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int saved_stdout_fd;
|
||||
int saved_stderr_fd;
|
||||
char stdout_path[300];
|
||||
char stderr_path[300];
|
||||
} capture_t;
|
||||
|
||||
static void capture_begin(capture_t* cap)
|
||||
{
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
cap->saved_stdout_fd = dup(fileno(stdout));
|
||||
cap->saved_stderr_fd = dup(fileno(stderr));
|
||||
TEST_ASSERT_TRUE(cap->saved_stdout_fd >= 0);
|
||||
TEST_ASSERT_TRUE(cap->saved_stderr_fd >= 0);
|
||||
|
||||
snprintf(cap->stdout_path, sizeof(cap->stdout_path), "%s/out", g_dir);
|
||||
snprintf(cap->stderr_path, sizeof(cap->stderr_path), "%s/err", g_dir);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(freopen(cap->stdout_path, "w", stdout));
|
||||
TEST_ASSERT_NOT_NULL(freopen(cap->stderr_path, "w", stderr));
|
||||
}
|
||||
|
||||
/* Restores the real stdout/stderr fds and reads both captured files back
|
||||
* into caller-provided, NUL-terminated, heap-allocated buffers (the long-
|
||||
* string test needs more than a fixed small stack buffer can safely
|
||||
* hold). Caller must free() both *out_stdout and *out_stderr. */
|
||||
static void capture_end(capture_t* cap, char** out_stdout, char** out_stderr)
|
||||
{
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
TEST_ASSERT_TRUE(dup2(cap->saved_stdout_fd, fileno(stdout)) >= 0);
|
||||
TEST_ASSERT_TRUE(dup2(cap->saved_stderr_fd, fileno(stderr)) >= 0);
|
||||
close(cap->saved_stdout_fd);
|
||||
close(cap->saved_stderr_fd);
|
||||
clearerr(stdout);
|
||||
clearerr(stderr);
|
||||
|
||||
FILE* f;
|
||||
long len;
|
||||
|
||||
f = fopen(cap->stdout_path, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fseek(f, 0, SEEK_END);
|
||||
len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
*out_stdout = malloc((size_t)len + 1);
|
||||
TEST_ASSERT_NOT_NULL(*out_stdout);
|
||||
size_t n = fread(*out_stdout, 1, (size_t)len, f);
|
||||
(*out_stdout)[n] = '\0';
|
||||
fclose(f);
|
||||
|
||||
f = fopen(cap->stderr_path, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fseek(f, 0, SEEK_END);
|
||||
len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
*out_stderr = malloc((size_t)len + 1);
|
||||
TEST_ASSERT_NOT_NULL(*out_stderr);
|
||||
n = fread(*out_stderr, 1, (size_t)len, f);
|
||||
(*out_stderr)[n] = '\0';
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ---- lifecycle tests: array/object plumbing, no printing involved ----
|
||||
* These build a cJSON tree directly with the module under test, inspect
|
||||
* it with cJSON's own accessors, and clean up with a plain cJSON_Delete
|
||||
* (NOT listfmt_json_finish(), which also prints -- these tests are about
|
||||
* the tree shape, not stdout). */
|
||||
|
||||
static void test_begin_returns_empty_array(void)
|
||||
{
|
||||
cJSON* array = listfmt_json_begin();
|
||||
TEST_ASSERT_NOT_NULL(array);
|
||||
TEST_ASSERT_TRUE(cJSON_IsArray(array));
|
||||
TEST_ASSERT_EQUAL_INT(0, cJSON_GetArraySize(array));
|
||||
cJSON_Delete(array);
|
||||
}
|
||||
|
||||
static void test_new_row_appends_object_and_returns_it(void)
|
||||
{
|
||||
cJSON* array = listfmt_json_begin();
|
||||
TEST_ASSERT_NOT_NULL(array);
|
||||
|
||||
cJSON* row1 = listfmt_json_new_row(array);
|
||||
TEST_ASSERT_NOT_NULL(row1);
|
||||
TEST_ASSERT_TRUE(cJSON_IsObject(row1));
|
||||
TEST_ASSERT_EQUAL_INT(1, cJSON_GetArraySize(array));
|
||||
|
||||
cJSON* row2 = listfmt_json_new_row(array);
|
||||
TEST_ASSERT_NOT_NULL(row2);
|
||||
TEST_ASSERT_TRUE(row2 != row1);
|
||||
TEST_ASSERT_EQUAL_INT(2, cJSON_GetArraySize(array));
|
||||
|
||||
TEST_ASSERT_TRUE(cJSON_GetArrayItem(array, 0) == row1);
|
||||
TEST_ASSERT_TRUE(cJSON_GetArrayItem(array, 1) == row2);
|
||||
|
||||
cJSON_Delete(array);
|
||||
}
|
||||
|
||||
static void test_new_row_returns_null_on_null_array(void)
|
||||
{
|
||||
TEST_ASSERT_NULL(listfmt_json_new_row(NULL));
|
||||
}
|
||||
|
||||
static void test_add_string_and_number_populate_row_fields(void)
|
||||
{
|
||||
cJSON* array = listfmt_json_begin();
|
||||
cJSON* row = listfmt_json_new_row(array);
|
||||
|
||||
listfmt_json_add_string(row, "host", "worker-07");
|
||||
listfmt_json_add_number(row, "ntasks", 4);
|
||||
|
||||
cJSON* host = cJSON_GetObjectItem(row, "host");
|
||||
TEST_ASSERT_NOT_NULL(host);
|
||||
TEST_ASSERT_TRUE(cJSON_IsString(host));
|
||||
TEST_ASSERT_EQUAL_STRING("worker-07", cJSON_GetStringValue(host));
|
||||
|
||||
cJSON* ntasks = cJSON_GetObjectItem(row, "ntasks");
|
||||
TEST_ASSERT_NOT_NULL(ntasks);
|
||||
TEST_ASSERT_TRUE(cJSON_IsNumber(ntasks));
|
||||
/* Unity is built without UNITY_INCLUDE_DOUBLE (this codebase's
|
||||
* convention, see test_host_runusage.c/test_e2e_capnp.c) -- compare
|
||||
* via TEST_ASSERT_EQUAL_FLOAT instead of the disabled DOUBLE variant. */
|
||||
TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)cJSON_GetNumberValue(ntasks));
|
||||
|
||||
cJSON_Delete(array);
|
||||
}
|
||||
|
||||
static void test_add_string_and_number_are_noop_on_null_row(void)
|
||||
{
|
||||
/* Must not crash. */
|
||||
listfmt_json_add_string(NULL, "key", "val");
|
||||
listfmt_json_add_number(NULL, "key", 1.0);
|
||||
}
|
||||
|
||||
static void test_add_string_null_value_stored_as_json_null(void)
|
||||
{
|
||||
cJSON* array = listfmt_json_begin();
|
||||
cJSON* row = listfmt_json_new_row(array);
|
||||
|
||||
listfmt_json_add_string(row, "note", NULL);
|
||||
|
||||
cJSON* note = cJSON_GetObjectItem(row, "note");
|
||||
TEST_ASSERT_NOT_NULL(note);
|
||||
TEST_ASSERT_TRUE(cJSON_IsNull(note));
|
||||
|
||||
cJSON_Delete(array);
|
||||
}
|
||||
|
||||
/* ---- listfmt_json_finish() tests: valid JSON, stdout-only, no
|
||||
* truncation (task 2.2's stated Observable Completion criterion) ------- */
|
||||
|
||||
static void test_finish_noop_on_null_array(void)
|
||||
{
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
listfmt_json_finish(NULL);
|
||||
char *out, *err;
|
||||
capture_end(&cap, &out, &err);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("", out);
|
||||
TEST_ASSERT_EQUAL_STRING("", err);
|
||||
free(out);
|
||||
free(err);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_finish_prints_valid_multirow_multifield_json_to_stdout_only(void)
|
||||
{
|
||||
cJSON* array = listfmt_json_begin();
|
||||
TEST_ASSERT_NOT_NULL(array);
|
||||
|
||||
/* Row 0: worker-like fields. */
|
||||
cJSON* row0 = listfmt_json_new_row(array);
|
||||
listfmt_json_add_string(row0, "host", "worker-01");
|
||||
listfmt_json_add_string(row0, "status", "UP");
|
||||
listfmt_json_add_number(row0, "maxmem", 67108864);
|
||||
|
||||
/* Row 1: a different field mix, to prove per-row field independence. */
|
||||
cJSON* row1 = listfmt_json_new_row(array);
|
||||
listfmt_json_add_string(row1, "host", "worker-02");
|
||||
listfmt_json_add_string(row1, "status", "DOWN");
|
||||
listfmt_json_add_number(row1, "maxmem", 134217728);
|
||||
|
||||
/* Row 2: a third row, with a numeric jobid-shaped field too. */
|
||||
cJSON* row2 = listfmt_json_new_row(array);
|
||||
listfmt_json_add_string(row2, "host", "worker-03");
|
||||
listfmt_json_add_string(row2, "status", "UP");
|
||||
listfmt_json_add_number(row2, "maxmem", 268435456);
|
||||
listfmt_json_add_number(row2, "jobcount", 12);
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
listfmt_json_finish(array);
|
||||
char *out, *err;
|
||||
capture_end(&cap, &out, &err);
|
||||
|
||||
/* Requirement 3: result data on stdout only, never stderr. */
|
||||
TEST_ASSERT_EQUAL_STRING("", err);
|
||||
TEST_ASSERT_TRUE(strlen(out) > 0);
|
||||
|
||||
/* Genuinely valid, parseable JSON -- don't just eyeball the string,
|
||||
* parse it back with cJSON's own parser and confirm the round-trip. */
|
||||
cJSON* parsed = cJSON_Parse(out);
|
||||
TEST_ASSERT_NOT_NULL(parsed);
|
||||
TEST_ASSERT_TRUE(cJSON_IsArray(parsed));
|
||||
TEST_ASSERT_EQUAL_INT(3, cJSON_GetArraySize(parsed));
|
||||
|
||||
/* Unity is built without UNITY_INCLUDE_DOUBLE (this codebase's
|
||||
* convention, see test_host_runusage.c/test_e2e_capnp.c) -- compare
|
||||
* numeric fields via TEST_ASSERT_EQUAL_FLOAT instead of the disabled
|
||||
* DOUBLE variant. Every value here is an exact power of two (or a
|
||||
* small integer), so the float cast loses no precision. */
|
||||
cJSON* p0 = cJSON_GetArrayItem(parsed, 0);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"worker-01", cJSON_GetStringValue(cJSON_GetObjectItem(p0, "host")));
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"UP", cJSON_GetStringValue(cJSON_GetObjectItem(p0, "status")));
|
||||
TEST_ASSERT_EQUAL_FLOAT(
|
||||
67108864.0f,
|
||||
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(p0, "maxmem")));
|
||||
|
||||
cJSON* p1 = cJSON_GetArrayItem(parsed, 1);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"worker-02", cJSON_GetStringValue(cJSON_GetObjectItem(p1, "host")));
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"DOWN", cJSON_GetStringValue(cJSON_GetObjectItem(p1, "status")));
|
||||
TEST_ASSERT_EQUAL_FLOAT(
|
||||
134217728.0f,
|
||||
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(p1, "maxmem")));
|
||||
|
||||
cJSON* p2 = cJSON_GetArrayItem(parsed, 2);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"worker-03", cJSON_GetStringValue(cJSON_GetObjectItem(p2, "host")));
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"UP", cJSON_GetStringValue(cJSON_GetObjectItem(p2, "status")));
|
||||
TEST_ASSERT_EQUAL_FLOAT(
|
||||
268435456.0f,
|
||||
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(p2, "maxmem")));
|
||||
TEST_ASSERT_EQUAL_FLOAT(
|
||||
12.0f, (float)cJSON_GetNumberValue(cJSON_GetObjectItem(p2, "jobcount")));
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
free(out);
|
||||
free(err);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_finish_does_not_truncate_long_string_field(void)
|
||||
{
|
||||
/* A deliberately long value -- several hundred characters, built from
|
||||
* a repeating, easily-verified pattern -- to prove Requirement 5.3:
|
||||
* no field is truncated or misaligned regardless of value length. */
|
||||
enum { LONG_LEN = 733 };
|
||||
char long_val[LONG_LEN + 1];
|
||||
for (int i = 0; i < LONG_LEN; i++)
|
||||
long_val[i] = (char)('a' + (i % 26));
|
||||
long_val[LONG_LEN] = '\0';
|
||||
|
||||
cJSON* array = listfmt_json_begin();
|
||||
cJSON* row = listfmt_json_new_row(array);
|
||||
listfmt_json_add_string(row, "description", long_val);
|
||||
listfmt_json_add_string(row, "host", "worker-09");
|
||||
|
||||
fresh_dir();
|
||||
capture_t cap;
|
||||
capture_begin(&cap);
|
||||
listfmt_json_finish(array);
|
||||
char *out, *err;
|
||||
capture_end(&cap, &out, &err);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("", err);
|
||||
|
||||
cJSON* parsed = cJSON_Parse(out);
|
||||
TEST_ASSERT_NOT_NULL(parsed);
|
||||
TEST_ASSERT_EQUAL_INT(1, cJSON_GetArraySize(parsed));
|
||||
|
||||
cJSON* p0 = cJSON_GetArrayItem(parsed, 0);
|
||||
const char* desc =
|
||||
cJSON_GetStringValue(cJSON_GetObjectItem(p0, "description"));
|
||||
TEST_ASSERT_NOT_NULL(desc);
|
||||
TEST_ASSERT_EQUAL_INT(LONG_LEN, (int)strlen(desc));
|
||||
TEST_ASSERT_EQUAL_STRING(long_val, desc);
|
||||
/* A field appearing after the long one in the same row must not have
|
||||
* been clipped/misaligned by it either. */
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"worker-09", cJSON_GetStringValue(cJSON_GetObjectItem(p0, "host")));
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
free(out);
|
||||
free(err);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
void test_listfmt_json(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_begin_returns_empty_array);
|
||||
RUN_TEST(test_new_row_appends_object_and_returns_it);
|
||||
RUN_TEST(test_new_row_returns_null_on_null_array);
|
||||
RUN_TEST(test_add_string_and_number_populate_row_fields);
|
||||
RUN_TEST(test_add_string_and_number_are_noop_on_null_row);
|
||||
RUN_TEST(test_add_string_null_value_stored_as_json_null);
|
||||
RUN_TEST(test_finish_noop_on_null_array);
|
||||
RUN_TEST(test_finish_prints_valid_multirow_multifield_json_to_stdout_only);
|
||||
RUN_TEST(test_finish_does_not_truncate_long_string_field);
|
||||
}
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "rcfile.h"
|
||||
|
||||
/* Saved prior state of one environment variable, captured before a test
|
||||
* case mutates it. Distinguishes "was unset" (getenv() returned NULL) from
|
||||
* "was set to X" (including X == "") so restoration is exact -- required
|
||||
* because controller_unit_test links 80+ test_*.c files into ONE process
|
||||
* running sequentially, and a leaked HOME/XDG_CONFIG_HOME value would
|
||||
* silently affect every test that runs after this file. */
|
||||
typedef struct {
|
||||
int was_set; /* 1 if getenv() returned non-NULL before mutation */
|
||||
char* value; /* heap copy of the prior value; valid only if was_set */
|
||||
} rcfile_envsave_t;
|
||||
|
||||
static rcfile_envsave_t env_save(const char* name)
|
||||
{
|
||||
rcfile_envsave_t s;
|
||||
const char* cur = getenv(name);
|
||||
if (cur) {
|
||||
s.was_set = 1;
|
||||
s.value = strdup(cur);
|
||||
TEST_ASSERT_NOT_NULL(s.value);
|
||||
} else {
|
||||
s.was_set = 0;
|
||||
s.value = NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Restores `name` to exactly the state `s` captured, then frees `s`'s heap
|
||||
* copy. Safe to call even if the test already unset/reset the variable
|
||||
* itself -- always drives it back to the captured prior state. */
|
||||
static void env_restore(const char* name, rcfile_envsave_t* s)
|
||||
{
|
||||
if (s->was_set) {
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv(name, s->value, 1));
|
||||
free(s->value);
|
||||
} else {
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv(name));
|
||||
}
|
||||
s->value = NULL;
|
||||
}
|
||||
|
||||
static void test_resolve_uses_xdg_config_home_when_set_and_nonempty(void)
|
||||
{
|
||||
rcfile_envsave_t save_xdg = env_save("XDG_CONFIG_HOME");
|
||||
rcfile_envsave_t save_home = env_save("HOME");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, setenv("XDG_CONFIG_HOME", "/tmp/xce_rcfile_test_xdg", 1));
|
||||
/* HOME is deliberately set to something recognizably different, so a
|
||||
* pass here proves HOME was NOT consulted, not just that the result
|
||||
* happens to match by coincidence. */
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, setenv("HOME", "/tmp/xce_rcfile_test_home_unused", 1));
|
||||
|
||||
char* path = rcfile_resolve_path();
|
||||
TEST_ASSERT_NOT_NULL(path);
|
||||
TEST_ASSERT_EQUAL_STRING("/tmp/xce_rcfile_test_xdg/xce_test/rc", path);
|
||||
free(path);
|
||||
|
||||
env_restore("HOME", &save_home);
|
||||
env_restore("XDG_CONFIG_HOME", &save_xdg);
|
||||
}
|
||||
|
||||
static void test_resolve_falls_back_to_home_when_xdg_unset(void)
|
||||
{
|
||||
rcfile_envsave_t save_xdg = env_save("XDG_CONFIG_HOME");
|
||||
rcfile_envsave_t save_home = env_save("HOME");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv("XDG_CONFIG_HOME"));
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv("HOME", "/tmp/xce_rcfile_test_home", 1));
|
||||
|
||||
char* path = rcfile_resolve_path();
|
||||
TEST_ASSERT_NOT_NULL(path);
|
||||
TEST_ASSERT_EQUAL_STRING("/tmp/xce_rcfile_test_home/.config/xce_test/rc",
|
||||
path);
|
||||
free(path);
|
||||
|
||||
env_restore("HOME", &save_home);
|
||||
env_restore("XDG_CONFIG_HOME", &save_xdg);
|
||||
}
|
||||
|
||||
static void test_resolve_falls_back_to_home_when_xdg_empty(void)
|
||||
{
|
||||
rcfile_envsave_t save_xdg = env_save("XDG_CONFIG_HOME");
|
||||
rcfile_envsave_t save_home = env_save("HOME");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv("XDG_CONFIG_HOME", "", 1));
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, setenv("HOME", "/tmp/xce_rcfile_test_home2", 1));
|
||||
|
||||
char* path = rcfile_resolve_path();
|
||||
TEST_ASSERT_NOT_NULL(path);
|
||||
TEST_ASSERT_EQUAL_STRING("/tmp/xce_rcfile_test_home2/.config/xce_test/rc",
|
||||
path);
|
||||
free(path);
|
||||
|
||||
env_restore("HOME", &save_home);
|
||||
env_restore("XDG_CONFIG_HOME", &save_xdg);
|
||||
}
|
||||
|
||||
static void test_resolve_returns_null_when_neither_var_usable_both_unset(void)
|
||||
{
|
||||
rcfile_envsave_t save_xdg = env_save("XDG_CONFIG_HOME");
|
||||
rcfile_envsave_t save_home = env_save("HOME");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv("XDG_CONFIG_HOME"));
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv("HOME"));
|
||||
|
||||
char* path = rcfile_resolve_path();
|
||||
TEST_ASSERT_NULL(path);
|
||||
|
||||
env_restore("HOME", &save_home);
|
||||
env_restore("XDG_CONFIG_HOME", &save_xdg);
|
||||
}
|
||||
|
||||
static void test_resolve_returns_null_when_neither_var_usable_both_empty(void)
|
||||
{
|
||||
rcfile_envsave_t save_xdg = env_save("XDG_CONFIG_HOME");
|
||||
rcfile_envsave_t save_home = env_save("HOME");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv("XDG_CONFIG_HOME", "", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv("HOME", "", 1));
|
||||
|
||||
char* path = rcfile_resolve_path();
|
||||
TEST_ASSERT_NULL(path);
|
||||
|
||||
env_restore("HOME", &save_home);
|
||||
env_restore("XDG_CONFIG_HOME", &save_xdg);
|
||||
}
|
||||
|
||||
/* Proves the save/restore mechanism itself round-trips exactly, for all
|
||||
* three prior-state shapes (unset / empty / a real value), regardless of
|
||||
* whatever ambient state this test happened to start with -- this is what
|
||||
* guarantees a test run AFTER this file never observes state leaked by it,
|
||||
* since there is no later test in this same file to leak into. */
|
||||
static void test_env_save_restore_roundtrips_all_three_prior_states(void)
|
||||
{
|
||||
const char* name = "XDG_CONFIG_HOME";
|
||||
rcfile_envsave_t real_prior = env_save(name);
|
||||
|
||||
/* Case 1: prior state was unset. */
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv(name));
|
||||
rcfile_envsave_t s1 = env_save(name);
|
||||
TEST_ASSERT_EQUAL_INT(0, s1.was_set);
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv(name, "/tmp/mutated_during_case1", 1));
|
||||
env_restore(name, &s1);
|
||||
TEST_ASSERT_NULL(getenv(name));
|
||||
|
||||
/* Case 2: prior state was set to the empty string. */
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv(name, "", 1));
|
||||
rcfile_envsave_t s2 = env_save(name);
|
||||
TEST_ASSERT_EQUAL_INT(1, s2.was_set);
|
||||
TEST_ASSERT_EQUAL_STRING("", s2.value);
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv(name, "/tmp/mutated_during_case2", 1));
|
||||
env_restore(name, &s2);
|
||||
TEST_ASSERT_NOT_NULL(getenv(name));
|
||||
TEST_ASSERT_EQUAL_STRING("", getenv(name));
|
||||
|
||||
/* Case 3: prior state was set to a real value. */
|
||||
TEST_ASSERT_EQUAL_INT(0, setenv(name, "/tmp/xce_rcfile_prior_value", 1));
|
||||
rcfile_envsave_t s3 = env_save(name);
|
||||
TEST_ASSERT_EQUAL_INT(1, s3.was_set);
|
||||
TEST_ASSERT_EQUAL_STRING("/tmp/xce_rcfile_prior_value", s3.value);
|
||||
TEST_ASSERT_EQUAL_INT(0, unsetenv(name));
|
||||
env_restore(name, &s3);
|
||||
TEST_ASSERT_NOT_NULL(getenv(name));
|
||||
TEST_ASSERT_EQUAL_STRING("/tmp/xce_rcfile_prior_value", getenv(name));
|
||||
|
||||
env_restore(name, &real_prior);
|
||||
}
|
||||
|
||||
/* ---- rcfile_load() tests (task 1.2) ---------------------------------- */
|
||||
|
||||
/* Test-local temp dir helper, mirroring test_configcompare.c's
|
||||
* fresh_dir()/cleanup_dir() pattern -- keeps rc-file fixtures out of the
|
||||
* shared /tmp leak this sandbox already tracks (bug-924) by cleaning up
|
||||
* after itself instead of leaving an mkdtemp() dir behind forever. */
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_rcfile_test_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);
|
||||
}
|
||||
|
||||
static void write_file(const char* path, const char* content, size_t len)
|
||||
{
|
||||
FILE* f = fopen(path, "wb");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
if (len > 0)
|
||||
TEST_ASSERT_EQUAL_INT((int)len, (int)fwrite(content, 1, len, f));
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Fake runner: records every line it is called with (heap copies, in call
|
||||
* order) and, once scripted via fail_at_call, returns fail_rc on that call
|
||||
* (1-based) -- lets a test assert both the exact propagated return value
|
||||
* and the exact point iteration stopped. */
|
||||
#define RCFILE_TEST_MAX_LINES 8
|
||||
|
||||
typedef struct {
|
||||
char* lines[RCFILE_TEST_MAX_LINES];
|
||||
int count;
|
||||
int fail_at_call; /* 1-based; 0 = never fail */
|
||||
int fail_rc;
|
||||
} rcfile_fake_runner_state_t;
|
||||
|
||||
static rcfile_fake_runner_state_t g_fake;
|
||||
|
||||
static void fake_runner_reset(void)
|
||||
{
|
||||
for (int i = 0; i < g_fake.count; i++) {
|
||||
free(g_fake.lines[i]);
|
||||
g_fake.lines[i] = NULL;
|
||||
}
|
||||
g_fake.count = 0;
|
||||
g_fake.fail_at_call = 0;
|
||||
g_fake.fail_rc = 0;
|
||||
}
|
||||
|
||||
static int fake_runner(char* line)
|
||||
{
|
||||
TEST_ASSERT_LESS_THAN_INT(RCFILE_TEST_MAX_LINES, g_fake.count);
|
||||
g_fake.lines[g_fake.count] = strdup(line);
|
||||
TEST_ASSERT_NOT_NULL(g_fake.lines[g_fake.count]);
|
||||
g_fake.count++;
|
||||
if (g_fake.fail_at_call != 0 && g_fake.count == g_fake.fail_at_call)
|
||||
return g_fake.fail_rc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_rcfile_load_returns_0_and_never_calls_runner_when_path_missing(void)
|
||||
{
|
||||
fresh_dir();
|
||||
fake_runner_reset();
|
||||
|
||||
char missing_path[300];
|
||||
snprintf(missing_path, sizeof(missing_path), "%s/does_not_exist", g_dir);
|
||||
|
||||
int rc = rcfile_load(missing_path, fake_runner);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_fake.count);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_rcfile_load_calls_runner_once_per_line_in_order_newline_stripped(void)
|
||||
{
|
||||
fresh_dir();
|
||||
fake_runner_reset();
|
||||
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/rc", g_dir);
|
||||
/* Last line deliberately has no trailing newline -- getline() must
|
||||
* still hand it to the runner intact, not require a final \n. */
|
||||
const char* content =
|
||||
"set masters host1:1,host2:2\nworker\njoin\nquit";
|
||||
write_file(path, content, strlen(content));
|
||||
|
||||
int rc = rcfile_load(path, fake_runner);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(4, g_fake.count);
|
||||
TEST_ASSERT_EQUAL_STRING("set masters host1:1,host2:2", g_fake.lines[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("worker", g_fake.lines[1]);
|
||||
TEST_ASSERT_EQUAL_STRING("join", g_fake.lines[2]);
|
||||
TEST_ASSERT_EQUAL_STRING("quit", g_fake.lines[3]);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_rcfile_load_stops_after_nonzero_runner_and_propagates_value(void)
|
||||
{
|
||||
fresh_dir();
|
||||
fake_runner_reset();
|
||||
g_fake.fail_at_call = 2;
|
||||
g_fake.fail_rc = 42;
|
||||
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/rc", g_dir);
|
||||
const char* content = "line1\nline2\nline3\nline4\n";
|
||||
write_file(path, content, strlen(content));
|
||||
|
||||
int rc = rcfile_load(path, fake_runner);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(42, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, g_fake.count);
|
||||
TEST_ASSERT_EQUAL_STRING("line1", g_fake.lines[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("line2", g_fake.lines[1]);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* A present-but-unreadable-as-a-regular-file path (a directory) must warn
|
||||
* to stderr (non-fatal) and return 0 without ever calling the runner --
|
||||
* proven here by actually capturing stderr via dup/dup2 (not just
|
||||
* asserting the return value), then restoring the original fd exactly so
|
||||
* later test files in this same combined binary see stderr unaffected. */
|
||||
static void test_rcfile_load_directory_path_warns_and_returns_0(void)
|
||||
{
|
||||
fresh_dir();
|
||||
fake_runner_reset();
|
||||
|
||||
fflush(stderr);
|
||||
int saved_stderr_fd = dup(fileno(stderr));
|
||||
TEST_ASSERT_TRUE(saved_stderr_fd >= 0);
|
||||
|
||||
char capture_path[300];
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/stderr_capture", g_dir);
|
||||
FILE* redirected = freopen(capture_path, "w", stderr);
|
||||
TEST_ASSERT_NOT_NULL(redirected);
|
||||
|
||||
int rc = rcfile_load(g_dir, fake_runner); /* g_dir is a directory */
|
||||
|
||||
fflush(stderr);
|
||||
TEST_ASSERT_TRUE(dup2(saved_stderr_fd, fileno(stderr)) >= 0);
|
||||
close(saved_stderr_fd);
|
||||
clearerr(stderr);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_fake.count);
|
||||
|
||||
FILE* f = fopen(capture_path, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
char buf[512];
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
||||
buf[n] = '\0';
|
||||
fclose(f);
|
||||
TEST_ASSERT_TRUE(n > 0);
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, g_dir));
|
||||
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* Proves the getline()-not-fgets() design decision actually holds: a
|
||||
* single line well beyond any typical small fixed-size buffer (e.g. the
|
||||
* historic 256/1024-byte stack buffers this design note calls out) must
|
||||
* reach the runner completely intact and unsplit -- a real content/length
|
||||
* equality check, not just "didn't crash". */
|
||||
static void test_rcfile_load_long_line_reaches_runner_intact_and_unsplit(void)
|
||||
{
|
||||
fresh_dir();
|
||||
fake_runner_reset();
|
||||
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), "%s/rc", g_dir);
|
||||
|
||||
/* Build a >4KB single line shaped like a real long `set masters` rc
|
||||
* command: many comma-separated host:port pairs on one line. */
|
||||
size_t cap = 8192;
|
||||
char* long_line = (char*)malloc(cap);
|
||||
TEST_ASSERT_NOT_NULL(long_line);
|
||||
size_t off = (size_t)snprintf(long_line, cap, "set masters ");
|
||||
int pair_no = 0;
|
||||
while (off < 4096 + 200) {
|
||||
int written = snprintf(long_line + off, cap - off,
|
||||
"%shost%d:1000%d", pair_no ? "," : "",
|
||||
pair_no, pair_no % 10);
|
||||
TEST_ASSERT_TRUE(written > 0);
|
||||
off += (size_t)written;
|
||||
pair_no++;
|
||||
}
|
||||
TEST_ASSERT_TRUE(off > 4096);
|
||||
|
||||
/* File content: the long line, then a short trailer line, so a
|
||||
* truncation/splitting bug would also show up as an extra/garbled
|
||||
* runner call rather than only a length mismatch. */
|
||||
size_t file_len = off + 1 /* \n */ + strlen("quit") + 1 /* \n */;
|
||||
char* file_content = (char*)malloc(file_len + 1);
|
||||
TEST_ASSERT_NOT_NULL(file_content);
|
||||
int fc_len = snprintf(file_content, file_len + 1, "%s\nquit\n", long_line);
|
||||
TEST_ASSERT_EQUAL_INT((int)file_len, fc_len);
|
||||
|
||||
write_file(path, file_content, file_len);
|
||||
|
||||
int rc = rcfile_load(path, fake_runner);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, g_fake.count);
|
||||
TEST_ASSERT_TRUE(strlen(g_fake.lines[0]) > 4096);
|
||||
TEST_ASSERT_EQUAL_INT((int)off, (int)strlen(g_fake.lines[0]));
|
||||
TEST_ASSERT_EQUAL_STRING(long_line, g_fake.lines[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("quit", g_fake.lines[1]);
|
||||
|
||||
free(long_line);
|
||||
free(file_content);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
void test_rcfile(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_resolve_uses_xdg_config_home_when_set_and_nonempty);
|
||||
RUN_TEST(test_resolve_falls_back_to_home_when_xdg_unset);
|
||||
RUN_TEST(test_resolve_falls_back_to_home_when_xdg_empty);
|
||||
RUN_TEST(test_resolve_returns_null_when_neither_var_usable_both_unset);
|
||||
RUN_TEST(test_resolve_returns_null_when_neither_var_usable_both_empty);
|
||||
RUN_TEST(test_env_save_restore_roundtrips_all_three_prior_states);
|
||||
RUN_TEST(test_rcfile_load_returns_0_and_never_calls_runner_when_path_missing);
|
||||
RUN_TEST(test_rcfile_load_calls_runner_once_per_line_in_order_newline_stripped);
|
||||
RUN_TEST(test_rcfile_load_stops_after_nonzero_runner_and_propagates_value);
|
||||
RUN_TEST(test_rcfile_load_directory_path_warns_and_returns_0);
|
||||
RUN_TEST(test_rcfile_load_long_line_reaches_runner_intact_and_unsplit);
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "snapdiff.h"
|
||||
|
||||
static char g_dir[256];
|
||||
|
||||
static void fresh_dir(void)
|
||||
{
|
||||
strcpy(g_dir, "/tmp/xce_snapdiff_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);
|
||||
}
|
||||
|
||||
static void write_file(const char* path, size_t size, uint8_t fill,
|
||||
size_t flip_at, uint8_t flip_to)
|
||||
{
|
||||
uint8_t* buf = malloc(size);
|
||||
memset(buf, fill, size);
|
||||
if (flip_at < size)
|
||||
buf[flip_at] = flip_to;
|
||||
FILE* fp = fopen(path, "wb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
fwrite(buf, 1, size, fp);
|
||||
fclose(fp);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_identical_files(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char p1[512], p2[512];
|
||||
snprintf(p1, sizeof(p1), "%s/a.bin", g_dir);
|
||||
snprintf(p2, sizeof(p2), "%s/b.bin", g_dir);
|
||||
write_file(p1, 1000, 0x42, 2000 /* no flip */, 0);
|
||||
write_file(p2, 1000, 0x42, 2000, 0);
|
||||
|
||||
snapdiff_report_t rep;
|
||||
int rc = snapdiff_paths(p1, p2, stdout, &rep);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.identical);
|
||||
TEST_ASSERT_EQUAL_INT(-1, (int)rep.diff_offset);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_size_mismatch(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char p1[512], p2[512];
|
||||
snprintf(p1, sizeof(p1), "%s/a.bin", g_dir);
|
||||
snprintf(p2, sizeof(p2), "%s/b.bin", g_dir);
|
||||
write_file(p1, 1000, 0x42, 2000, 0);
|
||||
write_file(p2, 999, 0x42, 2000, 0);
|
||||
|
||||
snapdiff_report_t rep;
|
||||
int rc = snapdiff_paths(p1, p2, stdout, &rep);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.size_mismatch);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.identical);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
/* bug-1389 regression shape: two SNAPDIFF_CHUNK_SIZE-sized chunks, content
|
||||
* diverges starting at the exact first byte of chunk index 1 — the OPCode_
|
||||
* snapshotTransfer chunk-boundary corruption signature. */
|
||||
static void test_diff_at_chunk_boundary_is_flagged(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char p1[512], p2[512];
|
||||
snprintf(p1, sizeof(p1), "%s/a.bin", g_dir);
|
||||
snprintf(p2, sizeof(p2), "%s/b.bin", g_dir);
|
||||
size_t size = SNAPDIFF_CHUNK_SIZE * 2;
|
||||
write_file(p1, size, 0x11, size, 0);
|
||||
write_file(p2, size, 0x11, SNAPDIFF_CHUNK_SIZE /* flip first byte of chunk 1 */,
|
||||
0x22);
|
||||
|
||||
snapdiff_report_t rep;
|
||||
int rc = snapdiff_paths(p1, p2, stdout, &rep);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.identical);
|
||||
TEST_ASSERT_EQUAL_INT(SNAPDIFF_CHUNK_SIZE, (int)rep.diff_offset);
|
||||
TEST_ASSERT_EQUAL_INT(1, rep.at_chunk_boundary);
|
||||
TEST_ASSERT_EQUAL_INT(1, (int)rep.chunk_index);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_diff_mid_chunk_is_not_flagged_as_boundary(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char p1[512], p2[512];
|
||||
snprintf(p1, sizeof(p1), "%s/a.bin", g_dir);
|
||||
snprintf(p2, sizeof(p2), "%s/b.bin", g_dir);
|
||||
write_file(p1, 1000, 0x11, 2000, 0);
|
||||
write_file(p2, 1000, 0x11, 500 /* mid-file, not a chunk boundary */, 0x22);
|
||||
|
||||
snapdiff_report_t rep;
|
||||
int rc = snapdiff_paths(p1, p2, stdout, &rep);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(500, (int)rep.diff_offset);
|
||||
TEST_ASSERT_EQUAL_INT(0, rep.at_chunk_boundary);
|
||||
cleanup_dir();
|
||||
}
|
||||
|
||||
static void test_missing_path_errors(void)
|
||||
{
|
||||
snapdiff_report_t rep;
|
||||
int rc = snapdiff_paths("/tmp/xce_snapdiff_missing_a_zzz",
|
||||
"/tmp/xce_snapdiff_missing_b_zzz", stdout, &rep);
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
||||
}
|
||||
|
||||
void test_snapdiff(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_identical_files);
|
||||
RUN_TEST(test_size_mismatch);
|
||||
RUN_TEST(test_diff_at_chunk_boundary_is_flagged);
|
||||
RUN_TEST(test_diff_mid_chunk_is_not_flagged_as_boundary);
|
||||
RUN_TEST(test_missing_path_errors);
|
||||
}
|
||||
|
|
@ -1,470 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,650 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "config.h" /* WAL_HDR_CRC_SIZE, WAL_CRC_NONE */
|
||||
#include "persist.h" /* WAL_LEGACY_TERM */
|
||||
#include "walcompare.h"
|
||||
#include "wal_types.h"
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Harness: hand-write raw WAL segments (LSN, term, payload all under
|
||||
* test control) into per-member temp dirs, then drive walcompare over
|
||||
* them. Mirrors test_waldump's writer so the on-disk format is pinned
|
||||
* independently of persist.c — and exercises the REAL waldump decoder
|
||||
* that walcompare reuses (no second parser).
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/* Append one entry to `fp`, chaining prev_crc from *last_crc. CRC32 over
|
||||
* WAL_HDR_CRC_SIZE header bytes + payload (identical to persist.c append). */
|
||||
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);
|
||||
}
|
||||
|
||||
/* A single hand-built WAL entry spec for a member. */
|
||||
typedef struct {
|
||||
uint64_t term;
|
||||
uint32_t type;
|
||||
const char* payload; /* NUL-terminated; length is strlen (0 => no payload) */
|
||||
} wal_spec_t;
|
||||
|
||||
/* Mint a fresh temp dir and write a single seg-0001.wal holding `specs`
|
||||
* (LSNs 1..n contiguous). Returns the dir path in `dir_out`. */
|
||||
static void build_member(char* dir_out, size_t dirsz, const wal_spec_t* specs,
|
||||
int n)
|
||||
{
|
||||
snprintf(dir_out, dirsz, "/tmp/xce_walcmp_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(dir_out));
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/seg-%016lx.wal", dir_out, 1UL);
|
||||
FILE* fp = fopen(path, "wb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
|
||||
uint32_t last = WAL_CRC_NONE;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint32_t plen =
|
||||
specs[i].payload ? (uint32_t)strlen(specs[i].payload) : 0;
|
||||
put_entry(fp, specs[i].term, (uint64_t)(i + 1), specs[i].type,
|
||||
specs[i].payload, plen, &last, i == 0);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Identical member WALs → success (exit 0). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_identical_members_ok(void)
|
||||
{
|
||||
wal_spec_t specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), specs, 3);
|
||||
build_member(b, sizeof(b), specs, 3);
|
||||
|
||||
walcompare_member_t m[2] = {
|
||||
{ a, 0 },
|
||||
{ b, 0 },
|
||||
};
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Hand-divergent pair: SAME LSN, DIFFERENT term within the committed */
|
||||
/* prefix → FAIL with the diverging LSN (R6.2/R6.3). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_same_lsn_diff_term_divergent(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 2 */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 3, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 3 — divergent */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 3);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, div.lsn); /* the diverging LSN */
|
||||
TEST_ASSERT_EQUAL_UINT64(2, div.member_term[0]);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, div.member_term[1]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SAME LSN, SAME term, DIFFERENT payload (pcrc) → FAIL (R6.2: a content */
|
||||
/* divergence is not masked by equal term + LSN range). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_same_lsn_diff_payload_divergent(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* LSN 2 */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobZ" }, /* LSN 2, same term, different bytes */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 2);
|
||||
build_member(b, sizeof(b), b_specs, 2);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, div.lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, div.member_term[0]);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, div.member_term[1]);
|
||||
TEST_ASSERT_NOT_EQUAL(div.member_pcrc[0], div.member_pcrc[1]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A member that is AHEAD (longer log) with a DIFFERENT uncommitted tail */
|
||||
/* is NOT a divergence — the shared committed prefix matches (§6.1). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_uncommitted_tail_not_flagged(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* shared committed prefix: LSN 1..2 */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "tailX" }, /* uncommitted tail — beyond shared tip */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 2); /* tip 2 */
|
||||
build_member(b, sizeof(b), b_specs, 3); /* tip 3 */
|
||||
|
||||
/* Shared boundary = min recovered tip = 2; the LSN-3 tail is not compared. */
|
||||
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* An explicit committed boundary SHORTER than a member's log: a */
|
||||
/* divergence ABOVE the boundary must NOT be flagged (§6.1 committed */
|
||||
/* prefix only). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_divergence_above_committed_boundary_not_flagged(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* LSN 2 committed */
|
||||
{ 2, WAL_JOB_SUBMIT, "uncA" }, /* LSN 3 uncommitted, divergent */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 9, WAL_JOB_SUBMIT, "uncB" }, /* LSN 3, different term+bytes */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 3);
|
||||
|
||||
/* committed boundary pinned at 2 → the LSN-3 divergence is below scope. */
|
||||
walcompare_member_t m[2] = { { a, 2 }, { b, 2 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
|
||||
/* Same data, but raise the boundary to 3 → now it IS a divergence. */
|
||||
walcompare_member_t m3[2] = { { a, 3 }, { b, 3 } };
|
||||
rc = walcompare(m3, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Harness for the per-member checkpoint-floor tests (raft-ha-core-safety
|
||||
* task 5.10a / ha-entity-snapshots task 7.1): builds `<dir_out>/wal/` (real
|
||||
* segs, LSNs starting at `start_lsn`, contiguous CRC chain) and, when
|
||||
* `floor_lsn > 0`, an empty `<dir_out>/snap/snap-<floor_lsn:016x>.capnp`
|
||||
* marker (walcompare only parses the FILENAME's embedded LSN, never the
|
||||
* content) — the sibling `wal/`+`snap/` layout `member_snapshot_floor`'s
|
||||
* "Candidate 1" path expects, matching the real deployed layout
|
||||
* (`<data>/wal/`, `<data>/snap/`).
|
||||
* ------------------------------------------------------------------ */
|
||||
static void build_member_floored(char* dir_out, size_t dirsz,
|
||||
const wal_spec_t* specs, int n,
|
||||
uint64_t start_lsn, uint64_t floor_lsn)
|
||||
{
|
||||
snprintf(dir_out, dirsz, "/tmp/xce_walcmp_fl_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(dir_out));
|
||||
|
||||
char waldir[288];
|
||||
snprintf(waldir, sizeof(waldir), "%s/wal", dir_out);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(waldir, 0755));
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/seg-%016lx.wal", waldir,
|
||||
(unsigned long)start_lsn);
|
||||
FILE* fp = fopen(path, "wb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
|
||||
uint32_t last = WAL_CRC_NONE;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint32_t plen =
|
||||
specs[i].payload ? (uint32_t)strlen(specs[i].payload) : 0;
|
||||
put_entry(fp, specs[i].term, start_lsn + (uint64_t)i, specs[i].type,
|
||||
specs[i].payload, plen, &last, i == 0);
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
if (floor_lsn > 0) {
|
||||
char snapdir[288];
|
||||
snprintf(snapdir, sizeof(snapdir), "%s/snap", dir_out);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(snapdir, 0755));
|
||||
char snapfile[512];
|
||||
snprintf(snapfile, sizeof(snapfile), "%s/snap-%016lx.capnp", snapdir,
|
||||
(unsigned long)floor_lsn);
|
||||
FILE* sf = fopen(snapfile, "wb");
|
||||
TEST_ASSERT_NOT_NULL(sf);
|
||||
fclose(sf); /* content is irrelevant — only the filename is parsed */
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A member that caught up via a full-checkpoint snapshot legitimately */
|
||||
/* holds NO raw WAL below its own installed checkpoint LSN — that range */
|
||||
/* must be excluded from comparison, never flagged MISSING/divergent */
|
||||
/* (raft-ha-core-safety task 5.10a / ha-entity-snapshots task 7.1). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_checkpoint_floor_excludes_compacted_range(void)
|
||||
{
|
||||
/* Member A: full history, LSNs 1..5, uniform term 1. */
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobB" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobC" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobD" },
|
||||
};
|
||||
/* Member B: caught up via a checkpoint at LSN 3 — its raw WAL only
|
||||
* physically holds LSNs 4..5 (matching A's LSN 4..5 exactly), and it
|
||||
* carries a snap-...0003.capnp marker declaring that floor. */
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_JOB_SUBMIT, "jobC" }, /* LSN 4 */
|
||||
{ 1, WAL_JOB_SUBMIT, "jobD" }, /* LSN 5 */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 5);
|
||||
build_member_floored(b, sizeof(b), b_specs, 2, /*start_lsn=*/4,
|
||||
/*floor_lsn=*/3);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 5 }, { b, 5 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WALCMP_OK, rc,
|
||||
"a member's own checkpoint floor must suppress the compacted range "
|
||||
"from comparison, not flag it MISSING");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* The checkpoint floor must NOT mask a genuine divergence within the */
|
||||
/* range the compacted member DOES claim to hold (past its own floor). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_checkpoint_floor_does_not_mask_real_divergence(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobB" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobC" },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobD" },
|
||||
};
|
||||
/* Member B's post-floor LSN 4 entry DISAGREES with A's (different
|
||||
* term+payload) — this is a real divergence and must still be caught. */
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 9, WAL_JOB_SUBMIT, "DIFFERENT" }, /* LSN 4 — diverges from A */
|
||||
{ 1, WAL_JOB_SUBMIT, "jobD" }, /* LSN 5 */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 5);
|
||||
build_member_floored(b, sizeof(b), b_specs, 2, /*start_lsn=*/4,
|
||||
/*floor_lsn=*/3);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 5 }, { b, 5 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WALCMP_DIVERGENT, rc,
|
||||
"a checkpoint floor excludes the compacted range only -- a real "
|
||||
"divergence within the member's post-floor range must still be caught");
|
||||
TEST_ASSERT_EQUAL_UINT64(4, div.lsn);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A member MISSING a committed entry (a hole inside the committed */
|
||||
/* prefix the boundary covers) is a divergence (R4.2/R6.4). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_missing_committed_entry_divergent(void)
|
||||
{
|
||||
/* Member A holds 1..3; member B holds only 1..2 but is told its
|
||||
* committed boundary is 3 — it is MISSING the committed entry at LSN 3. */
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 2);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 3 }, { b, 3 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
|
||||
TEST_ASSERT_EQUAL_INT(1, div.member_present[0]); /* A holds it */
|
||||
TEST_ASSERT_EQUAL_INT(0, div.member_present[1]); /* B is missing it */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Two legacy-0-term entries at the same LSN are NOT divergent: legacy 0 */
|
||||
/* maps to WAL_LEGACY_TERM for both (R1.8). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_legacy_zero_term_not_divergent(void)
|
||||
{
|
||||
wal_spec_t specs[] = {
|
||||
{ 0, WAL_NOOP, NULL }, /* legacy-0 term */
|
||||
{ 0, WAL_JOB_SUBMIT, "jobA" }, /* legacy-0 term, same payload */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), specs, 2);
|
||||
build_member(b, sizeof(b), specs, 2);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Three members, all identical → success (the live regression shape). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_three_members_identical_ok(void)
|
||||
{
|
||||
wal_spec_t specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "j1" },
|
||||
{ 1, WAL_CONFIG_MARKER, "cfgref" }, /* a config marker in the log */
|
||||
{ 2, WAL_JOB_SUBMIT, "j2" },
|
||||
};
|
||||
char a[256], b[256], c[256];
|
||||
build_member(a, sizeof(a), specs, 4);
|
||||
build_member(b, sizeof(b), specs, 4);
|
||||
build_member(c, sizeof(c), specs, 4);
|
||||
|
||||
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 3, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Three members, one with a divergent committed config marker → FAIL */
|
||||
/* (R6.6: config markers reconcile by (term,lsn) like job entries). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_three_members_divergent_config_marker(void)
|
||||
{
|
||||
wal_spec_t ok_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "j1" },
|
||||
{ 1, WAL_CONFIG_MARKER, "cfgX" }, /* LSN 3 config ref */
|
||||
};
|
||||
wal_spec_t bad_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "j1" },
|
||||
{ 1, WAL_CONFIG_MARKER, "cfgY" }, /* LSN 3, different config ref */
|
||||
};
|
||||
char a[256], b[256], c[256];
|
||||
build_member(a, sizeof(a), ok_specs, 3);
|
||||
build_member(b, sizeof(b), ok_specs, 3);
|
||||
build_member(c, sizeof(c), bad_specs, 3);
|
||||
|
||||
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 3, stdout, &div);
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, div.lsn);
|
||||
/* A and B agree; C diverges. */
|
||||
TEST_ASSERT_EQUAL_UINT32(div.member_pcrc[0], div.member_pcrc[1]);
|
||||
TEST_ASSERT_NOT_EQUAL(div.member_pcrc[0], div.member_pcrc[2]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A missing WAL source is a hard (negative) error, not a false OK. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_missing_member_errors(void)
|
||||
{
|
||||
wal_spec_t specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
};
|
||||
char a[256];
|
||||
build_member(a, sizeof(a), specs, 2);
|
||||
|
||||
walcompare_member_t m[2] = {
|
||||
{ a, 0 },
|
||||
{ "/tmp/xce_walcmp_does_not_exist_zzz", 0 },
|
||||
};
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, stdout, &div);
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc); /* negative errno, not OK/DIVERGENT */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Fewer than two members is a usage error. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_too_few_members_errors(void)
|
||||
{
|
||||
wal_spec_t specs[] = { { 1, WAL_NOOP, NULL } };
|
||||
char a[256];
|
||||
build_member(a, sizeof(a), specs, 1);
|
||||
walcompare_member_t m[1] = { { a, 0 } };
|
||||
int rc = walcompare(m, 1, stdout, NULL);
|
||||
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Machine-checkable report: a committed-prefix divergence must emit a */
|
||||
/* parseable per-member line set and a committed-vs-uncommitted */
|
||||
/* classification (task 1.1, R7.1/R7.5). Captured via open_memstream so */
|
||||
/* the exact greppable tokens are asserted, not just the verdict. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_machine_report_committed_divergence(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 2 */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 3, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 3 — divergent */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 3);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t len = 0;
|
||||
FILE* out = open_memstream(&buf, &len);
|
||||
TEST_ASSERT_NOT_NULL(out);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare(m, 2, out, &div);
|
||||
fclose(out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
/* committed-vs-uncommitted classification line. */
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP_DIVERGENCE"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
|
||||
/* parseable per-member lines with fixed field order. */
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=3"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=1 lsn=3"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
|
||||
/* divergence struct carries the committed classification flag. */
|
||||
TEST_ASSERT_EQUAL_INT(1, div.committed);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* walcompare_report emits a full per-member, per-committed-LSN table */
|
||||
/* (lsn, term, pcrc, status) for identical members and a clean verdict. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_report_full_member_table(void)
|
||||
{
|
||||
wal_spec_t specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||
};
|
||||
char a[256], b[256], c[256];
|
||||
build_member(a, sizeof(a), specs, 3);
|
||||
build_member(b, sizeof(b), specs, 3);
|
||||
build_member(c, sizeof(c), specs, 3);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t len = 0;
|
||||
FILE* out = open_memstream(&buf, &len);
|
||||
TEST_ASSERT_NOT_NULL(out);
|
||||
|
||||
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare_report(m, 3, out, &div);
|
||||
fclose(out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||
/* every member appears at every committed LSN with committed=1. */
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=1"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=2 lsn=3"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=1"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A MISSING committed entry is tagged status=MISSING in the report, */
|
||||
/* and classified class=committed (a committed-prefix hole, R4.2/R6.4). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_report_missing_tagged(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 2);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t len = 0;
|
||||
FILE* out = open_memstream(&buf, &len);
|
||||
TEST_ASSERT_NOT_NULL(out);
|
||||
|
||||
walcompare_member_t m[2] = { { a, 3 }, { b, 3 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare_report(m, 2, out, &div);
|
||||
fclose(out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "status=MISSING"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
|
||||
TEST_ASSERT_EQUAL_INT(1, div.committed);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A divergence in the UNCOMMITTED tail (above the committed boundary) is */
|
||||
/* reported class=uncommitted and is NON-fatal: the committed verdict is */
|
||||
/* still WALCMP_OK (task 1.1 classification; §6.1 committed-prefix only). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_report_uncommitted_tail_classified(void)
|
||||
{
|
||||
wal_spec_t a_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* committed LSN 2 */
|
||||
{ 2, WAL_JOB_SUBMIT, "uncA" }, /* LSN 3 uncommitted, divergent */
|
||||
};
|
||||
wal_spec_t b_specs[] = {
|
||||
{ 1, WAL_NOOP, NULL },
|
||||
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||
{ 9, WAL_JOB_SUBMIT, "uncB" }, /* LSN 3, different term+bytes */
|
||||
};
|
||||
char a[256], b[256];
|
||||
build_member(a, sizeof(a), a_specs, 3);
|
||||
build_member(b, sizeof(b), b_specs, 3);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t len = 0;
|
||||
FILE* out = open_memstream(&buf, &len);
|
||||
TEST_ASSERT_NOT_NULL(out);
|
||||
|
||||
/* committed boundary pinned at 2 → LSN-3 divergence is the tail. */
|
||||
walcompare_member_t m[2] = { { a, 2 }, { b, 2 } };
|
||||
walcompare_divergence_t div;
|
||||
int rc = walcompare_report(m, 2, out, &div);
|
||||
fclose(out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc); /* tail divergence is non-fatal */
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "class=uncommitted"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=0"));
|
||||
free(buf);
|
||||
}
|
||||
|
||||
void test_walcompare(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_identical_members_ok);
|
||||
RUN_TEST(test_same_lsn_diff_term_divergent);
|
||||
RUN_TEST(test_same_lsn_diff_payload_divergent);
|
||||
RUN_TEST(test_uncommitted_tail_not_flagged);
|
||||
RUN_TEST(test_divergence_above_committed_boundary_not_flagged);
|
||||
RUN_TEST(test_checkpoint_floor_excludes_compacted_range);
|
||||
RUN_TEST(test_checkpoint_floor_does_not_mask_real_divergence);
|
||||
RUN_TEST(test_missing_committed_entry_divergent);
|
||||
RUN_TEST(test_legacy_zero_term_not_divergent);
|
||||
RUN_TEST(test_three_members_identical_ok);
|
||||
RUN_TEST(test_three_members_divergent_config_marker);
|
||||
RUN_TEST(test_missing_member_errors);
|
||||
RUN_TEST(test_too_few_members_errors);
|
||||
RUN_TEST(test_machine_report_committed_divergence);
|
||||
RUN_TEST(test_report_full_member_table);
|
||||
RUN_TEST(test_report_missing_tagged);
|
||||
RUN_TEST(test_report_uncommitted_tail_classified);
|
||||
}
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#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);
|
||||
/* g_dir has no sibling data/snap/ at all → nothing can explain it */
|
||||
TEST_ASSERT_EQUAL_INT(0, st.leading_gap_explained);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* WAL_CONFIG_MARKER must decode as its own named type, not fall into */
|
||||
/* the generic UNKNOWN bucket (a real, defined type — wal_types.h). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_config_marker_type_decoded(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);
|
||||
|
||||
uint8_t marker[WAL_CONFIG_MARKER_PAYLOAD_LEN];
|
||||
uint64_t tag = 0x1122334455667788ULL;
|
||||
for (int i = 0; i < 8; i++)
|
||||
marker[i] = (uint8_t)(tag >> (56 - i * 8));
|
||||
memset(marker + 8, 0xab, 32);
|
||||
put_entry(fp, 3, 2, WAL_CONFIG_MARKER, marker, sizeof(marker), &last, 0);
|
||||
fclose(fp);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t bufsz = 0;
|
||||
FILE* mem = open_memstream(&buf, &bufsz);
|
||||
TEST_ASSERT_NOT_NULL(mem);
|
||||
|
||||
waldump_stats_t st;
|
||||
int rc = waldump_path(g_dir, mem, &st);
|
||||
fclose(mem);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "CONFIG_MARKER"));
|
||||
TEST_ASSERT_NULL(strstr(buf, "UNKNOWN"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "tag=1122334455667788"));
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A leading gap covered by a sibling data/snap/ snapshot is normal, */
|
||||
/* checkpoint-triggered WAL trim — waldump must say so, not alarm. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_leading_gap_explained_by_covering_snapshot(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char wal_dir[300], snap_dir[300];
|
||||
snprintf(wal_dir, sizeof(wal_dir), "%s/data/wal", g_dir);
|
||||
snprintf(snap_dir, sizeof(snap_dir), "%s/data/snap", g_dir);
|
||||
char data_dir[300];
|
||||
snprintf(data_dir, sizeof(data_dir), "%s/data", g_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(data_dir, 0755));
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(wal_dir, 0755));
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(snap_dir, 0755));
|
||||
|
||||
/* Snapshot at LSN 12 covers the trimmed prefix [1..12] below. */
|
||||
char snap_file[400];
|
||||
snprintf(snap_file, sizeof(snap_file), "%s/snap-%016x.capnp", snap_dir,
|
||||
0xc);
|
||||
FILE* sf = fopen(snap_file, "wb");
|
||||
TEST_ASSERT_NOT_NULL(sf);
|
||||
fclose(sf);
|
||||
|
||||
char segp[400];
|
||||
snprintf(segp, sizeof(segp), "%s/seg-%016lx.wal", wal_dir,
|
||||
(unsigned long)13);
|
||||
FILE* fp = fopen(segp, "wb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
uint32_t last = WAL_CRC_NONE;
|
||||
put_entry(fp, 0, 13, WAL_NOOP, NULL, 0, &last, 1);
|
||||
fclose(fp);
|
||||
|
||||
char* buf = NULL;
|
||||
size_t bufsz = 0;
|
||||
FILE* mem = open_memstream(&buf, &bufsz);
|
||||
TEST_ASSERT_NOT_NULL(mem);
|
||||
|
||||
waldump_stats_t st;
|
||||
int rc = waldump_path(g_dir, mem, &st);
|
||||
fclose(mem);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_UINT(1, st.n_gaps);
|
||||
TEST_ASSERT_NOT_EQUAL(0, st.leading_gap_explained);
|
||||
TEST_ASSERT_NOT_NULL(strstr(buf, "covered by snapshot"));
|
||||
TEST_ASSERT_NULL(strstr(buf, "committed prefix missing!"));
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* A snapshot that does NOT cover the full gap must NOT explain it — */
|
||||
/* the alarming wording stays, since the gap could still be real loss. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_leading_gap_not_explained_by_insufficient_snapshot(void)
|
||||
{
|
||||
fresh_dir();
|
||||
char wal_dir[300], snap_dir[300], data_dir[300];
|
||||
snprintf(data_dir, sizeof(data_dir), "%s/data", g_dir);
|
||||
snprintf(wal_dir, sizeof(wal_dir), "%s/data/wal", g_dir);
|
||||
snprintf(snap_dir, sizeof(snap_dir), "%s/data/snap", g_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(data_dir, 0755));
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(wal_dir, 0755));
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(snap_dir, 0755));
|
||||
|
||||
/* Snapshot at LSN 5 does NOT cover the trimmed prefix [1..12] below. */
|
||||
char snap_file[400];
|
||||
snprintf(snap_file, sizeof(snap_file), "%s/snap-%016x.capnp", snap_dir,
|
||||
0x5);
|
||||
FILE* sf = fopen(snap_file, "wb");
|
||||
TEST_ASSERT_NOT_NULL(sf);
|
||||
fclose(sf);
|
||||
|
||||
char segp[400];
|
||||
snprintf(segp, sizeof(segp), "%s/seg-%016lx.wal", wal_dir,
|
||||
(unsigned long)13);
|
||||
FILE* fp = fopen(segp, "wb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
uint32_t last = WAL_CRC_NONE;
|
||||
put_entry(fp, 0, 13, WAL_NOOP, NULL, 0, &last, 1);
|
||||
fclose(fp);
|
||||
|
||||
waldump_stats_t st;
|
||||
int rc = waldump_path(g_dir, stdout, &st);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_UINT(1, st.n_gaps);
|
||||
TEST_ASSERT_EQUAL_INT(0, st.leading_gap_explained);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 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)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_contiguous_log_has_no_gaps);
|
||||
RUN_TEST(test_missing_prefix_flagged_as_gap);
|
||||
RUN_TEST(test_config_marker_type_decoded);
|
||||
RUN_TEST(test_leading_gap_explained_by_covering_snapshot);
|
||||
RUN_TEST(test_leading_gap_not_explained_by_insufficient_snapshot);
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
/*
|
||||
* test_xcetest_interrupt.c -- proves, via a REAL, OS-delivered SIGINT
|
||||
* hitting a genuinely blocked read(), that xcetest_install_sigint()
|
||||
* (xcetest_interrupt.h, xce-test-cli-ux task 1.3, Requirement 9.9) fixes
|
||||
* the restart-vs-EINTR bug the bare `signal(SIGINT, sigbreak)` call in
|
||||
* test.c's main() used to have.
|
||||
*
|
||||
* Why a fork()-based process-level test rather than a plain unit test:
|
||||
* this is inherently about kernel/libc syscall-restart semantics, which
|
||||
* only a REAL signal delivered to a REAL process blocked in a REAL slow
|
||||
* syscall can demonstrate -- calling sigbreak() directly as a plain
|
||||
* function call would prove nothing about SA_RESTART. Each test below
|
||||
* forks a short-lived child that installs a SIGINT handler via the given
|
||||
* installer function, blocks in read() on a pipe that is never written
|
||||
* to (so it can only unblock via a signal or its own 1-second alarm
|
||||
* safety net), and reports which happened via its exit code -- the exact
|
||||
* fork/report pattern already established in this directory (see
|
||||
* test_ha_role_lifecycle.c's noop_classifies_retain()).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "xcetest_interrupt.h"
|
||||
|
||||
/* Child-side exit codes, decoded by the parent via WEXITSTATUS(). */
|
||||
#define CHILD_EXIT_EINTR_OBSERVED 0 /* read() returned -1/EINTR promptly */
|
||||
#define CHILD_EXIT_NO_EINTR 1 /* read() returned, but not via EINTR */
|
||||
#define CHILD_EXIT_TIMED_OUT 2 /* read() never returned before the
|
||||
* 1-second alarm fired -- i.e. the
|
||||
* interrupted syscall was silently
|
||||
* restarted instead */
|
||||
|
||||
/* 1-second safety net: if the installation under test lets the blocked
|
||||
* read() silently restart, this makes the child self-terminate quickly
|
||||
* (reporting CHILD_EXIT_TIMED_OUT) instead of hanging the whole suite.
|
||||
* Uses plain signal() -- fine here, since the handler calls _exit()
|
||||
* directly and never returns, so SIGALRM's own restart-vs-EINTR
|
||||
* disposition never comes into play. */
|
||||
static void child_alarm_handler(int signo)
|
||||
{
|
||||
(void)signo;
|
||||
_exit(CHILD_EXIT_TIMED_OUT);
|
||||
}
|
||||
|
||||
/* Stands in for test.c's sigbreak(): a real, non-default SIGINT handler.
|
||||
* Only its mere existence (vs SIG_DFL/SIG_IGN) matters for read()'s
|
||||
* restart-vs-EINTR decision -- this test is about the *installation*,
|
||||
* not what the handler itself does. */
|
||||
static void child_sigint_noop_handler(int signo)
|
||||
{
|
||||
(void)signo;
|
||||
}
|
||||
|
||||
static void install_via_plain_signal(void (*handler)(int))
|
||||
{
|
||||
signal(SIGINT, handler);
|
||||
}
|
||||
|
||||
/* Forks a child that installs SIGINT via `installer`, then blocks in
|
||||
* read() on an always-empty pipe. The parent sends a real SIGINT to the
|
||||
* child shortly after fork (giving it time to install the handler and
|
||||
* enter the blocking read()) -- genuine OS signal delivery, not a direct
|
||||
* call to any handler function. Returns the child's CHILD_EXIT_* exit
|
||||
* code. */
|
||||
static int run_forked_blocked_read(void (*installer)(void (*)(int)))
|
||||
{
|
||||
int pipefd[2];
|
||||
pid_t pid;
|
||||
pid_t waited;
|
||||
int status = 0;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pipe(pipefd));
|
||||
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE(pid >= 0);
|
||||
|
||||
if (pid == 0) {
|
||||
/* child */
|
||||
char buf[1];
|
||||
ssize_t n;
|
||||
|
||||
close(pipefd[1]); /* only the PARENT's copy of the write end must
|
||||
* stay open -- that's what keeps this pipe
|
||||
* "empty but not at EOF" for the duration of
|
||||
* the child's read() below. */
|
||||
|
||||
signal(SIGALRM, child_alarm_handler);
|
||||
alarm(1);
|
||||
|
||||
installer(child_sigint_noop_handler);
|
||||
|
||||
errno = 0;
|
||||
n = read(pipefd[0], buf, sizeof(buf));
|
||||
|
||||
if (n == -1 && errno == EINTR) {
|
||||
_exit(CHILD_EXIT_EINTR_OBSERVED);
|
||||
}
|
||||
_exit(CHILD_EXIT_NO_EINTR);
|
||||
}
|
||||
|
||||
/* parent */
|
||||
close(pipefd[0]);
|
||||
|
||||
usleep(200000); /* give the child time to install its handler and
|
||||
* reach the blocking read() before we signal it */
|
||||
TEST_ASSERT_EQUAL_INT(0, kill(pid, SIGINT));
|
||||
|
||||
waited = waitpid(pid, &status, 0);
|
||||
close(pipefd[1]);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(pid, waited);
|
||||
TEST_ASSERT_TRUE(WIFEXITED(status));
|
||||
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/* RED-phase documentation, kept as a permanent regression check: proves
|
||||
* the ROOT CAUSE this task fixes is real in this environment, not
|
||||
* assumed. glibc's signal() installs with SA_RESTART set by default, so
|
||||
* a single SIGINT delivered while a process is blocked in read() does
|
||||
* NOT make that read() return -- the kernel transparently restarts it,
|
||||
* and the read() keeps blocking (here, until this test's own 1-second
|
||||
* alarm safety net fires). */
|
||||
static void test_plain_signal_install_lets_sigint_restart_blocked_read(void)
|
||||
{
|
||||
int rc = run_forked_blocked_read(install_via_plain_signal);
|
||||
TEST_ASSERT_EQUAL_INT(CHILD_EXIT_TIMED_OUT, rc);
|
||||
}
|
||||
|
||||
/* GREEN: the fix under test. test.c's main() now installs SIGINT via
|
||||
* this exact function instead of the bare signal(SIGINT, sigbreak) call
|
||||
* it used before this task (Requirement 9.9). A real SIGINT delivered
|
||||
* while blocked in read() must make that read() return immediately with
|
||||
* errno == EINTR, not silently retry. */
|
||||
static void test_install_sigint_clears_sa_restart_so_blocked_read_returns_eintr(void)
|
||||
{
|
||||
int rc = run_forked_blocked_read(xcetest_install_sigint);
|
||||
TEST_ASSERT_EQUAL_INT(CHILD_EXIT_EINTR_OBSERVED, rc);
|
||||
}
|
||||
|
||||
void test_xcetest_interrupt(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_plain_signal_install_lets_sigint_restart_blocked_read);
|
||||
RUN_TEST(test_install_sigint_clears_sa_restart_so_blocked_read_returns_eintr);
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
/*
|
||||
* test_xcetest_process_entry.c -- xce-test-cli-ux task 1.6, Requirements
|
||||
* 4.1, 4.2, 4.3 (Process-Level Help and Version Discovery).
|
||||
*
|
||||
* These are black-box, subprocess tests: they exec the REAL, built
|
||||
* xce_test binary (a sibling of this test binary in the same build
|
||||
* directory -- see resolve_xce_test_path()) with a process-level flag and
|
||||
* inspect its exit code and captured stdout. This is deliberately NOT a
|
||||
* plain unit test that calls argp_parse()/parse_opt() in-process: what's
|
||||
* under test is main()'s own process-level argv handling, including
|
||||
* whether it reaches the interactive/scripted REPL loop at all, which can
|
||||
* only be observed by actually running the process to completion.
|
||||
*
|
||||
* This is a small, self-contained subprocess harness scoped to this one
|
||||
* task's needs (process-level -h/--help/--version) -- NOT the reusable
|
||||
* pty-driven black-box CLI harness that task 1.7 builds separately (that
|
||||
* one will live in test_test_cli.c and is needed for interactive-only
|
||||
* surfaces like tab completion that a plain pipe can't exercise).
|
||||
*
|
||||
* resolve_xce_test_path() below is deliberately non-static (task 1.7):
|
||||
* test_test_cli.c's pty-driven harness needs the exact same "find the real
|
||||
* built xce_test binary next to this test binary" logic and reuses this
|
||||
* copy via an `extern` declaration rather than duplicating it -- see this
|
||||
* project's own consolidation-over-copy-paste convention.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#define CAPTURE_BUF_SIZE 16384
|
||||
#define CHILD_TIMEOUT_TENTHS 50 /* 5 seconds, polled every 100ms */
|
||||
|
||||
/* xce_test is always built as a sibling of this test binary in the same
|
||||
* meson build directory (src/controller/meson.build builds both `xce_test`
|
||||
* and `controller_unit_test` into the same target dir) -- resolving via
|
||||
* /proc/self/exe's directory avoids depending on the caller's cwd or an
|
||||
* env var that could go stale. */
|
||||
int resolve_xce_test_path(char* out, size_t outsz)
|
||||
{
|
||||
char self[PATH_MAX];
|
||||
ssize_t n;
|
||||
char* dir;
|
||||
|
||||
n = readlink("/proc/self/exe", self, sizeof(self) - 1);
|
||||
if (n <= 0) {
|
||||
return -1;
|
||||
}
|
||||
self[n] = '\0';
|
||||
|
||||
dir = dirname(self); /* dirname() may modify its argument in place;
|
||||
* `self` is a disposable local copy, so that's
|
||||
* fine here. */
|
||||
|
||||
if (snprintf(out, outsz, "%s/xce_test", dir) >= (int)outsz) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Runs xce_test_path with argv (NULL-terminated; argv[0] conventionally
|
||||
* the program name) with stdin attached to /dev/null -- so that if a
|
||||
* regression makes it fall through into the interactive REPL instead of
|
||||
* exiting, readline() sees immediate EOF rather than hanging -- and
|
||||
* captures its stdout into out_buf. Kills and fails the test if the child
|
||||
* does not exit within CHILD_TIMEOUT_TENTHS/10 seconds, so a genuine hang
|
||||
* can't wedge the whole suite. */
|
||||
static void run_and_capture(const char* xce_test_path, char* const argv[],
|
||||
char* out_buf, size_t out_buf_size,
|
||||
int* exit_code)
|
||||
{
|
||||
int outpipe[2];
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
int elapsed = 0;
|
||||
pid_t waited;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pipe(outpipe));
|
||||
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE(pid >= 0);
|
||||
|
||||
if (pid == 0) {
|
||||
int devnull = open("/dev/null", O_RDONLY);
|
||||
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDIN_FILENO);
|
||||
close(devnull);
|
||||
}
|
||||
dup2(outpipe[1], STDOUT_FILENO);
|
||||
close(outpipe[0]);
|
||||
close(outpipe[1]);
|
||||
|
||||
execv(xce_test_path, argv);
|
||||
_exit(127); /* only reached if execv() itself failed */
|
||||
}
|
||||
|
||||
close(outpipe[1]);
|
||||
|
||||
for (;;) {
|
||||
waited = waitpid(pid, &status, WNOHANG);
|
||||
if (waited == pid) {
|
||||
break;
|
||||
}
|
||||
if (elapsed >= CHILD_TIMEOUT_TENTHS) {
|
||||
kill(pid, SIGKILL);
|
||||
waitpid(pid, &status, 0);
|
||||
close(outpipe[0]);
|
||||
TEST_FAIL_MESSAGE(
|
||||
"xce_test did not exit within timeout -- it likely fell "
|
||||
"through into the REPL instead of exiting after a "
|
||||
"process-level help/version flag");
|
||||
}
|
||||
usleep(100000);
|
||||
elapsed++;
|
||||
}
|
||||
|
||||
{
|
||||
ssize_t total = 0;
|
||||
ssize_t r;
|
||||
|
||||
while (total < (ssize_t)out_buf_size - 1) {
|
||||
r = read(outpipe[0], out_buf + total,
|
||||
out_buf_size - 1 - (size_t)total);
|
||||
if (r <= 0) {
|
||||
break;
|
||||
}
|
||||
total += r;
|
||||
}
|
||||
out_buf[total] = '\0';
|
||||
}
|
||||
close(outpipe[0]);
|
||||
|
||||
TEST_ASSERT_TRUE(WIFEXITED(status));
|
||||
*exit_code = WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/* Word-wrapped help text can break a multi-word phrase across lines at any
|
||||
* space -- argp wraps the doc string to the terminal width. Folding
|
||||
* newlines to spaces before substring-matching makes assertions robust to
|
||||
* exactly where those wraps land. */
|
||||
static void flatten_newlines(char* s)
|
||||
{
|
||||
for (; *s; s++) {
|
||||
if (*s == '\n') {
|
||||
*s = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_help_flag_prints_usage_and_exits_without_repl(void)
|
||||
{
|
||||
char xce_test_path[PATH_MAX];
|
||||
char out[CAPTURE_BUF_SIZE];
|
||||
int exit_code;
|
||||
char* const argv[] = { "xce_test", "--help", NULL };
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)));
|
||||
|
||||
run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, exit_code);
|
||||
TEST_ASSERT_NOT_NULL(strstr(out, "Usage:"));
|
||||
/* Never reached the interactive session's top-level prompt. */
|
||||
TEST_ASSERT_NULL(strstr(out, "xce>"));
|
||||
}
|
||||
|
||||
/* Requirement 4.3: the process-level help output must describe the
|
||||
* in-session `help` command as the way to list/learn available commands
|
||||
* -- not just document its own --help/-?/-h flags. */
|
||||
static void test_help_flag_describes_insession_help_command(void)
|
||||
{
|
||||
char xce_test_path[PATH_MAX];
|
||||
char out[CAPTURE_BUF_SIZE];
|
||||
int exit_code;
|
||||
char* const argv[] = { "xce_test", "--help", NULL };
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)));
|
||||
|
||||
run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code);
|
||||
TEST_ASSERT_EQUAL_INT(0, exit_code);
|
||||
|
||||
flatten_newlines(out);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(out, "type `help' at the prompt to list"),
|
||||
"xce_test --help must describe the in-session `help' command as "
|
||||
"the way to list/learn available commands (Requirement 4.3)");
|
||||
}
|
||||
|
||||
/* Requirement 4.1 explicitly calls out `-h` alongside `--help`. GNU argp's
|
||||
* OWN built-in short help alias is `-?`, not `-h` -- so unlike --help/-?,
|
||||
* this is not automatic and must be verified (and, if missing, added). */
|
||||
static void test_dash_h_flag_prints_usage_and_exits_without_repl(void)
|
||||
{
|
||||
char xce_test_path[PATH_MAX];
|
||||
char out[CAPTURE_BUF_SIZE];
|
||||
int exit_code;
|
||||
char* const argv[] = { "xce_test", "-h", NULL };
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)));
|
||||
|
||||
run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, exit_code);
|
||||
TEST_ASSERT_NOT_NULL(strstr(out, "Usage:"));
|
||||
TEST_ASSERT_NULL(strstr(out, "xce>"));
|
||||
}
|
||||
|
||||
static void test_version_flag_prints_version_and_exits_without_repl(void)
|
||||
{
|
||||
char xce_test_path[PATH_MAX];
|
||||
char out[CAPTURE_BUF_SIZE];
|
||||
int exit_code;
|
||||
char* const argv[] = { "xce_test", "--version", NULL };
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)));
|
||||
|
||||
run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, exit_code);
|
||||
TEST_ASSERT_NOT_NULL(strstr(out, "xce"));
|
||||
/* xce_compose_version_ex()'s output includes a "built <timestamp>"
|
||||
* segment -- distinguishes real version info from a coincidental
|
||||
* "xce" substring elsewhere. */
|
||||
TEST_ASSERT_NOT_NULL(strstr(out, "built"));
|
||||
TEST_ASSERT_NULL(strstr(out, "Usage:"));
|
||||
TEST_ASSERT_NULL(strstr(out, "xce>"));
|
||||
}
|
||||
|
||||
void test_xcetest_process_entry(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_help_flag_prints_usage_and_exits_without_repl);
|
||||
RUN_TEST(test_help_flag_describes_insession_help_command);
|
||||
RUN_TEST(test_dash_h_flag_prints_usage_and_exits_without_repl);
|
||||
RUN_TEST(test_version_flag_prints_version_and_exits_without_repl);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue