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:
parent
e863d69993
commit
d258aa3d91
2 changed files with 288 additions and 1 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
286
src/controller/testsuite/test_rcfile_integration.sh
Executable file
286
src/controller/testsuite/test_rcfile_integration.sh
Executable 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 "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue