Type at the window with a real keyboard: the akgl_typing test
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 18s
akbasic CI Build / sanitizers (push) Failing after 13s
akbasic CI Build / coverage (push) Failing after 16s
akbasic CI Build / akgl_build (push) Failing after 17s
akbasic CI Build / mutation_test (push) Failing after 17s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 18s
akbasic CI Build / sanitizers (push) Failing after 13s
akbasic CI Build / coverage (push) Failing after 16s
akbasic CI Build / akgl_build (push) Failing after 17s
akbasic CI Build / mutation_test (push) Failing after 17s
xdotool closes the gap that shipped the missing SDL_StartTextInput(). Every other keyboard test synthesises SDL events, which covers the code downstream of SDL and cannot cover the code upstream of it -- so the suite stayed green while the real keyboard was dead. akgl_typing starts the driver under a pty, waits for the window with xdotool search --sync, gives it focus, and types a program containing a string literal and a lower-case one, polling the mirrored stdout for what they print. Verified by reverting both halves of the text-input fix and watching it fail with the reported symptom: READY, and nothing after it. Skips rather than fails without a display, xdotool, script(1) or a window manager -- none of those means the answer is no. It steals keyboard focus for about fifteen seconds; AKBASIC_SKIP_INTERACTIVE=1 skips it deliberately. xdotool and script(1) are documented as optional test dependencies, alongside what gcovr and python3 already bought. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
115
tests/akgl_typing.sh
Executable file
115
tests/akgl_typing.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user