diff options
| author | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:26:02 +0200 |
|---|---|---|
| committer | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:26:02 +0200 |
| commit | 34fcd16e59e3087e9bf84ebce4e05d439eea9198 (patch) | |
| tree | e07b9dfc235e3a0b67047d59a9c521c6e98a28ae /tests/live_input.py | |
| parent | sl0ptty: input decoder + per-pane re-encode (diff) | |
| download | slosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.tar.gz slosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.tar.xz slosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.zip | |
sl0ptty: M0.5 — the headless driver and the harness everything rests on
--script runs the whole compositor with no tty: commands in (send, raw,
settle, resize, snapshot, alive), composited screen out as JSON — rows of
text, style runs, cursor, and the hit list.
Styles are emitted as runs rather than per-cell objects, because a test wants
'columns 0..5 of row 0 are bold red', not 1920 objects.
The hit list is empty until M1 paints something clickable, but the type, the
JSON, and hit_test() exist now: one geometry, established before there is
anything to get wrong.
Also fixes two host-policy defaults found by the cursor never appearing.
lib-vt is a VT library, not a terminal, so it starts with DECTCEM (mode 25)
off and grapheme clustering (2027) off. Both are now set via MODE_DEFAULT, so
they survive RIS.
tests/harness.py + tests/test_screen.py: 19 checks over composition, styling,
wide chars/clusters, scrollback, resize and the full input round trip —
including the kitty/legacy re-encoding matrix, which no longer needs a pty.
make test is now 2.4s for 59 checks; make test-live keeps the four that
genuinely need a tty.
Diffstat (limited to 'tests/live_input.py')
| -rw-r--r-- | tests/live_input.py | 109 |
1 files changed, 28 insertions, 81 deletions
diff --git a/tests/live_input.py b/tests/live_input.py index 30dd12b..31dade8 100644 --- a/tests/live_input.py +++ b/tests/live_input.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 -"""Live check of decode -> re-encode. +"""The interactive client's input path, which headless cannot cover. -The pane runs `cat -v`, which prints control bytes visibly, so whatever the -inner app *actually receives* ends up on our screen. That is the only honest -way to test that we re-encode for the pane rather than forwarding bytes. +The encoding matrix (kitty/legacy/mouse/paste, per pane) lives in +tests/test_screen.py, where it runs deterministically through the headless +driver. What is left here is the part that only exists with a real tty: raw +mode, reading stdin, and the prefix key. """ import os, pty, select, struct, sys, termios, time, fcntl, signal BIN = os.path.join(os.path.dirname(__file__), "..", "build", "sl0ptty") INNER = ["--", "/bin/sh", "-c", "stty raw -echo; cat -v"] +fails = 0 -def spawn(cols=80, rows=10, inner=None): - pid, fd = pty.fork() - if pid == 0: - os.environ["TERM"] = "xterm-ghostty" - os.execv(BIN, [BIN] + (inner or INNER)) - os._exit(127) - fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0)) - return pid, fd +def check(name, cond, detail=""): + global fails + if not cond: + fails += 1 + print(f"{'ok ' if cond else 'FAIL'} {name}{'' if cond else ' <- ' + detail}") -def drain(fd, idle=0.3, limit=3.0): + +def drain(fd, idle=0.25, limit=2.0): buf, last, start = b"", time.time(), time.time() while time.time() - start < limit: r, _, _ = select.select([fd], [], [], 0.05) @@ -39,79 +39,26 @@ def drain(fd, idle=0.3, limit=3.0): return buf -fails = 0 - - -def check(name, cond, detail=""): - global fails - if not cond: - fails += 1 - print(f"{'ok ' if cond else 'FAIL'} {name}{'' if cond else ' <- ' + detail}") - - def main(): - pid, fd = spawn() + pid, fd = pty.fork() + if pid == 0: + os.environ["TERM"] = "xterm-ghostty" + os.execv(BIN, [BIN] + INNER) + os._exit(127) + fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 10, 80, 0, 0)) drain(fd) - cases = [ - # name, what the outer terminal sends us, what the pane must receive - ("legacy ctrl-up survives", b"\x1b[1;5A", "^[[1;5A"), - # ctrl-a is the prefix, so ctrl-b is the honest test of a kitty ctrl key - ("kitty ctrl-b -> legacy ^B", b"\x1b[98;5u", "^B"), - ("kitty plain 'z' -> 'z'", b"\x1b[122u", "z"), - # each case must leave the prefix state clean for the next one - ("prefix + unbound key is swallowed", b"\x01x", ""), - ("C-a C-a sends a literal ^A", b"\x01\x01", "^A"), - ("kitty enter -> \\r", b"\x1b[13u", "^M"), - ("alt-b -> ESC b", b"\x1bb", "^[b"), - ("utf8 passes through", "é".encode(), "M-CM-)"), - ("paste is unwrapped for a plain app", b"\x1b[200~xy\x1b[201~", "xy"), - ] - - for name, send, want in cases: - os.write(fd, send) - out = drain(fd, idle=0.25, limit=1.5) - text = out.decode("utf-8", "replace") - if want == "": - # nothing may reach the pane: no repaint at all - check(name, out == b"", repr(text[-80:])) - else: - check(name, want in text, repr(text[-80:])) - - # A key release must not reach an app that never asked for one. - os.write(fd, b"\x1b[97;1:3u") - out = drain(fd, idle=0.25, limit=1.0).decode("utf-8", "replace") - check("key release is not sent to a legacy app", "a" not in out.replace("\x1b", ""), - repr(out[-80:])) - - # Mouse events must not leak into an app with no mouse tracking on. - os.write(fd, b"\x1b[<0;5;5M\x1b[<0;5;5m") - out = drain(fd, idle=0.25, limit=1.0).decode("utf-8", "replace") - check("mouse does not leak into a non-mouse app", "[<" not in out, repr(out[-80:])) - - os.write(fd, b"\x01q") - drain(fd, idle=0.2, limit=1.0) - try: - os.kill(pid, signal.SIGKILL) - os.waitpid(pid, 0) - except (ProcessLookupError, ChildProcessError): - pass - - # --- the other direction: a pane that negotiates modern protocols ----- - # Same keys, same client; only the pane's own state differs. Passing here - # *and* above means we encode per pane rather than forwarding bytes. - inner = r'stty raw -echo; printf "\033[>1u\033[?1000h\033[?1006h"; cat -v' - pid, fd = spawn(inner=["--", "/bin/sh", "-c", inner]) - drain(fd) + os.write(fd, b"\x1b[1;5A") + out = drain(fd).decode("utf-8", "replace") + check("a key typed at a real tty reaches the pane", "^[[1;5A" in out, repr(out[-60:])) - os.write(fd, b"\x02") # legacy ctrl-b in - out = drain(fd, idle=0.3, limit=2.0).decode("utf-8", "replace") - check("ctrl-b -> kitty CSI u for a kitty pane", "^[[98;5u" in out, repr(out[-80:])) + os.write(fd, b"\x01x") # prefix + unbound + out = drain(fd) + check("prefix + unbound key is swallowed", out == b"", repr(out[-60:])) - os.write(fd, b"\x1b[<0;7;3M") # SGR mouse press in - out = drain(fd, idle=0.3, limit=2.0).decode("utf-8", "replace") - check("mouse reaches a pane that asked for it", "^[[<0;7;3M" in out, - repr(out[-80:])) + os.write(fd, b"\x01\x01") # prefix + prefix = literal + out = drain(fd).decode("utf-8", "replace") + check("C-a C-a sends a literal ^A", "^A" in out, repr(out[-60:])) os.write(fd, b"\x01q") drain(fd, idle=0.2, limit=1.0) |
