mail archive of the barebox mailing list
 help / color / mirror / Atom feed
From: Ahmad Fatoum <a.fatoum@barebox.org>
To: barebox@lists.infradead.org
Cc: Ahmad Fatoum <a.fatoum@barebox.org>
Subject: [PATCH 2/5] test: move interactive QEMU launcher out of pytest
Date: Fri, 26 Jun 2026 14:45:43 +0200	[thread overview]
Message-ID: <20260626124555.1644951-2-a.fatoum@barebox.org> (raw)
In-Reply-To: <20260626124555.1644951-1-a.fatoum@barebox.org>

pytest --interactive apparently starts two QEMUs, the interactive one in
addition to the one started directly by Labgrid. This leads to issues
when QEMU does something mutually exclusive like listening on a port or
taking a file lock.

Arguably, running an interactive QEMU with pytest as a runner was
a strange idea in the first place. Let's stop doing that and add a
dedicated qemu_interactive.py script.

The new script is imported by conftest.py to get support for custom
options like --blk or --rng. --interactive becomes an error.

To avoid cross-dependencies between test suite and the new script, the
CONFIG_NAME resolution is reimplemented without test/py/helper.py, but
apart from that it's mostly copy-paste and adding some boilerplate.

In the future[1], we can adapt this to use the new QEMUDriver.interact().

[1]: https://github.com/labgrid-project/labgrid/pull/1892

Assisted-by: Codex:gpt-5.5
Reported-by: Marc Kleine-Budde <mkl@pengutronix.de>
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
 Documentation/boards/emulated.rst        |  10 +-
 Documentation/devel/contributing.rst     |   8 +-
 Documentation/user/security-policies.rst |   2 +-
 conftest.py                              | 243 ++---------
 scripts/qemu_interactive.py              | 534 +++++++++++++++--------
 5 files changed, 375 insertions(+), 422 deletions(-)
 mode change 100644 => 100755 scripts/qemu_interactive.py

diff --git a/Documentation/boards/emulated.rst b/Documentation/boards/emulated.rst
index 0d6dd85dc759..bc3e2566eaec 100644
--- a/Documentation/boards/emulated.rst
+++ b/Documentation/boards/emulated.rst
@@ -23,15 +23,15 @@ VirtIO over PCI.
 Running in QEMU
 ---------------
 
-Emulated targets can be started interactively with ``pytest --interactive``::
+Emulated targets can be started interactively with ``scripts/qemu_interactive.py``::
 
   # Run x86 VM runnig the EFI payload from efi_defconfig
-  pytest --lg-env test/x86/efi_defconfig.yaml --interactive
+  scripts/qemu_interactive.py test/x86/efi_defconfig.yaml
 
   # Identical to above, provided the CONFIG_NAME=efi_defconfig
-  pytest --interactive
+  scripts/qemu_interactive.py
 
-The test suite can be run by omitting the ``--interactive``.
+The test suite can be run with ``pytest`` instead.
 For more information, see the :ref:`labgrid` section in the
 :ref:`contributing` guide.
 
@@ -43,7 +43,7 @@ be used in combination with QEMU. With user-mode networking (SLIRP),
 guest-to-host UDP works via NAT out of the box,
 but unsolicited host-to-guest UDP requires an explicit port forward::
 
-  pytest --lg-env test/arm/multi_v8_defconfig.yaml --interactive \
+  scripts/qemu_interactive.py test/arm/multi_v8_defconfig.yaml    \
     --env nv/dev.netconsole.ip=10.0.2.2                          \
     --env nv/dev.netconsole.port=6666                            \
     --env init/netconsole="ifup -a1; netconsole.active=ioe"      \
diff --git a/Documentation/devel/contributing.rst b/Documentation/devel/contributing.rst
index cb3820f23c63..8b92c6f483aa 100644
--- a/Documentation/devel/contributing.rst
+++ b/Documentation/devel/contributing.rst
@@ -134,7 +134,7 @@ it directly from PyPI instead of your distro's package repositories::
 Example usage::
 
   # Run x86 VM runnig the EFI payload from efi_defconfig
-  pytest --lg-env test/x86/efi_defconfig.yaml --interactive
+  scripts/qemu_interactive.py test/x86/efi_defconfig.yaml
 
   # Run the test suite against the same
   pytest --lg-env test/x86/efi_defconfig.yaml
@@ -145,10 +145,10 @@ built out-of-tree, the build directory must be pointed at by
 ``LG_BUILDDIR``, ``KBUILD_OUTPUT`` or a ``build`` symlink.
 
 Additional QEMU command-line options can be added by specifying
-them after the ``--qemu`` option::
+them after ``--qemu``::
 
   # appends -device ? to the command line. Add --dry-run to see the final result
-  pytest --lg-env test/riscv/rv64i_defconfig.yaml --interactive --qemu -device '?'
+  scripts/qemu_interactive.py test/riscv/rv64i_defconfig.yaml --qemu -device '?'
 
 Some of the QEMU options that are used more often also have explicit
 support in the test runner, so paravirtualized devices can be added
@@ -158,7 +158,7 @@ more easily::
   pytest --lg-env test/arm/virt@multi_v8_defconfig.yaml --blk=rootfs.ext4
 
   # Run interactively with graphics output
