tests/scripts/fullscreen-toggle-tracks-real-state.txt pins the debug channel's `fullscreen toggle` deciding from the dashboard's real state rather than a private flag that `home` and a real tap left stale (rig: 14 ok). tests/test-ui-drive-rs485.sh runs offline with a fake run.sh and socat and proves ui-drive.sh --rs485-devices fails closed when the socket, pty or control socket never appears, dumps mbsim.log on failure, and kills a simulator that ignores SIGTERM. README lists both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
193 lines
8.7 KiB
Bash
Executable File
193 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Offline regression test for ui-drive.sh's rs485 simulator startup and its
|
|
# process cleanup: no real QEMU or kernel image. A fake qemu/run.sh stands in
|
|
# for the VM (it optionally binds the rs.sock qemu's --rs485 chardev would
|
|
# create) and, for one case, a fake socat stands in ahead of it on PATH. This
|
|
# is the same "copy the real script beside stand-in siblings" trick
|
|
# tests/flows/run-flow-run-all-tests.sh uses for flow-run-rig.sh, one layer
|
|
# down at ui-drive.sh's own dependencies.
|
|
#
|
|
# What is worth pinning:
|
|
# - the rs.sock/rs.pty/rs.ctl waits fail closed instead of printing
|
|
# "== rs485 simulator: ..." over a bus nothing is actually serving --
|
|
# before this fix, a socat or mbsim.py that never came up looked, from
|
|
# ui-drive.sh's own output, exactly like a live one, and every later
|
|
# rs485 assertion failed for a reason buried in a log nobody was pointed
|
|
# at.
|
|
# - cleanup() escalates to SIGKILL for a sim process that ignores SIGTERM,
|
|
# instead of leaving it to outlive the script (an accumulation of
|
|
# orphaned simulator processes on a loaded CI runner).
|
|
#
|
|
# bash test-ui-drive-rs485.sh
|
|
set -uo pipefail
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SCRATCH="$(mktemp -d /tmp/uidrs485.XXXXXX)"
|
|
trap 'rm -rf "$SCRATCH"' EXIT
|
|
FAIL=0
|
|
pass() { printf '[PASS] %s\n' "$1"; }
|
|
fail() { printf '[FAIL] %s\n' "$1"; FAIL=1; }
|
|
|
|
ZIMAGE="$SCRATCH/fake-zImage"
|
|
SCRIPT="$SCRATCH/fake.txt"
|
|
: > "$ZIMAGE"
|
|
printf 'wake\n' > "$SCRIPT"
|
|
|
|
# build_rig WORKDIR: lay out WORKDIR/qemu with a copy of the real (fixed)
|
|
# ui-drive.sh and qmp.py next to stand-in mkinitramfs.sh/mkimage.sh/run.sh/
|
|
# payload, so ui-drive.sh's own HERE/QDIR resolve inside WORKDIR and nothing
|
|
# under the real qemu/out/ is ever touched.
|
|
build_rig() {
|
|
local work="$1"
|
|
mkdir -p "$work/qemu/tests" "$work/qemu/payload"
|
|
cp "$HERE/ui-drive.sh" "$work/qemu/tests/ui-drive.sh"
|
|
cp "$HERE/qmp.py" "$work/qemu/tests/qmp.py"
|
|
chmod +x "$work/qemu/tests/ui-drive.sh"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$work/qemu/mkinitramfs.sh"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$work/qemu/mkimage.sh"
|
|
cat > "$work/qemu/run.sh" <<'EOS'
|
|
#!/usr/bin/env bash
|
|
# Stand-in for qemu/run.sh: no real VM, just enough to drive ui-drive.sh's
|
|
# rs485 startup logic offline. TEST_MAKE_RS_SOCK=1 binds a listening AF_UNIX
|
|
# socket at --rs485's path (as qemu's server=on,wait=off chardev would);
|
|
# unset/0 leaves it absent, simulating qemu never wiring up the bus. Stays
|
|
# up 8s -- past ui-drive.sh's 3s post-launch liveness check and the rs485
|
|
# waits' poll budgets -- then exits, so a run that gets past the rs485 block
|
|
# still fails fast (VM exited early) instead of idling out the 180s deadline.
|
|
rs485_sock=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--rs485) rs485_sock="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
exec python3 -c '
|
|
import os, socket, sys, time
|
|
sock_path = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
if sock_path and os.environ.get("TEST_MAKE_RS_SOCK") == "1":
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.bind(sock_path)
|
|
s.listen(1)
|
|
time.sleep(8)
|
|
' "$rs485_sock"
|
|
EOS
|
|
chmod +x "$work/qemu/mkinitramfs.sh" "$work/qemu/mkimage.sh" "$work/qemu/run.sh"
|
|
: > "$work/qemu/payload/warden-ui"
|
|
chmod +x "$work/qemu/payload/warden-ui"
|
|
}
|
|
|
|
# run_rig WORKDIR: invoke the copied ui-drive.sh with a bounded outer timeout
|
|
# (the test's own safety net, matching run-flow-run-all-tests.sh) and stash
|
|
# combined output + exit code in the globals below.
|
|
RIG_OUT=""
|
|
RIG_RC=0
|
|
run_rig() {
|
|
local work="$1"
|
|
RIG_OUT="$(cd "$work" && timeout -k 5 40 bash "$work/qemu/tests/ui-drive.sh" \
|
|
--rs485-devices "5:bare" "$ZIMAGE" "$SCRIPT" "$work/out" 2>&1)"
|
|
RIG_RC=$?
|
|
}
|
|
|
|
# --- case A: qemu's rs.sock never appears -----------------------------------
|
|
work="$SCRATCH/a"; build_rig "$work"
|
|
FLARE_EDGE="$SCRATCH/flare-edge-a"
|
|
mkdir -p "$FLARE_EDGE/tools/modbus-sim"
|
|
printf '#!/usr/bin/env python3\nimport sys; sys.exit(1)\n' > "$FLARE_EDGE/tools/modbus-sim/mbsim.py"
|
|
RIG_OUT="$(unset TEST_MAKE_RS_SOCK; export FLARE_EDGE; run_rig "$work"; echo "$RIG_OUT"; echo "RC=$RIG_RC")"
|
|
rc_line="$(printf '%s\n' "$RIG_OUT" | grep '^RC=')"; RIG_RC="${rc_line#RC=}"
|
|
[ "$RIG_RC" -ne 0 ] \
|
|
&& pass "case A: never-appeared rs.sock fails the run (rc=$RIG_RC)" \
|
|
|| fail "case A: never-appeared rs.sock fails the run: rc=$RIG_RC, out: $RIG_OUT"
|
|
printf '%s\n' "$RIG_OUT" | grep -qF 'FATAL: rs485 bus socket never appeared' \
|
|
&& pass "case A: FATAL names the missing rs.sock" \
|
|
|| fail "case A: FATAL names the missing rs.sock: $RIG_OUT"
|
|
printf '%s\n' "$RIG_OUT" | grep -qF '== rs485 simulator:' \
|
|
&& fail "case A: printed the rs485-live success line over a bus that was never up: $RIG_OUT" \
|
|
|| pass "case A: never claims the bus is live"
|
|
|
|
# --- case B: rs.sock is up but socat never creates rs.pty -------------------
|
|
work="$SCRATCH/b"; build_rig "$work"
|
|
FLARE_EDGE="$SCRATCH/flare-edge-b"
|
|
mkdir -p "$FLARE_EDGE/tools/modbus-sim"
|
|
printf '#!/usr/bin/env python3\nimport sys; sys.exit(1)\n' > "$FLARE_EDGE/tools/modbus-sim/mbsim.py"
|
|
FAKEBIN="$SCRATCH/fakebin-b"; mkdir -p "$FAKEBIN"
|
|
printf '#!/usr/bin/env bash\necho "FAKE SOCAT: simulated failure" >&2\nexit 1\n' > "$FAKEBIN/socat"
|
|
chmod +x "$FAKEBIN/socat"
|
|
RIG_OUT="$(export TEST_MAKE_RS_SOCK=1 FLARE_EDGE; export PATH="$FAKEBIN:$PATH"; run_rig "$work"; echo "$RIG_OUT"; echo "RC=$RIG_RC")"
|
|
rc_line="$(printf '%s\n' "$RIG_OUT" | grep '^RC=')"; RIG_RC="${rc_line#RC=}"
|
|
[ "$RIG_RC" -ne 0 ] \
|
|
&& pass "case B: socat never linking rs.pty fails the run (rc=$RIG_RC)" \
|
|
|| fail "case B: socat never linking rs.pty fails the run: rc=$RIG_RC, out: $RIG_OUT"
|
|
printf '%s\n' "$RIG_OUT" | grep -qF 'FATAL: rs485 socat never created rs.pty' \
|
|
&& pass "case B: FATAL names the missing rs.pty" \
|
|
|| fail "case B: FATAL names the missing rs.pty: $RIG_OUT"
|
|
|
|
# --- case C: rs.sock+rs.pty are up but mbsim.py crashes before rs.ctl -------
|
|
work="$SCRATCH/c"; build_rig "$work"
|
|
FLARE_EDGE="$SCRATCH/flare-edge-c"
|
|
mkdir -p "$FLARE_EDGE/tools/modbus-sim"
|
|
cat > "$FLARE_EDGE/tools/modbus-sim/mbsim.py" <<'EOS'
|
|
#!/usr/bin/env python3
|
|
import sys
|
|
sys.stderr.write("FAKE MBSIM: simulated crash before opening the control socket\n")
|
|
sys.exit(1)
|
|
EOS
|
|
RIG_OUT="$(export TEST_MAKE_RS_SOCK=1 FLARE_EDGE; unset TEST_MBSIM_PID_FILE; run_rig "$work"; echo "$RIG_OUT"; echo "RC=$RIG_RC")"
|
|
rc_line="$(printf '%s\n' "$RIG_OUT" | grep '^RC=')"; RIG_RC="${rc_line#RC=}"
|
|
[ "$RIG_RC" -ne 0 ] \
|
|
&& pass "case C: mbsim.py crashing before rs.ctl fails the run (rc=$RIG_RC)" \
|
|
|| fail "case C: mbsim.py crashing before rs.ctl fails the run: rc=$RIG_RC, out: $RIG_OUT"
|
|
printf '%s\n' "$RIG_OUT" | grep -qF 'FATAL: rs485 simulator (mbsim.py) never came up' \
|
|
&& pass "case C: FATAL names the dead simulator" \
|
|
|| fail "case C: FATAL names the dead simulator: $RIG_OUT"
|
|
printf '%s\n' "$RIG_OUT" | grep -qF 'FAKE MBSIM: simulated crash' \
|
|
&& pass "case C: mbsim.log is dumped on failure" \
|
|
|| fail "case C: mbsim.log is dumped on failure: $RIG_OUT"
|
|
|
|
# --- case D: a sim process that ignores SIGTERM is still reaped ------------
|
|
work="$SCRATCH/d"; build_rig "$work"
|
|
FLARE_EDGE="$SCRATCH/flare-edge-d"
|
|
mkdir -p "$FLARE_EDGE/tools/modbus-sim"
|
|
cat > "$FLARE_EDGE/tools/modbus-sim/mbsim.py" <<'EOS'
|
|
#!/usr/bin/env python3
|
|
# Comes up cleanly (binds --control so ui-drive.sh's rs.ctl wait passes),
|
|
# then ignores SIGTERM -- the process cleanup() must still SIGKILL.
|
|
import argparse, os, signal, socket, sys, time
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('--port')
|
|
ap.add_argument('--control')
|
|
ap.add_argument('--device', action='append', default=[])
|
|
args = ap.parse_args()
|
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
|
if args.control:
|
|
try:
|
|
os.unlink(args.control)
|
|
except FileNotFoundError:
|
|
pass
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.bind(args.control)
|
|
s.listen(1)
|
|
pidfile = os.environ.get('TEST_MBSIM_PID_FILE')
|
|
if pidfile:
|
|
with open(pidfile, 'w') as f:
|
|
f.write(str(os.getpid()))
|
|
while True:
|
|
time.sleep(1)
|
|
EOS
|
|
PIDFILE="$SCRATCH/mbsim-d.pid"
|
|
RIG_OUT="$(export TEST_MAKE_RS_SOCK=1 FLARE_EDGE TEST_MBSIM_PID_FILE="$PIDFILE"; run_rig "$work"; echo "$RIG_OUT"; echo "RC=$RIG_RC")"
|
|
rc_line="$(printf '%s\n' "$RIG_OUT" | grep '^RC=')"; RIG_RC="${rc_line#RC=}"
|
|
if [ -s "$PIDFILE" ]; then
|
|
mbsim_pid="$(cat "$PIDFILE")"
|
|
if ! kill -0 "$mbsim_pid" 2>/dev/null; then
|
|
pass "case D: a SIGTERM-ignoring sim process is dead once ui-drive.sh returns (SIGKILL fallback)"
|
|
else
|
|
fail "case D: pid $mbsim_pid is still alive after ui-drive.sh returned -- SIGKILL fallback did not fire"
|
|
kill -KILL "$mbsim_pid" 2>/dev/null || true # don't leak it out of the test either
|
|
fi
|
|
else
|
|
fail "case D: the fake mbsim.py never wrote its pid file, out: $RIG_OUT"
|
|
fi
|
|
|
|
[ "$FAIL" -eq 0 ] && echo "ALL UI-DRIVE RS485 TESTS PASSED" || echo "UI-DRIVE RS485 TESTS FAILED"
|
|
[ "$FAIL" -eq 0 ]
|