aboutsummaryrefslogtreecommitdiffstats
path: root/tests/live_input.py
blob: 6e9c61aa82362f13e547f5d49ece4c078cc3fe1d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3
"""The interactive client's input path, which headless cannot cover.

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 fcntl
import os
import pty
import select
import signal
import struct
import sys
import termios
import time
import uuid

BIN = os.environ.get(
    "SLOSH_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "slosh")
)
INNER = ["--", "/bin/sh", "-c", "stty raw -echo; cat -v"]

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 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)
        if r:
            try:
                chunk = os.read(fd, 65536)
            except OSError:
                break
            if not chunk:
                break
            buf += chunk
            last = time.time()
        elif time.time() - last > idle:
            break
    return buf


def main():
    session = "test-" + uuid.uuid4().hex[:8]
    pid, fd = pty.fork()
    if pid == 0:
        os.environ["TERM"] = "xterm-ghostty"
        os.environ["SLOSH_CONFIG"] = "/nonexistent/slosh.kdl"
        os.execv(BIN, [BIN, "-s", session] + INNER)
        os._exit(127)
    fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 10, 80, 0, 0))
    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"\x01g")  # prefix + a key that is bound to nothing
    out = drain(fd)
    check("prefix + unbound key is swallowed", out == b"", repr(out[-60:]))

    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)
    try:
        os.kill(pid, signal.SIGKILL)
        os.waitpid(pid, 0)
    except (ProcessLookupError, ChildProcessError):
        pass

    print(f"\n{'FAILED' if fails else 'all green'} ({fails} failures)")
    return 1 if fails else 0


if __name__ == "__main__":
    sys.exit(main())