Files
MeshCore/test/test_hil_linux_web_serial.py
T

181 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Actual Linux pseudoterminal tests; never opens a physical radio."""
import copy
import errno
import importlib.util
import os
from pathlib import Path
import pty
import select
import termios
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"hil_linux_web_serial", ROOT / "tools/hil/linux_web_serial.py")
HIL = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(HIL)
class NativeTTYTests(unittest.TestCase):
def setUp(self):
self.master, slave = pty.openpty()
self.path = os.ttyname(slave)
self.assertTrue(self.path.startswith("/dev/pts/"))
attrs = termios.tcgetattr(slave)
attrs[3] &= ~(termios.ICANON | termios.ECHO | termios.ISIG)
attrs[1] &= ~termios.OPOST
attrs[6][termios.VMIN] = 0
attrs[6][termios.VTIME] = 0
termios.tcsetattr(slave, termios.TCSANOW, attrs)
self.original = termios.tcgetattr(slave)
os.close(slave)
self.addCleanup(os.close, self.master)
def attributes(self):
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
try:
return termios.tcgetattr(fd)
finally:
os.close(fd)
def test_inherited_zero_read_is_fixed_and_exact_attributes_restored(self):
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
self.assertEqual(os.read(fd, 16), b"")
os.close(fd)
with HIL.prepared_linux_web_serial(self.path) as evidence:
self.assertEqual(evidence["vmin_before"], 0)
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
try:
expected = copy.deepcopy(self.original)
expected[6][termios.VMIN] = 1
self.assertEqual(termios.tcgetattr(fd), expected)
with self.assertRaises(BlockingIOError) as raised:
os.read(fd, 16)
self.assertEqual(raised.exception.errno, errno.EAGAIN)
os.write(self.master, b"x")
readable, _, _ = select.select([fd], [], [], 1.0)
self.assertEqual(readable, [fd])
self.assertEqual(os.read(fd, 16), b"x")
finally:
os.close(fd)
self.assertEqual(self.attributes(), self.original)
def test_test_failure_restores_all_attributes(self):
with self.assertRaisesRegex(RuntimeError, "browser failed"):
with HIL.prepared_linux_web_serial(self.path):
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
try:
attrs = termios.tcgetattr(fd)
attrs[0] ^= termios.IXOFF
attrs[6][termios.VTIME] = 7
termios.tcsetattr(fd, termios.TCSANOW, attrs)
finally:
os.close(fd)
raise RuntimeError("browser failed")
self.assertEqual(self.attributes(), self.original)
def test_interrupted_test_restores_attributes(self):
with self.assertRaises(KeyboardInterrupt):
with HIL.prepared_linux_web_serial(self.path):
raise KeyboardInterrupt()
self.assertEqual(self.attributes(), self.original)
def test_failed_preparation_readback_restores_before_yield(self):
original_apply = HIL._apply_and_check
def fail_after_normalization(fd, attrs):
original_apply(fd, attrs)
if attrs[6][termios.VMIN] == 1:
raise RuntimeError("simulated preparation readback failure")
with mock.patch.object(HIL, "_apply_and_check", side_effect=fail_after_normalization):
with self.assertRaisesRegex(RuntimeError, "preparation readback failure"):
with HIL.prepared_linux_web_serial(self.path):
self.fail("failed preflight must not yield")
self.assertEqual(self.attributes(), self.original)
def test_ignored_write_is_rejected_by_actual_readback(self):
with mock.patch.object(termios, "tcsetattr") as setter:
with self.assertRaisesRegex(RuntimeError, "readback differs"):
with HIL.prepared_linux_web_serial(self.path):
self.fail("ignored normalization must not yield")
self.assertEqual(setter.call_count, 1)
self.assertEqual(self.attributes(), self.original)
def test_already_normalized_tty_needs_no_attribute_write(self):
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
attrs = termios.tcgetattr(fd)
attrs[6][termios.VMIN] = 1
termios.tcsetattr(fd, termios.TCSANOW, attrs)
os.close(fd)
with mock.patch.object(termios, "tcsetattr", wraps=termios.tcsetattr) as setter:
with HIL.prepared_linux_web_serial(self.path):
pass
setter.assert_not_called()
self.assertEqual(self.attributes(), attrs)
def test_canonical_attribute_representation_is_preserved(self):
fd = os.open(self.path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
attrs = termios.tcgetattr(fd)
attrs[3] |= termios.ICANON
termios.tcsetattr(fd, termios.TCSANOW, attrs)
original = termios.tcgetattr(fd)
os.close(fd)
with HIL.prepared_linux_web_serial(self.path) as evidence:
self.assertEqual(evidence["vmin_before"], 0)
expected = copy.deepcopy(original)
expected[6][termios.VMIN] = b"\x01"
self.assertEqual(self.attributes(), expected)
self.assertEqual(self.attributes(), original)
def test_no_second_descriptor_is_held_during_the_trial(self):
opened = []
original_open = HIL._open_tty
def capture(path):
fd = original_open(path)
opened.append(fd)
return fd
with mock.patch.object(HIL, "_open_tty", side_effect=capture):
with HIL.prepared_linux_web_serial(self.path):
with self.assertRaises(OSError):
os.fstat(opened[0])
self.assertEqual(self.attributes(), self.original)
def test_replaced_endpoint_is_not_modified(self):
other_master, other_slave = pty.openpty()
other_path = os.ttyname(other_slave)
self.addCleanup(os.close, other_master)
self.addCleanup(os.close, other_slave)
attrs = termios.tcgetattr(other_slave)
original_open = HIL._open_tty
calls = 0
def substitute_restore(path):
nonlocal calls
calls += 1
return original_open(path if calls == 1 else other_path)
with mock.patch.object(HIL, "_open_tty", side_effect=substitute_restore):
with self.assertRaisesRegex(RuntimeError, "endpoint changed"):
with HIL.prepared_linux_web_serial(self.path):
pass
self.assertEqual(termios.tcgetattr(other_slave), attrs)
def test_explicit_absolute_tty_required(self):
with self.assertRaisesRegex(ValueError, "absolute TTY"):
with HIL.prepared_linux_web_serial("ttyACM0"):
self.fail("relative device must not be opened")
with self.assertRaisesRegex(ValueError, "must identify a TTY"):
with HIL.prepared_linux_web_serial("/dev/null"):
self.fail("non-TTY must not be yielded")
if __name__ == "__main__":
unittest.main()