#!/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 }" 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 # Backspace, end to end and through what it produces rather than what it draws. # Typing PRINX, rubbing out the X and finishing the word only runs if backspace # actually removed a character from the line buffer -- otherwise this is a # syntax error. Reported as "the backspace key does not clear the character # underneath it". type_line '30 PRINX'$'\b''T "BS OK"' type_line 'RUN' if ! await "BS OK" 15; then echo "--- what the driver actually wrote ---" tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p' fail "backspace did not remove a character: the line reached the parser unedited" fi # More lines than the window has rows, so the text has to scroll. Checked # through stdout, which proves the interpreter kept running across the scroll; # that the *screen* scrolls correctly is asserted at pixel level in # tests/akgl_frontend.c, which can read the render target back and this cannot. type_line '40 FOR I# = 1 TO 40' type_line '50 PRINT "SCROLL"' type_line '60 NEXT I#' type_line 'RUN 40' if ! await "SCROLL" 20; then echo "--- what the driver actually wrote ---" tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p' fail "a program long enough to scroll the window produced nothing" fi COUNT="$(tr -d '\r' < "${OUT}" | grep -c '^SCROLL$')" [ "${COUNT}" -eq 40 ] || fail "expected 40 scrolled lines, got ${COUNT}" type_line 'QUIT' await "" 1 >/dev/null 2>&1 || true echo "PASS: typed at a real focused window; backspace, scrolling and RUN all worked" exit 0