-  pytest --lg-env test/mips/qemu-malta_defconfig.yaml --interactive --graphics
+  scripts/qemu_interactive.py test/mips/qemu-malta_defconfig.yaml --graphics
 
 For testing, the QEMU fw_cfg and virtfs support is particularly useful::
 
diff --git a/Documentation/user/security-policies.rst b/Documentation/user/security-policies.rst
index 8d515508d106..296191c4f8e5 100644
--- a/Documentation/user/security-policies.rst
+++ b/Documentation/user/security-policies.rst
@@ -97,7 +97,7 @@ policy development and evaluation. ``images/barebox-dt-2nd.img`` that results
 from building it can be passed as argument to ``qemu-system-arm -M virt -kernel``.
 
 The easiest way to do this is probably installing labgrid and running
-``pytest --interactive`` after having built the config.
+``scripts/qemu_interactive.py`` after having built the config.
 
 Differences from Kconfig
 ------------------------
diff --git a/conftest.py b/conftest.py
index 6f76586e010b..4ab76b295e59 100644
--- a/conftest.py
+++ b/conftest.py
@@ -3,6 +3,7 @@ import pytest
 import os
 import argparse
 from labgrid.exceptions import NoDriverFoundError
+from scripts import qemu_interactive
 from test.py import helper
 
 
@@ -38,68 +39,24 @@ def barebox_config(request, strategy, target):
     return helper.get_config(command)
 
 
-def get_enabled_arch(config):
-    # Get the absolute path to the directory containing this script
-    base_dir = os.path.dirname(os.path.abspath(__file__))
-
-    # Path to the 'arch' directory relative to this script
-    arch_dir = os.path.join(base_dir, "arch")
-
-    if not os.path.isdir(arch_dir):
-        return None
-
-    # Optional mapping from directory names to config key suffixes
-    arch_map = {"powerpc": "ppc"}
-
-    for entry in os.listdir(arch_dir):
-        path = os.path.join(arch_dir, entry)
-        if os.path.isdir(path):
-            key_suffix = arch_map.get(entry, entry).upper()
-            config_key = f"CONFIG_{key_suffix}"
-            if config.get(config_key):
-                return entry
-
-    return None
-
-
-def guess_lg_env():
-    config_file = helper.open_config_file(os.environ['LG_BUILDDIR'] + "/.config")
-    config = helper.parse_config(config_file)
-    if not config or not config.get('CONFIG_NAME'):
-        return None
-    arch = get_enabled_arch(config)
-    if not arch:
-        return None
-    filename = os.path.join("test", arch, f"{config['CONFIG_NAME']}.yaml")
-    if os.path.exists(filename):
-        return filename
-    return None
-
-
 lg_env = lg_builddir = None
 
 
 def pytest_configure(config):
-    if 'LG_BUILDDIR' not in os.environ:
-        if 'KBUILD_OUTPUT' in os.environ:
-            os.environ['LG_BUILDDIR'] = os.environ['KBUILD_OUTPUT']
-        elif os.path.isdir('build'):
-            os.environ['LG_BUILDDIR'] = os.path.realpath('build')
-        else:
-            os.environ['LG_BUILDDIR'] = os.getcwd()
-
-    if os.environ['LG_BUILDDIR'] is not None:
-        os.environ['LG_BUILDDIR'] = os.path.realpath(os.environ['LG_BUILDDIR'])
+    removed_mode = getattr(config.option, 'qemu_interactive_removed', None)
+    if removed_mode is not None:
+        pytest.exit(qemu_interactive.replacement_message(removed_mode),
+                    returncode=2)
 
     global lg_env
     global lg_builddir
 
-    lg_builddir = os.environ['LG_BUILDDIR']
+    lg_builddir = qemu_interactive.resolve_lg_builddir()
     lg_env = config.option.lg_env
     if lg_env is None:
         lg_env = os.environ.get('LG_ENV')
     if lg_env is None:
-        if lg_env := guess_lg_env():
+        if lg_env := qemu_interactive.guess_lg_env(lg_builddir):
             os.environ['LG_ENV'] = lg_env
 
 
@@ -113,39 +70,22 @@ def pytest_report_header(config):
 
 
 def pytest_addoption(parser):
-    def assignment(arg):
-        return arg.split('=', 1)
+    parser.addoption('--interactive', action='store_const',
+                     const='--interactive', dest='qemu_interactive_removed',
+                     help=argparse.SUPPRESS)
+    parser.addoption('--dry-run', action='store_const',
+                     const='--dry-run', dest='qemu_interactive_removed',
+                     help=argparse.SUPPRESS)
+    parser.addoption('--dump-dtb', action='store_const',
+                     const='--dump-dtb', dest='qemu_interactive_removed',
+                     help=argparse.SUPPRESS)
 
