65816-llvm-mos/scripts/probeReplSmoke.sh
Scott Duensing 3388f3c5a5 More updates
2026-06-03 20:46:31 -05:00

169 lines
5.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# probeReplSmoke.sh - non-interactive smoke check for mameDebug.py
# --repl mode. Pipes a canned script (`break main`, `run`, `where`,
# `quit`) into the REPL and asserts that:
# 1. The REPL parses each command without error
# 2. A breakpoint resolves through the link816 map
# 3. MAME launches with the bp installed and surfaces a BP-HIT line
# 4. `where` resolves the captured PC to a source line via DWARF
#
# Exit 0 on full pass. Exit 77 (autotools "skip") if MAME / toolchain
# missing. Exit 1 on any unexpected REPL output or missing capture.
#
# Usage: probeReplSmoke.sh [--verbose]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
VERBOSE=0
if [ "${1:-}" = "--verbose" ]; then
VERBOSE=1
fi
CLANG="$ROOT/tools/llvm-mos-build/bin/clang"
LLVMMC="$ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$ROOT/tools/link816"
if [ ! -x "$CLANG" ] || [ ! -x "$LLVMMC" ] || [ ! -x "$LINK" ]; then
echo "probeReplSmoke: missing toolchain (clang/llvm-mc/link816)" >&2
exit 77
fi
if ! command -v mame >/dev/null 2>&1; then
echo "probeReplSmoke: mame not on PATH; skipping" >&2
exit 77
fi
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
CFILE="$WORK/repltest.c"
OFILE="$WORK/repltest.o"
OCRT0="$WORK/crt0.o"
OLIBGCC="$WORK/libgcc.o"
BIN="$WORK/repltest.bin"
MAP="$WORK/repltest.map"
DWARF="$WORK/repltest.dwarf"
OUT="$WORK/repl.out"
cat > "$CFILE" <<'EOF'
int gAnswer = 42;
int add(int a, int b) {
int c = a + b;
return c;
}
int main(void) {
int r = add(3, 4);
gAnswer = r;
while (1) { }
return r;
}
EOF
"$CLANG" --target=w65816 -O0 -g -ffunction-sections \
-c "$CFILE" -o "$OFILE" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
"$ROOT/runtime/src/crt0.s" -o "$OCRT0" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
"$ROOT/runtime/src/libgcc.s" -o "$OLIBGCC" 2>/dev/null
"$LINK" -o "$BIN" --text-base 0x1000 \
--map "$MAP" --debug-out "$DWARF" \
"$OCRT0" "$OFILE" "$OLIBGCC" >/dev/null 2>&1 || true
[ -s "$BIN" ] || { echo "probeReplSmoke: empty .bin"; exit 1; }
[ -s "$DWARF" ] || { echo "probeReplSmoke: empty DWARF sidecar"; exit 1; }
[ -s "$MAP" ] || { echo "probeReplSmoke: empty map"; exit 1; }
# Phase 1: existing single-frame `bp main` smoke (kept to ensure the
# baseline path still works). Then Phase 2: `bp add` + `--start-at
# main` to exercise the multi-frame `bt` walker.
printf 'break main\nrun\nwhere\nquit\n' \
| timeout 60 python3 "$HERE/mameDebug.py" --repl \
--bin "$BIN" --map "$MAP" --dwarf "$DWARF" \
--seconds 4 > "$OUT" 2>&1 || {
echo "probeReplSmoke: mameDebug.py --repl failed" >&2
cat "$OUT" >&2
exit 1
}
if [ "$VERBOSE" -eq 1 ]; then
cat "$OUT" >&2
fi
# Required output lines:
# "(dbg) break main" - command echo
# " bp #1 at 0x...... (main)" - bp set ack
# "(dbg) run" - command echo
# " PC=0x...... ... FUNC=main ..." - where output after run
# "(dbg) where" - command echo
# " PC=0x...... ... FUNC=main ..." - where output (manual)
# "(dbg) quit" - command echo
if ! grep -q "bp #1 at 0x" "$OUT"; then
echo "probeReplSmoke: missing 'bp #1 at 0x...' breakpoint ack" >&2
cat "$OUT" >&2
exit 1
fi
if ! grep -q "FUNC=main" "$OUT"; then
echo "probeReplSmoke: missing FUNC=main in 'where' output" >&2
cat "$OUT" >&2
exit 1
fi
# The `where` command (run AFTER the `run` command) must produce
# output too — verify by counting occurrences of "PC=0x" prefix lines.
PC_HITS=$(grep -c "^ PC=0x" "$OUT" || true)
if [ "$PC_HITS" -lt 2 ]; then
echo "probeReplSmoke: expected >= 2 PC=0x lines (run + where), got $PC_HITS" >&2
cat "$OUT" >&2
exit 1
fi
# Bonus: verify the captured PC equals the map entry for `main`.
MAIN_PC=$(awk '$2 == "main" { print $1; exit }' "$MAP")
[ -n "$MAIN_PC" ] || { echo "probeReplSmoke: no 'main' symbol in map"; exit 1; }
MAIN_PC_LC=$(echo "$MAIN_PC" | tr 'A-Z' 'a-z')
if ! grep -qi "PC=$MAIN_PC_LC " "$OUT"; then
echo "probeReplSmoke: captured PC does not match map[main]=$MAIN_PC" >&2
cat "$OUT" >&2
exit 1
fi
# Phase 2: multi-frame `bt` test. Breakpoint at `add` with --start-at
# main: the JSL frame from main->add is live at the snapshot, so `bt`
# should walk back up at least one parent (>= 2 total frames). This
# regression-checks both the .debug_frame_w65816 sidecar emit (link816)
# and the walker in mameDebug.py.
OUT2="$WORK/repl2.out"
printf 'break add\nrun\nbt\nquit\n' \
| timeout 60 python3 "$HERE/mameDebug.py" --repl \
--bin "$BIN" --map "$MAP" --dwarf "$DWARF" \
--start-at main --seconds 4 > "$OUT2" 2>&1 || {
echo "probeReplSmoke: mameDebug.py --repl (bt) failed" >&2
cat "$OUT2" >&2
exit 1
}
if [ "$VERBOSE" -eq 1 ]; then
cat "$OUT2" >&2
fi
# Count frame lines (` #N PC=0x...`) in the bt output. Need >= 2 to
# prove the .debug_frame_w65816 sidecar drove a real parent-frame walk.
FRAME_LINES=$(grep -cE "^ #[0-9]+ PC=0x" "$OUT2" || true)
if [ "$FRAME_LINES" -lt 2 ]; then
echo "probeReplSmoke: bt produced $FRAME_LINES frame lines (need >= 2)" >&2
cat "$OUT2" >&2
exit 1
fi
# Verify frame #0 is `add` and frame #1 is `main`.
if ! grep -q "^ #0 PC=0x.* FUNC=add " "$OUT2"; then
echo "probeReplSmoke: bt frame #0 is not 'add'" >&2
cat "$OUT2" >&2
exit 1
fi
if ! grep -q "^ #1 PC=0x.* FUNC=main " "$OUT2"; then
echo "probeReplSmoke: bt frame #1 is not 'main'" >&2
cat "$OUT2" >&2
exit 1
fi
echo "probeReplSmoke: OK (single-frame where + multi-frame bt OK)"
exit 0