Files
TerminalBasic/tests/support/ide-execution-pty.py

120 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Headless Unix PTY smoke: real terminal handoff, child Ctrl+C and IDE resume.
Run after cargo build -p tb-ide: python3 tests/support/ide-execution-pty.py
"""
import argparse
import fcntl
import os
from pathlib import Path
import select
import signal
import struct
import subprocess
import tempfile
import termios
import time
def exercise(binary, transcript_dir=None, abort=False, file_shell=False):
with tempfile.TemporaryDirectory(prefix='tb-pty-') as directory:
source = Path(directory) / 'main.bas'
command = "printf '\\nWAITING_CHILD\\n'; exec sleep 30" if abort else "printf '\\nSHELL_PROOF\\n'; exit 7"
source.write_text(f'PRINT SHELL("{command}")\nSTOP\nPRINT "CONTINUED"\nEND\n')
master, slave = os.openpty()
original = termios.tcgetattr(slave)
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))
pid = os.fork()
if pid == 0:
os.setsid()
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
for fd in (0, 1, 2):
os.dup2(slave, fd)
os.close(master)
os.close(slave)
os.environ.setdefault('TERM', 'xterm-256color')
os.environ['XDG_CONFIG_HOME'] = directory
# Keep the controlling terminal alive while checking restoration.
signal.signal(signal.SIGINT, lambda *_: None)
result = subprocess.run([str(binary), str(source)], check=False)
restored = termios.tcgetattr(0) == original
os._exit(result.returncode if restored else 99)
transcript = bytearray()
reaped = False
def until(marker, start=0):
deadline = time.monotonic() + 10
while marker not in transcript[start:]:
assert time.monotonic() < deadline, (marker, bytes(transcript[-2000:]))
if select.select([master], [], [], .1)[0]:
transcript.extend(os.read(master, 65536))
try:
until(b'?1049h')
start = len(transcript)
if file_shell:
os.write(master, b'\x1bfh') # File -> Shell
until(b'?1049l', start)
os.write(master, b"printf '\\nFILE_SHELL_PROOF\\n'; exit\n")
until(b'\r\nFILE_SHELL_PROOF\r\n', start)
until(b'?1049h', start)
else:
os.write(master, b'\x1b[15~') # F5
until(b'\r\nWAITING_CHILD\r\n' if abort else b'\r\nSHELL_PROOF\r\n', start)
if abort:
os.write(master, b'\x03') # cooked terminal SIGINT to child + IDE
until(b'Paused', start)
start = len(transcript)
os.write(master, b'\x1b[15~')
until(b'Ended', start)
start = len(transcript)
os.write(master, b'\x1b[1;3S') # Alt+F4
until(b'Ungespeicherte', start) # standalone BAS has an unsaved project manifest
os.write(master, b'\x1b[C\x1b[C\r') # discard temporary project
deadline = time.monotonic() + 10
while True:
child, status = os.waitpid(pid, os.WNOHANG)
if child:
reaped = True
assert os.waitstatus_to_exitcode(status) == 0
break
assert time.monotonic() < deadline, bytes(transcript[-1000:])
if select.select([master], [], [], .05)[0]:
transcript.extend(os.read(master, 65536))
assert transcript.count(b'?1049h') >= 2
assert transcript.count(b'?1049l') >= 2
# Tatsächliche Ausgabe muss auch bei NO_COLOR/fehlender Erkennung
# dieselben absoluten IDE-Farben enthalten, ohne Palettenänderung.
assert b'38;2;170;170;170' in transcript
assert b'48;2;0;0;170' in transcript
assert b'48;2;170;0;170' in transcript
assert b'38;5;' not in transcript and b'48;5;' not in transcript
assert b'\x1b]' not in transcript
finally:
if transcript_dir:
name = 'file-shell' if file_shell else 'shell-abort' if abort else 'shell-exit'
(transcript_dir / (name + '.ansi')).write_bytes(transcript)
if not reaped:
os.killpg(pid, signal.SIGKILL)
os.waitpid(pid, 0)
os.close(master)
os.close(slave)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=Path, default=Path(__file__).resolve().parents[2] / 'target/debug/tb')
parser.add_argument('--transcript-dir', type=Path)
args = parser.parse_args()
binary = args.binary.resolve(strict=True)
if args.transcript_dir:
args.transcript_dir.mkdir(parents=True, exist_ok=True)
for kwargs in ({}, {'abort': True}, {'file_shell': True}):
exercise(binary, args.transcript_dir, **kwargs)
print('PASS', kwargs or {'shell_exit': 7})
if __name__ == '__main__':
main()