joeyagi/scripts/test-agi.sh
2026-10-06 16:59:41 -05:00

204 lines
7.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# test-agi.sh - JoeyAGI's host gate: the whole interpreter, built for the
# build machine on JoeyLib's headless BLANK port (tests/hostPort.c), plays
# scripted sessions and every frame it is asked for must hash as expected.
#
# 1. testResources: every resource of every game in games/ loads, every
# PICTURE decodes and every VIEW parses; KQ3's pictures (visual and
# priority) must hash as tests/expected/resources-kq3.txt says --
# those frames were checked pixel for pixel against Sierra's
# interpreter.
# 2. The probes: small games tests/probes.py builds from scratch, one per
# subsystem (pictures, windows, motion, control lines, priority,
# parser, menus, inventory, sound timing, ...), each played by its
# own script. Expected hashes in tests/expected/probes/.
# 3. With games/kq3 present, the KQ3 sessions in tests/kq3/*.scr (the
# opening, walking, typing, menus, debug teleports). Expected hashes
# in tests/expected/kq3/.
#
# Time inside a run is virtual (one frame per jlWaitVBL), so a run is
# exactly reproducible. Every run leaves its frames as PPMs in
# build/tests/out/NAME/.
#
# Usage: scripts/test-agi.sh [--update] [NAME...]
# NAME is a probe, kq3-SESSION or resources-GAME; --update
# rewrites the expected hashes from this run (check the frames
# first, e.g. with scripts/oracle.sh).
# Env: JOEYLIB the JoeyLib checkout (default ../joeylib)
# Exit: 0 pass, 1 fail, 2 skipped (no JoeyLib / host compiler / python3)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
JOEYLIB=${JOEYLIB:-$repo/../joeylib}
[[ -f $JOEYLIB/include/joey/joey.h ]] || { echo "test-agi: no JoeyLib checkout at $JOEYLIB (set JOEYLIB)" >&2; exit 2; }
command -v cc >/dev/null || { echo "test-agi: no host C compiler" >&2; exit 2; }
command -v python3 >/dev/null || { echo "test-agi: no python3" >&2; exit 2; }
update=0
if [[ ${1:-} == --update ]]; then
update=1
shift
fi
only=" $* "
build=$repo/build/tests
obj=$build/obj
out=$build/out
expected=$repo/tests/expected
mkdir -p "$obj" "$out"
# ----- Build: JoeyLib's core and generic code with the host port -----
jlFlags=(-std=c99 -O1 -DJOEYLIB_PLATFORM_BLANK -I "$JOEYLIB/include" -I "$JOEYLIB/include/joey" -I "$JOEYLIB/src/core" -I "$JOEYLIB/src/codegen")
agiFlags=("${jlFlags[@]}" -Wall -Wextra -Werror -I "$repo" -I "$repo/tests")
engine=(agi agiRes agiPic agiView agiGfx agiObj agiText agiFont agiInput agiSound agiSave agiVm)
# compile SRC OBJ FLAGS...: when the source, or any header, is newer.
compile() {
local src=$1 o=$2
shift 2
if [[ -f $o && ! $src -nt $o ]] && [[ -z $(find "$repo" "$repo/tests" "$JOEYLIB/include" "$JOEYLIB/src/core" -maxdepth 2 -name '*.h' -newer "$o" -print -quit) ]]; then
return 0
fi
cc "$@" -c "$src" -o "$o"
}
echo "Building the host interpreter..."
ok=1
jlObjs=()
for f in "$JOEYLIB"/src/core/*.c "$JOEYLIB"/src/generic/*.c; do
o=$obj/jl_$(basename "${f%.c}").o
compile "$f" "$o" "${jlFlags[@]}" -w || ok=0
jlObjs+=("$o")
done
agiObjs=()
for f in "${engine[@]}"; do
o=$obj/$f.o
if [[ $f == agi ]]; then
compile "$repo/$f.c" "$o" "${agiFlags[@]}" -Dmain=agiMain || ok=0
else
compile "$repo/$f.c" "$o" "${agiFlags[@]}" || ok=0
fi
agiObjs+=("$o")
done
for f in hostPort agiHost testResources; do
compile "$repo/tests/$f.c" "$obj/$f.o" "${agiFlags[@]}" || ok=0
done
[[ $ok == 1 ]] || { echo "test-agi: FAIL (build)" >&2; exit 1; }
cc -o "$build/agiHost" "$obj/agiHost.o" "$obj/hostPort.o" "${agiObjs[@]}" "${jlObjs[@]}" -lm || exit 1
cc -o "$build/testResources" "$obj/testResources.o" "$obj/hostPort.o" "${agiObjs[@]}" "${jlObjs[@]}" -lm || exit 1
passed=0
failed=0
skipped=0
# check NAME EXPECTFILE ACTUALFILE: compare, or with --update record.
check() {
local name=$1 want=$2 got=$3
if [[ $update == 1 ]]; then
mkdir -p "$(dirname "$want")"
cp "$got" "$want"
echo " $name: recorded $(wc -l < "$got") lines"
passed=$((passed + 1))
return
fi
if [[ ! -f $want ]]; then
echo " $name: FAIL (no $want; record it with --update once the frames are checked)"
failed=$((failed + 1))
return
fi
if diff -q "$want" "$got" >/dev/null; then
echo " $name: PASS ($(wc -l < "$got") checked)"
passed=$((passed + 1))
else
echo " $name: FAIL -- what differs (< expected, > got); frames in $out/$name/"
diff "$want" "$got" | grep '^[<>]' | head -20 | sed 's/^/ /'
failed=$((failed + 1))
fi
}
# selected NAME: was it asked for (everything is when nothing was named)?
selected() {
[[ $only == " " || $only == *" $1 "* ]]
}
# play NAME GAMEDIR SCRIPT EXPECTFILE: one scripted session.
play() {
local name=$1 game=$2 script=$3 want=$4 run
selected "$name" || return 0
run=$(mktemp -d "$build/run.XXXXXX")
ln -s "$game" "$run/DATA"
# The host port cannot make directories; saving needs SAVES/ there.
mkdir "$run/SAVES"
rm -rf "${out:?}/$name"
mkdir -p "$out/$name"
(cd "$run" && timeout -s KILL 600 "$build/agiHost" "$script" "$out/$name") > "$out/$name/log.txt" 2>&1
rm -rf "$run"
sed -n 's/^\[ *[0-9.]*\] shot \([^ ]*\) \([0-9a-f]*\)$/\1 \2/p' "$out/$name/log.txt" > "$out/$name/hashes.txt"
if ! grep -q 'script done' "$out/$name/log.txt"; then
echo " $name: FAIL (the run stopped before its script ended; see $out/$name/log.txt)"
failed=$((failed + 1))
return
fi
check "$name" "$want" "$out/$name/hashes.txt"
}
# ----- 1. Resources -----
echo
echo "--- resources ---"
for dir in "$repo"/games/*/; do
[[ -f $dir/LOGDIR ]] || continue
game=$(basename "$dir")
name=resources-$game
selected "$name" || continue
run=$(mktemp -d "$build/run.XXXXXX")
ln -s "$dir" "$run/DATA"
mkdir -p "$out/$name"
(cd "$run" && timeout -s KILL 600 "$build/testResources") > "$out/$name/hashes.txt" 2> "$out/$name/log.txt"
status=$?
rm -rf "$run"
if [[ $status != 0 ]]; then
echo " $name: FAIL ($(tail -1 "$out/$name/log.txt"))"
failed=$((failed + 1))
elif [[ -f $expected/$name.txt || $update == 1 ]]; then
check "$name" "$expected/$name.txt" "$out/$name/hashes.txt"
else
echo " $name: PASS ($(tail -1 "$out/$name/log.txt"); no expected picture hashes for this game)"
passed=$((passed + 1))
fi
done
# ----- 2. Probes -----
echo
echo "--- probes ---"
python3 "$repo/tests/probes.py" build "$build/probes" || { echo "test-agi: FAIL (probes.py)" >&2; exit 1; }
for name in $(python3 "$repo/tests/probes.py" list); do
play "$name" "$build/probes/$name" "$build/probes/$name/probe.scr" "$expected/probes/$name.txt"
done
# ----- 3. King's Quest III -----
echo
echo "--- kq3 ---"
if [[ -f $repo/games/kq3/LOGDIR ]]; then
for scr in "$repo"/tests/kq3/*.scr; do
[[ -f $scr ]] || continue
session=$(basename "${scr%.scr}")
play "kq3-$session" "$repo/games/kq3" "$scr" "$expected/kq3/$session.txt"
done
else
echo " skipped: no games/kq3"
skipped=1
fi
echo
echo "===== test-agi: $passed passed, $failed failed ====="
if [[ $failed -gt 0 ]]; then
exit 1
fi
if [[ $skipped -gt 0 && $passed -eq 0 ]]; then
exit 2
fi
exit 0