ptyharness.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env python3
  2. """Shared pty helpers for driving tpshell non-interactively.
  3. The shell is a full-screen curses-style program: it switches the tty to raw
  4. mode and reads one key byte at a time (Term.GetCh), so a test has to drive it
  5. through a real pty, one key at a time, and wait for silence between keys.
  6. Provides:
  7. drain/send - read-until-quiet and "write keys, then settle"
  8. visible() - strip ANSI escapes from captured output
  9. Screen - minimal VT100 emulator (CSI 2J / H / r;cH / nD / m)
  10. so assertions can run against the rendered screen
  11. """
  12. import fcntl
  13. import os
  14. import pty
  15. import select
  16. import struct
  17. import termios
  18. import time
  19. ROWS, COLS = 25, 80
  20. SHELL_DIR = "/home/eric/Projets/Projets-Modula2/MyWork/TP3-comp/shell"
  21. ESC = b"\x1b"
  22. CTRL_K = b"\x0b"
  23. def set_winsize(fd, rows=ROWS, cols=COLS):
  24. fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
  25. def drain(fd, quiet=0.45, total=8.0):
  26. """Read until the child has been silent for `quiet` seconds."""
  27. out = b""
  28. deadline = time.time() + total
  29. while time.time() < deadline:
  30. r, _, _ = select.select([fd], [], [], 0.05)
  31. if r:
  32. try:
  33. d = os.read(fd, 65536)
  34. except OSError:
  35. break
  36. if not d:
  37. break
  38. out += d
  39. deadline = time.time() + quiet
  40. return out
  41. def send(fd, data, quiet=0.45):
  42. """Write keys, then wait for the child to settle."""
  43. os.write(fd, data)
  44. return drain(fd, quiet)
  45. def spawn(exe="./tpshell", cwd=SHELL_DIR):
  46. """Fork a pty running tpshell. Returns (pid, fd)."""
  47. os.chdir(cwd)
  48. pid, fd = pty.fork()
  49. if pid == 0:
  50. os.environ["TERM"] = "xterm"
  51. os.execv(exe, [exe])
  52. os._exit(127)
  53. set_winsize(fd)
  54. return pid, fd
  55. def reap(fd, pid):
  56. try:
  57. os.close(fd)
  58. except OSError:
  59. pass
  60. _, status = os.waitpid(pid, 0)
  61. return status
  62. def status_str(status):
  63. if os.WIFSIGNALED(status):
  64. sig = os.WTERMSIG(status)
  65. names = {6: "SIGABRT", 11: "SIGSEGV", 15: "SIGTERM", 9: "SIGKILL"}
  66. return "SIGNAL %d (%s)" % (sig, names.get(sig, "?"))
  67. if os.WIFEXITED(status):
  68. return "EXIT %d" % os.WEXITSTATUS(status)
  69. return "raw %d" % status
  70. def visible(b):
  71. """Strip ANSI escapes from captured output (latin-1 safe)."""
  72. out, i = [], 0
  73. while i < len(b):
  74. if b[i] == 0x1B:
  75. j = i + 1
  76. if j < len(b) and b[j] == ord("["):
  77. j += 1
  78. while j < len(b) and b[j] not in b"@ABCDEFGHJKSTfmnsulh":
  79. j += 1
  80. i = j + 1
  81. continue
  82. i += 2
  83. continue
  84. out.append(b[i : i + 1])
  85. i += 1
  86. return b"".join(out).decode("latin-1")
  87. class Screen:
  88. """Enough VT100 to render what Term.mod emits."""
  89. def __init__(self, rows=ROWS, cols=COLS):
  90. self.rows, self.cols = rows, cols
  91. self.r = self.c = 0
  92. self.clear()
  93. def clear(self):
  94. self.buf = [[" "] * self.cols for _ in range(self.rows)]
  95. def put(self, ch):
  96. if ch == "\n":
  97. self.r = min(self.r + 1, self.rows - 1)
  98. return
  99. if ch == "\r":
  100. self.c = 0
  101. return
  102. if 0 <= self.r < self.rows and 0 <= self.c < self.cols:
  103. self.buf[self.r][self.c] = ch
  104. self.c += 1
  105. if self.c >= self.cols:
  106. self.c = 0
  107. self.r = min(self.r + 1, self.rows - 1)
  108. def feed(self, data):
  109. if isinstance(data, bytes):
  110. data = data.decode("latin-1")
  111. i, n = 0, len(data)
  112. while i < n:
  113. ch = data[i]
  114. if ch == "\x1b":
  115. j = i + 1
  116. if j < n and data[j] == "[":
  117. j += 1
  118. start = j
  119. while j < n and data[j] not in "@ABCDEFGHJKSTfmnsulh":
  120. j += 1
  121. if j >= n:
  122. break
  123. params, final = data[start:j], data[j]
  124. if final == "H":
  125. p = params.split(";")
  126. self.r = (int(p[0]) if len(p) > 0 and p[0] else 1) - 1
  127. self.c = (int(p[1]) if len(p) > 1 and p[1] else 1) - 1
  128. elif final == "J":
  129. if params == "2":
  130. self.clear()
  131. elif final == "D":
  132. self.c = max(0, self.c - (int(params) if params else 1))
  133. i = j + 1
  134. continue
  135. i += 2
  136. continue
  137. self.put(ch)
  138. i += 1
  139. def text(self):
  140. return "\n".join("".join(row).rstrip() for row in self.buf)
  141. def row_with(self, needle):
  142. for idx, row in enumerate(self.buf):
  143. if needle in "".join(row):
  144. return idx
  145. return None