test(xce-test-rc-file): task 3.1 end-to-end rc-file discovery/loading/--norc

Adds test_rcfile_integration.sh, mirroring clean_failover_preservation.sh's
shell-driven black-box pattern against the real built xce_test binary:

- a valid rc file's set masters override takes effect in both a piped run
  and a genuine pty-driven interactive run, before the normal session begins
- --norc leaves the default masters value untouched with the same rc file
  present

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
Rongsong Shen 2026-08-26 16:37:04 +00:00
parent e863d69993
commit d258aa3d91
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 288 additions and 1 deletions

View file

@ -33,7 +33,7 @@
- _Boundary: test.c::main_ - _Boundary: test.c::main_
- [ ] 3. Validation: end-to-end behavior against the built binary - [ ] 3. Validation: end-to-end behavior against the built binary
- [ ] 3.1 Verify rc-file discovery, loading, and skip-flag behavior end-to-end - [x] 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: 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 - 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 - Observable: an integration test run demonstrates both scenarios pass against the real built binary
@ -60,3 +60,4 @@
- Task 1.1: the 2 pre-existing `test_config_repl.c` failures (`test_conf_trim_keeps_only_n_versions`, `test_conf_get_saved_tag_no_config`) are the tracked `bug-1561` overlayfs-artifact regression, unrelated to this spec — expect them to keep appearing in every subsequent task's `controller_unit_test` run; do not treat them as a new regression. - Task 1.1: the 2 pre-existing `test_config_repl.c` failures (`test_conf_trim_keeps_only_n_versions`, `test_conf_get_saved_tag_no_config`) are the tracked `bug-1561` overlayfs-artifact regression, unrelated to this spec — expect them to keep appearing in every subsequent task's `controller_unit_test` run; do not treat them as a new regression.
- Task 1.2: `fopen()` succeeds on a directory path on this platform's glibc — the EISDIR failure only surfaces on the first `getline()` call (`errno=EISDIR`, `ferror(f)` set). A naive `if (!f)` open-failure check misses this; a post-loop `ferror(f)` check is required to catch it (see `rcfile_load()`, bug-1716). - Task 1.2: `fopen()` succeeds on a directory path on this platform's glibc — the EISDIR failure only surfaces on the first `getline()` call (`errno=EISDIR`, `ferror(f)` set). A naive `if (!f)` open-failure check misses this; a post-loop `ferror(f)` check is required to catch it (see `rcfile_load()`, bug-1716).
- Task 1.2: an ASAN heap-use-after-free was observed ONCE in an ad-hoc standalone `controller_unit_test` run (`commit_resolve_pending`/`fsync_worker`/`tap_teardown` in `test_apply_pump.c`) — did not reproduce on a second run or in the canonical `xce-build.sh` run. Unrelated to this spec's files; logged as bug-1717 (unreproduced flake) for tracking. If it recurs on a later task's run, capture the full ASAN report before assuming it's this spec's fault. - Task 1.2: an ASAN heap-use-after-free was observed ONCE in an ad-hoc standalone `controller_unit_test` run (`commit_resolve_pending`/`fsync_worker`/`tap_teardown` in `test_apply_pump.c`) — did not reproduce on a second run or in the canonical `xce-build.sh` run. Unrelated to this spec's files; logged as bug-1717 (unreproduced flake) for tracking. If it recurs on a later task's run, capture the full ASAN report before assuming it's this spec's fault.
- Task 3.1: `test_rcfile_integration.sh`'s per-test scratch `$HOME` dirs are cleaned via explicit `rm -rf` in each test function's normal flow, but there's no script-level `trap ... EXIT INT TERM` (unlike `clean_failover_preservation.sh`'s global trap) — an interrupted run could leak a small scratch dir. Verified low-impact (small footprint, no leak in normal runs) but worth adding a trap if this script is extended later.

View file

