From 91235cb5aa21c07154a33abf1047467a7b75a1c3 Mon Sep 17 00:00:00 2001 From: Joschka Seydell Date: Wed, 10 Sep 2025 21:21:14 +0200 Subject: [PATCH 1/4] qemudriver: Allow machine-specific options. Signed-off-by: Joschka Seydell --- labgrid/driver/qemudriver.py | 7 ++++--- tests/test_qemudriver.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/labgrid/driver/qemudriver.py b/labgrid/driver/qemudriver.py index a9dc0829d..83ab5a0ab 100644 --- a/labgrid/driver/qemudriver.py +++ b/labgrid/driver/qemudriver.py @@ -156,19 +156,20 @@ def get_qemu_base_args(self): disk_opts = "" if self.disk_opts: disk_opts = f",{self.disk_opts}" - if self.machine == "vexpress-a9": + machine_base = self.machine.split(',')[0] + if machine_base == "vexpress-a9": cmd.append("-drive") cmd.append( f"if=sd,format={disk_format},file={disk_path},id=mmc0{disk_opts}") boot_args.append("root=/dev/mmcblk0p1 rootfstype=ext4 rootwait") - elif self.machine in ["pc", "q35", "virt"]: + elif machine_base in ["pc", "q35", "virt"]: cmd.append("-drive") cmd.append( f"if=virtio,format={disk_format},file={disk_path}{disk_opts}") boot_args.append("root=/dev/vda rootwait") else: raise NotImplementedError( - f"QEMU disk image support not implemented for machine '{self.machine}'" + f"QEMU disk image support not implemented for machine '{machine_base}'" ) if self.rootfs is not None: cmd.append("-fsdev") diff --git a/tests/test_qemudriver.py b/tests/test_qemudriver.py index bfbe08edb..205c0ccc3 100644 --- a/tests/test_qemudriver.py +++ b/tests/test_qemudriver.py @@ -13,7 +13,8 @@ def qemu_env(tmpdir): role: foo images: kernel: "test.zImage" - dtb: test.dtb" + disk: "test.qcow2" + dtb: "test.dtb" tools: qemu: "qemu-system-arm" paths: @@ -69,6 +70,15 @@ def qemu_version_mock(mocker): def test_qemu_instance(qemu_target, qemu_driver): assert (isinstance(qemu_driver, QEMUDriver)) +def test_qemu_get_qemu_base_args_disk(qemu_target, qemu_driver): + qemu_driver.disk = 'disk' + supported_machines = ['vexpress-a9', 'pc', 'q35', 'virt'] + for machine in supported_machines: + qemu_driver.machine = machine + qemu_driver.get_qemu_base_args() + qemu_driver.machine = machine + ',option=value' + qemu_driver.get_qemu_base_args() + def test_qemu_activate_deactivate(qemu_target, qemu_driver, qemu_version_mock): qemu_target.activate(qemu_driver) qemu_target.deactivate(qemu_driver) From d50974cb8b1b8fe28582a974d4f1b2409bfe34d9 Mon Sep 17 00:00:00 2001 From: Joschka Seydell Date: Wed, 10 Sep 2025 22:14:16 +0200 Subject: [PATCH 2/4] qemudriver: Support interactions via monitor_command before spinning off the instance. Signed-off-by: Joschka Seydell --- doc/configuration.rst | 4 ++++ labgrid/driver/qemudriver.py | 15 +++++++++++---- tests/test_qemudriver.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index 08429d67f..9ea9ccb10 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -2867,6 +2867,10 @@ The QEMUDriver also requires the specification of: specify the build device tree - a path key, this is the path to the rootfs +To allow interactions with a prepared, not yet started QEMU instance, the ``prepare()`` method +can be explicitly called, after which ``monitor_command(...)`` can be run before eventually +releasing the CPU(s) via ``on()``. + SigrokDriver ~~~~~~~~~~~~ The :any:`SigrokDriver` uses a `SigrokDevice`_ resource to record samples and provides diff --git a/labgrid/driver/qemudriver.py b/labgrid/driver/qemudriver.py index 83ab5a0ab..89fec2d01 100644 --- a/labgrid/driver/qemudriver.py +++ b/labgrid/driver/qemudriver.py @@ -259,9 +259,9 @@ def on_deactivate(self): shutil.rmtree(self._tempdir) @step() - def on(self): - """Start the QEMU subprocess, accept the unix socket connection and - afterwards start the emulator using a QMP Command""" + def prepare(self): + """Start the QEMU subprocess and accept the unix socket connection + if not already prepared.""" if self.status: return self.logger.debug("Starting with: %s", self._cmd) @@ -287,7 +287,14 @@ def on(self): # Restore port forwards for v in self._forwarded_ports.values(): - self._add_port_forward(*v) + self._add_port_forward(*v) + + @step() + def on(self): + """Prepare the instance (only if not done already) and start the emulator + using a QMP Command""" + if not self._child: + self.prepare() self.monitor_command("cont") diff --git a/tests/test_qemudriver.py b/tests/test_qemudriver.py index 205c0ccc3..ad9519567 100644 --- a/tests/test_qemudriver.py +++ b/tests/test_qemudriver.py @@ -1,6 +1,7 @@ import pytest from labgrid.driver import QEMUDriver +from labgrid.driver.exception import ExecutionError from labgrid import Environment @pytest.fixture @@ -61,6 +62,12 @@ def qemu_mock(mocker): socket_mock = mocker.patch('socket.socket') socket_mock.return_value.accept.return_value = mocker.MagicMock(), '' +@pytest.fixture +def qemu_qmp_mock(mocker): + monitor_mock = mocker.patch('labgrid.driver.qemudriver.QMPMonitor') + monitor_mock.return_value.execute.return_value = {'return': {}} + return monitor_mock + @pytest.fixture def qemu_version_mock(mocker): run_mock = mocker.patch('subprocess.run') @@ -91,6 +98,27 @@ def test_qemu_on_off(qemu_target, qemu_driver, qemu_mock, qemu_version_mock): qemu_target.deactivate(qemu_driver) +def test_qemu_prepare(qemu_target, qemu_driver, qemu_mock, qemu_version_mock): + qemu_target.activate(qemu_driver) + + qemu_driver.prepare() + qemu_driver.on() + +def test_qemu_monitor_command_without_prepare(qemu_target, qemu_driver, qemu_mock, qemu_version_mock, qemu_qmp_mock): + qemu_target.activate(qemu_driver) + + with pytest.raises(ExecutionError): + qemu_driver.monitor_command("info") + qemu_qmp_mock.assert_not_called() + +def test_qemu_prepare_with_monitor_command(qemu_target, qemu_driver, qemu_mock, qemu_version_mock, qemu_qmp_mock): + qemu_target.activate(qemu_driver) + + qemu_driver.prepare() + qemu_driver.monitor_command("info") + qemu_qmp_mock.assert_called_once() + qemu_qmp_mock.return_value.execute.assert_called_with("info", {}) + def test_qemu_read_write(qemu_target, qemu_driver, qemu_mock, qemu_version_mock): qemu_target.activate(qemu_driver) From 7c74baf15ec5c2edff72b04b4545285317c6d6a6 Mon Sep 17 00:00:00 2001 From: Joschka Seydell Date: Mon, 6 Oct 2025 06:29:51 +0200 Subject: [PATCH 3/4] dockerfiles: Add missing VERSION parameter for building containers. Signed-off-by: Joschka Seydell --- dockerfiles/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerfiles/README.rst b/dockerfiles/README.rst index b0f8ff150..0128b03ba 100644 --- a/dockerfiles/README.rst +++ b/dockerfiles/README.rst @@ -22,7 +22,7 @@ Example showing how to build labgrid-client image: .. code-block:: bash - $ docker build --target labgrid-client -t docker.io/labgrid/client -f dockerfiles/Dockerfile . + $ docker build --build-arg VERSION="$(python -m setuptools_scm)" --target labgrid-client -t docker.io/labgrid/client -f dockerfiles/Dockerfile . Using `BuildKit `_ is recommended to reduce build times. From a77880073c1b7905642db312e41fde74d2c13820 Mon Sep 17 00:00:00 2001 From: Joschka Seydell Date: Mon, 6 Oct 2025 06:34:55 +0200 Subject: [PATCH 4/4] qemudriver: Add type hints. Signed-off-by: Joschka Seydell --- labgrid/driver/qemudriver.py | 52 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/labgrid/driver/qemudriver.py b/labgrid/driver/qemudriver.py index 89fec2d01..0f77d1525 100644 --- a/labgrid/driver/qemudriver.py +++ b/labgrid/driver/qemudriver.py @@ -8,6 +8,7 @@ import re import tempfile import time +from typing import List, Optional, Dict, Tuple, Any, Callable, Union import attr from pexpect import TIMEOUT @@ -95,18 +96,18 @@ class QEMUDriver(ConsoleExpectMixin, Driver, PowerProtocol, ConsoleProtocol): default=None, validator=attr.validators.optional(attr.validators.instance_of(str))) - def __attrs_post_init__(self): + def __attrs_post_init__(self) -> None: super().__attrs_post_init__() - self.status = 0 - self.txdelay = None - self._child = None - self._tempdir = None - self._socket = None - self._clientsocket = None - self._forwarded_ports = {} + self.status: int = 0 + self.txdelay: Optional[float] = None + self._child: Optional[subprocess.Popen] = None + self._tempdir: Optional[str] = None + self._socket: Optional[socket.socket] = None + self._clientsocket: Optional[socket.socket] = None + self._forwarded_ports: Dict[Tuple[str, str, int], Tuple[str, str, int, str, int]] = {} atexit.register(self._atexit) - def _atexit(self): + def _atexit(self) -> None: if not self._child: return self._child.terminate() @@ -116,7 +117,7 @@ def _atexit(self): self._child.kill() self._child.communicate(timeout=1) - def get_qemu_version(self, qemu_bin): + def get_qemu_version(self, qemu_bin: str) -> Tuple[int, int, int]: p = subprocess.run([qemu_bin, "-version"], stdout=subprocess.PIPE, encoding="utf-8") if p.returncode != 0: raise ExecutionError(f"Unable to get QEMU version. QEMU exited with: {p.returncode}") @@ -127,7 +128,7 @@ def get_qemu_version(self, qemu_bin): return (int(m.group('major')), int(m.group('minor')), int(m.group('micro'))) - def get_qemu_base_args(self): + def get_qemu_base_args(self) -> List[str]: """Returns the base command line used for Qemu without the options related to QMP. These options can be used to start an interactive Qemu manually for debugging tests @@ -230,7 +231,7 @@ def get_qemu_base_args(self): return cmd - def on_activate(self): + def on_activate(self) -> None: self._tempdir = tempfile.mkdtemp(prefix="labgrid-qemu-tmp-") sockpath = f"{self._tempdir}/serialrw" self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -248,7 +249,7 @@ def on_activate(self): self._cmd.append("-serial") self._cmd.append("chardev:serialsocket") - def on_deactivate(self): + def on_deactivate(self) -> None: if self.status: self.off() if self._clientsocket: @@ -259,7 +260,7 @@ def on_deactivate(self): shutil.rmtree(self._tempdir) @step() - def prepare(self): + def prepare(self) -> None: """Start the QEMU subprocess and accept the unix socket connection if not already prepared.""" if self.status: @@ -290,7 +291,7 @@ def prepare(self): self._add_port_forward(*v) @step() - def on(self): + def on(self) -> None: """Prepare the instance (only if not done already) and start the emulator using a QMP Command""" if not self._child: @@ -299,7 +300,7 @@ def on(self): self.monitor_command("cont") @step() - def off(self): + def off(self) -> None: """Stop the emulator using a monitor command and await the exitcode""" if not self.status: return @@ -310,37 +311,38 @@ def off(self): self._child = None self.status = 0 - def cycle(self): + def cycle(self) -> None: """Cycle the emulator by restarting it""" self.off() self.on() @step(result=True, args=['command', 'arguments']) - def monitor_command(self, command, arguments={}): + def monitor_command(self, command: str, arguments: Dict[str, Any] = {}) -> Any: """Execute a monitor_command via the QMP""" if not self.status: raise ExecutionError( "Can't use monitor command on non-running target") return self.qmp.execute(command, arguments) - def _add_port_forward(self, proto, local_address, local_port, remote_address, remote_port): + def _add_port_forward(self, proto: str, local_address: str, local_port: int, remote_address: str, remote_port: int) -> None: self.monitor_command( "human-monitor-command", {"command-line": f"hostfwd_add {proto}:{local_address}:{local_port}-{remote_address}:{remote_port}"}, ) - def add_port_forward(self, proto, local_address, local_port, remote_address, remote_port): + def add_port_forward(self, proto: str, local_address: str, local_port: int, remote_address: str, remote_port: int) -> None: self._add_port_forward(proto, local_address, local_port, remote_address, remote_port) - self._forwarded_ports[(proto, local_address, local_port)] = (proto, local_address, local_port, remote_address, remote_port) + self._forwarded_ports[(proto, local_address, local_port)] = ( + proto, local_address, local_port, remote_address, remote_port) - def remove_port_forward(self, proto, local_address, local_port): + def remove_port_forward(self, proto: str, local_address: str, local_port: int) -> None: del self._forwarded_ports[(proto, local_address, local_port)] self.monitor_command( "human-monitor-command", {"command-line": f"hostfwd_remove {proto}:{local_address}:{local_port}"}, ) - def _read(self, size=1, timeout=10, max_size=None): + def _read(self, size: int = 1, timeout: float = 10, max_size: Optional[int] = None) -> bytes: ready, _, _ = select.select([self._clientsocket], [], [], timeout) if ready: # Collect some more data @@ -353,8 +355,8 @@ def _read(self, size=1, timeout=10, max_size=None): raise TIMEOUT(f"Timeout of {timeout:.2f} seconds exceeded") return res - def _write(self, data): + def _write(self, data: bytes) -> int: return self._clientsocket.send(data) - def __str__(self): + def __str__(self) -> str: return f"QemuDriver({self.target.name})"