qemu: two tests the review pass left on disk
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
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# `fullscreen toggle` must read the dashboard's REAL state, not a belief the
|
||||
# FIFO's own `fullscreen` verb tracked on the side.
|
||||
#
|
||||
# warden_debug.c's `fullscreen` command used to keep a private `static bool
|
||||
# on` as its only record of state, and nothing kept it in step with the real
|
||||
# dashboard: `home` clears fullscreen by calling
|
||||
# warden_screen_overview_set_fullscreen(false) directly (every hardware run's
|
||||
# preamble, since a live panel is wherever the last flow or an operator left
|
||||
# it), and a real tap on the dashboard toggles it through the UI's own
|
||||
# tap-catcher. Either way the FIFO's belief was left stale. A `fullscreen
|
||||
# toggle` right after either one then decided its next state from that stale
|
||||
# belief: if `on` was still true from an earlier `fullscreen on`, toggling
|
||||
# set it to false -- which coincidentally MATCHED the already-off real state,
|
||||
# so the dashboard never actually turned on, yet the reply still said
|
||||
# "fullscreen off: ok". See test_qmp_drive.py's FullscreenToggleTracksRealState
|
||||
# for the same scenario driven offline against a faked control channel (both
|
||||
# the pre-fix and fixed shapes).
|
||||
#
|
||||
# `wake` first, as every compiled flow does: by the time the control bridge
|
||||
# is up the rig may already sit dimmed, and sleep.c swallows the touch that
|
||||
# wakes a dimmed panel (flare-edge #148).
|
||||
wake
|
||||
sleep 1
|
||||
assert_page Dashboard/Dashboard
|
||||
assert_hit 47 676 obj box=12,640,72x72
|
||||
ctl fullscreen on
|
||||
sleep 1
|
||||
assert_hit 47 676 widget box=0,0,720x720
|
||||
home
|
||||
sleep 1
|
||||
assert_page Dashboard/Dashboard
|
||||
# home must have cleared the REAL state -- this is home-leaves-fullscreen.txt's
|
||||
# own check, repeated here so a failure below is unambiguously the toggle's
|
||||
# fault and not a home regression.
|
||||
assert_hit 47 676 obj box=12,640,72x72
|
||||
# The dashboard is really off now. A correct toggle from here can only go on.
|
||||
ctl fullscreen toggle
|
||||
sleep 1
|
||||
assert_hit 47 676 widget box=0,0,720x720
|
||||
Executable
+192
@@ -0,0 +1,192 @@
|
||||
#!/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 ]
|
||||
Reference in New Issue
Block a user