Skip to content

Commit 95f0d33

Browse files
committed
Add some tests
1 parent 301f361 commit 95f0d33

File tree

2 files changed

+135
-1
lines changed

2 files changed

+135
-1
lines changed

src/trio/_repl.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ def terminal_newline() -> None:
2727
import termios
2828

2929
# Fake up a newline char as if user had typed it at the terminal
30-
fcntl.ioctl(sys.stdin, termios.TIOCSTI, b"\n")
30+
# on a best-effort basis
31+
with contextlib.suppress(OSError):
32+
fcntl.ioctl(sys.stdin, termios.TIOCSTI, b"\n")
3133

3234

3335
@final

src/trio/_tests/test_repl.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from __future__ import annotations
22

3+
import os
4+
import pty
5+
import signal
36
import subprocess
47
import sys
8+
from functools import partial
59
from typing import Protocol
610

711
import pytest
@@ -239,3 +243,131 @@ def test_main_entrypoint() -> None:
239243
"""
240244
repl = subprocess.run([sys.executable, "-m", "trio"], input=b"exit()")
241245
assert repl.returncode == 0
246+
247+
248+
@pytest.mark.skipif(sys.platform == "win32", reason="uses PTYs")
249+
def test_ki_newline_injection() -> None:
250+
# NOTE: this cannot be subprocess.Popen because pty.fork
251+
# does some magic to set the controlling terminal.
252+
# (which I don't know how to replicate... so I copied this
253+
# structure from pty.spawn...)
254+
pid, pty_fd = pty.fork()
255+
if pid == 0:
256+
os.execlp(sys.executable, *[sys.executable, "-u", "-m", "trio"])
257+
258+
# setup:
259+
buffer = b""
260+
while not buffer.endswith(b"import trio\r\n>>> "):
261+
buffer += os.read(pty_fd, 4096)
262+
263+
# sanity check:
264+
print(buffer.decode())
265+
buffer = b""
266+
os.write(pty_fd, b'print("hello!")\n')
267+
while not buffer.endswith(b">>> "):
268+
buffer += os.read(pty_fd, 4096)
269+
270+
assert buffer.count(b"hello!") == 2
271+
272+
# press ctrl+c
273+
print(buffer.decode())
274+
buffer = b""
275+
os.kill(pid, signal.SIGINT)
276+
while not buffer.endswith(b">>> "):
277+
buffer += os.read(pty_fd, 4096)
278+
279+
assert b"KeyboardInterrupt" in buffer
280+
281+
# press ctrl+c later
282+
print(buffer.decode())
283+
buffer = b""
284+
os.write(pty_fd, b'print("hello!")')
285+
os.kill(pid, signal.SIGINT)
286+
while not buffer.endswith(b">>> "):
287+
buffer += os.read(pty_fd, 4096)
288+
289+
assert b"KeyboardInterrupt" in buffer
290+
print(buffer.decode())
291+
os.close(pty_fd)
292+
os.waitpid(pid, 0)[1]
293+
294+
295+
async def test_ki_in_repl() -> None:
296+
async with trio.open_nursery() as nursery:
297+
proc = await nursery.start(
298+
partial(
299+
trio.run_process,
300+
[sys.executable, "-u", "-m", "trio"],
301+
stdout=subprocess.PIPE,
302+
stderr=subprocess.STDOUT,
303+
stdin=subprocess.PIPE,
304+
)
305+
)
306+
307+
async with proc.stdout:
308+
# setup
309+
buffer = b""
310+
async for part in proc.stdout:
311+
buffer += part
312+
if buffer.endswith(b"import trio\n>>> "):
313+
break
314+
315+
# ensure things work
316+
buffer = b""
317+
await proc.stdin.send_all(b'print("hello!")\n')
318+
async for part in proc.stdout:
319+
buffer += part
320+
if buffer.endswith(b">>> "):
321+
break
322+
323+
assert b"hello!" in buffer
324+
325+
# ensure that ctrl+c on a prompt works
326+
os.kill(proc.pid, signal.SIGINT)
327+
if sys.platform != "win32":
328+
# we test injection separately
329+
await proc.stdin.send_all(b"\n")
330+
331+
buffer = b""
332+
async for part in proc.stdout:
333+
buffer += part
334+
if buffer.endswith(b">>> "):
335+
break
336+
337+
assert b"KeyboardInterrupt" in buffer
338+
339+
# ensure ctrl+c while a command runs works
340+
await proc.stdin.send_all(b'print("READY"); await trio.sleep_forever()\n')
341+
killed = False
342+
buffer = b""
343+
async for part in proc.stdout:
344+
buffer += part
345+
if buffer.endswith(b"READY\n") and not killed:
346+
os.kill(proc.pid, signal.SIGINT)
347+
killed = True
348+
if buffer.endswith(b">>> "):
349+
break
350+
351+
assert b"trio" in buffer
352+
assert b"KeyboardInterrupt" in buffer
353+
354+
# make sure it works for sync commands too
355+
# (though this would be hard to break)
356+
await proc.stdin.send_all(
357+
b'import time; print("READY"); time.sleep(99999)\n'
358+
)
359+
killed = False
360+
buffer = b""
361+
async for part in proc.stdout:
362+
buffer += part
363+
if buffer.endswith(b"READY\n") and not killed:
364+
os.kill(proc.pid, signal.SIGINT)
365+
killed = True
366+
if buffer.endswith(b">>> "):
367+
break
368+
369+
assert b"Traceback" in buffer
370+
assert b"KeyboardInterrupt" in buffer
371+
372+
# kill the process
373+
nursery.cancel_scope.cancel()

0 commit comments

Comments
 (0)