@ -0,0 +1,286 @@
#!/usr/bin/env bash
#
# test_rcfile_integration.sh — xce-test-rc-file integration test, shell-driven
# against the REAL built `xce_test` binary. Mirrors clean_failover_preservation.sh's
# harness conventions in this directory: mktemp -d scratch fixtures, a
# log()/pass()/failcase() reporting style, explicit per-scenario PASS/FAIL, and
# a real process exit code (0 == every scenario passed).
#
# Design: .kiro/specs/xce-test-rc-file/design.md, "Testing Strategy" ->
# "Integration Tests". Requirements: .kiro/specs/xce-test-rc-file/requirements.md
# Requirement 1 (rc-file discovery/loading) and Requirement 2 (--norc opt-out).
#
# Task 3.1 scope (implemented here):
# test_rcfile_takes_effect_piped() Req 1.1, 1.2, 1.4, 1.5, 2.3
# test_rcfile_takes_effect_interactive() Req 1.1, 1.2, 1.4, 1.5, 2.3
# test_norc_skips_rcfile() Req 2.1, 2.3
#
# Task 3.2 (a LATER task, NOT implemented by this file) is expected to append
# below, reusing the same setup_rc_home/cleanup_rc_home/run_piped/
# run_interactive_pty helpers and the same pass()/failcase() accounting:
# test_rcfile_failure_aborts_piped() Req 3.1, 3.3, 3.4
# test_rcfile_failure_continues_interactive() Req 3.1, 3.2, 3.4
# and both would be added to the scenario list in main() below.
#
# Exit 0 == every scenario PASSed; nonzero == at least one scenario FAILed.
set -u
# ── Config ───────────────────────────────────────────────────────────────────
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HERE/../../.." && pwd)"
XCE_TEST="${XCE_TEST_BIN:-$REPO_ROOT/src/xce.build/controller/xce_test}"
# xce_test's `masters` setting defaults to "localhost:9990" (test.c's
# settings[] table) and prints as "<not set>" until something actually
# assigns it (KVAL() macro in cmd_set). Pick an override value that can never
# collide with either, so a positive match is unambiguous.
OVERRIDE_MASTERS="rchost-a:19991,rchost-b:19992"
NOT_SET="<not set>"
RUN_TIMEOUT_S="${XCE_RCFILE_TEST_TIMEOUT_S:-20}"
FAILURES=0
RESULTS=()
# The interactive-pty driver (see run_interactive_pty below) is written ONCE
# to its own temp file rather than fed via a heredoc on the python3 command
# line -- a heredoc there would compete with the SEPARATE pipe that must also
# deliver the xce_test command script to the same process's stdin (`python3 -`
# reads its whole program from stdin, consuming it, leaving nothing for the
# script itself to read via sys.stdin). Cleaned up on exit.
PTY_DRIVER="$(mktemp "${TMPDIR:-/tmp}/xce-rcfile-pty-driver.XXXXXX")"
trap 'rm -f "$PTY_DRIVER"' EXIT
cat >"$PTY_DRIVER" <<'PYEOF'
import os, pty, select, signal, sys, time
def read_available(fd, timeout):
r, _, _ = select.select([fd], [], [], timeout)
if fd in r:
try:
return os.read(fd, 65536)
except OSError:
return b""
return b""
def main():
xce_test_bin, homedir, xdg, extra, timeout_s = sys.argv[1:6]
timeout_s = float(timeout_s)
commands = sys.stdin.read().splitlines()
env = dict(os.environ)
env["HOME"] = homedir
if xdg:
env["XDG_CONFIG_HOME"] = xdg
else:
env.pop("XDG_CONFIG_HOME", None)
args = [xce_test_bin] + ([extra] if extra else [])
pid, master_fd = pty.fork()
if pid == 0:
os.execvpe(args[0], args, env)
os._exit(127)
output = b""
deadline = time.time() + timeout_s
# Let xce_test start up and print its first prompt before sending input.
time.sleep(0.4)
output += read_available(master_fd, 0.4)
for cmd in commands:
os.write(master_fd, (cmd + "\n").encode())
time.sleep(0.3)
output += read_available(master_fd, 0.3)
status = None
while time.time() < deadline:
chunk = read_available(master_fd, 0.3)
if chunk:
output += chunk
continue
pid_done, st = os.waitpid(pid, os.WNOHANG)
if pid_done == pid:
status = st
break
if status is None:
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
_, status = os.waitpid(pid, 0)
while True:
chunk = read_available(master_fd, 0.2)
if not chunk:
break
output += chunk
try:
os.close(master_fd)
except OSError:
pass
exitcode = os.waitstatus_to_exitcode(status)
sys.stdout.buffer.write(output)
sys.exit(exitcode if exitcode >= 0 else 1)
main()
PYEOF
# ── Reporting helpers ────────────────────────────────────────────────────────
log() { printf '[rcfile-integration] %s\n' "$*" >&2; }
pass() { RESULTS+=("PASS: $1"); log "PASS: $1"; }
failcase() {
RESULTS+=("FAIL: $1 -- $2")
log "FAIL: $1 -- $2"
FAILURES=$((FAILURES + 1))
}
# ── Fixture helpers ──────────────────────────────────────────────────────────
# Creates a scratch $HOME with $HOME/config/xce_test/rc containing an
# OVERRIDE_MASTERS `set masters` line, and echoes "<homedir>|<xdg_config_home>".
setup_rc_home() {
local dir
dir="$(mktemp -d "${TMPDIR:-/tmp}/xce-rcfile-it.XXXXXX")" || return 1
mkdir -p "$dir/config/xce_test" || return 1
printf 'set masters %s\n' "$OVERRIDE_MASTERS" >"$dir/config/xce_test/rc"
printf '%s|%s\n' "$dir" "$dir/config"
}
cleanup_rc_home() {
local dir="$1"
[ -n "$dir" ] && [ -d "$dir" ] && rm -rf "$dir"
}
# Extracts the current-value column of the `masters` line from a `set`
# command's output (cmd_set's "%-12s:%s default: %s\n" format). Robust to the
# exact column padding and to a value containing an embedded space (the
# unset-sentinel "<not set>") by capturing everything between the first ':'
# and the literal " default:" tail.
masters_value_in_output() {
printf '%s\n' "$1" | grep '^masters' | head -1 \
| sed -E 's/^masters[[:space:]]*:(.*) default:.*/\1/'
}
# Runs xce_test PIPED (stdin is not a tty -> scripted/batch mode -- the
# session's existing abort-on-first-failure default). Args:
# $1 homedir $2 xdg_config_home $3 script (newline-joined commands)
# $4... extra xce_test flags (e.g. --norc)
# Prints combined stdout+stderr; returns xce_test's own exit code.
run_piped() {
local homedir="$1" xdg="$2" script="$3"
shift 3
printf '%s\n' "$script" \
| env HOME="$homedir" XDG_CONFIG_HOME="$xdg" \
timeout "$RUN_TIMEOUT_S" "$XCE_TEST" "$@" 2>&1
}
# Runs xce_test behind a REAL pty (genuine interactive/TTY session), so
# libedit's readline() and isatty(STDIN_FILENO)-gated g_interactive/
# g_continue_on_error behave exactly as they do for a human at a terminal --
# a piped run cannot exercise this. Uses a small embedded Python driver
# (pty.fork()), the same technique task 2.2's own implementer used for manual
# interactive verification of this feature.
# Args: $1 homedir $2 xdg_config_home $3 script $4 extra flag ("" or e.g. "--norc")
# Prints the captured pty transcript; returns xce_test's own exit code.
run_interactive_pty() {
local homedir="$1" xdg="$2" script="$3" extra="${4:-}"
printf '%s\n' "$script" \
| python3 "$PTY_DRIVER" "$XCE_TEST" "$homedir" "$xdg" "$extra" "$RUN_TIMEOUT_S"
}
# ── Scenarios (task 3.1) ─────────────────────────────────────────────────────
test_rcfile_takes_effect_piped() {
local name="rcfile_takes_effect_piped"
local fixture homedir xdg out rc mval
fixture="$(setup_rc_home)" || { failcase "$name" "setup_rc_home failed"; return; }
homedir="${fixture%%|*}"; xdg="${fixture##*|}"
out="$(run_piped "$homedir" "$xdg" $'set\nquit')"
rc=$?
mval="$(masters_value_in_output "$out")"
if [ "$rc" -ne 0 ]; then
failcase "$name" "xce_test exited $rc (expected 0); output:"$'\n'"$out"
elif [ "$mval" != "$OVERRIDE_MASTERS" ]; then
failcase "$name" "expected masters=='$OVERRIDE_MASTERS', got '$mval'; output:"$'\n'"$out"
else
pass "$name"
fi
cleanup_rc_home "$homedir"
}
test_rcfile_takes_effect_interactive() {
local name="rcfile_takes_effect_interactive"
local fixture homedir xdg out rc mval
fixture="$(setup_rc_home)" || { failcase "$name" "setup_rc_home failed"; return; }
homedir="${fixture%%|*}"; xdg="${fixture##*|}"
out="$(run_interactive_pty "$homedir" "$xdg" $'set\nquit')"
rc=$?
mval="$(masters_value_in_output "$out")"
if [ "$rc" -ne 0 ]; then
failcase "$name" "xce_test exited $rc (expected 0); transcript:"$'\n'"$out"
elif [ "$mval" != "$OVERRIDE_MASTERS" ]; then
failcase "$name" "expected masters=='$OVERRIDE_MASTERS', got '$mval'; transcript:"$'\n'"$out"
elif ! grep -qF 'xce>' <<<"$out"; then
failcase "$name" "no 'xce>' interactive prompt seen in transcript -- session may not have been a real tty; transcript:"$'\n'"$out"
else
pass "$name"
fi
cleanup_rc_home "$homedir"
}
test_norc_skips_rcfile() {
local name="norc_skips_rcfile"
local fixture homedir xdg out rc mval
fixture="$(setup_rc_home)" || { failcase "$name" "setup_rc_home failed"; return; }
homedir="${fixture%%|*}"; xdg="${fixture##*|}"
out="$(run_piped "$homedir" "$xdg" $'set\nquit' --norc)"
rc=$?
mval="$(masters_value_in_output "$out")"
if [ "$rc" -ne 0 ]; then
failcase "$name" "xce_test --norc exited $rc (expected 0); output:"$'\n'"$out"
elif [ "$mval" = "$OVERRIDE_MASTERS" ]; then
failcase "$name" "--norc did not skip rc-file load -- override value leaked into 'set' output; output:"$'\n'"$out"
elif [ "$mval" != "$NOT_SET" ]; then
failcase "$name" "expected masters=='$NOT_SET' (never assigned) with --norc, got '$mval'; output:"$'\n'"$out"
else
pass "$name"
fi
cleanup_rc_home "$homedir"
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
[ -x "$XCE_TEST" ] \
|| { log "FAIL: xce_test not found/executable: $XCE_TEST (run /xce-build controller)"; exit 1; }
test_rcfile_takes_effect_piped
test_rcfile_takes_effect_interactive
test_norc_skips_rcfile
log "---- summary ----"
local r
for r in "${RESULTS[@]}"; do
log "$r"
done
if [ "$FAILURES" -eq 0 ]; then
log "ALL SCENARIOS PASSED"
exit 0
else
log "$FAILURES SCENARIO(S) FAILED"
exit 1
fi
}
main "$@"