Files
akbasic/tests/akgl_typing.sh

116 lines
4.8 KiB
Bash
Raw Normal View History

#!/bin/bash
#
# Type at the SDL window with a real keyboard, and check what comes out.
#
# This is the only test in the repository that exercises the path a person
# actually uses: X11 delivers a key press to a focused window, SDL composes it,
# libakgl rings it, the line editor echoes it, and the interpreter runs it. Every
# other keyboard test synthesises SDL events, which is a fine way to test the
# code downstream of SDL and no way at all to test the code upstream of it.
#
# **It exists because that gap shipped a bug.** The frontend never called
# SDL_StartTextInput(), so SDL emitted no text-input events, every keystroke
# arrived with an empty `text`, and the editor dropped the lot -- no echo, and
# nothing on stdout either, because RUN could never be typed. The suite was
# green throughout, because it pushed the text-input events itself.
#
# Skips rather than fails when it cannot run: no display, no xdotool, no
# script(1), no window, or no focus are all "this machine cannot answer the
# question", not "the answer is no". CTest is told exit 77 means skipped.
#
# **It steals keyboard focus for a few seconds.** That is inherent -- the point
# is a focused window. Set AKBASIC_SKIP_INTERACTIVE=1 to skip it while you work.
set -u
SKIP=77
BASIC="${1:?usage: akgl_typing.sh <path to basic>}"
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
skip() { echo "SKIP: $*"; exit "${SKIP}"; }
fail() { echo "FAIL: $*"; exit 1; }
# ---------------------------------------------------------------- environment
[ "${AKBASIC_SKIP_INTERACTIVE:-0}" = "1" ] && skip "AKBASIC_SKIP_INTERACTIVE=1"
[ -n "${DISPLAY:-}" ] || skip "no DISPLAY; this needs a real X server, not the dummy driver"
command -v xdotool >/dev/null 2>&1 || skip "xdotool is not installed"
command -v script >/dev/null 2>&1 || skip "script(1) is not installed"
xdotool getactivewindow >/dev/null 2>&1 || skip "no window manager is answering on ${DISPLAY}"
# script(1) gives the driver a pty for stdin, which is what makes it choose the
# *window* as its console. Redirected stdin is a deliberate signal to read the
# pipe instead -- see run_akgl() in src/main.c -- and under CTest stdin is never
# a terminal, so without this the driver would ignore the keyboard entirely and
# the test would be measuring nothing.
OUT="${WORK}/typescript"
script -qefc "${BASIC}" "${OUT}" >/dev/null 2>&1 &
DRIVER=$!
# shellcheck disable=SC2317 # reached only through the EXIT trap below
cleanup_driver() {
kill "${DRIVER}" 2>/dev/null
wait "${DRIVER}" 2>/dev/null
}
trap 'cleanup_driver; rm -rf "${WORK}"' EXIT
# ------------------------------------------------------------------ the window
# --sync blocks until it appears rather than sleeping and hoping.
WID="$(timeout 20 xdotool search --sync --onlyvisible --name '^BASIC$' 2>/dev/null | head -1)"
[ -n "${WID}" ] || skip "the BASIC window never appeared (headless, or a WM that does not map it)"
xdotool windowactivate --sync "${WID}" >/dev/null 2>&1
xdotool windowfocus --sync "${WID}" >/dev/null 2>&1
FOCUS="$(xdotool getwindowfocus 2>/dev/null)"
[ "${FOCUS}" = "${WID}" ] || skip "could not give the window keyboard focus (got '${FOCUS}', wanted '${WID}')"
# ------------------------------------------------------------------- the typing
# Poll for expected output instead of sleeping a guessed interval: a fixed sleep
# is either flaky or slow, and usually both.
await() {
local want="$1" limit="${2:-15}" i=0
while [ "${i}" -lt $((limit * 10)) ]; do
if tr -d '\r' < "${OUT}" 2>/dev/null | grep -qF "${want}"; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
return 1
}
type_line() {
xdotool type --clearmodifiers --delay 30 "$1"
xdotool key --clearmodifiers Return
}
await "READY" 20 || skip "the interpreter never reached its prompt"
# A string literal on purpose: the double quote is the character that was
# unreachable before the keystroke ring carried composed text, and a BASIC
# program that cannot contain a string is not much of a BASIC.
type_line '10 PRINT "TYPED OK"'
type_line 'RUN'
if ! await "TYPED OK" 15; then
echo "--- what the driver actually wrote ---"
tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,20p'
fail "typing at the window produced nothing: the keyboard path is broken"
fi
# Lower case has to survive too. It could not before libakgl 0.3.0, because a
# keycode cannot express shift and the editor folded everything to upper case.
type_line '20 PRINT "lower"'
type_line 'RUN'
if ! await "lower" 15; then
echo "--- what the driver actually wrote ---"
tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,20p'
fail "lower case did not survive the keyboard: composed text is not reaching the editor"
fi
type_line 'QUIT'
await "" 1 >/dev/null 2>&1 || true
echo "PASS: typed at a real focused window and the interpreter ran it"
exit 0