Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/reusable-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
${{ runner.os }}-pip-
- name: Install system dependencies
run: |
sudo apt-get install -yq libow-dev openssh-server openssh-client graphviz openocd
sudo apt-get install -yq libow-dev openssh-server openssh-client graphviz openocd qemu-system-arm
sudo mkdir -p /var/cache/labgrid/runner && sudo chown runner /var/cache/labgrid/runner
- name: Prepare local SSH
run: |
Expand Down
4 changes: 4 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.docker.com/develop/develop-images/build_enhancements/>`_
is recommended to reduce build times.
Expand Down
70 changes: 40 additions & 30 deletions labgrid/driver/qemudriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import tempfile
import time
from typing import List, Optional, Dict, Tuple, Any

import attr
from pexpect import TIMEOUT
Expand Down Expand Up @@ -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()
Expand All @@ -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}")
Expand All @@ -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
Expand Down Expand Up @@ -156,19 +157,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")
Expand Down Expand Up @@ -229,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)
Expand All @@ -247,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:
Expand All @@ -258,9 +260,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) -> None:
"""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)
Expand All @@ -286,12 +288,19 @@ 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) -> None:
"""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")

@step()
def off(self):
def off(self) -> None:
"""Stop the emulator using a monitor command and await the exitcode"""
if not self.status:
return
Expand All @@ -302,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
Expand All @@ -345,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})"
40 changes: 39 additions & 1 deletion tests/test_qemudriver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from labgrid.driver import QEMUDriver
from labgrid.driver.exception import ExecutionError
from labgrid import Environment

@pytest.fixture
Expand All @@ -13,7 +14,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:
Expand Down Expand Up @@ -60,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')
Expand All @@ -69,6 +77,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)
Expand All @@ -81,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)

Expand Down