diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/serial_raw_uart_test.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/serial_raw_uart_test.py new file mode 100755 index 0000000000..c01c9e3aec --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/serial_raw_uart_test.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +"""List and test raw UART self-loopback devices for CE OEM serial jobs.""" + +import argparse +import fcntl +import glob +import os +import re +import struct +import time + +import serial + +TIOCGSERIAL = 0x541E +SERIAL_STRUCT_FORMAT = "iiiiiiiiiiiHHi" +RAW_UART_CONFIG_FORMAT = "NAME:TARGET:BAUD" +BAUD_MODES = ("max", "sweep", "autoscan-max") +PING_PATTERN = b"PING" +USB_SERIAL_PREFIXES = ("ttyUSB", "ttyACM") +STANDARD_RATES = [ + 9600, + 19200, + 38400, + 57600, + 115200, + 230400, + 460800, + 500000, + 576000, + 921600, + 1000000, + 1152000, + 1500000, + 2000000, + 2500000, + 3000000, + 3500000, + 4000000, +] +DRIVER_DEV_PREFIX = { + "serial": "ttyS", + "fsl-lpuart": "ttyLP", + "IMX-uart": "ttymxc", + "max310x": "ttyMAX", + "ttyAMA": "ttyAMA", +} + + +class RawUartPort: + """A configured raw UART self-loopback target.""" + + def __init__(self, name, target, baud): + self.name = name + self.target = target + self.baud = baud + self.target_type = "node" if target.startswith("/dev/") else "addr" + + @property + def name_id(self): + """Return the configured name normalized for generated job IDs.""" + + return sanitize_id(self.name) + + @property + def target_id(self): + """Return the configured target normalized for generated job IDs.""" + + return sanitize_id(self.target) + + +class UartDevice: + """Resolved raw UART device metadata.""" + + def __init__(self, target, dev_node, baud_base, is_console): + self.target = target + self.dev_node = dev_node + self.baud_base = baud_base + self.is_console = is_console + + +class LoopbackResult: + """Result of a single loopback test.""" + + def __init__(self, baud_rate, passed, message): + self.baud_rate = baud_rate + self.passed = passed + self.message = message + + +def sanitize_id(value): + """Return a short identifier safe for generated Checkbox job IDs.""" + + return re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("_") + + +def parse_positive_int(raw_value, field_name): + """Parse a positive integer or raise a field-specific ValueError.""" + + try: + value = int(raw_value) + except ValueError as exc: + raise ValueError("{} must be an integer".format(field_name)) from exc + if value < 1: + raise ValueError("{} must be greater than 0".format(field_name)) + return value + + +def parse_baud(raw_value): + """Parse a RAW_UART_PORTS baud value.""" + + normalized = raw_value.lower() + if normalized in BAUD_MODES: + return normalized + return parse_positive_int(raw_value, "BAUD") + + +def parse_raw_uart_port(entry): + """Parse one RAW_UART_PORTS entry.""" + + parts = entry.split(":") + if len(parts) != 3: + raise ValueError( + "invalid format, expected {}".format(RAW_UART_CONFIG_FORMAT) + ) + name, target, baud = parts + if not name: + raise ValueError("NAME must not be empty") + if not sanitize_id(name): + raise ValueError("NAME must contain an ID-safe character") + if not target: + raise ValueError("TARGET must not be empty") + if not sanitize_id(target): + raise ValueError("TARGET must contain an ID-safe character") + return RawUartPort(name=name, target=target, baud=parse_baud(baud)) + + +def parse_raw_uart_ports(config): + """Parse space-separated RAW_UART_PORTS entries.""" + + return [parse_raw_uart_port(entry) for entry in config.split()] + + +def format_baud(baud): + """Return a resource-friendly baud string.""" + + return str(baud) + + +def print_raw_uart_resources(config): + """Print Checkbox resource records for configured raw UART ports.""" + + for port in parse_raw_uart_ports(config): + print("name: {}".format(port.name)) + print("name_id: {}".format(port.name_id)) + print("target: {}".format(port.target)) + print("target_id: {}".format(port.target_id)) + print("target_type: {}".format(port.target_type)) + print("baud: {}".format(format_baud(port.baud))) + print() + + +def normalize_addr(addr): + """Normalize a hardware address to lowercase hex form.""" + + value = addr.lower().strip() + if value.startswith("0x"): + return value + try: + return hex(int(value)) + except ValueError: + return value + + +def is_usb_serial_node(dev_node): + """Return True if a device node looks like a USB serial adapter.""" + + basename = os.path.basename(dev_node) + return basename.startswith(USB_SERIAL_PREFIXES) + + +def get_driver_baud_base(device_path): + """Return the kernel driver baud_base for a serial node, or 0.""" + + try: + fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK) + try: + serial_struct = struct.pack("i" * 19, *([0] * 19)) + result = fcntl.ioctl(fd, TIOCGSERIAL, serial_struct) + unpacked = struct.unpack( + SERIAL_STRUCT_FORMAT, + result[: struct.calcsize(SERIAL_STRUCT_FORMAT)], + ) + return unpacked[7] + finally: + os.close(fd) + except (OSError, struct.error): + return 0 + + +def scan_available_uarts(raw_uart_only=True): + """Scan Linux UART driver data and return address-to-node mappings.""" + + results = {} + for filepath in glob.glob("/proc/tty/driver/*"): + driver_name = os.path.basename(filepath) + if raw_uart_only and driver_name == "usb-serial": + continue + dev_prefix = DRIVER_DEV_PREFIX.get(driver_name) + if not dev_prefix and "serial" in driver_name: + dev_prefix = "ttyS" + if not dev_prefix: + continue + + try: + with open(filepath, "r") as driver_file: + for line in driver_file: + match = re.search( + r"^\s*(\d+):.*(?:mmio|port|I/O):\s*" + r"(0x[0-9a-fA-F]+|[0-9]+)", + line, + ) + if not match or "uart:unknown" in line: + continue + index = match.group(1) + addr = normalize_addr(match.group(2)) + results[addr] = "/dev/{}{}".format(dev_prefix, index) + except (OSError, IOError): + continue + return results + + +def get_active_consoles(): + """Return active console tty basenames.""" + + try: + with open("/sys/class/tty/console/active", "r") as console_file: + return set(console_file.read().strip().split()) + except (OSError, IOError): + return set() + + +def resolve_device(target, allow_console=False, raw_uart_only=True): + """Resolve a configured target to a usable raw UART device.""" + + if target.startswith("/dev/"): + dev_node = target + resolved_target = target + else: + resolved_target = normalize_addr(target) + uarts = scan_available_uarts(raw_uart_only=raw_uart_only) + dev_node = uarts.get(resolved_target) + if not dev_node: + raise RuntimeError( + "Raw UART target {} was not found".format(resolved_target) + ) + + if raw_uart_only and is_usb_serial_node(dev_node): + raise RuntimeError( + "USB serial adapters are not allowed for raw UART loopback" + ) + + is_console = os.path.basename(dev_node) in get_active_consoles() + if is_console and not allow_console: + raise RuntimeError("{} is an active system console".format(dev_node)) + + return UartDevice( + target=resolved_target, + dev_node=dev_node, + baud_base=get_driver_baud_base(dev_node), + is_console=is_console, + ) + + +def get_rates_to_test(baud_base, baud): + """Return baud rates for an integer or symbolic baud mode.""" + + if isinstance(baud, int): + return [baud] + if baud == "max": + return [baud_base if baud_base > 0 else 115200] + + limit = baud_base if baud_base > 0 else max(STANDARD_RATES) + rates = [rate for rate in STANDARD_RATES if rate <= limit] + if baud_base > 0 and baud_base not in rates: + rates.append(baud_base) + rates = sorted(set(rates)) + + if baud == "sweep": + return rates + if baud == "autoscan-max": + return list(reversed(rates)) + raise ValueError("unsupported baud mode: {}".format(baud)) + + +def get_sweep_down_rates(baud_base, baud): + """Return descending standard rates from the configured baud to 9600.""" + + if baud == "max": + start_baud = baud_base if baud_base > 0 else 115200 + elif isinstance(baud, int): + start_baud = baud + else: + raise ValueError("--sweep-down requires an integer baud rate or max") + + rates = [rate for rate in STANDARD_RATES if 9600 <= rate <= start_baud] + if start_baud not in rates: + rates.append(start_baud) + return sorted(set(rates), reverse=True) + + +def run_ping(serial_conn): + """Run a quick self-loopback ping on an open serial connection.""" + + serial_conn.timeout = 1.0 + serial_conn.reset_input_buffer() + serial_conn.write(PING_PATTERN) + serial_conn.flush() + time.sleep(0.05) + received = serial_conn.read(len(PING_PATTERN)) + return received == PING_PATTERN + + +def run_stress(serial_conn, count, size, timeout_factor=3.0): + """Run self-loopback stress traffic on an open serial connection.""" + + byte_time = (size * 10) / serial_conn.baudrate + serial_conn.timeout = max(2.0, byte_time * timeout_factor) + serial_conn.reset_input_buffer() + serial_conn.reset_output_buffer() + + for _index in range(count): + payload = os.urandom(size) + serial_conn.write(payload) + serial_conn.flush() + received = serial_conn.read(size) + if received != payload: + return False + return True + + +def open_serial_and_run(dev_node, baud_rate, run_fn): + """Open a serial node at a baud rate and run a callback.""" + + serial_conn = serial.Serial(dev_node, baud_rate, timeout=1.0) + try: + return run_fn(serial_conn) + finally: + serial_conn.close() + time.sleep(0.1) + + +def run_loopback( + target, + baud, + mode, + stress_count=100, + stress_size=1024, + sweep_down=False, +): + """Run quick or stress self-loopback tests for one target.""" + + device = resolve_device(target) + rates = ( + get_sweep_down_rates(device.baud_base, baud) + if sweep_down + else get_rates_to_test(device.baud_base, baud) + ) + results = [] + + for rate in rates: + try: + if mode == "quick": + passed = open_serial_and_run(device.dev_node, rate, run_ping) + elif mode == "stress": + passed = open_serial_and_run( + device.dev_node, + rate, + lambda serial_conn: run_stress( + serial_conn, stress_count, stress_size + ), + ) + else: + raise ValueError("unsupported mode: {}".format(mode)) + except (OSError, serial.SerialException) as exc: + passed = False + message = str(exc) + else: + message = "PASSED" if passed else "FAILED" + + result = LoopbackResult(rate, passed, message) + results.append(result) + print( + "{}: {} {} baud {}".format( + "PASS" if passed else "FAIL", + device.dev_node, + rate, + message, + ) + ) + + if baud == "autoscan-max" and passed: + print("Max working baud: {}".format(rate)) + return results + + return results + + +def parse_args(argv=None): + """Parse command-line arguments.""" + + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command") + + list_parser = subparsers.add_parser( + "list", help="Print Checkbox resources from RAW_UART_PORTS" + ) + list_parser.add_argument( + "config", + help=( + "Space-separated entries using the format " + "{}".format(RAW_UART_CONFIG_FORMAT) + ), + ) + + quick_parser = subparsers.add_parser( + "quick", help="Run a raw UART self-loopback ping test" + ) + quick_parser.add_argument("--target", required=True) + quick_parser.add_argument("--baud", required=True, type=parse_baud) + quick_parser.add_argument( + "--sweep-down", + action="store_true", + help="Sweep from --baud down to 9600; requires integer --baud", + ) + + stress_parser = subparsers.add_parser( + "stress", help="Run a raw UART self-loopback stress test" + ) + stress_parser.add_argument("--target", required=True) + stress_parser.add_argument("--baud", required=True, type=parse_baud) + stress_parser.add_argument( + "--sweep-down", + action="store_true", + help="Sweep from --baud down to 9600; requires integer --baud", + ) + stress_parser.add_argument( + "--count", + required=True, + type=lambda value: parse_positive_int(value, "COUNT"), + ) + stress_parser.add_argument( + "--size", + required=True, + type=lambda value: parse_positive_int(value, "SIZE"), + ) + + return parser, parser.parse_args(argv) + + +def main(argv=None): + parser, args = parse_args(argv) + if not args.command: + parser.print_usage() + raise SystemExit(1) + + try: + if args.command == "list": + print_raw_uart_resources(args.config) + return + if args.command == "quick": + results = run_loopback( + args.target, + args.baud, + "quick", + sweep_down=args.sweep_down, + ) + elif args.command == "stress": + results = run_loopback( + args.target, + args.baud, + "stress", + stress_count=args.count, + stress_size=args.size, + sweep_down=args.sweep_down, + ) + else: + parser.print_usage() + raise SystemExit(1) + except (RuntimeError, ValueError) as exc: + print("Error: {}".format(exc)) + raise SystemExit(1) + + if not all(result.passed for result in results): + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_serial_raw_uart_test.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_serial_raw_uart_test.py new file mode 100644 index 0000000000..506b5f8815 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_serial_raw_uart_test.py @@ -0,0 +1,350 @@ +import unittest +from io import StringIO +from unittest import mock + +import serial +import serial_raw_uart_test + + +class FakeSerial: + def __init__(self, baudrate=115200, echo=True): + self.baudrate = baudrate + self.echo = echo + self.closed = False + self.timeout = None + self.last_write = b"" + self.reset_input_called = False + self.reset_output_called = False + + def reset_input_buffer(self): + self.reset_input_called = True + + def reset_output_buffer(self): + self.reset_output_called = True + + def write(self, payload): + self.last_write = payload + + def flush(self): + pass + + def read(self, size): + if self.echo: + return self.last_write[:size] + return b"" + + def close(self): + self.closed = True + + +class SerialRawUartTest(unittest.TestCase): + """Unit tests for serial_raw_uart_test.py.""" + + def test_parse_raw_uart_ports(self): + ports = serial_raw_uart_test.parse_raw_uart_ports( + "uart0:0x2260000:max uart1:/dev/ttyS1:115200" + ) + + self.assertEqual(len(ports), 2) + self.assertEqual(ports[0].name, "uart0") + self.assertEqual(ports[0].name_id, "uart0") + self.assertEqual(ports[0].target, "0x2260000") + self.assertEqual(ports[0].target_id, "0x2260000") + self.assertEqual(ports[0].target_type, "addr") + self.assertEqual(ports[0].baud, "max") + self.assertEqual(ports[1].target_type, "node") + self.assertEqual(ports[1].baud, 115200) + + def test_parse_raw_uart_ports_rejects_malformed_entry(self): + with self.assertRaisesRegex(ValueError, "invalid format"): + serial_raw_uart_test.parse_raw_uart_port("uart0:/dev/ttyS1") + + def test_parse_raw_uart_ports_rejects_bad_baud(self): + with self.assertRaisesRegex(ValueError, "BAUD"): + serial_raw_uart_test.parse_raw_uart_port( + "uart0:/dev/ttyS1:not-a-baud" + ) + + def test_print_raw_uart_resources(self): + output = StringIO() + with mock.patch("sys.stdout", output): + serial_raw_uart_test.print_raw_uart_resources( + "uart0:0x2260000:max uart1:/dev/ttyS1:115200" + ) + + self.assertEqual( + output.getvalue(), + "name: uart0\n" + "name_id: uart0\n" + "target: 0x2260000\n" + "target_id: 0x2260000\n" + "target_type: addr\n" + "baud: max\n\n" + "name: uart1\n" + "name_id: uart1\n" + "target: /dev/ttyS1\n" + "target_id: dev_ttyS1\n" + "target_type: node\n" + "baud: 115200\n\n", + ) + + def test_get_rates_to_test(self): + self.assertEqual( + serial_raw_uart_test.get_rates_to_test(0, 9600), [9600] + ) + self.assertEqual( + serial_raw_uart_test.get_rates_to_test(0, "max"), [115200] + ) + self.assertEqual( + serial_raw_uart_test.get_rates_to_test(115200, "sweep"), + [9600, 19200, 38400, 57600, 115200], + ) + self.assertEqual( + serial_raw_uart_test.get_rates_to_test(115200, "autoscan-max"), + [115200, 57600, 38400, 19200, 9600], + ) + + def test_get_sweep_down_rates(self): + self.assertEqual( + serial_raw_uart_test.get_sweep_down_rates(0, 115200), + [115200, 57600, 38400, 19200, 9600], + ) + self.assertEqual( + serial_raw_uart_test.get_sweep_down_rates(0, 123456), + [123456, 115200, 57600, 38400, 19200, 9600], + ) + self.assertEqual( + serial_raw_uart_test.get_sweep_down_rates(115200, "max"), + [115200, 57600, 38400, 19200, 9600], + ) + + def test_sweep_down_requires_integer_baud(self): + with self.assertRaisesRegex(ValueError, "integer baud rate or max"): + serial_raw_uart_test.get_sweep_down_rates(115200, "sweep") + + @mock.patch( + "serial_raw_uart_test.get_driver_baud_base", return_value=3000000 + ) + @mock.patch("serial_raw_uart_test.get_active_consoles", return_value=set()) + def test_resolve_device_by_node(self, mock_consoles, mock_baud_base): + device = serial_raw_uart_test.resolve_device("/dev/ttyS1") + + self.assertEqual(device.target, "/dev/ttyS1") + self.assertEqual(device.dev_node, "/dev/ttyS1") + self.assertEqual(device.baud_base, 3000000) + self.assertFalse(device.is_console) + + @mock.patch( + "serial_raw_uart_test.scan_available_uarts", + return_value={"0x2260000": "/dev/ttyS0"}, + ) + @mock.patch( + "serial_raw_uart_test.get_driver_baud_base", return_value=115200 + ) + @mock.patch("serial_raw_uart_test.get_active_consoles", return_value=set()) + def test_resolve_device_by_address( + self, mock_consoles, mock_baud_base, mock_scan + ): + device = serial_raw_uart_test.resolve_device("0x2260000") + + self.assertEqual(device.target, "0x2260000") + self.assertEqual(device.dev_node, "/dev/ttyS0") + self.assertEqual(device.baud_base, 115200) + + def test_resolve_device_blocks_usb_serial_node(self): + with self.assertRaisesRegex(RuntimeError, "USB serial"): + serial_raw_uart_test.resolve_device("/dev/ttyUSB0") + + @mock.patch( + "serial_raw_uart_test.get_active_consoles", return_value={"ttyS0"} + ) + def test_resolve_device_blocks_console(self, mock_consoles): + with self.assertRaisesRegex(RuntimeError, "active system console"): + serial_raw_uart_test.resolve_device("/dev/ttyS0") + + def test_run_ping_success(self): + serial_conn = FakeSerial(echo=True) + + self.assertTrue(serial_raw_uart_test.run_ping(serial_conn)) + self.assertTrue(serial_conn.reset_input_called) + + def test_run_ping_failure(self): + self.assertFalse(serial_raw_uart_test.run_ping(FakeSerial(echo=False))) + + @mock.patch("os.urandom", return_value=b"abcd") + def test_run_stress_success(self, mock_urandom): + serial_conn = FakeSerial(echo=True) + + self.assertTrue( + serial_raw_uart_test.run_stress(serial_conn, count=2, size=4) + ) + self.assertTrue(serial_conn.reset_input_called) + self.assertTrue(serial_conn.reset_output_called) + + @mock.patch("os.urandom", return_value=b"abcd") + def test_run_stress_failure(self, mock_urandom): + self.assertFalse( + serial_raw_uart_test.run_stress( + FakeSerial(echo=False), count=2, size=4 + ) + ) + + @mock.patch("time.sleep") + @mock.patch("serial.Serial") + def test_open_serial_and_run_closes_connection( + self, mock_serial, mock_sleep + ): + serial_conn = FakeSerial() + mock_serial.return_value = serial_conn + + result = serial_raw_uart_test.open_serial_and_run( + "/dev/ttyS0", 115200, lambda conn: conn.baudrate + ) + + self.assertEqual(result, 115200) + self.assertTrue(serial_conn.closed) + mock_serial.assert_called_once_with("/dev/ttyS0", 115200, timeout=1.0) + + @mock.patch("serial_raw_uart_test.open_serial_and_run") + @mock.patch("serial_raw_uart_test.resolve_device") + def test_run_loopback_autoscan_stops_at_first_pass( + self, mock_resolve, mock_open + ): + mock_resolve.return_value = serial_raw_uart_test.UartDevice( + "0x2260000", "/dev/ttyS0", 115200, False + ) + mock_open.side_effect = [False, True] + + output = StringIO() + with mock.patch("sys.stdout", output): + results = serial_raw_uart_test.run_loopback( + "0x2260000", "autoscan-max", "quick" + ) + + self.assertEqual( + [result.baud_rate for result in results], [115200, 57600] + ) + self.assertFalse(results[0].passed) + self.assertTrue(results[1].passed) + self.assertIn("Max working baud: 57600", output.getvalue()) + + @mock.patch("serial_raw_uart_test.open_serial_and_run") + @mock.patch("serial_raw_uart_test.resolve_device") + def test_run_loopback_sweep_down(self, mock_resolve, mock_open): + mock_resolve.return_value = serial_raw_uart_test.UartDevice( + "/dev/ttyS0", "/dev/ttyS0", 4000000, False + ) + mock_open.return_value = True + + with mock.patch("sys.stdout", StringIO()): + results = serial_raw_uart_test.run_loopback( + "/dev/ttyS0", 115200, "quick", sweep_down=True + ) + + self.assertEqual( + [result.baud_rate for result in results], + [115200, 57600, 38400, 19200, 9600], + ) + + @mock.patch("serial_raw_uart_test.run_loopback") + def test_quick_command_exits_nonzero_on_failure(self, mock_run_loopback): + mock_run_loopback.return_value = [ + serial_raw_uart_test.LoopbackResult(115200, False, "FAILED") + ] + + with self.assertRaises(SystemExit) as cm: + serial_raw_uart_test.main( + ["quick", "--target", "/dev/ttyS0", "--baud", "115200"] + ) + + self.assertEqual(cm.exception.code, 1) + + @mock.patch("serial_raw_uart_test.run_loopback") + def test_stress_command_uses_count_and_size(self, mock_run_loopback): + mock_run_loopback.return_value = [ + serial_raw_uart_test.LoopbackResult(115200, True, "PASSED") + ] + + serial_raw_uart_test.main( + [ + "stress", + "--target", + "/dev/ttyS0", + "--baud", + "115200", + "--count", + "50", + "--size", + "512", + ] + ) + + mock_run_loopback.assert_called_once_with( + "/dev/ttyS0", + 115200, + "stress", + stress_count=50, + stress_size=512, + sweep_down=False, + ) + + @mock.patch("serial_raw_uart_test.run_loopback") + def test_quick_command_uses_sweep_down(self, mock_run_loopback): + mock_run_loopback.return_value = [ + serial_raw_uart_test.LoopbackResult(115200, True, "PASSED") + ] + + serial_raw_uart_test.main( + [ + "quick", + "--target", + "/dev/ttyS0", + "--baud", + "115200", + "--sweep-down", + ] + ) + + mock_run_loopback.assert_called_once_with( + "/dev/ttyS0", + 115200, + "quick", + sweep_down=True, + ) + + @mock.patch("serial_raw_uart_test.run_loopback") + def test_stress_command_uses_sweep_down(self, mock_run_loopback): + mock_run_loopback.return_value = [ + serial_raw_uart_test.LoopbackResult(115200, True, "PASSED") + ] + + serial_raw_uart_test.main( + [ + "stress", + "--target", + "/dev/ttyS0", + "--baud", + "115200", + "--count", + "50", + "--size", + "512", + "--sweep-down", + ] + ) + + mock_run_loopback.assert_called_once_with( + "/dev/ttyS0", + 115200, + "stress", + stress_count=50, + stress_size=512, + sweep_down=True, + ) + + def test_serial_exception_is_available_for_mocks(self): + self.assertTrue(hasattr(serial, "SerialException")) + + +if __name__ == "__main__": + unittest.main() diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/README.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/README.md new file mode 100644 index 0000000000..d9feec7d67 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/README.md @@ -0,0 +1,186 @@ +# Serial raw UART self-loopback tests + +The serial raw UART self-loopback tests check that one or more raw UART ports can +transmit and receive data when each port has its TX and RX pins connected +together on the same physical UART port. + +These tests only validate self-loopback wiring. They do not validate +communication between two different UART ports or two different devices. + +## Test plan + +The generated jobs are included by `ce-oem-serial-automated` through these +template IDs: + +```text +ce-oem-serial/raw-uart-self-loopback-quick-tests +ce-oem-serial/raw-uart-self-loopback-stress-tests +``` + +The jobs also use `flags: also-after-suspend`, so Checkbox can generate +before-suspend and after-suspend variants for suspend flows. + +## Manifest + +Enable the manifest gate before running the generated jobs: + +```text +has_serial_raw_uart_self_loopback: True +``` + +If this manifest value is not `True`, the generated raw UART jobs are not +selected even when `RAW_UART_PORTS` is configured. + +## Environment + +Set `RAW_UART_PORTS` in the Checkbox configuration. The value is a +space-separated list of entries with this format: + +```text +NAME:TARGET:BAUD +``` + +Fields: + +| Field | Description | +| --- | --- | +| `NAME` | Short unique name for the UART. It is normalized for the generated job ID. | +| `TARGET` | Hardware address such as `0x2260000`, or a device node such as `/dev/ttyS1`. | +| `BAUD` | Integer baud rate, `max`, `sweep`, or `autoscan-max`. | + +Examples: + +```text +RAW_UART_PORTS=uart0:0x2260000:max +RAW_UART_PORTS="uart0:0x2260000:max uart1:/dev/ttyS1:115200" +RAW_UART_PORTS="uart0:0x2260000:sweep uart1:/dev/ttyS1:autoscan-max" +``` + +Quick and stress jobs sweep downward from an explicit integer baud rate to 9600 +by default. This default is used when `RAW_UART_SWEEP_DOWN` is not set: + +```text +RAW_UART_SWEEP_DOWN=true +``` + +`RAW_UART_SWEEP_DOWN` supports integer baud values, such as `115200`, and +`max`, which starts from the driver-reported baud base. It is not valid with +`sweep` or `autoscan-max`. + +Disable sweep-down explicitly when you only want to test the configured integer +baud rate: + +```text +RAW_UART_SWEEP_DOWN=false +``` + +Stress jobs also support optional environment overrides. The PXU command uses +these defaults when the variables are not set: + +```text +RAW_UART_STRESS_COUNT=100 +RAW_UART_STRESS_SIZE=1024 +``` + +Example override: + +```text +RAW_UART_STRESS_COUNT=50 +RAW_UART_STRESS_SIZE=512 +``` + +## Hardware connection + +Connect the same UART port's TX and RX pins together before running the test: + +```text +UART TX -----+ + | (Jumper short) +UART RX -----+ +``` + +The TX and RX pins must belong to the same UART controller and the same +physical port. This is why the test is named self-loopback. Do not connect TX +from one UART port to RX on a different UART port for this test case. + +USB serial adapters are blocked by default because they are not raw UART +self-loopback targets for this CE OEM test. + +## Resource input and output + +Input: + +```bash +serial_raw_uart_test.py list "uart0:0x2260000:max uart1:/dev/ttyS1:115200" +``` + +Output: + +```text +name: uart0 +name_id: uart0 +target: 0x2260000 +target_id: 0x2260000 +target_type: addr +baud: max + +name: uart1 +name_id: uart1 +target: /dev/ttyS1 +target_id: dev_ttyS1 +target_type: node +baud: 115200 +``` + +## Test input and output + +Quick self-loopback test input: + +```bash +serial_raw_uart_test.py quick --target 0x2260000 --baud max +``` + +Example success output: + +```text +PASS: /dev/ttyS0 115200 baud PASSED +``` + +Example failure output: + +```text +FAIL: /dev/ttyS0 115200 baud FAILED +``` + +Quick self-loopback sweep-down input: + +```bash +serial_raw_uart_test.py quick --target /dev/ttyS1 --baud 115200 --sweep-down +``` + +Stress self-loopback test input: + +```bash +serial_raw_uart_test.py stress --target /dev/ttyS1 --baud 115200 --count 100 --size 1024 +``` + +Stress self-loopback sweep-down input: + +```bash +serial_raw_uart_test.py stress --target /dev/ttyS1 --baud 115200 --count 100 --size 1024 --sweep-down +``` + +Example success output: + +```text +PASS: /dev/ttyS1 115200 baud PASSED +``` + +## Safety defaults + +The helper is conservative for certification runs: + +* USB serial adapters are blocked for raw UART self-loopback. +* Active system console UARTs are blocked. +* Hardware-address targets are resolved from Linux UART driver data. +* Device-node targets are used directly after the same safety checks. diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/manifest.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/manifest.pxu index 73c84b2f4f..469a7e8177 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/manifest.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/manifest.pxu @@ -7,3 +7,8 @@ unit: manifest entry id: has_serial_console_loopback _name: Does serial console port loopback to itself? value-type: bool + +unit: manifest entry +id: has_serial_raw_uart_self_loopback +_name: Has raw UART ports connected for self-loopback? +value-type: bool diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/serial_raw_uart.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/serial_raw_uart.pxu new file mode 100644 index 0000000000..547a519401 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/serial_raw_uart.pxu @@ -0,0 +1,94 @@ +id: ce-oem-serial/raw-uart-list +_summary: + Generates raw UART self-loopback resources from user configuration +_purpose: + Generates one resource for each raw UART listed in RAW_UART_PORTS. + NAME:TARGET:BAUD +plugin: resource +estimated_duration: 1.0 +environ: + RAW_UART_PORTS +command: + if [ -z "$RAW_UART_PORTS" ]; then + exit 0 + fi + serial_raw_uart_test.py list "$RAW_UART_PORTS" + +unit: template +template-resource: ce-oem-serial/raw-uart-list +template-unit: job +template-engine: jinja2 +template-id: ce-oem-serial/raw-uart-self-loopback-quick-tests +id: ce-oem-serial/raw-uart-self-loopback-quick-{{ name_id }}-{{ target_id }} +imports: from com.canonical.plainbox import manifest +requires: + manifest.has_serial_raw_uart_self_loopback == 'True' +_template-summary: + Quick raw UART self-loopback tests +_summary: Quick raw UART self-loopback test for {{ name }} +_purpose: + To check that raw UART {{ name }} can send and receive self-loopback data. + This test opens {{ target }} as a raw UART self-loopback target and verifies + a short transmit/receive ping at baud setting {{ baud }}. +plugin: shell +user: root +category_id: com.canonical.certification::serial +estimated_duration: 10 +flags: also-after-suspend +environ: + RAW_UART_SWEEP_DOWN +command: + case "${RAW_UART_SWEEP_DOWN:-true}" in + 0|false|False|FALSE|no|No|NO) + SWEEP_DOWN_OPTION="" + ;; + *) + SWEEP_DOWN_OPTION="--sweep-down" + ;; + esac + serial_raw_uart_test.py quick \ + --target "{{ target }}" \ + --baud "{{ baud }}" \ + $SWEEP_DOWN_OPTION + +unit: template +template-resource: ce-oem-serial/raw-uart-list +template-unit: job +template-engine: jinja2 +template-id: ce-oem-serial/raw-uart-self-loopback-stress-tests +id: ce-oem-serial/raw-uart-self-loopback-stress-{{ name_id }}-{{ target_id }} +imports: from com.canonical.plainbox import manifest +requires: + manifest.has_serial_raw_uart_self_loopback == 'True' +_template-summary: + Stress raw UART self-loopback tests +_summary: Stress raw UART self-loopback test for {{ name }} +_purpose: + To check that raw UART {{ name }} can send and receive repeated self-loopback + payloads. + This test opens {{ target }} as a raw UART self-loopback target and verifies + repeated transmit/receive payloads at baud setting {{ baud }}. +plugin: shell +user: root +category_id: com.canonical.certification::serial +estimated_duration: 60 +flags: also-after-suspend +environ: + RAW_UART_STRESS_COUNT + RAW_UART_STRESS_SIZE + RAW_UART_SWEEP_DOWN +command: + case "${RAW_UART_SWEEP_DOWN:-true}" in + 0|false|False|FALSE|no|No|NO) + SWEEP_DOWN_OPTION="" + ;; + *) + SWEEP_DOWN_OPTION="--sweep-down" + ;; + esac + serial_raw_uart_test.py stress \ + --target "{{ target }}" \ + --baud "{{ baud }}" \ + --count "${RAW_UART_STRESS_COUNT:-100}" \ + --size "${RAW_UART_STRESS_SIZE:-1024}" \ + $SWEEP_DOWN_OPTION diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/test-plan.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/test-plan.pxu index fd8ac4aef1..bc263779a1 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/test-plan.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/serial/test-plan.pxu @@ -19,9 +19,12 @@ _description: bootstrap_include: ce-oem-serial/serial-list ce-oem-serial/serial-console-list + ce-oem-serial/raw-uart-list include: ce-oem-serial/serial-transmit-data-tests ce-oem-serial/serial-console-tests + ce-oem-serial/raw-uart-self-loopback-quick-tests + ce-oem-serial/raw-uart-self-loopback-stress-tests id: before-suspend-ce-oem-serial-automated unit: test plan @@ -31,9 +34,12 @@ _description: bootstrap_include: ce-oem-serial/serial-list ce-oem-serial/serial-console-list + ce-oem-serial/raw-uart-list include: ce-oem-serial/serial-transmit-data-.* ce-oem-serial/serial-console-(?!list\b).*$ + ce-oem-serial/raw-uart-self-loopback-quick-tests + ce-oem-serial/raw-uart-self-loopback-stress-tests id: after-suspend-ce-oem-serial-automated unit: test plan @@ -43,9 +49,12 @@ _description: bootstrap_include: ce-oem-serial/serial-list ce-oem-serial/serial-console-list + ce-oem-serial/raw-uart-list include: after-suspend-ce-oem-serial/serial-transmit-data-.* after-suspend-ce-oem-serial/serial-console-.* + after-suspend-ce-oem-serial/raw-uart-self-loopback-quick-tests + after-suspend-ce-oem-serial/raw-uart-self-loopback-stress-tests id: ce-oem-serial-stress unit: test plan