feat(xce-test-rc-file): task 1.1 rc-file path resolution with XDG precedence
Adds rcfile_resolve_path() (rcfile.c/rcfile.h): resolves $XDG_CONFIG_HOME/xce_test/rc when set and non-empty, else $HOME/.config/xce_test/rc, else NULL. Standalone libc-only module mirroring the configcompare.c pattern; wired into xce_test_src and controller_unit_test_src, registered in controller_test.c's Unity runner. 6 unit tests in test_rcfile.c cover all XDG/HOME combinations plus a dedicated env-var save/restore isolation test, addressing the cross-test leak risk found during design review (controller_unit_test runs 80+ test files in one process). Adds the xce-test-rc-file spec (requirements/design/tasks/research), including design fixes from validation: getline()-based line reading (task 1.2, avoids truncating long rc-file lines) and the env-isolation requirement above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
parent
f9e8d97eac
commit
9de4db95d3
10 changed files with 632 additions and 1 deletions
|
|
@ -54,7 +54,7 @@ controller = executable('xce_controller',
|
|||
link_args: wrap_link_args,
|
||||
install: true)
|
||||
|
||||
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c']
|
||||
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c', 'testsuite/rcfile.c']
|
||||
|
||||
# BSD-licensed readline replacement (scheduler-core spec, task 12.1 review
|
||||
# finding): GNU readline is GPL; xce_test's REPL only needs readline()/
|
||||
|
|
@ -165,6 +165,8 @@ controller_unit_test_src = [
|
|||
'testsuite/snapdiff.c',
|
||||
'testsuite/test_configcompare.c',
|
||||
'testsuite/configcompare.c',
|
||||
'testsuite/test_rcfile.c',
|
||||
'testsuite/rcfile.c',
|
||||
'testsuite/test_persist_snapshot_entities.c',
|
||||
'testsuite/test_apply_seam.c',
|
||||
'testsuite/test_apply_committed_entry.c',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ extern void test_walcompare(void);
|
|||
extern void test_snapverify(void);
|
||||
extern void test_snapdiff(void);
|
||||
extern void test_configcompare(void);
|
||||
extern void test_rcfile(void);
|
||||
extern void test_drive_async_config(void);
|
||||
extern void test_slowlane(void);
|
||||
extern void test_commit_admission_pool(void);
|
||||
|
|
@ -247,6 +248,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_snapverify);
|
||||
TEST(test_snapdiff);
|
||||
TEST(test_configcompare);
|
||||
TEST(test_rcfile);
|
||||
TEST(test_drive_async_config);
|
||||
TEST(test_slowlane);
|
||||
TEST(test_commit_admission_pool);
|
||||
|
|
|
|||
38
src/controller/testsuite/rcfile.c
Normal file
38
src/controller/testsuite/rcfile.c
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#define _GNU_SOURCE
|
||||
#include "rcfile.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Suffixes appended to the resolved base directory to form the rc-file
|
||||
* path -- kept as two separate constants (rather than one shared tail)
|
||||
* since the XDG_CONFIG_HOME branch is already rooted at a "config home"
|
||||
* directory while the HOME fallback still needs the "/.config" segment. */
|
||||
#define RCFILE_XDG_SUFFIX "/xce_test/rc"
|
||||
#define RCFILE_HOME_SUFFIX "/.config/xce_test/rc"
|
||||
|
||||
char* rcfile_resolve_path(void)
|
||||
{
|
||||
const char* xdg_config_home = getenv("XDG_CONFIG_HOME");
|
||||
if (xdg_config_home && *xdg_config_home) {
|
||||
size_t len = strlen(xdg_config_home) + strlen(RCFILE_XDG_SUFFIX) + 1;
|
||||
char* path = (char*)malloc(len);
|
||||
if (!path)
|
||||
return NULL;
|
||||
snprintf(path, len, "%s%s", xdg_config_home, RCFILE_XDG_SUFFIX);
|
||||
return path;
|
||||
}
|
||||
|
||||
const char* home = getenv("HOME");
|
||||
if (home && *home) {
|
||||
size_t len = strlen(home) + strlen(RCFILE_HOME_SUFFIX) + 1;
|
||||
char* path = (char*)malloc(len);
|
||||
if (!path)
|
||||
return NULL;
|
||||
snprintf(path, len, "%s%s", home, RCFILE_HOME_SUFFIX);
|
||||
return path;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
20
src/controller/testsuite/rcfile.h
Normal file
20
src/controller/testsuite/rcfile.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
/*
|
||||
* rcfile.h -- xce_test startup rc-file path resolution (xce-test-rc-file
|
||||
* spec, task 1.1).
|
||||
*
|
||||
* A standalone, dependency-light module following the same pattern as
|
||||
* configcompare.c/h -- no dependency on test.c internals, linked into both
|
||||
* the xce_test executable and controller_unit_test.
|
||||
*
|
||||
* This task implements path resolution only (rcfile_resolve_path()).
|
||||
* Line-loading (rcfile_load()) is a later task in the same spec and will
|
||||
* extend this file, not replace it.
|
||||
*/
|
||||
|
||||
/* Resolves the rc-file path per the XDG Base Directory Specification:
|
||||
* "$XDG_CONFIG_HOME/xce_test/rc" when XDG_CONFIG_HOME is set and non-empty,
|
||||
* else "$HOME/.config/xce_test/rc" when HOME is set and non-empty. Returns
|
||||
* a heap string the caller must free(), or NULL if neither variable yields
|
||||
* a usable base directory. */
|
||||
char* rcfile_resolve_path(void);
|
||||
188
src/controller/testsuite/test_rcfile.c
Normal file
188
src/controller/testsuite/test_rcfile.c
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdlib.h>
|
||||
#include <string.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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue