Skip to content

Commit c83fccf

Browse files
committed
tests/run-tests: Add itest_ framework for interactive PTY tests.
This adds support for itest_* test files that require CPython to control MicroPython via a PTY with signal generation enabled. The framework handles all PTY setup, terminal configuration (ISIG, controlling terminal), subprocess spawning, and cleanup. Test files contain only test-specific logic and are exec'd with the master PTY fd available in their global scope. This allows testing terminal features like Ctrl-C interrupt handling that require signal generation and cannot be tested via the existing repl_ framework (which sends complete lines and cannot interrupt mid-execution). Signed-off-by: Andrew Leech <[email protected]>
1 parent 3551ee7 commit c83fccf

File tree

1 file changed

+80
-1
lines changed

1 file changed

+80
-1
lines changed

tests/run-tests.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,10 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False):
604604
# special handling for tests of the unix cmdline program
605605
is_special = True
606606

607+
# Interactive tests (itest_*) also need special handling
608+
if os.path.basename(test_file).startswith("itest_"):
609+
is_special = True
610+
607611
if is_special:
608612
# check for any cmdline options needed for this test
609613
args = [MICROPYTHON]
@@ -615,7 +619,82 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False):
615619

616620
# run the test, possibly with redirected input
617621
try:
618-
if os.path.basename(test_file).startswith("repl_"):
622+
if os.path.basename(test_file).startswith("itest_"):
623+
# Interactive test - CPython code that controls MicroPython via PTY
624+
try:
625+
import pty
626+
import termios
627+
import fcntl
628+
except ImportError:
629+
# in case pty/termios module is not available, like on Windows
630+
return b"SKIP\n"
631+
import select
632+
from io import StringIO
633+
634+
# Even though these might have the pty module, it's unlikely to function.
635+
if sys.platform in ["win32", "msys", "cygwin"]:
636+
return b"SKIP\n"
637+
638+
# Set up PTY with ISIG enabled for signal generation
639+
master, slave = pty.openpty()
640+
641+
# Configure terminal attributes to enable Ctrl-C signal generation
642+
attrs = termios.tcgetattr(slave)
643+
attrs[3] |= termios.ISIG # Enable signal generation (ISIG flag)
644+
attrs[6][termios.VINTR] = 3 # Set Ctrl-C (0x03) as interrupt character
645+
termios.tcsetattr(slave, termios.TCSANOW, attrs)
646+
647+
def setup_controlling_terminal():
648+
"""Set up the child process with the PTY as controlling terminal."""
649+
os.setsid() # Create a new session
650+
fcntl.ioctl(0, termios.TIOCSCTTY, 0) # Make PTY the controlling terminal
651+
652+
# Spawn MicroPython with the PTY as its stdin/stdout/stderr
653+
p = subprocess.Popen(
654+
args,
655+
stdin=slave,
656+
stdout=slave,
657+
stderr=subprocess.STDOUT,
658+
bufsize=0,
659+
preexec_fn=setup_controlling_terminal,
660+
)
661+
662+
# Capture stdout while running the test code
663+
old_stdout = sys.stdout
664+
sys.stdout = StringIO()
665+
666+
try:
667+
# Execute test file with 'master' fd available in globals
668+
test_globals = {"master": master}
669+
with open(test_file_abspath, "rb") as f:
670+
exec(compile(f.read(), test_file_abspath, "exec"), test_globals)
671+
output_mupy = sys.stdout.getvalue().encode("utf-8")
672+
except SystemExit:
673+
# Test requested to exit (e.g., after printing SKIP)
674+
output_mupy = sys.stdout.getvalue().encode("utf-8")
675+
except Exception as e:
676+
output_mupy = f"CRASH: {e}\n".encode("utf-8")
677+
finally:
678+
sys.stdout = old_stdout
679+
680+
# Clean up: send Ctrl-D to exit REPL and close PTY
681+
try:
682+
os.write(master, b"\x04") # Ctrl-D to exit
683+
except OSError:
684+
pass # Process may have already exited
685+
686+
try:
687+
p.wait(timeout=1)
688+
except subprocess.TimeoutExpired:
689+
p.kill()
690+
p.wait()
691+
except ProcessLookupError:
692+
pass
693+
694+
os.close(master)
695+
os.close(slave)
696+
697+
elif os.path.basename(test_file).startswith("repl_"):
619698
# Need to use a PTY to test command line editing
620699
try:
621700
import pty

0 commit comments

Comments
 (0)