mirror of
https://github.com/micropython/micropython.git
synced 2026-06-11 00:55:15 +02:00
tests/cmdline: Add Ctrl-C interrupt test for the repl_ test framework.
Adds a "# sigint:" directive for repl_ tests that need Ctrl-C to generate SIGINT via the PTY terminal driver. When present, the child process is set up with the PTY as its controlling terminal (via setsid/TIOCSCTTY/tcsetpgrp) so that \x03 written to the PTY master generates SIGINT for the child's process group. This works because MicroPython's REPL restores original terminal settings (with ISIG enabled) before executing user code, allowing the terminal driver to convert \x03 into SIGINT during blocking operations. Test added: - repl_ctrl_c_interrupt_execution.py: Verifies Ctrl-C interrupts a blocking time.sleep() call and the REPL remains functional afterward. Also wraps PTY fd handling in try/finally for all repl_ tests. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
This commit is contained in:
committed by
Damien George
parent
a876a2aced
commit
30781dce0a
@@ -51,6 +51,7 @@ exclude = [ # Ruff finds Python SyntaxError in these files
|
||||
"tests/cmdline/repl_autoindent.py",
|
||||
"tests/cmdline/repl_basic.py",
|
||||
"tests/cmdline/repl_cont.py",
|
||||
"tests/cmdline/repl_ctrl_c_interrupt_execution.py",
|
||||
"tests/cmdline/repl_emacs_keys.py",
|
||||
"tests/cmdline/repl_paste.py",
|
||||
"tests/cmdline/repl_words_move.py",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# sigint: deliver via controlling terminal
|
||||
# Test that Ctrl-C (SIGINT) interrupts blocking code execution.
|
||||
# MicroPython restores original terminal mode (ISIG on) during
|
||||
# execution, so the PTY terminal driver generates SIGINT from \x03.
|
||||
import time
|
||||
time.sleep(10)
|
||||
{\x03}
|
||||
print('repl still responds')
|
||||
@@ -0,0 +1,15 @@
|
||||
MicroPython \.\+ version
|
||||
Type "help()" for more information.
|
||||
>>> # sigint: deliver via controlling terminal
|
||||
>>> # Test that Ctrl-C (SIGINT) interrupts blocking code execution.
|
||||
>>> # MicroPython restores original terminal mode (ISIG on) during
|
||||
>>> # execution, so the PTY terminal driver generates SIGINT from \x03.
|
||||
>>> import time
|
||||
>>> time.sleep(10)
|
||||
########
|
||||
\.\*Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
KeyboardInterrupt: \$
|
||||
>>> print('repl still responds')
|
||||
repl still responds
|
||||
>>> \$
|
||||
+75
-21
@@ -460,11 +460,16 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False):
|
||||
if is_special:
|
||||
# check for any cmdline options needed for this test
|
||||
cmdlist = [MICROPYTHON]
|
||||
send_sigint = False
|
||||
with open(test_file, "rb") as f:
|
||||
line = f.readline()
|
||||
if line.startswith(b"# cmdline:"):
|
||||
# subprocess.check_output on Windows only accepts strings, not bytes
|
||||
cmdlist += [str(c, "utf-8") for c in line[10:].strip().split()]
|
||||
for line in f:
|
||||
if line.startswith(b"# cmdline:"):
|
||||
# subprocess.check_output on Windows only accepts strings, not bytes
|
||||
cmdlist += [str(c, "utf-8") for c in line[10:].strip().split()]
|
||||
elif line.startswith(b"# sigint:"):
|
||||
send_sigint = True
|
||||
elif not line.startswith(b"#"):
|
||||
break
|
||||
|
||||
# run the test, possibly with redirected input
|
||||
try:
|
||||
@@ -499,27 +504,76 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False):
|
||||
os.write(master, what)
|
||||
return get()
|
||||
|
||||
def send_ctrl_c():
|
||||
# Send \x03 without trailing newline and wait for
|
||||
# the full response (traceback + new prompt).
|
||||
os.write(master, b"\x03")
|
||||
return get(True)
|
||||
|
||||
with open(test_file, "rb") as f:
|
||||
# instead of: output_mupy = subprocess.check_output(cmdlist, stdin=f)
|
||||
master, slave = pty.openpty()
|
||||
p = subprocess.Popen(
|
||||
cmdlist, stdin=slave, stdout=slave, stderr=subprocess.STDOUT, bufsize=0
|
||||
)
|
||||
banner = get(True)
|
||||
output_mupy = banner + b"".join(send_get(line) for line in f)
|
||||
send_get(b"\x04") # exit the REPL, so coverage info is saved
|
||||
# At this point the process might have exited already, but trying to
|
||||
# kill it 'again' normally doesn't result in exceptions as Python and/or
|
||||
# the OS seem to try to handle this nicely. When running Linux on WSL
|
||||
# though, the situation differs and calling Popen.kill after the process
|
||||
# terminated results in a ProcessLookupError. Just catch that one here
|
||||
# since we just want the process to be gone and that's the case.
|
||||
try:
|
||||
p.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
preexec_fn = None
|
||||
use_sigint_kill = False
|
||||
# Tests with "# sigint:" need Ctrl-C (\x03) to
|
||||
# generate SIGINT. MicroPython restores original
|
||||
# terminal mode (ISIG on) during code execution,
|
||||
# so on Linux we set up the PTY as a controlling
|
||||
# terminal for proper signal delivery. On macOS,
|
||||
# setsid/TIOCSCTTY breaks PTY I/O, so we fall
|
||||
# back to os.kill().
|
||||
if send_sigint:
|
||||
if sys.platform == "darwin":
|
||||
use_sigint_kill = True
|
||||
else:
|
||||
import fcntl
|
||||
import termios
|
||||
|
||||
def preexec_fn():
|
||||
os.setsid()
|
||||
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
||||
os.tcsetpgrp(0, os.getpid())
|
||||
|
||||
p = subprocess.Popen(
|
||||
cmdlist,
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=0,
|
||||
preexec_fn=preexec_fn,
|
||||
)
|
||||
banner = get(True)
|
||||
if send_sigint:
|
||||
import signal
|
||||
|
||||
parts = []
|
||||
for line in f:
|
||||
if b"{\\x03}" in line:
|
||||
if use_sigint_kill:
|
||||
os.kill(p.pid, signal.SIGINT)
|
||||
parts.append(get(True))
|
||||
else:
|
||||
parts.append(send_ctrl_c())
|
||||
else:
|
||||
parts.append(send_get(line))
|
||||
output_mupy = banner + b"".join(parts)
|
||||
else:
|
||||
output_mupy = banner + b"".join(send_get(line) for line in f)
|
||||
send_get(b"\x04") # exit the REPL, so coverage info is saved
|
||||
# At this point the process might have exited already, but trying to
|
||||
# kill it 'again' normally doesn't result in exceptions as Python and/or
|
||||
# the OS seem to try to handle this nicely. When running Linux on WSL
|
||||
# though, the situation differs and calling Popen.kill after the process
|
||||
# terminated results in a ProcessLookupError. Just catch that one here
|
||||
# since we just want the process to be gone and that's the case.
|
||||
try:
|
||||
p.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
finally:
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
else:
|
||||
output_mupy = subprocess.check_output(
|
||||
cmdlist + [test_file], stderr=subprocess.STDOUT
|
||||
|
||||
Reference in New Issue
Block a user