116 lines
4.8 KiB
Bash
116 lines
4.8 KiB
Bash
|
|
#!/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
|