#!/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 "" 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="" 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 "|". 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 "") 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 "$@"