-    parser.addoption('--interactive', action='store_const', const='qemu_interactive',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just start Qemu interactively'))
-    parser.addoption('--dry-run', action='store_const', const='qemu_dry_run',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just print Qemu command line'))
-    parser.addoption('--dump-dtb', action='store_const', const='qemu_dump_dtb',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just dump the Qemu device tree'))
-    parser.addoption('--graphic', '--graphics', action='store_true', dest='qemu_graphics',
-                     help=('enable QEMU graphics output'))
-    parser.addoption('--rng', action='count', dest='qemu_rng',
-                     help=('instantiate Virt I/O random number generator'))
-    parser.addoption('--console', action='count', dest='qemu_console', default=0,
-                     help=('Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox'))
-    parser.addoption('--fs', action='append', dest='qemu_fs',
-                     default=[], metavar="[tag=]DIR", type=assignment,
-                     help=('Pass directory trees to emulated barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--blk', action='append', dest='qemu_block',
-                     default=[], metavar="FILE",
-                     help=('Pass block device to emulated barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--env', action='append', dest='qemu_fw_cfg', type=assignment,
-                     default=[], metavar="[envpath=]content | [envpath=]@filepath",
-                     help=('Pass barebox environment files to barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--qemu', dest='qemu_arg', nargs=argparse.REMAINDER, default=[],
-                     help=('Pass all remaining options to QEMU as is'))
-    parser.addoption('--bootarg', action='append', dest='bootarg', default=[],
-                     help=('Pass boot arguments to barebox for debugging purposes'))
-    parser.addoption('--port-forward', metavar="PORT", action='append', dest='qemu_port', default=[],
-                     help=('Forward incoming TCP or UDP connections on specified PORT'))
+    group = parser.getgroup('barebox-qemu', 'barebox QEMU options')
+    qemu_interactive.register_shared_options(group)
+
+
+def _pytest_exit(message, returncode=1):
+    pytest.exit(message, returncode=returncode)
 
 
 @pytest.fixture(scope="session")
@@ -155,142 +95,9 @@ def strategy(request, target, pytestconfig):  # noqa: max-complexity=30
     except NoDriverFoundError as e:
         pytest.exit(e)
 
-    try:
-        main = target.env.config.data["targets"]["main"]
-        features = main["features"]
-    except KeyError:
-        features = []
-
-    try:
-        main = target.env.config.data["targets"]["main"]
-        yaml_env = main["env"]
-    except KeyError:
-        yaml_env = {}
-
-    try:
-        main = target.env.config.data["targets"]["main"]
-        qemu_bin = main["drivers"]["QEMUDriver"]["qemu_bin"]
-        features.append("qemu")
-    except KeyError:
-        pass
-
-    virtio = None
-
-    if "virtio-mmio" in features:
-        virtio = "device"
-        strategy.append_qemu_args('-global virtio-mmio.force-legacy=false')
-    if "virtio-pci" in features:
-        virtio = "pci,disable-modern=off"
-        features.append("pci")
-
-    if virtio and pytestconfig.option.qemu_rng:
-        for i in range(pytestconfig.option.qemu_rng):
-            strategy.append_qemu_args("-device", f"virtio-rng-{virtio}")
-
-    for i in range(pytestconfig.option.qemu_console):
-        if virtio and i == 0:
-            strategy.append_qemu_args(
-                "-device", f"virtio-serial-{virtio}",
-                "-chardev", f"pty,id=virtcon{i}",
-                "-device", f"virtconsole,chardev=virtcon{i},name=console.virtcon{i}"
-            )
-            continue
-
-        # ns16550 serial driver only works with x86 so far
-        if 'pci' in features:
-            strategy.append_qemu_args(
-                "-chardev", f"pty,id=pcicon{i}",
-                "-device", f"pci-serial,chardev=pcicon{i}"
-            )
-        else:
-            pytest.exit("barebox currently supports only a single extra virtio console\n", 1)
-
-    if "qemu" in features:
-        if not pytestconfig.option.qemu_graphics:
-            graphics = '-nographic'
-        elif qemu_bin == "qemu-system-x86_64":
-            graphics = '-device isa-vga'
-        elif 'pci' in features:
-            graphics = '-device VGA'
-        elif virtio:
-            graphics = '-vga none -device ramfb'
-            graphics += f' -device virtio-keyboard-{virtio}'
-        else:
-            pytest.exit("--graphics unsupported for target\n", 1)
-
-        if graphics is not None and \
-                pytestconfig.option.lg_initial_state != 'qemu_interactive':
-            graphics += ' -display none'
-
-        strategy.append_qemu_args(graphics)
-
-    for i, blk in enumerate(pytestconfig.option.qemu_block):
-        if virtio:
-            strategy.append_qemu_args(
-                "-drive", f"if=none,format=raw,id=hd{i},file={blk}",
-                "-device", f"virtio-blk-{virtio},drive=hd{i}"
-            )
-        else:
-            pytest.exit("--blk unsupported for target\n", 1)
-
-    envopts = {}
-
-    for i, fw_cfg in enumerate(pytestconfig.option.qemu_fw_cfg):
-        value = fw_cfg.pop()
-        envpath = fw_cfg.pop() if fw_cfg else f"data/fw_cfg{i}"
-
-        envopts[envpath] = value
-
-    for envpath, value in (yaml_env | envopts).items():
-        if virtio:
-            if isinstance(value, str) and value.startswith('@'):
-                source = f"file='{value[1:]}'"
-            else:
-                source = f"string='{value}'"
-
-            strategy.append_qemu_args(
-                '-fw_cfg', f'name=opt/org.barebox.env/{envpath},{source}'
-            )
-        else:
-            pytest.exit("env unsupported for target\n", 1)
-
-    if len(pytestconfig.option.bootarg) > 0:
-        strategy.append_qemu_bootargs(pytestconfig.option.bootarg)
-
-    for arg in pytestconfig.option.qemu_arg:
-        strategy.append_qemu_args(arg)
-
-    qemu_nic = "user,id=net0"
-
-    for port in pytestconfig.option.qemu_port:
-        qemu_nic += f",hostfwd=udp:127.0.0.2:{port}-:{port}"
-        qemu_nic += f",hostfwd=tcp:127.0.0.2:{port}-:{port}"
-
-    if "testfs" in features:
-        if not any(fs and fs[0] == "testfs" for fs in pytestconfig.option.qemu_fs):
-            testfs_path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
-            pytestconfig.option.qemu_fs.append(["testfs", testfs_path])
-            os.makedirs(testfs_path, exist_ok=True)
-            qemu_nic += f",tftp={testfs_path}"
-
-    if "qemu" in features:
-        strategy.append_qemu_args("-nic", qemu_nic)
-
-    for i, fs in enumerate(pytestconfig.option.qemu_fs):
-        if virtio:
-            path = fs.pop()
-            tag = fs.pop() if fs else f"fs{i}"
-
-            strategy.append_qemu_args(
-                "-fsdev", f"local,security_model=none,id=fs{i},path={path}",
-                "-device", f"virtio-9p-{virtio},id=fs{i},fsdev=fs{i},mount_tag={tag}"
-            )
-        else:
-            pytest.exit("--fs unsupported for target\n", 1)
-
-    state = request.config.option.lg_initial_state
-    if state is not None:
-        strategy.force(state)
+    qemu_interactive.apply_shared_options(
+        strategy, target, pytestconfig.option, interactive=False,
+        fail=_pytest_exit)
 
     return strategy
 
diff --git a/scripts/qemu_interactive.py b/scripts/qemu_interactive.py
old mode 100644
new mode 100755
index 6f76586e010b..a8605d23b132
--- a/scripts/qemu_interactive.py
+++ b/scripts/qemu_interactive.py
@@ -1,195 +1,218 @@
+#!/usr/bin/env python3
+# /// script
+# requires-python = ">=3.9"
+# dependencies = ["labgrid"]
+# ///
 # SPDX-License-Identifier: GPL-2.0-only
-import pytest
-import os
+
 import argparse
-from labgrid.exceptions import NoDriverFoundError
-from test.py import helper
+import glob
+import os
+import re
+import subprocess
+import sys
 
 
-def transition_to_barebox(request, strategy):
-    try:
-        strategy.transition('barebox')
-    except Exception as e:
-        # If a normal strategy transition fails, there's no point in
-        # continuing the test. Let's print stderr and exit.
-        capmanager = request.config.pluginmanager.getplugin("capturemanager")
-        with capmanager.global_and_fixture_disabled():
-            _, stderr = capmanager.read_global_capture()
-            pytest.exit(f"{type(e).__name__}(\"{e}\"). Standard error:\n{stderr}",
-                        returncode=3)
+MODE_INTERACTIVE = "qemu_interactive"
+MODE_DRY_RUN = "qemu_dry_run"
+MODE_DUMP_DTB = "qemu_dump_dtb"
 
 
-@pytest.fixture(scope='function')
-def barebox(request, strategy, target):
-    transition_to_barebox(request, strategy)
-    return target.get_driver('BareboxDriver')
+class QemuInteractiveError(Exception):
+    def __init__(self, message, returncode=1):
+        super().__init__(message)
+        self.returncode = returncode
 
 
-@pytest.fixture(scope='function')
-def shell(strategy, target):
-    strategy.transition('shell')
-    return target.get_driver('ShellDriver')
+def _add_option(parser, *opts, **attrs):
+    if hasattr(parser, "addoption"):
+        return parser.addoption(*opts, **attrs)
+
+    return parser.add_argument(*opts, **attrs)
 
 
-@pytest.fixture(scope="session")
-def barebox_config(request, strategy, target):
-    transition_to_barebox(request, strategy)
-    command = target.get_driver("BareboxDriver")
-    return helper.get_config(command)
+def _assignment(arg):
+    return arg.split("=", 1)
 
 
-def get_enabled_arch(config):
-    # Get the absolute path to the directory containing this script
-    base_dir = os.path.dirname(os.path.abspath(__file__))
-
-    # Path to the 'arch' directory relative to this script
-    arch_dir = os.path.join(base_dir, "arch")
-
-    if not os.path.isdir(arch_dir):
-        return None
-
-    # Optional mapping from directory names to config key suffixes
-    arch_map = {"powerpc": "ppc"}
-
-    for entry in os.listdir(arch_dir):
-        path = os.path.join(arch_dir, entry)
-        if os.path.isdir(path):
-            key_suffix = arch_map.get(entry, entry).upper()
-            config_key = f"CONFIG_{key_suffix}"
-            if config.get(config_key):
-                return entry
-
-    return None
+def register_shared_options(parser):
+    _add_option(parser, "--graphic", "--graphics", action="store_true",
+                dest="qemu_graphics", help="enable QEMU graphics output")
+    _add_option(parser, "--rng", action="count", dest="qemu_rng",
+                help="instantiate Virt I/O random number generator")
+    _add_option(parser, "--console", action="count", dest="qemu_console",
+                default=0,
+                help="Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox")
+    _add_option(parser, "--fs", action="append", dest="qemu_fs",
+                default=[], metavar="[tag=]DIR", type=_assignment,
+                help="Pass directory trees to emulated barebox. Can be specified more than once")
+    _add_option(parser, "--blk", action="append", dest="qemu_block",
+                default=[], metavar="FILE",
+                help="Pass block device to emulated barebox. Can be specified more than once")
+    _add_option(parser, "--env", action="append", dest="qemu_fw_cfg",
+                type=_assignment, default=[],
+                metavar="[envpath=]content | [envpath=]@filepath",
+                help="Pass barebox environment files to barebox. Can be specified more than once")
+    _add_option(parser, "--qemu", dest="qemu_arg",
+                nargs=argparse.REMAINDER, default=[],
+                help="Pass all remaining options to QEMU as is")
+    _add_option(parser, "--bootarg", action="append", dest="bootarg",
+                default=[],
+                help="Pass boot arguments to barebox for debugging purposes")
+    _add_option(parser, "--port-forward", metavar="PORT", action="append",
+                dest="qemu_port", default=[],
+                help="Forward incoming TCP or UDP connections on specified PORT")
 
 
-def guess_lg_env():
-    config_file = helper.open_config_file(os.environ['LG_BUILDDIR'] + "/.config")
-    config = helper.parse_config(config_file)
-    if not config or not config.get('CONFIG_NAME'):
-        return None
-    arch = get_enabled_arch(config)
-    if not arch:
-        return None
-    filename = os.path.join("test", arch, f"{config['CONFIG_NAME']}.yaml")
-    if os.path.exists(filename):
-        return filename
-    return None
+def register_interactive_options(parser):
+    _add_option(parser, "--lg-env", dest="lg_env", metavar="LG_ENV",
+                help="labgrid environment config file")
+
+    if hasattr(parser, "add_mutually_exclusive_group"):
+        mode_parser = parser.add_mutually_exclusive_group()
+    else:
+        mode_parser = parser
+
+    _add_option(mode_parser, "--dry-run", action="store_const",
+                const=MODE_DRY_RUN, default=MODE_INTERACTIVE,
+                dest="qemu_mode",
+                help="print the QEMU command line instead of executing it")
+    _add_option(mode_parser, "--dump-dtb", action="store_const",
+                const=MODE_DUMP_DTB, dest="qemu_mode",
+                help="run QEMU only to dump the device tree")
 
 
-lg_env = lg_builddir = None
+def repo_root():
+    return os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir))
 
 
-def pytest_configure(config):
-    if 'LG_BUILDDIR' not in os.environ:
-        if 'KBUILD_OUTPUT' in os.environ:
-            os.environ['LG_BUILDDIR'] = os.environ['KBUILD_OUTPUT']
-        elif os.path.isdir('build'):
-            os.environ['LG_BUILDDIR'] = os.path.realpath('build')
+def resolve_lg_builddir():
+    if "LG_BUILDDIR" not in os.environ:
+        if "KBUILD_OUTPUT" in os.environ:
+            os.environ["LG_BUILDDIR"] = os.environ["KBUILD_OUTPUT"]
+        elif os.path.isdir("build"):
+            os.environ["LG_BUILDDIR"] = os.path.realpath("build")
         else:
-            os.environ['LG_BUILDDIR'] = os.getcwd()
+            os.environ["LG_BUILDDIR"] = os.getcwd()
 
-    if os.environ['LG_BUILDDIR'] is not None:
-        os.environ['LG_BUILDDIR'] = os.path.realpath(os.environ['LG_BUILDDIR'])
-
-    global lg_env
-    global lg_builddir
-
-    lg_builddir = os.environ['LG_BUILDDIR']
-    lg_env = config.option.lg_env
-    if lg_env is None:
-        lg_env = os.environ.get('LG_ENV')
-    if lg_env is None:
-        if lg_env := guess_lg_env():
-            os.environ['LG_ENV'] = lg_env
+    os.environ["LG_BUILDDIR"] = os.path.realpath(os.environ["LG_BUILDDIR"])
+    return os.environ["LG_BUILDDIR"]
 
 
-def pytest_report_header(config):
-    report = []
-    if lg_builddir is not None:
-        report += [f"Build diectory: {lg_builddir}"]
-    if lg_env is not None:
-        report += [f"Labgrid Environment: {lg_env}"]
-    return "\n".join(report)
-
-
-def pytest_addoption(parser):
-    def assignment(arg):
-        return arg.split('=', 1)
-
-    parser.addoption('--interactive', action='store_const', const='qemu_interactive',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just start Qemu interactively'))
-    parser.addoption('--dry-run', action='store_const', const='qemu_dry_run',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just print Qemu command line'))
-    parser.addoption('--dump-dtb', action='store_const', const='qemu_dump_dtb',
-                     dest='lg_initial_state',
-                     help=('(for debugging) skip tests and just dump the Qemu device tree'))
-    parser.addoption('--graphic', '--graphics', action='store_true', dest='qemu_graphics',
-                     help=('enable QEMU graphics output'))
-    parser.addoption('--rng', action='count', dest='qemu_rng',
-                     help=('instantiate Virt I/O random number generator'))
-    parser.addoption('--console', action='count', dest='qemu_console', default=0,
-                     help=('Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox'))
-    parser.addoption('--fs', action='append', dest='qemu_fs',
-                     default=[], metavar="[tag=]DIR", type=assignment,
-                     help=('Pass directory trees to emulated barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--blk', action='append', dest='qemu_block',
-                     default=[], metavar="FILE",
-                     help=('Pass block device to emulated barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--env', action='append', dest='qemu_fw_cfg', type=assignment,
-                     default=[], metavar="[envpath=]content | [envpath=]@filepath",
-                     help=('Pass barebox environment files to barebox. Can be specified more than once'))  # noqa
-    parser.addoption('--qemu', dest='qemu_arg', nargs=argparse.REMAINDER, default=[],
-                     help=('Pass all remaining options to QEMU as is'))
-    parser.addoption('--bootarg', action='append', dest='bootarg', default=[],
-                     help=('Pass boot arguments to barebox for debugging purposes'))
-    parser.addoption('--port-forward', metavar="PORT", action='append', dest='qemu_port', default=[],
-                     help=('Forward incoming TCP or UDP connections on specified PORT'))
-
-
-@pytest.fixture(scope="session")
-def strategy(request, target, pytestconfig):  # noqa: max-complexity=30
+def get_config_name(builddir):
     try:
-        strategy = target.get_driver("Strategy")
-    except NoDriverFoundError as e:
-        pytest.exit(e)
-
-    try:
-        main = target.env.config.data["targets"]["main"]
-        features = main["features"]
-    except KeyError:
-        features = []
-
-    try:
-        main = target.env.config.data["targets"]["main"]
-        yaml_env = main["env"]
-    except KeyError:
-        yaml_env = {}
-
-    try:
-        main = target.env.config.data["targets"]["main"]
-        qemu_bin = main["drivers"]["QEMUDriver"]["qemu_bin"]
-        features.append("qemu")
-    except KeyError:
+        with open(os.path.join(builddir, ".config")) as config:
+            for line in config:
+                match = re.match(r'^CONFIG_NAME="(.*)"$', line)
+                if match:
+                    return match.group(1)
+    except OSError:
         pass
 
+    return None
+
+
+def guess_lg_env(builddir=None):
+    if builddir is None:
+        builddir = os.environ.get("LG_BUILDDIR")
+    if builddir is None:
+        return None
+
+    config_name = get_config_name(builddir)
+    if config_name is None:
+        return None
+
+    matches = glob.glob(os.path.join(repo_root(), "test", "*",
+                                     f"{config_name}.yaml"))
+    if matches:
+        return matches[0]
+
+    return None
+
+
+def resolve_lg_env(lg_env=None):
+    if lg_env is None:
+        lg_env = os.environ.get("LG_ENV")
+    if lg_env is None:
+        lg_env = guess_lg_env()
+
+    if lg_env is not None:
+        os.environ["LG_ENV"] = lg_env
+
+    return lg_env
+
+
+def _get_option(options, name, default=None):
+    if isinstance(options, dict):
+        return options.get(name, default)
+
+    return getattr(options, name, default)
+
+
+def _get_list_option(options, name):
+    value = _get_option(options, name, [])
+    if value is None:
+        return []
+    return list(value)
+
+
+def _fail(message, returncode=1):
+    raise QemuInteractiveError(message, returncode)
+
+
+def _append_qemu_args(strategy, fail, *args):
+    if strategy.qemu is None:
+        fail("Qemu option supplied for non-Qemu target")
+
+    for arg in args:
+        strategy.console.extra_args += " " + arg
+
+
+def _append_qemu_bootargs(strategy, fail, args):
+    if strategy.qemu is None:
+        fail("Qemu option supplied for non-Qemu target")
+    if strategy.console.boot_args is None:
+        strategy.console.boot_args = ""
+    strategy.console.boot_args += " ".join(args)
+
+
+def apply_shared_options(strategy, target, options, *, interactive, fail=None):  # noqa: max-complexity=30
+    if fail is None:
+        fail = _fail
+
+    try:
+        main = target.env.config.data["targets"]["main"]
+    except KeyError:
+        main = {}
+
+    features = list(main.get("features", []))
+    yaml_env = dict(main.get("env", {}))
+    qemu_driver = main.get("drivers", {}).get("QEMUDriver")
+    qemu_bin = None
+
+    if qemu_driver is not None:
+        qemu_bin = qemu_driver.get("qemu_bin")
+        features.append("qemu")
+
     virtio = None
 
     if "virtio-mmio" in features:
         virtio = "device"
-        strategy.append_qemu_args('-global virtio-mmio.force-legacy=false')
+        _append_qemu_args(strategy, fail, '-global virtio-mmio.force-legacy=false')
     if "virtio-pci" in features:
         virtio = "pci,disable-modern=off"
         features.append("pci")
 
-    if virtio and pytestconfig.option.qemu_rng:
-        for i in range(pytestconfig.option.qemu_rng):
-            strategy.append_qemu_args("-device", f"virtio-rng-{virtio}")
+    qemu_rng = _get_option(options, "qemu_rng", 0) or 0
+    for _ in range(qemu_rng):
+        if virtio:
+            _append_qemu_args(strategy, fail, "-device", f"virtio-rng-{virtio}")
 
-    for i in range(pytestconfig.option.qemu_console):
+    qemu_console = _get_option(options, "qemu_console", 0) or 0
+    for i in range(qemu_console):
         if virtio and i == 0:
-            strategy.append_qemu_args(
+            _append_qemu_args(
+                strategy, fail,
                 "-device", f"virtio-serial-{virtio}",
                 "-chardev", f"pty,id=virtcon{i}",
                 "-device", f"virtconsole,chardev=virtcon{i},name=console.virtcon{i}"
@@ -198,15 +221,16 @@ def strategy(request, target, pytestconfig):  # noqa: max-complexity=30
 
         # ns16550 serial driver only works with x86 so far
         if 'pci' in features:
-            strategy.append_qemu_args(
+            _append_qemu_args(
+                strategy, fail,
                 "-chardev", f"pty,id=pcicon{i}",
                 "-device", f"pci-serial,chardev=pcicon{i}"
             )
         else:
-            pytest.exit("barebox currently supports only a single extra virtio console\n", 1)
+            fail("barebox currently supports only a single extra virtio console\n")
 
     if "qemu" in features:
-        if not pytestconfig.option.qemu_graphics:
+        if not _get_option(options, "qemu_graphics", False):
             graphics = '-nographic'
         elif qemu_bin == "qemu-system-x86_64":
             graphics = '-device isa-vga'
@@ -216,89 +240,211 @@ def strategy(request, target, pytestconfig):  # noqa: max-complexity=30
             graphics = '-vga none -device ramfb'
             graphics += f' -device virtio-keyboard-{virtio}'
         else:
-            pytest.exit("--graphics unsupported for target\n", 1)
+            fail("--graphics unsupported for target\n")
 
-        if graphics is not None and \
-                pytestconfig.option.lg_initial_state != 'qemu_interactive':
+        if graphics is not None and not interactive:
             graphics += ' -display none'
 
-        strategy.append_qemu_args(graphics)
+        _append_qemu_args(strategy, fail, graphics)
 
-    for i, blk in enumerate(pytestconfig.option.qemu_block):
+    for i, blk in enumerate(_get_list_option(options, "qemu_block")):
         if virtio:
-            strategy.append_qemu_args(
+            _append_qemu_args(
+                strategy, fail,
                 "-drive", f"if=none,format=raw,id=hd{i},file={blk}",
                 "-device", f"virtio-blk-{virtio},drive=hd{i}"
             )
         else:
-            pytest.exit("--blk unsupported for target\n", 1)
+            fail("--blk unsupported for target\n")
 
     envopts = {}
 
-    for i, fw_cfg in enumerate(pytestconfig.option.qemu_fw_cfg):
+    for i, fw_cfg in enumerate(_get_list_option(options, "qemu_fw_cfg")):
         value = fw_cfg.pop()
         envpath = fw_cfg.pop() if fw_cfg else f"data/fw_cfg{i}"
 
         envopts[envpath] = value
 
-    for envpath, value in (yaml_env | envopts).items():
+    for envpath, value in {**yaml_env, **envopts}.items():
         if virtio:
             if isinstance(value, str) and value.startswith('@'):
                 source = f"file='{value[1:]}'"
             else:
                 source = f"string='{value}'"
 
-            strategy.append_qemu_args(
+            _append_qemu_args(
+                strategy, fail,
                 '-fw_cfg', f'name=opt/org.barebox.env/{envpath},{source}'
             )
         else:
-            pytest.exit("env unsupported for target\n", 1)
+            fail("env unsupported for target\n")
 
-    if len(pytestconfig.option.bootarg) > 0:
-        strategy.append_qemu_bootargs(pytestconfig.option.bootarg)
+    bootargs = _get_list_option(options, "bootarg")
+    if bootargs:
+        _append_qemu_bootargs(strategy, fail, bootargs)
 
-    for arg in pytestconfig.option.qemu_arg:
-        strategy.append_qemu_args(arg)
+    for arg in _get_list_option(options, "qemu_arg"):
+        _append_qemu_args(strategy, fail, arg)
 
     qemu_nic = "user,id=net0"
 
-    for port in pytestconfig.option.qemu_port:
+    for port in _get_list_option(options, "qemu_port"):
         qemu_nic += f",hostfwd=udp:127.0.0.2:{port}-:{port}"
         qemu_nic += f",hostfwd=tcp:127.0.0.2:{port}-:{port}"
 
+    qemu_fs = [list(fs) for fs in _get_list_option(options, "qemu_fs")]
+
     if "testfs" in features:
-        if not any(fs and fs[0] == "testfs" for fs in pytestconfig.option.qemu_fs):
+        if not any(fs and fs[0] == "testfs" for fs in qemu_fs):
             testfs_path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
-            pytestconfig.option.qemu_fs.append(["testfs", testfs_path])
+            qemu_fs.append(["testfs", testfs_path])
             os.makedirs(testfs_path, exist_ok=True)
             qemu_nic += f",tftp={testfs_path}"
 
     if "qemu" in features:
-        strategy.append_qemu_args("-nic", qemu_nic)
+        _append_qemu_args(strategy, fail, "-nic", qemu_nic)
 
-    for i, fs in enumerate(pytestconfig.option.qemu_fs):
+    for i, fs in enumerate(qemu_fs):
         if virtio:
             path = fs.pop()
             tag = fs.pop() if fs else f"fs{i}"
 
-            strategy.append_qemu_args(
+            _append_qemu_args(
+                strategy, fail,
                 "-fsdev", f"local,security_model=none,id=fs{i},path={path}",
                 "-device", f"virtio-9p-{virtio},id=fs{i},fsdev=fs{i},mount_tag={tag}"
             )
         else:
-            pytest.exit("--fs unsupported for target\n", 1)
-
-    state = request.config.option.lg_initial_state
-    if state is not None:
-        strategy.force(state)
-
-    return strategy
+            fail("--fs unsupported for target\n")
 
 
-@pytest.fixture(scope="session")
-def testfs(strategy, env):
-    if "testfs" not in env.get_target_features():
-        pytest.skip("testfs not supported on this platform")
+def replacement_message(mode):
+    return (f"pytest {mode} is no longer supported. Use "
+            "scripts/qemu_interactive.py instead.")
 
-    path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
-    return path
+
+def _split_qemu_args(argv):
+    for i, arg in enumerate(argv):
+        if arg in ("--qemu", "--"):
+            return argv[:i], argv[i + 1:]
+        if arg.startswith("--qemu="):
+            return argv[:i], [arg.split("=", 1)[1]] + argv[i + 1:]
+
+    return argv, []
+
+
+def build_arg_parser():
+    parser = argparse.ArgumentParser(
+        description="Start a barebox labgrid QEMU target interactively",
+        epilog="Raw QEMU arguments may follow either --qemu or --."
+    )
+
+    register_interactive_options(
+        parser.add_argument_group("qemu_interactive.py options"))
+    register_shared_options(parser.add_argument_group("QEMU options"))
+    parser.add_argument("lg_env_pos", nargs="*", metavar="LG_ENV",
+                        help="labgrid environment config file")
+
+    return parser
+
+
+def parse_args(argv=None):
+    if argv is None:
+        argv = sys.argv[1:]
+
+    parser = build_arg_parser()
+    argv, qemu_args = _split_qemu_args(list(argv))
+    args = parser.parse_args(argv)
+
+    if len(args.lg_env_pos) > 1:
+        parser.error("only one positional labgrid environment may be specified")
+
+    if args.lg_env_pos:
+        if args.lg_env is not None:
+            parser.error("labgrid environment specified both positionally and with --lg-env")
+        args.lg_env = args.lg_env_pos[0]
+
+    del args.lg_env_pos
+    args.qemu_arg = qemu_args
+
+    return args
+
+
+def quote_cmd(cmd):
+    quoted = map(lambda s: s if s.find(" ") == -1 else "'" + s + "'", cmd)
+    return " ".join(quoted)
+
+
+def build_qemu_command(strategy, mode):
+    if strategy.qemu is None:
+        raise QemuInteractiveError(f"Can't enter {mode} for non-QEMU target")
+
+    if mode != MODE_DRY_RUN:
+        strategy.transition("off")
+
+    if mode == MODE_DUMP_DTB:
+        strategy.qemu.machine += f",dumpdtb={strategy.target.name}.dtb"
+
+    cmd = strategy.qemu.get_qemu_base_args()
+    cmd.extend(["-serial", "mon:stdio", "-trace", "file=/dev/null"])
+
+    return cmd
+
+
+def run_qemu(strategy, mode):
+    if mode not in (MODE_INTERACTIVE, MODE_DRY_RUN, MODE_DUMP_DTB):
+        raise QemuInteractiveError(
+            "Can only force to: qemu_dry_run, qemu_interactive, qemu_dump_dtb")
+
+    cmd = build_qemu_command(strategy, mode)
+    command = "running: \n{}\n".format(quote_cmd(cmd))
+
+    if mode == MODE_DRY_RUN:
+        print(command, end="")
+        return 0
+
+    with open("/dev/tty", "r+b", buffering=0) as tty:
+        tty.write(bytes(command, "UTF-8"))
+        return subprocess.run(cmd, stdin=tty, stdout=tty, stderr=tty).returncode
+
+
+def main(argv=None):
+    from labgrid import Environment
+    from labgrid.exceptions import NoDriverFoundError
+
+    args = parse_args(argv)
+    env = None
+
+    try:
+        resolve_lg_builddir()
+        lg_env = resolve_lg_env(args.lg_env)
+        if lg_env is None:
+            raise QemuInteractiveError(
+                "no labgrid environment specified; pass --lg-env, "
+                "a positional LG_ENV, set LG_ENV, or build a config with CONFIG_NAME",
+                2)
+
+        env = Environment(lg_env)
+        target = env.get_target("main")
+        if target is None:
+            raise QemuInteractiveError("labgrid environment has no main target")
+
+        try:
+            strategy = target.get_driver("Strategy")
+        except NoDriverFoundError as e:
+            raise QemuInteractiveError(str(e)) from e
+
+        apply_shared_options(strategy, target, args,
+                             interactive=args.qemu_mode != MODE_DUMP_DTB)
+
+        return run_qemu(strategy, args.qemu_mode)
+    except QemuInteractiveError as e:
+        print(f"error: {e}", file=sys.stderr)
+        return e.returncode
+    finally:
+        if env is not None:
+            env.cleanup()
+
+
+if __name__ == "__main__":
+    sys.exit(main())
-- 
2.47.3




  reply	other threads:[~2026-06-26 12:46 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
2026-06-26 12:45 ` Ahmad Fatoum [this message]
2026-06-26 12:45 ` [PATCH 3/5] usb: xhci-hcd: add XHCI over PCI driver Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 4/5] scripts: qemu_interactive.py: add new --usbblk option Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 5/5] scripts: qemu_interactive.py: add new --nvmeblk option Ahmad Fatoum

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260626124555.1644951-2-a.fatoum@barebox.org \
    --to=a.fatoum@barebox.org \
    --cc=barebox@lists.infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox