Follow-up to the xce-harness-component spec, which relocated xce_test from src/controller/testsuite/ into a standalone src/harness/ component (commits cf55d691..cf380750). These four files were outside that spec's own file boundary (owned by other specs / general tooling) so were deliberately left alone at the time, but their hardcoded default path and error-message advice were now stale and would have misled anyone who hit them: - .claude/skills/xce-build/SKILL.md: added `harness` to both component lists (frontmatter description + args section), and replaced the "controller always builds xce_test too" claim with an accurate description of the harness component, its own build command, and the accepted Meson test-runner quirk (bug-1806) that can still transitively rebuild xce_test as a side effect of another component's default-profile test run. - src/controller/testsuite/test_rcfile_integration.sh (owned by the xce-test-rc-file spec): XCE_TEST_BIN default and failure message. - src/controller/testsuite/clean_failover_preservation.sh (owned by ha-term-safety task 5.2): same. - src/tools/test-commit-durability.sh: same, plus its header comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
377 lines
16 KiB
Bash
Executable file
377 lines
16 KiB
Bash
Executable file
#!/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 scope (implemented here), reusing the same setup_rc_home-style
|
|
# fixture pattern/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
|
|
#
|
|
# 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/harness/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>"
|
|
|
|
# A command name guaranteed absent from every xce_test command table (checked
|
|
# against every `{ "name", cmd_fn, ... }` entry in test.c) -- used as an rc-file
|
|
# line that deterministically fails via execute_line()'s find_command() miss
|
|
# ("<name>: No such command for xce test." on stderr, execute_line returns -1),
|
|
# the simplest reliable way to force a command failure (design.md Testing
|
|
# Strategy / task 3.2).
|
|
RCFILE_BOGUS_CMD="xce_rcfile_test_bogus_cmd_does_not_exist"
|
|
|
|
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"
|
|
}
|
|
|
|
# Creates a scratch $HOME with $HOME/config/xce_test/rc containing a
|
|
# deliberately-failing line (RCFILE_BOGUS_CMD, an unrecognized command name)
|
|
# FOLLOWED by an OVERRIDE_MASTERS `set masters` line. One fixture serves both
|
|
# task 3.2 scenarios: the piped-abort scenario only needs a failing line to be
|
|
# present (rcfile_load stops at the first nonzero runner result, so the
|
|
# trailing `set masters` line is never reached -- design.md's Architecture
|
|
# Pattern & Boundary Map "Stopped -- yes --> Exit" edge); the interactive
|
|
# scenario relies on that SAME trailing line still running afterward, to
|
|
# prove Requirement 3.2's "continue executing the remaining rc-file commands"
|
|
# rather than merely "didn't crash". Echoes "<homedir>|<xdg_config_home>",
|
|
# mirroring setup_rc_home().
|
|
setup_rc_home_failing() {
|
|
local dir
|
|
dir="$(mktemp -d "${TMPDIR:-/tmp}/xce-rcfile-it-fail.XXXXXX")" || return 1
|
|
mkdir -p "$dir/config/xce_test" || return 1
|
|
printf '%s\nset masters %s\n' "$RCFILE_BOGUS_CMD" "$OVERRIDE_MASTERS" \
|
|
>"$dir/config/xce_test/rc"
|
|
printf '%s|%s\n' "$dir" "$dir/config"
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# ── Scenarios (task 3.2) ─────────────────────────────────────────────────────
|
|
|
|
# A failing rc-file command in scripted/piped mode must abort BEFORE any of
|
|
# the piped script's own commands run, exiting nonzero (Req 3.1, 3.3, 3.4).
|
|
# The piped script's own `echo PIPED_SCRIPT_MARKER` is the observable that
|
|
# proves the read loop never got to read the piped script at all -- not just
|
|
# that the run "failed" for some other reason.
|
|
test_rcfile_failure_aborts_piped() {
|
|
local name="rcfile_failure_aborts_piped"
|
|
local fixture homedir xdg out rc
|
|
fixture="$(setup_rc_home_failing)" || { failcase "$name" "setup_rc_home_failing failed"; return; }
|
|
homedir="${fixture%%|*}"; xdg="${fixture##*|}"
|
|
|
|
out="$(run_piped "$homedir" "$xdg" $'echo PIPED_SCRIPT_MARKER\nquit')"
|
|
rc=$?
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
failcase "$name" "xce_test exited 0 (expected nonzero -- a failing rc-file command should fail the session, Req 3.4); output:"$'\n'"$out"
|
|
elif grep -qF 'PIPED_SCRIPT_MARKER' <<<"$out"; then
|
|
failcase "$name" "piped script's own PIPED_SCRIPT_MARKER output was present -- rc-file abort did not happen before the read loop consumed the piped script (Req 3.3); output:"$'\n'"$out"
|
|
elif ! grep -qF "$RCFILE_BOGUS_CMD" <<<"$out"; then
|
|
failcase "$name" "rc-file command failure was not reported (expected '$RCFILE_BOGUS_CMD: No such command...', Req 3.1); output:"$'\n'"$out"
|
|
else
|
|
pass "$name"
|
|
fi
|
|
cleanup_rc_home "$homedir"
|
|
}
|
|
|
|
# The same failing rc-file command in an interactive (TTY-simulated) session
|
|
# must NOT abort: the rc file's remaining lines still run (proving "continue",
|
|
# not just "didn't crash" -- Req 3.2) and the session still reaches the normal
|
|
# interactive prompt afterward. Per Req 3.4 the same session-verdict/exit-code
|
|
# accounting still applies as for any other failing command, so -- exactly as
|
|
# for a failing command typed directly at the prompt -- the process's own
|
|
# final exit code stays nonzero even though the session itself continued
|
|
# normally through to `quit`; a manual mutation check (rc file with no failing
|
|
# line) confirms this flips to exit 0, so this is asserting real behavior, not
|
|
# vacuously true.
|
|
test_rcfile_failure_continues_interactive() {
|
|
local name="rcfile_failure_continues_interactive"
|
|
local fixture homedir xdg out rc mval
|
|
fixture="$(setup_rc_home_failing)" || { failcase "$name" "setup_rc_home_failing failed"; return; }
|
|
homedir="${fixture%%|*}"; xdg="${fixture##*|}"
|
|
|
|
out="$(run_interactive_pty "$homedir" "$xdg" $'set\nquit')"
|
|
rc=$?
|
|
mval="$(masters_value_in_output "$out")"
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
failcase "$name" "xce_test exited 0 (expected nonzero -- a failing rc-file command should still fail the session's verdict, Req 3.4); transcript:"$'\n'"$out"
|
|
elif ! grep -qF "$RCFILE_BOGUS_CMD" <<<"$out"; then
|
|
failcase "$name" "rc-file command failure was not reported (expected '$RCFILE_BOGUS_CMD: No such command...', Req 3.1); transcript:"$'\n'"$out"
|
|
elif [ "$mval" != "$OVERRIDE_MASTERS" ]; then
|
|
failcase "$name" "expected masters=='$OVERRIDE_MASTERS' -- the rc-file line AFTER the failing one should still have run (Req 3.2), got '$mval'; transcript:"$'\n'"$out"
|
|
elif ! grep -qF 'xce>' <<<"$out"; then
|
|
failcase "$name" "no 'xce>' interactive prompt seen after the rc-file failure -- session did not reach the normal prompt (Req 3.2); transcript:"$'\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 harness)"; exit 1; }
|
|
|
|
test_rcfile_takes_effect_piped
|
|
test_rcfile_takes_effect_interactive
|
|
test_norc_skips_rcfile
|
|
test_rcfile_failure_aborts_piped
|
|
test_rcfile_failure_continues_interactive
|
|
|
|
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 "$@"
|