2026-07-31 14:35:07 -04:00
|
|
|
#!/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
|
|
|
|
|
|
Erase what the text layer drew: three GUI bugs, one cause
The sink painted glyphs on top of whatever was already on the renderer and
never erased anything, and the host deliberately does not clear the frame so a
DRAW survives. The grid was always correct; the screen kept the previous
frame. That surfaced as three separate-looking faults:
- a backspaced character stayed on screen, including across a line wrap
- a scroll left the old rows behind, "papered over with garbage"
- the editing cursor smeared an underscore along every column it passed
through, which read as an underline under the text being typed
render() now fills each row it is about to draw, and each row that has emptied
since it last drew, with the background first. Row by row rather than one
clear over the whole area: the text layer is authoritative over the rows it
occupies and leaves every other pixel alone, so a picture behind the text
loses only the strips the text uses instead of all of it.
echo_line() also truncates rows that hold nothing but the spaces its own erase
pass wrote, which is what backspacing back across a wrap leaves behind. A row
of spaces is text as far as render() is concerned, so it would have been
repainted forever and kept erasing whatever was under it.
The cursor glyph is removed outright, as asked. The smearing was the erase bug
rather than the cursor, so one could come back and behave -- a block would
read better than an underscore.
Four pixel-level tests, because grid assertions could never have caught any of
this. Each was checked by reverting the fix and watching it fail; two of them
were vacuous when first written and are noted as such where they are fixed.
The real-keyboard test also now rubs out a character and scrolls forty lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
|
|
|
# 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}"
|
|
|
|
|
|
2026-07-31 14:35:07 -04:00
|
|
|
type_line 'QUIT'
|
|
|
|
|
await "" 1 >/dev/null 2>&1 || true
|
|
|
|
|
|
Erase what the text layer drew: three GUI bugs, one cause
The sink painted glyphs on top of whatever was already on the renderer and
never erased anything, and the host deliberately does not clear the frame so a
DRAW survives. The grid was always correct; the screen kept the previous
frame. That surfaced as three separate-looking faults:
- a backspaced character stayed on screen, including across a line wrap
- a scroll left the old rows behind, "papered over with garbage"
- the editing cursor smeared an underscore along every column it passed
through, which read as an underline under the text being typed
render() now fills each row it is about to draw, and each row that has emptied
since it last drew, with the background first. Row by row rather than one
clear over the whole area: the text layer is authoritative over the rows it
occupies and leaves every other pixel alone, so a picture behind the text
loses only the strips the text uses instead of all of it.
echo_line() also truncates rows that hold nothing but the spaces its own erase
pass wrote, which is what backspacing back across a wrap leaves behind. A row
of spaces is text as far as render() is concerned, so it would have been
repainted forever and kept erasing whatever was under it.
The cursor glyph is removed outright, as asked. The smearing was the erase bug
rather than the cursor, so one could come back and behave -- a block would
read better than an underscore.
Four pixel-level tests, because grid assertions could never have caught any of
this. Each was checked by reverting the fix and watching it fail; two of them
were vacuous when first written and are noted as such where they are fixed.
The real-keyboard test also now rubs out a character and scrolls forty lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
|
|
|
echo "PASS: typed at a real focused window; backspace, scrolling and RUN all worked"
|
2026-07-31 14:35:07 -04:00
|
|
|
exit 0
|