diff --git a/CHANGES.md b/CHANGES.md index ea545cb..408554e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +- Add Renode device support. + ## 0.0.3 - Add GDB and coredump debugging tools. diff --git a/Documentation/api.rst b/Documentation/api.rst index 6f369ee..b680641 100644 --- a/Documentation/api.rst +++ b/Documentation/api.rst @@ -36,6 +36,7 @@ Device Interface ntfc.device.host ntfc.device.nuttx ntfc.device.qemu + ntfc.device.renode ntfc.device.serial ntfc.device.sim diff --git a/Documentation/config-yaml.rst b/Documentation/config-yaml.rst index 57f4f1f..06d663b 100644 --- a/Documentation/config-yaml.rst +++ b/Documentation/config-yaml.rst @@ -16,12 +16,12 @@ This file defines device-under-test (DUT) setup and global configuration. cores: # List of product cores core0: # Core0 entry name: 'core0-name' - device: 'sim|qemu|serial' + device: 'sim|qemu|renode|serial' # Device-specific configuration core1: # Core1 entry name: 'core1-name' - device: 'sim|qemu|serial' + device: 'sim|qemu|renode|serial' # Device-specific configuration @@ -37,7 +37,7 @@ This file defines device-under-test (DUT) setup and global configuration. cores: core0: name: 'core-name' - device: 'sim|qemu|serial' + device: 'sim|qemu|renode|serial' # Device-specific configuration product1: @@ -45,7 +45,7 @@ This file defines device-under-test (DUT) setup and global configuration. cores: core0: name: 'core-name' - device: 'sim|qemu|serial' + device: 'sim|qemu|renode|serial' # Device-specific configuration @@ -228,6 +228,24 @@ At default NTFC automatically add the ``-kernel path_to_elf_image`` option to ``exec_args``. You can also add your custom boot parameter with ``$IMAGE_ELF``, where ``$IMAGE_ELF`` will be replaced with the path to the ELF. +Renode +------ + +This device type is implemented in :class:`ntfc.device.renode.DeviceRenode`. + +.. code-block:: yaml + + cores: + core0: + name: 'main' + device: 'renode' + exec_path: 'renode' + exec_args: '-e "include @config/renode/board.resc"' + +NTFC sets ``$bin`` to the ELF and creates a socket terminal named ``ntfc`` +before ``exec_args`` runs. The script must load ``$bin`` and connect its +console UART with ``connector Connect ntfc``. See ``config/renode/``. + Serial Device ------------- diff --git a/Documentation/config.yaml b/Documentation/config.yaml index a0d6f90..f1a8d5e 100644 --- a/Documentation/config.yaml +++ b/Documentation/config.yaml @@ -63,14 +63,18 @@ product: # many products can be supported in tests (pro cores: # Cores configuration core0: # Main core (core 0) name: '' # Core name - device: '' # Choose a device: sim, qemu + device: '' # Choose a device: sim, qemu, renode, serial exec_path: '' # Path of emulator execution for QEMU targets eg: `qemu-system-x86_64` + # Renode launcher for renode targets, eg: `renode` # Path of serial port for serial targets, eg: `/dev/ttyACM0` # Empty for simulator. exec_args: '' # Args for emulator execution for QEMU tragets. + # Renode args for renode targets, eg '-e "include @board.resc"'. + # The script loads $bin and connects the console UART to + # the `ntfc` terminal, see config/renode/. # Serial port configuration for serial targets, eg '9600,n,8,1' # Empty for simulator. - exec_cwd: '' # (optional) working directory for the spawned sim/qemu process. + exec_cwd: '' # (optional) working directory for the spawned sim/qemu/renode process. # Kernel-mode hostfs mounts resolve relative to this directory. # Defaults to the core build directory for kernel-mode builds. boot_timeout: 5 # (optional) seconds to wait for the first shell prompt. Defaults to 5 diff --git a/Documentation/debug/gdb.rst b/Documentation/debug/gdb.rst index c567b13..13e2162 100644 --- a/Documentation/debug/gdb.rst +++ b/Documentation/debug/gdb.rst @@ -20,7 +20,7 @@ and connects to it one of two ways: * **Remote target** (``debug.gdb.target``) - for QEMU or real hardware with a debug probe. Set it to the ``host:port`` (or Unix socket path) of a GDB stub. For QEMU, add ``-s`` to ``exec_args`` to get a stub on - ``localhost:1234``. + ``localhost:1234``. For Renode, add ``-e "machine StartGdbServer 3333"``. * **Simulator PID-attach** (``debug.gdb.attach: true``) - for the host-based NuttX simulator, where there's no remote stub, just a diff --git a/README.md b/README.md index 7c999a7..08aa984 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ versions of Python. ## Features -- NuttX simulator, QEMU and devices with serial port are supported +- NuttX simulator, QEMU, Renode and devices with serial port are supported - Run the command from NSH and compare with the expected output diff --git a/config/nuttx-renode-nrf52840-dk.yaml b/config/nuttx-renode-nrf52840-dk.yaml new file mode 100644 index 0000000..e8a59e1 --- /dev/null +++ b/config/nuttx-renode-nrf52840-dk.yaml @@ -0,0 +1,16 @@ +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-renode-nrf52840-dk" + cores: + core0: + name: 'main' + device: 'renode' + exec_path: 'renode' + exec_args: '-e "include @config/renode/nrf52840-dk.resc"' + defconfig: 'boards/arm/nrf52/nrf52840-dk/configs/ntfc' + boot_timeout: 30 + flash: '' diff --git a/config/nuttx-renode-nucleo-h743zi.yaml b/config/nuttx-renode-nucleo-h743zi.yaml new file mode 100644 index 0000000..26764f4 --- /dev/null +++ b/config/nuttx-renode-nucleo-h743zi.yaml @@ -0,0 +1,18 @@ +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-renode-nucleo-h743zi" + cores: + core0: + name: 'main' + device: 'renode' + exec_path: 'renode' + exec_args: '-e "include @config/renode/nucleo-h743zi.resc"' + defconfig: 'boards/arm/stm32h7/nucleo-h743zi/configs/ntfc' + boot_timeout: 30 + flash: '' + kv: + CONFIG_STM32_PWR_IGNORE_ACTVOSRDY: "y" diff --git a/config/nuttx-renode-stm32f4discovery.yaml b/config/nuttx-renode-stm32f4discovery.yaml new file mode 100644 index 0000000..ccf0f83 --- /dev/null +++ b/config/nuttx-renode-stm32f4discovery.yaml @@ -0,0 +1,17 @@ +# No CCM in Renode: single 112 KiB heap. +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-renode-stm32f4discovery" + cores: + core0: + name: 'main' + device: 'renode' + exec_path: 'renode' + exec_args: '-e "include @config/renode/stm32f4discovery.resc"' + defconfig: 'boards/arm/stm32f4/stm32f4discovery/configs/ntfc' + boot_timeout: 30 + flash: '' diff --git a/config/nuttx-renode-stm32f746g-disco.yaml b/config/nuttx-renode-stm32f746g-disco.yaml new file mode 100644 index 0000000..6646d81 --- /dev/null +++ b/config/nuttx-renode-stm32f746g-disco.yaml @@ -0,0 +1,16 @@ +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-renode-stm32f746g-disco" + cores: + core0: + name: 'main' + device: 'renode' + exec_path: 'renode' + exec_args: '-e "include @config/renode/stm32f746g-disco.resc"' + defconfig: 'boards/arm/stm32f7/stm32f746g-disco/configs/ntfc' + boot_timeout: 30 + flash: '' diff --git a/config/renode/nrf52840-dk.repl b/config/renode/nrf52840-dk.repl new file mode 100644 index 0000000..72d486a --- /dev/null +++ b/config/renode/nrf52840-dk.repl @@ -0,0 +1,6 @@ +// nrf52840-dk for NuttX: the NuttX nRF52 serial driver uses the legacy +// (non-EasyDMA) UART registers, while Renode defaults to UARTE. +using "platforms/cpus/nrf52840.repl" + +uart0: + easyDMA: false diff --git a/config/renode/nrf52840-dk.resc b/config/renode/nrf52840-dk.resc new file mode 100644 index 0000000..a3d2af6 --- /dev/null +++ b/config/renode/nrf52840-dk.resc @@ -0,0 +1,17 @@ +:name: nrf52840-dk +:description: NuttX on the nrf52840-dk. Console on sysbus.uart0, connected to the NTFC terminal. + +using sysbus +mach create "nrf52840-dk" +machine LoadPlatformDescription $ORIGIN/nrf52840-dk.repl + +$bin?=@nuttx + +macro reset +""" + sysbus LoadELF $bin +""" + +runMacro $reset + +connector Connect sysbus.uart0 ntfc diff --git a/config/renode/nucleo-h743zi.resc b/config/renode/nucleo-h743zi.resc new file mode 100644 index 0000000..fa1e062 --- /dev/null +++ b/config/renode/nucleo-h743zi.resc @@ -0,0 +1,17 @@ +:name: nucleo-h743zi +:description: NuttX on the nucleo-h743zi. Console on sysbus.usart3, connected to the NTFC terminal. + +using sysbus +mach create "nucleo-h743zi" +machine LoadPlatformDescription @platforms/boards/nucleo_h753zi.repl + +$bin?=@nuttx + +macro reset +""" + sysbus LoadELF $bin +""" + +runMacro $reset + +connector Connect sysbus.usart3 ntfc diff --git a/config/renode/stm32f4discovery.resc b/config/renode/stm32f4discovery.resc new file mode 100644 index 0000000..630de5d --- /dev/null +++ b/config/renode/stm32f4discovery.resc @@ -0,0 +1,19 @@ +:name: stm32f4discovery +:description: NuttX on the stm32f4discovery. Console on sysbus.usart2, connected to the NTFC terminal. + +using sysbus +mach create "stm32f4discovery" +machine LoadPlatformDescription @platforms/boards/stm32f4_discovery-kit.repl + +cpu PerformanceInMips 125 + +$bin?=@nuttx + +macro reset +""" + sysbus LoadELF $bin +""" + +runMacro $reset + +connector Connect sysbus.usart2 ntfc diff --git a/config/renode/stm32f746g-disco.resc b/config/renode/stm32f746g-disco.resc new file mode 100644 index 0000000..f6c1d02 --- /dev/null +++ b/config/renode/stm32f746g-disco.resc @@ -0,0 +1,17 @@ +:name: stm32f746g-disco +:description: NuttX on the stm32f746g-disco. Console on sysbus.usart1, connected to the NTFC terminal. + +using sysbus +mach create "stm32f746g-disco" +machine LoadPlatformDescription @platforms/boards/stm32f7_discovery-bb.repl + +$bin?=@nuttx + +macro reset +""" + sysbus LoadELF $bin +""" + +runMacro $reset + +connector Connect sysbus.usart1 ntfc diff --git a/src/ntfc/device/getdev.py b/src/ntfc/device/getdev.py index 5a75ee8..7156274 100644 --- a/src/ntfc/device/getdev.py +++ b/src/ntfc/device/getdev.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Callable, Dict from .qemu import DeviceQemu +from .renode import DeviceRenode from .serial import DeviceSerial from .sim import DeviceSim @@ -36,6 +37,7 @@ "sim": DeviceSim, "qemu": DeviceQemu, "serial": DeviceSerial, + "renode": DeviceRenode, } ############################################################################### diff --git a/src/ntfc/device/host.py b/src/ntfc/device/host.py index e188e65..2aed060 100644 --- a/src/ntfc/device/host.py +++ b/src/ntfc/device/host.py @@ -181,12 +181,21 @@ def host_open(self, cmd: List[str], uptime: int = 0) -> pexpect.spawn: time.sleep(uptime) + self._post_spawn() + ret = self._wait_for_boot(self._conf.boot_timeout) if ret is False: # pragma: no cover raise TimeoutError("device boot timeout") return self._child + def _post_spawn(self) -> None: + """Run after the child is spawned and before the boot wait. + + Transports that do not talk to the child's pty (e.g. a socket + opened by the emulator) attach here. Runs on every reopen. + """ + def _stop_impl(self) -> None: """Stop host device and kill the underlying process.""" if not self._child: diff --git a/src/ntfc/device/renode.py b/src/ntfc/device/renode.py new file mode 100644 index 0000000..98d6a6b --- /dev/null +++ b/src/ntfc/device/renode.py @@ -0,0 +1,210 @@ +############################################################################ +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +"""Renode device: a spawned emulator with the console on a TCP socket.""" + +import os +import select +import shlex +import socket +import time +from typing import TYPE_CHECKING, Optional + +import pexpect # type: ignore + +from ntfc.log.logger import logger + +from .host import DeviceHost + +if TYPE_CHECKING: + from ntfc.coreconfig import CoreConfig + +############################################################################### +# Class: DeviceRenode +############################################################################### + + +class DeviceRenode(DeviceHost): + """Host-based Renode emulator. + + Renode runs headless with its monitor on the child pty. NTFC creates a + raw TCP socket terminal named ``ntfc`` that the target script connects + its console UART to. Process ownership comes from :class:`DeviceHost`. + """ + + DEFAULT_EXEC = "renode" + RENODE_FLAGS = ("--disable-gui", "--console", "--plain") + TERMINAL_NAME = "ntfc" + CONNECT_POLL = 0.1 + SOCK_TIMEOUT = 5.0 + QUIT_TIMEOUT = 3.0 + + def __init__(self, conf: "CoreConfig"): + """Initialize Renode device. + + :param conf: configuration handler + """ + DeviceHost.__init__(self, conf) + self._sock: Optional[socket.socket] = None + self._port = 0 + + @property + def name(self) -> str: + """Get device name.""" + return "renode" + + @staticmethod + def _free_port() -> int: + """Draw an ephemeral localhost port for the socket terminal.""" + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) + + def _start_impl(self) -> None: + """Start Renode. + + NTFC sets ``$bin`` to the ELF and creates the socket terminal + ``ntfc`` before the user arguments run; the script given there is + expected to load ``$bin`` and connect its console UART to ``ntfc``. + """ + elf = os.path.abspath(self._image_path()) + exec_args = self._conf.exec_args + if not exec_args: + raise KeyError("no exec_args in configuration file!") + + self._port = self._free_port() + terminal = ( + f'CreateServerSocketTerminal {self._port} "{self.TERMINAL_NAME}"' + ) + + cmd = [self._conf.exec_path or self.DEFAULT_EXEC] + cmd.append(" " + " ".join(self.RENODE_FLAGS)) + cmd.append(" -e " + shlex.quote(f"$bin=@{elf}")) + cmd.append(" -e " + shlex.quote(f"emulation {terminal} false")) + cmd.append(" " + exec_args) + cmd.append(" -e start") + + self.host_open(cmd, self._conf.uptime) + + def _dev_reopen(self) -> pexpect.spawn: + """Restart Renode on a fresh socket terminal port.""" + self._stop_impl() + self._start_impl() + assert self._child + return self._child + + def _drain_child(self) -> None: + """Forward Renode's own monitor/log output to the debug log.""" + if not self._child: + return + try: + while True: + out = self._child.read_nonblocking(size=5120, timeout=0) + logger.debug(f"renode: {out!r}") + except (pexpect.TIMEOUT, pexpect.EOF): + pass + + def _post_spawn(self) -> None: + """Connect to the socket terminal once Renode listens on it.""" + deadline = time.monotonic() + self._conf.boot_timeout + while True: + try: + sock = socket.create_connection( + ("127.0.0.1", self._port), timeout=self.SOCK_TIMEOUT + ) + break + except OSError: + self._drain_child() + if not self._child or not self._child.isalive(): + raise IOError("renode exited before opening console") + if time.monotonic() >= deadline: + raise TimeoutError("renode console connect timeout") + time.sleep(self.CONNECT_POLL) + + sock.settimeout(self.SOCK_TIMEOUT) + self._sock = sock + logger.info(f"renode console connected on port {self._port}") + + def _close_sock(self) -> None: + """Close the console socket.""" + if self._sock: + self._sock.close() + self._sock = None + + def _dev_is_health_priv(self) -> bool: + """Check if Renode runs and the console is connected.""" + return DeviceHost._dev_is_health_priv(self) and self._sock is not None + + def _read(self) -> bytes: + """Read console data from the socket terminal.""" + self._drain_child() + if not self.dev_is_health(): + return b"" + + assert self._sock + ready, _, _ = select.select([self._sock], [], [], 0) + if not ready: + return b"" + + try: + data = self._sock.recv(5120) + except OSError: + data = b"" + + if not data: + # peer closed the console + self._close_sock() + return data + + def _write(self, data: bytes) -> None: + """Write to the console. One write per command: TCP is a stream.""" + if not self.dev_is_health(): + return + + if data[-1] != ord("\n"): + data += self.NEWLINE_PAD + + assert self._sock + self._sock.sendall(data) + + def _write_ctrl(self, c: str) -> None: + """Write a control character to the console.""" + if not self.dev_is_health(): + return + + assert self._sock + self._sock.sendall(bytes([ord(c.upper()) & 0x1F])) + + def _stop_impl(self) -> None: + """Close the console, ask the monitor to quit, then kill if needed.""" + self._close_sock() + + child = self._child + if child and child.isalive(): + child.sendline("quit") + deadline = time.monotonic() + self.QUIT_TIMEOUT + while child.isalive() and time.monotonic() < deadline: + time.sleep(self.CONNECT_POLL) + + if child and child.isalive(): + self._kill_process_group(child) + + self._child = None + logger.info("renode device closed") diff --git a/tests/device/test_host.py b/tests/device/test_host.py index 1d1f57c..65aeb4e 100644 --- a/tests/device/test_host.py +++ b/tests/device/test_host.py @@ -259,3 +259,33 @@ def fake_wait(timeout=5): # - test for timeout # - test for very long output # - test for + + +def test_device_host_post_spawn_hook(envconfig_dummy, monkeypatch): + """host_open() calls _post_spawn() once the child exists, before the + boot wait, and again on every reopen.""" + + conf = envconfig_dummy.product[0].cfg_core(0) + path = "./tests/resources/nuttx/sim/nuttx" + dev = DeviceHost2(conf) + + order = [] + + def post_spawn(): + assert dev._child is not None + order.append("post_spawn") + + def wait_for_boot(timeout): + order.append("boot") + return True + + monkeypatch.setattr(dev, "_post_spawn", post_spawn) + monkeypatch.setattr(dev, "_wait_for_boot", wait_for_boot) + + dev.host_open([path]) + assert order == ["post_spawn", "boot"] + + dev._dev_reopen() + assert order == ["post_spawn", "boot", "post_spawn", "boot"] + + dev.stop() diff --git a/tests/device/test_renode.py b/tests/device/test_renode.py new file mode 100644 index 0000000..f579bbf --- /dev/null +++ b/tests/device/test_renode.py @@ -0,0 +1,311 @@ +############################################################################ +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +import os +import shlex +import socket +import threading + +import pytest + +from ntfc.coreconfig import CoreConfig +from ntfc.device.getdev import get_device +from ntfc.device.renode import DeviceRenode + + +def _conf(**extra): + cfg = { + "name": "t", + "device": "renode", + "exec_args": '-e "include @config/renode/board.resc"', + "boot_timeout": 2, + "uptime": 0, + } + cfg.update(extra) + conf = CoreConfig(cfg) + conf._config["elf_path"] = "some/nuttx" + return conf + + +class FakeChild: + def __init__(self, alive=True, output=(), quits=True): + self.quits = quits + self.alive = alive + self.output = list(output) + self.sent = [] + self.pid = 4242 + + def isalive(self): + return self.alive + + def read_nonblocking(self, size=0, timeout=0): + import pexpect + + if not self.output: + raise pexpect.TIMEOUT("no data") + return self.output.pop(0) + + def sendline(self, data): + self.sent.append(data) + if data == "quit" and self.quits: + self.alive = False + + +class Listener: + """Minimal TCP peer standing in for Renode's socket terminal.""" + + def __init__(self): + self.srv = socket.socket() + self.srv.bind(("127.0.0.1", 0)) + self.srv.listen(1) + self.port = self.srv.getsockname()[1] + self.conn = None + self.thread = threading.Thread(target=self._accept, daemon=True) + self.thread.start() + + def _accept(self): + self.conn, _ = self.srv.accept() + + def wait(self): + self.thread.join(timeout=5) + assert self.conn is not None + return self.conn + + def close(self): + self.conn.close() + self.srv.close() + + +def test_device_renode_factory(envconfig_dummy): + envconfig_dummy.product_get(0)["cores"]["core0"]["device"] = "renode" + dev = get_device(envconfig_dummy.product[0].cfg_core(0)) + assert isinstance(dev, DeviceRenode) + assert dev.name == "renode" + + +def test_device_renode_start_command(): + dev = DeviceRenode(_conf()) + cmds = [] + dev.host_open = lambda cmd, uptime: cmds.append((cmd, uptime)) + dev.start() + + cmd, uptime = cmds[0] + assert uptime == 0 + argv = shlex.split("".join(cmd)) + + assert argv[0] == "renode" + assert argv[1:4] == ["--disable-gui", "--console", "--plain"] + + execs = [argv[i + 1] for i, a in enumerate(argv) if a == "-e"] + assert execs[0] == "$bin=@" + os.path.abspath("some/nuttx") + assert execs[1] == ( + f'emulation CreateServerSocketTerminal {dev._port} "ntfc" false' + ) + # user arguments run after the terminal exists, before start + assert execs[2] == "include @config/renode/board.resc" + assert execs[3] == "start" + assert dev._port > 0 + + +def test_device_renode_start_custom_exec_path(): + dev = DeviceRenode(_conf(exec_path="/opt/renode/renode")) + cmds = [] + dev.host_open = lambda cmd, uptime: cmds.append(cmd) + dev.start() + assert shlex.split("".join(cmds[0]))[0] == "/opt/renode/renode" + + +def test_device_renode_start_requires_exec_args(): + dev = DeviceRenode(_conf(exec_args="")) + with pytest.raises(KeyError): + dev.start() + + +def test_device_renode_post_spawn_connects(): + listener = Listener() + dev = DeviceRenode(_conf()) + dev._child = FakeChild() + dev._port = listener.port + + assert dev._dev_is_health_priv() is False + dev._post_spawn() + listener.wait() + assert dev._dev_is_health_priv() is True + + dev._stop_impl = lambda: None # keep FakeChild, only exercise socket + dev._close_sock() + assert dev._dev_is_health_priv() is False + listener.close() + + +def test_device_renode_post_spawn_timeout(): + # bound but not listening: connect fails until the deadline + dev = DeviceRenode(_conf(boot_timeout=0)) + dev._child = FakeChild() + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + dev._port = s.getsockname()[1] + with pytest.raises(TimeoutError): + dev._post_spawn() + + +def test_device_renode_post_spawn_retries_until_listening(): + dev = DeviceRenode(_conf(boot_timeout=5)) + dev._child = FakeChild() + srv = socket.socket() + srv.bind(("127.0.0.1", 0)) + dev._port = srv.getsockname()[1] + + # not listening yet: the first connects are refused + timer = threading.Timer(0.3, srv.listen, args=(1,)) + timer.start() + dev._post_spawn() + conn, _ = srv.accept() + assert dev._dev_is_health_priv() is True + + dev._close_sock() + conn.close() + srv.close() + + +def test_device_renode_read_without_child(): + dev = DeviceRenode(_conf()) + assert dev._read() == b"" + + +def test_device_renode_read_connection_reset(): + import struct + + listener = Listener() + dev = DeviceRenode(_conf()) + dev._child = FakeChild() + dev._port = listener.port + dev._post_spawn() + peer = listener.wait() + + # RST instead of FIN: recv raises and the console is dropped + peer.setsockopt( + socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0) + ) + peer.close() + import time + + time.sleep(0.05) + assert dev._read() == b"" + assert dev._dev_is_health_priv() is False + listener.srv.close() + + +def test_device_renode_post_spawn_child_died(): + dev = DeviceRenode(_conf(boot_timeout=5)) + dev._child = FakeChild(alive=False, output=[b"Renode error log"]) + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + dev._port = s.getsockname()[1] + with pytest.raises(IOError): + dev._post_spawn() + + +def test_device_renode_read_write(): + listener = Listener() + dev = DeviceRenode(_conf()) + child = FakeChild(output=[b"renode log line"]) + dev._child = child + dev._port = listener.port + dev._post_spawn() + peer = listener.wait() + + # nothing pending + assert dev._read() == b"" + # renode's own output was drained from the child + assert child.output == [] + + peer.sendall(b"nsh> ") + import time + + time.sleep(0.05) + assert dev._read() == b"nsh> " + + dev._write(b"help") + assert peer.recv(100) == b"help\n" + dev._write(b"ls\n") + assert peer.recv(100) == b"ls\n" + + dev._write_ctrl("c") + assert peer.recv(100) == b"\x03" + + # peer closes: read returns nothing and the device turns unhealthy + peer.close() + time.sleep(0.05) + assert dev._read() == b"" + assert dev._dev_is_health_priv() is False + assert dev._read() == b"" + dev._write(b"ignored") + dev._write_ctrl("c") + listener.close() + + +def test_device_renode_stop_quits_monitor_and_kills(): + listener = Listener() + dev = DeviceRenode(_conf()) + child = FakeChild() + dev._child = child + dev._port = listener.port + dev._post_spawn() + listener.wait() + + killed = [] + dev._kill_process_group = lambda proc: killed.append(proc) + + dev._stop_impl() + assert child.sent == ["quit"] + assert killed == [] + assert dev._child is None + assert dev._sock is None + listener.close() + + # a monitor that ignores quit is killed + dev.QUIT_TIMEOUT = 0.2 + dev._child = FakeChild(quits=False) + dev._stop_impl() + assert len(killed) == 1 + assert dev._child is None + + # stop on a stopped device is a no-op + dev._stop_impl() + + +def test_device_renode_reopen_picks_new_port(): + dev = DeviceRenode(_conf()) + ports = [] + + def fake_host_open(cmd, uptime=0): + ports.append(dev._port) + dev._child = FakeChild() + return dev._child + + dev.host_open = fake_host_open + dev.start() + dev._dev_reopen() + assert len(ports) == 2 + assert all(p > 0 for p in ports) + # the old process is gone before the new one starts, so a fresh + # ephemeral port is drawn each time rather than reusing the command + assert dev._child is not None