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
204
.kiro/specs/xce-test-rc-file/design.md
Normal file
204
.kiro/specs/xce-test-rc-file/design.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
**Purpose**: This feature adds a startup rc file to `xce_test`, `src/controller/testsuite/test.c`'s REPL/scripting client, so a user's usual settings (`set masters ...`, `set secret ...`) and starting context are applied automatically instead of retyped every session.
|
||||
|
||||
**Users**: Engineers driving controller/worker/scheduler/HA soak and E2E testing through `xce_test`, both interactively and via piped scripts.
|
||||
|
||||
**Impact**: `xce_test` gains one new startup step (rc-file discovery and loading, before its read loop begins) and one new flag (`--norc`). No existing command, output format, or scripting primitive changes.
|
||||
|
||||
### Goals
|
||||
- Auto-load and execute an rc file at a fixed, XDG-compliant path before `xce_test`'s interactive/scripted read loop starts, for both session kinds.
|
||||
- Let `--norc` skip this entirely for a given invocation.
|
||||
- Make a failing rc-file command behave exactly like a failing regular command — no new failure-handling mechanism.
|
||||
|
||||
### Non-Goals
|
||||
- A `--rcfile <path>` custom-location override (deferred; orthogonal addition if wanted later).
|
||||
- Any new scripting syntax/primitive beyond what `execute_line`/`preprocess_and_run` already supports.
|
||||
- A per-project/cwd rc file, or a legacy dotfile fallback (e.g. `~/.xce_testrc`) — there is no prior rc-file convention to preserve.
|
||||
- `xce_test` creating, writing, or scaffolding the rc file or its directory.
|
||||
- Fixing `xce_test`'s general `-h`/error-stream/flag-validation issues — those are owned by the separate `xce-test-cli-ux` spec.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
- Resolving the rc-file path per the XDG Base Directory Specification's own defined precedence/fallback.
|
||||
- Reading the rc file (if present) and feeding each line into the existing command-execution path before the read loop starts.
|
||||
- The `--norc` flag and its `argp`/help wiring.
|
||||
- Non-fatal handling of an unreadable (but present) rc file.
|
||||
|
||||
### Out of Boundary
|
||||
- The behavior of any individual `xce_test` command once invoked (unchanged — commands run exactly as they do today, whether typed, piped, or rc-sourced).
|
||||
- `preprocess_and_run`'s/`execute_line`'s abort-on-failure, continue-on-error, auto-variable, or exit-code-verdict logic (owned by `xce-test-e2e-primitives`; this spec only calls into it).
|
||||
- `xce_test`'s process-level `--help`/`-h` completeness (owned by `xce-test-cli-ux`); this spec only guarantees `--norc` is documented via the existing `--help`/`-?` mechanism.
|
||||
|
||||
### Allowed Dependencies
|
||||
- `preprocess_and_run` (`test.c:955`) as the per-line runner — called by function pointer, not reimplemented.
|
||||
- Standard C library only (`getenv`, `fopen`/`getline`/`fclose`, `strdup`/`free`) — no new third-party dependency.
|
||||
- `argp` (already a dependency of `xce_test`) for the `--norc` flag.
|
||||
|
||||
### Revalidation Triggers
|
||||
- A change to `preprocess_and_run`'s return-code contract (0 = continue, nonzero = abort) — `rcfile_load`'s early-stop logic depends on it unchanged.
|
||||
- A change to how/when `g_interactive`/`g_continue_on_error` are computed in `main()` — the rc-file load call must stay after that computation and before the first read-loop iteration.
|
||||
- Introduction of a `--rcfile <path>` override or a legacy-dotfile fallback in a future spec — must compose with, not replace, this path-resolution contract.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
`xce_test`'s `testsuite/` directory already has an established pattern for a small, dependency-free capability: a standalone `.c`/`.h` module (e.g. `waldump.c`/`waldump.h`, `configcompare.c`/`configcompare.h`) linked into both the `xce_test` executable (`meson.build:57`) and, paired with its own `test_<name>.c`, into `controller_unit_test` (e.g. `meson.build:166-167`). `test.c` itself only wires a thin `cmd_*` wrapper or, in this feature's case, a call from `main()`. This feature follows that exact pattern rather than growing `main()`/`test.c` inline.
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[main starts] --> ParseArgs[argp_parse fills norc flag]
|
||||
ParseArgs --> ComputeMode[Compute g_interactive and g_continue_on_error]
|
||||
ComputeMode --> NorcCheck{norc flag set}
|
||||
NorcCheck -- yes --> ReadLoop[Enter while not done read loop]
|
||||
NorcCheck -- no --> Resolve[rcfile_resolve_path]
|
||||
Resolve --> HasPath{Path resolved}
|
||||
HasPath -- no --> ReadLoop
|
||||
HasPath -- yes --> Load[rcfile_load path preprocess_and_run]
|
||||
Load --> Stopped{Runner returned nonzero}
|
||||
Stopped -- yes --> Exit[Return session_failed exit code]
|
||||
Stopped -- no --> ReadLoop
|
||||
```
|
||||
|
||||
**Architecture Integration**:
|
||||
- Selected pattern: standalone module (`rcfile.c`/`rcfile.h`) + one call site in `test.c`'s `main()`.
|
||||
- Domain/feature boundaries: `rcfile.c` owns path resolution and line iteration only; it has no knowledge of commands, contexts, or client state — it receives a runner callback and calls it once per line.
|
||||
- Existing patterns preserved: `configcompare.c`-style module pairing in both build targets; `preprocess_and_run`'s existing per-line contract untouched.
|
||||
- New components rationale: `rcfile.c`/`rcfile.h` (new capability, independently testable); one new `argp` option and one new call site in `test.c` (integration only).
|
||||
- Steering compliance: one domain per file (`structure.md`); test-first, `testsuite/test_<unit>.c` naming (`testing.md`).
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Choice / Version | Role in Feature | Notes |
|
||||
|-------|------------------|-----------------|-------|
|
||||
| CLI (C) | `argp` (existing dependency) | Declares `--norc` | Same mechanism as the existing `-v`/`--verbose` option (`test.c:674-677`) |
|
||||
| Runtime | C standard library (`getenv`, `stdio.h`, POSIX `getline`) | Path resolution + file reading | No new dependency; `getline` avoids a fixed-size line buffer |
|
||||
|
||||
## File Structure Plan
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/controller/testsuite/
|
||||
├── test.c # Modified: --norc option, call site in main()
|
||||
├── rcfile.c # New: path resolution + line-loading loop
|
||||
├── rcfile.h # New: public interface for rcfile.c
|
||||
└── test_rcfile.c # New: Unity unit tests for rcfile.c (fake runner)
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
- `src/controller/testsuite/test.c` — add `int norc` to `struct arguments`; add a `norc` entry to `options[]`; add a symbolic `OPT_NORC` key constant and its case in `parse_opt`; in `main()`, after `g_continue_on_error` is computed and before the `while (!done)` loop, call `rcfile_resolve_path()`/`rcfile_load()` when `arguments.norc` is not set, returning early with the existing `g_session_failed ? 1 : 0` convention if `rcfile_load` signals abort.
|
||||
- `src/controller/meson.build` — add `'testsuite/rcfile.c'` to `xce_test_src` (`meson.build:57`); add `'testsuite/test_rcfile.c'` and `'testsuite/rcfile.c'` to `controller_unit_test_src`, following the `test_configcompare.c`/`configcompare.c` pairing already there (`meson.build:166-167`).
|
||||
|
||||
### New Files
|
||||
- `src/controller/testsuite/rcfile.h` — declares `rcfile_resolve_path(void)` and `rcfile_load(const char* path, int (*runner)(char*))`.
|
||||
- `src/controller/testsuite/rcfile.c` — implements both functions; no dependency beyond libc.
|
||||
- `src/controller/testsuite/test_rcfile.c` — Unity tests using a fake `int (*)(char*)` runner (records calls, returns a scripted result) plus `setenv`/`unsetenv` to drive `rcfile_resolve_path`.
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
| Requirement | Summary | Components | Interfaces | Flows |
|
||||
|-------------|---------|------------|------------|-------|
|
||||
| 1.1 | Resolve rc path with XDG precedence | `rcfile.c` | `rcfile_resolve_path` | Resolve step |
|
||||
| 1.2 | Execute rc lines through the existing command path | `rcfile.c`, `test.c::main` | `rcfile_load(path, preprocess_and_run)` | Load step |
|
||||
| 1.3 | Missing rc file is silent, not an error | `rcfile.c` | `rcfile_load` (ENOENT branch) | HasPath=no / open ENOENT |
|
||||
| 1.4 | Load before the first read-loop line, both session kinds | `test.c::main` | call site placement | NorcCheck → Resolve → Load → ReadLoop |
|
||||
| 1.5 | Context/settings carry into the read loop | `test.c` (existing `active_ctx`/`settings` globals) | none new | implied by shared global state |
|
||||
| 2.1 | `--norc` skips loading entirely | `test.c::main`, `parse_opt` | `arguments.norc` | NorcCheck=yes |
|
||||
| 2.2 | `--norc` documented in `--help` | `test.c::options[]` | `argp` doc string | n/a |
|
||||
| 2.3 | Default (no `--norc`) still loads | `test.c::main` | NorcCheck=no branch | Resolve → Load |
|
||||
| 3.1 | Rc failure reported like any command failure | `preprocess_and_run` (reused, unmodified) | `rcfile_load`'s runner callback | Load step |
|
||||
| 3.2 | Interactive: continue past a failing rc line | `preprocess_and_run` (existing `g_continue_on_error` branch) | runner return 0 on recorded failure | Load loop continues |
|
||||
| 3.3 | Scripted: abort before piped script runs | `preprocess_and_run` (existing abort branch), `rcfile_load` | runner return nonzero → early stop | Stopped=yes → Exit |
|
||||
| 3.4 | Same session-verdict/exit-code accounting | `preprocess_and_run` (existing `g_last_status`/`g_session_failed`) | none new | n/a |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|
||||
|-----------|--------------|--------|---------------|---------------------------|-----------|
|
||||
| `rcfile.c`/`rcfile.h` | CLI startup | Resolve rc path; load and run its lines via a caller-supplied runner | 1.1, 1.2, 1.3 | none (P0: libc only) | Service |
|
||||
| `test.c::main` integration | CLI startup | Wire `--norc`, call `rcfile.c`, honor its abort signal | 1.4, 1.5, 2.1, 2.3 | `rcfile.c` (P0), `preprocess_and_run` (P0) | Service |
|
||||
| `test.c::options[]`/`parse_opt` | CLI arg parsing | Declare and parse `--norc` | 2.1, 2.2 | `argp` (P0) | Service |
|
||||
|
||||
### CLI Startup
|
||||
|
||||
#### rcfile module
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Resolve the rc-file path and, if present, execute its lines through a caller-supplied runner |
|
||||
| Requirements | 1.1, 1.2, 1.3 |
|
||||
|
||||
**Responsibilities & Constraints**
|
||||
- Owns path resolution (`$XDG_CONFIG_HOME` / `$HOME/.config` precedence) and line iteration only.
|
||||
- Has no knowledge of `xce_test` commands, contexts, or client state — the runner is an opaque callback.
|
||||
- Never writes to the rc file or creates its directory.
|
||||
|
||||
**Dependencies**
|
||||
- Outbound: caller-supplied runner function pointer (P0) — invoked once per line, trailing newline stripped.
|
||||
- External: libc `getenv`/`fopen`/`getline` (P0).
|
||||
|
||||
**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
|
||||
|
||||
##### Service Interface
|
||||
```c
|
||||
/* 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". Returns a heap string the caller must
|
||||
* free(), or NULL if neither variable yields a usable base directory. */
|
||||
char* rcfile_resolve_path(void);
|
||||
|
||||
/* Opens `path` and calls `runner(line)` once per line (trailing newline
|
||||
* stripped), stopping early if `runner` returns nonzero. Lines are read via
|
||||
* getline() with no length cap -- a line is never truncated or split
|
||||
* regardless of length.
|
||||
*
|
||||
* Returns:
|
||||
* 0 - path is NULL, the file does not exist (ENOENT),
|
||||
* the file could not be opened for another reason
|
||||
* (a warning is printed to stderr; non-fatal), or
|
||||
* every line ran and every runner call returned 0.
|
||||
* nonzero (== runner's) - the runner requested an abort; no further lines
|
||||
* are read. */
|
||||
int rcfile_load(const char* path, int (*runner)(char* line));
|
||||
```
|
||||
- Preconditions: `runner` is non-NULL when `path` is non-NULL.
|
||||
- Postconditions: the file handle is always closed before returning.
|
||||
- Invariants: lines are run in file order; iteration stops at the first nonzero runner result.
|
||||
|
||||
**Implementation Notes**
|
||||
- Integration: `main()` calls `rcfile_resolve_path()`, and — if non-NULL — `rcfile_load(path, preprocess_and_run)`, then `free()`s the path. A nonzero `rcfile_load` result short-circuits straight to `return g_session_failed ? 1 : 0;`, skipping the `while (!done)` loop entirely (Requirement 3.3).
|
||||
- Line reading: `rcfile_load` reads each line with POSIX `getline()` (a dynamically-growing heap buffer, freed once at loop exit), not a fixed-size `fgets()` buffer. `rcfile.c` deliberately has no dependency on `test.c`'s `MAX_LINE_LEN`, so a fixed buffer sized independently of it risks silently truncating (or splitting into two runner calls) a long line — e.g. a multi-peer `set masters host1:port,host2:port,...` line — producing a mangled command with no error reported. `getline()` has no such cap and keeps `rcfile.c` dependency-free (libc only), consistent with its stated architecture.
|
||||
- Validation: `test_rcfile.c` uses a fake runner to prove the stop-on-nonzero and ENOENT-is-silent contracts without depending on `preprocess_and_run` or any client/network state, plus a case with a line well beyond a typical small fixed-buffer size (e.g. >4KB) to prove no truncation/splitting occurs.
|
||||
- Risks: none identified — the module has no I/O beyond one file read and no shared mutable state.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Strategy
|
||||
- **Rc file absent (`ENOENT`)**: silent, non-fatal — `rcfile_load` returns 0 (Requirement 1.3).
|
||||
- **Rc file present but unreadable** (permission denied, is a directory, etc.): non-fatal — `rcfile_load` prints one `stderr` warning naming the path and `strerror(errno)`, then returns 0 so startup proceeds normally.
|
||||
- **A line in the rc file fails**: handled entirely by the reused `preprocess_and_run` — its existing error output, `g_last_status`/`g_session_failed` accounting, and mode-dependent abort/continue decision apply unchanged (Requirement 3).
|
||||
- **Neither `$XDG_CONFIG_HOME` nor `$HOME` resolves a base directory**: `rcfile_resolve_path` returns `NULL`; `main()` treats this identically to "no rc file found" — no error reported.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (`test_rcfile.c`, added to `controller_unit_test`)
|
||||
- **Test isolation**: `controller_unit_test` links 80+ `test_*.c` files into one process running sequentially, so any `setenv`/`unsetenv` on `$HOME`/`$XDG_CONFIG_HOME` here must not leak into tests that run after it. Before each test case that touches either variable, capture the prior state with `getenv()` (distinguishing "was unset" from "was empty" — e.g. via a `char*` that is `NULL` vs a duplicated non-NULL string); after the test case, restore that exact prior state (`unsetenv()` if it had been unset, `setenv(..., 1)` with the saved value otherwise). Apply this consistently across every test below that sets either variable.
|
||||
- `rcfile_resolve_path` returns `$XDG_CONFIG_HOME/xce_test/rc` when `XDG_CONFIG_HOME` is set and non-empty.
|
||||
- `rcfile_resolve_path` returns `$HOME/.config/xce_test/rc` when `XDG_CONFIG_HOME` is unset or empty.
|
||||
- `rcfile_resolve_path` returns `NULL` when neither `XDG_CONFIG_HOME` nor `HOME` is usable.
|
||||
- `rcfile_load` returns 0 and never calls the runner when the path does not exist.
|
||||
- `rcfile_load` calls the runner once per line, in order, with the trailing newline stripped.
|
||||
- `rcfile_load` stops after a runner call that returns nonzero, propagates that value, and does not call the runner for remaining lines (proves Requirement 3.3's "no further input" contract at the module level).
|
||||
- `rcfile_load` on a non-`ENOENT` open failure (e.g. path is a directory) prints a warning and returns 0 without invoking the runner.
|
||||
|
||||
### Integration Tests (new `testsuite/test_rcfile_integration.sh`, shell-driven against the built `xce_test` binary — mirrors the existing `clean_failover_preservation.sh` pattern)
|
||||
- With `HOME`/`XDG_CONFIG_HOME` pointed at a temp dir containing an rc file with `set masters ...`, piping a script into `xce_test` shows the overridden setting took effect.
|
||||
- `--norc` with the same rc file present leaves the default `masters` value untouched.
|
||||
- A deliberately failing rc-file line (e.g. malformed `set masters`) in piped mode causes `xce_test` to exit nonzero before any of the piped script's own commands produce output.
|
||||
- The same failing rc-file line in an interactive (TTY-simulated) session still reaches the normal prompt afterward.
|
||||
- **Compatibility regression check**: with no rc file present (the state every existing caller runs in today), the `xce-test-ha` and `xce-test-resources` skills' existing soak/verification scripts — the known live consumers of `xce_test` — produce the same pass/fail result they did before this feature, proving Requirement 1.3's "no rc file → unchanged behavior" guarantee holds for real, pre-existing automation and not only for this feature's own new integration script.
|
||||
43
.kiro/specs/xce-test-rc-file/requirements.md
Normal file
43
.kiro/specs/xce-test-rc-file/requirements.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
Every `xce_test` invocation starts from a blank slate: users repeatedly retype the same `set masters ...`/`set secret ...` commands (and sometimes mistype them) before they can do anything useful against a cluster. This feature adds an rc file, auto-executed at startup through `xce_test`'s existing command path, so a user's usual settings and starting context are applied automatically — the same convenience shells (`.bashrc`), gdb (`.gdbinit`), and readline (`.inputrc`) already provide. It reuses `execute_line`/`preprocess_and_run` as-is; no new scripting syntax is introduced.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: rc-file discovery at a fixed, XDG-compliant path; loading and executing the rc file's commands through the existing command-execution path before the interactive/scripted read loop starts, for both interactive and scripted/piped sessions; a `--norc` flag to skip rc-file loading for a given invocation; failure handling for a bad rc-file command that reuses the session's existing abort-on-failure/continue-on-error and exit-code-verdict machinery.
|
||||
- **Out of scope**: a `--rcfile <path>` custom-location override (deferred — can be added later as an orthogonal flag); any new scripting syntax or primitive beyond what `execute_line` already supports; a per-project/cwd rc file; any legacy dotfile convention (e.g. `~/.xce_testrc`) — there is no prior rc-file feature to stay compatible with; `xce_test` creating, writing, or scaffolding the rc file or its directory — it only reads a file the user already created; any change to the auto-variable, exit-code-verdict, or shell-escape mechanisms already shipped by `xce-test-e2e-primitives`.
|
||||
- **Adjacent expectations**: this feature relies on `$XDG_CONFIG_HOME`/`$HOME` already being set correctly in the operator's environment, and on the XDG Base Directory Specification's own defined fallback (`~/.config` when `$XDG_CONFIG_HOME` is unset or empty) — `xce_test` does not manage, create, or validate permissions on that directory. Every command inside the rc file is an ordinary `xce_test` command and remains subject to that command's own existing validation (e.g. `set masters` still requires valid peer syntax). `xce_test` is already the harness underneath other existing automation (the `xce-test-ha` and `xce-test-resources` skills' soak/verification scripts, and other specs' `testsuite/*.sh` integration scripts) that invoke it with no rc file present today and no awareness this feature exists — Requirement 1.3's "no rc file → unchanged behavior" guarantee must hold for these existing callers, not just for a new rc-file user, so this feature does not require any prior caller of `xce_test` to opt in or adapt.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: RC File Discovery and Loading
|
||||
|
||||
**Objective:** As a user who repeatedly retypes (and sometimes mistypes) the same setup commands every `xce_test` session, I want `xce_test` to automatically run a personal rc file at startup, so that my usual settings and starting context are applied without manual repetition.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The xce_test process shall, before executing the first line from its interactive or scripted read loop, look for an rc file at `$XDG_CONFIG_HOME/xce_test/rc`, or at `~/.config/xce_test/rc` when `$XDG_CONFIG_HOME` is unset or empty.
|
||||
2. If the rc file exists, then the xce_test process shall execute each of its lines through the same command path used for a line typed at the prompt or read from a piped script.
|
||||
3. If the rc file does not exist, then the xce_test process shall proceed directly to its normal read loop without reporting an error.
|
||||
4. The xce_test process shall perform rc-file discovery and loading the same way regardless of whether the session is interactive (TTY) or scripted/piped.
|
||||
5. When the rc file changes the active context (worker/scheduler/client) or a setting (e.g. `masters`, `secret`, `myname`), the xce_test session shall carry that context and those settings forward into the read loop that follows.
|
||||
|
||||
### Requirement 2: Opt-Out via --norc
|
||||
|
||||
**Objective:** As a user who needs a run with no local rc-file state (for example, a CI/soak script that requires a clean, reproducible environment), I want a flag that skips rc-file loading entirely, so that my run's behavior never depends on what happens to be in my rc file.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. When xce_test is invoked with `--norc`, the xce_test process shall skip rc-file discovery and loading entirely and proceed directly to its normal read loop.
|
||||
2. The xce_test process's process-level help output shall document the `--norc` flag.
|
||||
3. Where `--norc` is not given, the xce_test process shall perform rc-file discovery and loading as described in Requirement 1.
|
||||
|
||||
### Requirement 3: RC File Failure Handling
|
||||
|
||||
**Objective:** As a user or script author, I want a failing command inside my rc file to be reported and handled exactly like any other failing command, so that a typo neither locks me out of the tool nor runs silently unnoticed.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. If a command in the rc file fails, then the xce_test process shall report that failure the same way it reports the failure of any other command.
|
||||
2. While the session is running in its default interactive continue-on-error mode, if a command in the rc file fails, the xce_test process shall continue executing the remaining rc-file commands and then proceed to the normal interactive prompt.
|
||||
3. While the session is running in its default scripted/piped abort-on-first-failure mode, if a command in the rc file fails, the xce_test process shall stop processing further input — including the remainder of the rc file and any piped script commands — and exit with a nonzero status.
|
||||
4. The xce_test process shall apply the same session-verdict and exit-code accounting to a failing rc-file command that it applies to any other failing command.
|
||||
57
.kiro/specs/xce-test-rc-file/research.md
Normal file
57
.kiro/specs/xce-test-rc-file/research.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
## Summary
|
||||
- **Feature**: `xce-test-rc-file`
|
||||
- **Discovery Scope**: Extension (existing `xce_test` REPL client)
|
||||
- **Key Findings**:
|
||||
- `xce_test`'s own `testsuite/` directory already has an established pattern for a small, dependency-free capability implemented as its own `.c`/`.h` module and linked into both `xce_test` and `controller_unit_test` (e.g. `configcompare.c` at `meson.build:57` and `meson.build:166-167`). The new rc-file logic should follow this exact pattern rather than growing `test.c` (already ~4500 lines) with inline logic.
|
||||
- `preprocess_and_run(char* line)` (`test.c:955`) already implements the exact per-line contract this feature needs: blank/comment handling, `!`-shell-escape, auto-var substitution, `execute_line` dispatch, and a return-code convention — `0` = keep going, nonzero = abort-on-failure — that already differs by session mode (`g_continue_on_error`, set from `isatty(STDIN_FILENO)` at `test.c:4476-4477`). Feeding rc-file lines through this exact function, unmodified, satisfies Requirement 3 with no new failure-handling code.
|
||||
- `main()` computes `g_interactive`/`g_continue_on_error` at `test.c:4476-4477`, immediately before the `while (!done)` read loop at `test.c:4479`. This is the correct insertion point for rc-file loading: after mode detection (so the reused abort/continue behavior is correct) and before the first real input line is read (satisfying Requirement 1.4).
|
||||
|
||||
## Research Log
|
||||
|
||||
### XDG Base Directory Specification — default path resolution
|
||||
- **Context**: Requirements call for `$XDG_CONFIG_HOME/xce_test/rc`, falling back to `~/.config/xce_test/rc` when `$XDG_CONFIG_HOME` is unset or empty.
|
||||
- **Sources Consulted**: XDG Base Directory Specification (freedesktop.org) — defines `$XDG_CONFIG_HOME` with a required fallback of `$HOME/.config` when unset.
|
||||
- **Findings**: The spec explicitly names `$HOME/.config` as the fallback, so the fallback in Requirement 1.1 is not a project-specific guess — it is the spec's own defined default. The spec is silent on what to do when even `$HOME` is unavailable.
|
||||
- **Implications**: Path resolution is a pure function of two environment variables. When neither is usable, there is no defined path — treat this the same as "rc file not found" (silent skip), not an error, since no plausible location exists to report.
|
||||
|
||||
## Architecture Pattern Evaluation
|
||||
|
||||
| Option | Description | Strengths | Risks / Limitations | Notes |
|
||||
|--------|-------------|-----------|---------------------|-------|
|
||||
| New `rcfile.c`/`rcfile.h` module | Path resolution + line-loading loop as a standalone, dependency-free pair, wired into `test.c`'s `main()` | Matches existing `configcompare.c`/`waldump.c` precedent; independently unit-testable via `controller_unit_test`; keeps `test.c` unchanged except for the call site and one new `argp` option | None significant — smallest addition that satisfies all requirements | **Selected** |
|
||||
| Inline logic in `test.c`'s `main()` | Add the loop and path resolution directly in `main()` | Fewer files | Not unit-testable in isolation (would require driving the full `main()`/`argp`/`readline` stack); breaks from this directory's established module pattern | Rejected |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Decision: Reuse `preprocess_and_run` as the rc-file line runner, unmodified
|
||||
- **Context**: Requirement 3 needs a failing rc-file command to be reported and handled exactly like any other failing command, without introducing a parallel failure-handling mechanism.
|
||||
- **Alternatives Considered**:
|
||||
1. Write rc-file-specific error handling (its own abort/continue logic, its own error formatting).
|
||||
2. Pass `preprocess_and_run` itself as a callback into a generic line-loading loop.
|
||||
- **Selected Approach**: (2). `rcfile_load(path, runner)` takes a `int (*runner)(char*)` function pointer; `main()` passes `preprocess_and_run` directly.
|
||||
- **Rationale**: `preprocess_and_run`'s existing return contract (0 = continue, nonzero = abort) already encodes the mode-dependent abort-vs-continue decision (Requirement 3.2/3.3) and already updates `g_last_status`/`g_session_failed` (Requirement 3.4). Reusing it verbatim means Requirement 3 requires zero new logic — only a thin loop that stops when the runner says to.
|
||||
- **Trade-offs**: `rcfile.c` cannot be linked/tested with a real `preprocess_and_run` in isolation (it lives in `test.c`, coupled to the whole command-table/client-context state) — unit tests use a fake runner instead, which is sufficient since `rcfile_load`'s own contract (call runner per line, stop on nonzero, handle ENOENT/other errors) is what needs proving, not `preprocess_and_run` itself (already covered by `xce-test-e2e-primitives`'s tests).
|
||||
- **Follow-up**: None.
|
||||
|
||||
### Decision: Distinguish ENOENT from other `fopen` failures
|
||||
- **Context**: Requirement 1.3 only specifies silence when the rc file does not exist. It does not say what happens on a real failure (e.g. permission denied, or the path is a directory).
|
||||
- **Alternatives Considered**:
|
||||
1. Treat every `fopen` failure identically (silent skip).
|
||||
2. Distinguish `ENOENT` (silent) from other errors (non-fatal stderr warning, then proceed).
|
||||
- **Selected Approach**: (2).
|
||||
- **Rationale**: A misconfigured rc file (wrong permissions, a directory at the expected path) is a real misconfiguration worth surfacing — silently ignoring it could leave a user wondering why their settings never took effect. It must still be non-fatal (startup must proceed) since this feature must not become a new way for `xce_test` to fail to start.
|
||||
- **Trade-offs**: One extra branch in `rcfile_load`; no downside identified.
|
||||
- **Follow-up**: None.
|
||||
|
||||
### Decision: `--norc` as a long-only `argp` option
|
||||
- **Context**: Requirement 2 needs a flag that skips rc-file loading, documented in `--help` output.
|
||||
- **Selected Approach**: Add `{ "norc", OPT_NORC, 0, 0, "..." }` to `test.c`'s existing `options[]` (`test.c:674-677`), using a symbolic `key` above the printable-ASCII range (the standard `argp` idiom for a long-only flag with no short letter), and a new `int norc` field on `struct arguments`.
|
||||
- **Rationale**: Matches this project's existing single-flag `argp` setup (`-v`/`--verbose`) with minimal surface; `argp` auto-generates the `--help`/`-?` entry from the option's doc string, satisfying Requirement 2.2 with no additional code.
|
||||
- **Trade-offs**: None identified for this scope.
|
||||
|
||||
## Risks & Mitigations
|
||||
- A user's rc file blocks on stdin (e.g. contains a command that itself prompts) — mitigated by the fact that every existing `xce_test` command is already non-interactive/non-prompting; this feature does not add any new blocking primitive.
|
||||
- A missing/unset `$HOME` in a stripped-down container changes rc-file behavior in a way an operator doesn't expect — mitigated by treating "no resolvable path" identically to "file not found" (silent, non-fatal), consistent with Requirement 1.3's spirit.
|
||||
|
||||
## References
|
||||
- [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) — defines `$XDG_CONFIG_HOME` and its `$HOME/.config` fallback.
|
||||
22
.kiro/specs/xce-test-rc-file/spec.json
Normal file
22
.kiro/specs/xce-test-rc-file/spec.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"feature_name": "xce-test-rc-file",
|
||||
"created_at": "2026-08-26T08:49:11Z",
|
||||
"updated_at": "2026-08-26T09:18:22Z",
|
||||
"language": "en",
|
||||
"phase": "tasks-generated",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"design": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
},
|
||||
"tasks": {
|
||||
"generated": true,
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": true
|
||||
}
|
||||
55
.kiro/specs/xce-test-rc-file/tasks.md
Normal file
55
.kiro/specs/xce-test-rc-file/tasks.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Implementation Plan
|
||||
|
||||
- [ ] 1. Foundation: rc-file path resolution and line-loading capability
|
||||
- [x] 1.1 Implement rc-file path resolution following XDG precedence, with unit tests
|
||||
- Resolve the rc-file location using the XDG config variable when it is set and non-empty
|
||||
- Fall back to the home-directory XDG default when that variable is unset or empty, and to "no resolvable path" when neither variable is usable
|
||||
- Wire the new module and its test file into both existing build targets that need it, so both compile and the new unit tests run under the existing unit test suite
|
||||
- Save each of `$HOME`/`$XDG_CONFIG_HOME`'s prior state (unset vs. empty vs. a value) before any test case mutates it, and restore that exact prior state afterward, since `controller_unit_test` runs 80+ test files sequentially in one process and a leaked env var would silently affect unrelated tests
|
||||
- Observable: a unit test run demonstrates the correct location is chosen for each of the three environment combinations above, and a subsequent unrelated test's own `$HOME`/`$XDG_CONFIG_HOME` reads are unaffected
|
||||
- _Requirements: 1.1_
|
||||
|
||||
- [ ] 1.2 Implement rc-file line-loading with a pluggable command runner, with unit tests
|
||||
- Read the rc file line by line using `getline()` (no fixed-size buffer), invoking a caller-supplied runner once per line with the trailing newline removed
|
||||
- Treat a missing rc file as a no-op success (no error), and a present-but-unreadable rc file as a non-fatal warning that still allows startup to proceed
|
||||
- Stop reading immediately the first time the runner signals a stop, and propagate that signal to the caller without processing any remaining lines
|
||||
- Observable: a unit test run demonstrates every line is processed when the runner keeps succeeding, remaining lines are skipped the first time the runner signals a stop, the runner is never invoked when the file is absent, and a line well beyond a typical small fixed-buffer size (e.g. >4KB) reaches the runner intact and unsplit
|
||||
- _Requirements: 1.2, 1.3_
|
||||
|
||||
- [ ] 2. Core: xce_test startup integration
|
||||
- [ ] 2.1 Add a flag that skips rc-file loading for a single invocation
|
||||
- Register the flag with xce_test's existing command-line option parsing so it is documented in xce_test's own help output
|
||||
- When the flag is given, record that rc-file loading should be skipped for this run
|
||||
- Observable: xce_test's help output lists the new flag with a description, and invoking xce_test with the flag parses successfully and records the skip state (verified directly, independent of task 2.2's wiring)
|
||||
- _Requirements: 2.1, 2.2_
|
||||
- _Boundary: test.c::options[]/parse_opt_
|
||||
|
||||
- [ ] 2.2 Wire rc-file loading into xce_test startup for both interactive and scripted sessions
|
||||
- After xce_test determines whether the session is interactive or scripted and before it reads the first line of its normal session, resolve and load the rc file unless the skip flag was given, using the session's existing command-execution path as the per-line runner
|
||||
- When rc-file loading signals a stop, exit using the session's existing pass/fail exit-code convention without entering the normal session loop
|
||||
- Observable: a valid rc file's settings/context are visible once the normal session begins, in both a piped run and an interactive run; the same is confirmed absent when the skip flag is given
|
||||
- _Requirements: 1.4, 1.5, 2.3_
|
||||
- _Depends: 1.1, 1.2_
|
||||
- _Boundary: test.c::main_
|
||||
|
||||
- [ ] 3. Validation: end-to-end behavior against the built binary
|
||||
- [ ] 3.1 Verify rc-file discovery, loading, and skip-flag behavior end-to-end
|
||||
- Scenario: a valid rc file's settings/context take effect in both a piped run and an interactive run
|
||||
- Scenario: the skip flag leaves default settings untouched even with a valid rc file present
|
||||
- Observable: an integration test run demonstrates both scenarios pass against the real built binary
|
||||
- _Requirements: 1.1, 1.2, 1.4, 1.5, 2.1, 2.3_
|
||||
- _Depends: 2.2_
|
||||
|
||||
- [ ] 3.2 Verify rc-file failure handling matches the session's existing abort/continue behavior end-to-end
|
||||
- Scenario: a failing rc-file command in scripted/piped mode aborts before any of the piped script's own commands run, and the process exits with a failure status
|
||||
- Scenario: the same failing rc-file command in an interactive session still reaches the normal prompt afterward
|
||||
- Observable: an integration test run demonstrates both scenarios pass against the real built binary
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
||||
- _Depends: 2.2_
|
||||
|
||||
- [ ] 3.3 Confirm existing xce_test automation is unaffected when no rc file is present
|
||||
- Run the existing HA soak and resource-verification scripts (the known live callers of xce_test today) in an environment with no rc file at the resolved location
|
||||
- Compare their pass/fail result against their pre-feature baseline behavior
|
||||
- Observable: the existing scripts' outcomes are unchanged, demonstrating this feature introduces no regression for callers that know nothing about it
|
||||
- _Requirements: 1.3_
|
||||
- _Depends: 2.2_
|
||||
|
|
@ -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