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