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