From 2e4f4b0d39a3294af355c66a9bedb6f9ffad31a4 Mon Sep 17 00:00:00 2001 From: Ozan Durgut Date: Thu, 7 May 2026 13:14:15 +0200 Subject: [PATCH 1/2] remote/client: support OpenOCD bootstrap without environment The bootstrap fallback path was too narrow for OpenOCD-based setups without a local environment. So far, `labgrid-client bootstrap` only auto-created `OpenOCDDriver` for `NetworkAlteraUSBBlaster`. If a place exposed only `NetworkUSBDebugger`, the client failed with "target has no compatible resource available" even though `OpenOCDDriver` can bind to that resource type. In addition, the existing `bootstrap_args` path only handled raw `key=value` strings. This was not sufficient for `OpenOCDDriver` arguments such as `load_commands`, `search`, or `config`, which often need structured values like lists. Extend the bootstrap fallback to `NetworkUSBDebugger` and parse bootstrap driver arguments as YAML values. This allows CLI-only OpenOCD bootstrap flows to pass structured driver arguments without requiring a local environment file. Signed-off-by: Ozan Durgut --- labgrid/remote/client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 4d2eb0bfa..d9f9fbf8e 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -28,6 +28,7 @@ import attr import grpc +import yaml # TODO: drop if Python >= 3.11 guaranteed from exceptiongroup import ExceptionGroup # pylint: disable=redefined-builtin @@ -932,6 +933,19 @@ def _get_driver_or_new(self, target, cls, *, name=None, activate=True): target.activate(drv) return drv + def _parse_driver_args(self, args): + parsed = {} + for arg in args: + try: + key, value = arg.split("=", 1) + except ValueError: + raise UserError(f"invalid bootstrap argument '{arg}', expected key=value") + try: + parsed[key] = yaml.safe_load(value) + except yaml.YAMLError as e: + raise UserError(f"invalid value for bootstrap argument '{key}': {e}") from e + return parsed + def power(self): place = self.get_acquired_place() action = self.args.action @@ -1176,10 +1190,12 @@ def bootstrap(self): NetworkIMXUSBLoader, NetworkRKUSBLoader, NetworkAlteraUSBBlaster, + NetworkUSBDebugger, ) from ..driver import OpenOCDDriver drv = None + args = self._parse_driver_args(self.args.bootstrap_args) try: drv = target.get_driver("BootstrapProtocol", name=name) except NoDriverFoundError: @@ -1192,8 +1208,7 @@ def bootstrap(self): elif isinstance(resource, NetworkMXSUSBLoader): drv = self._get_driver_or_new(target, "MXSUSBDriver", activate=False, name=name) drv.loader.timeout = self.args.wait - elif isinstance(resource, NetworkAlteraUSBBlaster): - args = dict(arg.split("=", 1) for arg in self.args.bootstrap_args) + elif isinstance(resource, (NetworkAlteraUSBBlaster, NetworkUSBDebugger)): try: drv = target.get_driver("OpenOCDDriver", activate=False, name=name) except NoDriverFoundError: From 8026458bc86874b96463a37292a40b684474a588 Mon Sep 17 00:00:00 2001 From: Ozan Durgut Date: Mon, 1 Jun 2026 15:26:07 +0200 Subject: [PATCH 2/2] tests/openocd: add coverage for client bootstrap fallback Add unit tests for OpenOCD bootstrap argument parsing and the NetworkUSBDebugger fallback path in the client. Keep these tests separate from the OpenOCD integration tests so they do not depend on the openocd binary being installed. Signed-off-by: Ozan Durgut --- tests/test_openocd_client.py | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_openocd_client.py diff --git a/tests/test_openocd_client.py b/tests/test_openocd_client.py new file mode 100644 index 000000000..9ca6a3449 --- /dev/null +++ b/tests/test_openocd_client.py @@ -0,0 +1,81 @@ +import argparse + +import pytest + +from labgrid import Target +from labgrid.driver.openocddriver import OpenOCDDriver +from labgrid.remote.client import ClientSession, UserError +from labgrid.resource.remote import NetworkUSBDebugger + + +def test_parse_driver_args(): + session = object.__new__(ClientSession) + + args = session._parse_driver_args([ + 'search=["path"]', + 'load_commands=["init", "shutdown"]', + 'board_config=board.cfg', + ]) + + assert args == { + "search": ["path"], + "load_commands": ["init", "shutdown"], + "board_config": "board.cfg", + } + + +def test_parse_driver_args_invalid(): + session = object.__new__(ClientSession) + + with pytest.raises(UserError, match="expected key=value"): + session._parse_driver_args(["load_commands"]) + + with pytest.raises(UserError, match="invalid value for bootstrap argument 'search'"): + session._parse_driver_args(['search=["path"']) + + +def test_bootstrap_network_usb_debugger(monkeypatch): + target = Target("test") + debugger = NetworkUSBDebugger( + target, + name=None, + host="host", + busnum=1, + devnum=2, + path="1-2", + vendor_id=1, + model_id=2, + ) + monkeypatch.setattr(debugger.manager, "poll", lambda: None) + debugger.avail = True + + session = object.__new__(ClientSession) + session.args = argparse.Namespace( + wait=12.5, + name=None, + filename="dummy", + bootstrap_args=[ + 'search=["path"]', + 'load_commands=["init", "shutdown"]', + 'interface_config=interface.cfg', + ], + ) + session.get_acquired_place = lambda: argparse.Namespace(name="test") + session._get_target = lambda place: target + + load_calls = [] + + def fake_load(self, filename=None): + load_calls.append((self, filename)) + + monkeypatch.setattr(OpenOCDDriver, "load", fake_load) + + session.bootstrap() + + driver = target.get_driver(OpenOCDDriver, activate=False) + assert driver.interface is debugger + assert driver.interface.timeout == 12.5 + assert driver.search == ["path"] + assert driver.load_commands == ["init", "shutdown"] + assert driver.interface_config == "interface.cfg" + assert load_calls == [(driver, "dummy")]