| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- #!/usr/bin/env python3
- """Shared pty helpers for driving tpshell non-interactively.
- The shell is a full-screen curses-style program: it switches the tty to raw
- mode and reads one key byte at a time (Term.GetCh), so a test has to drive it
- through a real pty, one key at a time, and wait for silence between keys.
- Provides:
- drain/send - read-until-quiet and "write keys, then settle"
- visible() - strip ANSI escapes from captured output
- Screen - minimal VT100 emulator (CSI 2J / H / r;cH / nD / m)
- so assertions can run against the rendered screen
- """
- import fcntl
- import os
- import pty
- import select
- import struct
- import termios
- import time
- ROWS, COLS = 25, 80
- SHELL_DIR = "/home/eric/Projets/Projets-Modula2/MyWork/TP3-comp/shell"
- ESC = b"\x1b"
- CTRL_K = b"\x0b"
- def set_winsize(fd, rows=ROWS, cols=COLS):
- fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
- def drain(fd, quiet=0.45, total=8.0):
- """Read until the child has been silent for `quiet` seconds."""
- out = b""
- deadline = time.time() + total
- while time.time() < deadline:
- r, _, _ = select.select([fd], [], [], 0.05)
- if r:
- try:
- d = os.read(fd, 65536)
- except OSError:
- break
- if not d:
- break
- out += d
- deadline = time.time() + quiet
- return out
- def send(fd, data, quiet=0.45):
- """Write keys, then wait for the child to settle."""
- os.write(fd, data)
- return drain(fd, quiet)
- def spawn(exe="./tpshell", cwd=SHELL_DIR):
- """Fork a pty running tpshell. Returns (pid, fd)."""
- os.chdir(cwd)
- pid, fd = pty.fork()
- if pid == 0:
- os.environ["TERM"] = "xterm"
- os.execv(exe, [exe])
- os._exit(127)
- set_winsize(fd)
- return pid, fd
- def reap(fd, pid):
- try:
- os.close(fd)
- except OSError:
- pass
- _, status = os.waitpid(pid, 0)
- return status
- def status_str(status):
- if os.WIFSIGNALED(status):
- sig = os.WTERMSIG(status)
- names = {6: "SIGABRT", 11: "SIGSEGV", 15: "SIGTERM", 9: "SIGKILL"}
- return "SIGNAL %d (%s)" % (sig, names.get(sig, "?"))
- if os.WIFEXITED(status):
- return "EXIT %d" % os.WEXITSTATUS(status)
- return "raw %d" % status
- def visible(b):
- """Strip ANSI escapes from captured output (latin-1 safe)."""
- out, i = [], 0
- while i < len(b):
- if b[i] == 0x1B:
- j = i + 1
- if j < len(b) and b[j] == ord("["):
- j += 1
- while j < len(b) and b[j] not in b"@ABCDEFGHJKSTfmnsulh":
- j += 1
- i = j + 1
- continue
- i += 2
- continue
- out.append(b[i : i + 1])
- i += 1
- return b"".join(out).decode("latin-1")
- class Screen:
- """Enough VT100 to render what Term.mod emits."""
- def __init__(self, rows=ROWS, cols=COLS):
- self.rows, self.cols = rows, cols
- self.r = self.c = 0
- self.clear()
- def clear(self):
- self.buf = [[" "] * self.cols for _ in range(self.rows)]
- def put(self, ch):
- if ch == "\n":
- self.r = min(self.r + 1, self.rows - 1)
- return
- if ch == "\r":
- self.c = 0
- return
- if 0 <= self.r < self.rows and 0 <= self.c < self.cols:
- self.buf[self.r][self.c] = ch
- self.c += 1
- if self.c >= self.cols:
- self.c = 0
- self.r = min(self.r + 1, self.rows - 1)
- def feed(self, data):
- if isinstance(data, bytes):
- data = data.decode("latin-1")
- i, n = 0, len(data)
- while i < n:
- ch = data[i]
- if ch == "\x1b":
- j = i + 1
- if j < n and data[j] == "[":
- j += 1
- start = j
- while j < n and data[j] not in "@ABCDEFGHJKSTfmnsulh":
- j += 1
- if j >= n:
- break
- params, final = data[start:j], data[j]
- if final == "H":
- p = params.split(";")
- self.r = (int(p[0]) if len(p) > 0 and p[0] else 1) - 1
- self.c = (int(p[1]) if len(p) > 1 and p[1] else 1) - 1
- elif final == "J":
- if params == "2":
- self.clear()
- elif final == "D":
- self.c = max(0, self.c - (int(params) if params else 1))
- i = j + 1
- continue
- i += 2
- continue
- self.put(ch)
- i += 1
- def text(self):
- return "\n".join("".join(row).rstrip() for row in self.buf)
- def row_with(self, needle):
- for idx, row in enumerate(self.buf):
- if needle in "".join(row):
- return idx
- return None
|