#!/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