From 0640cfc617916b4245378067d9def9f983e76167 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Sat, 22 Aug 2026 15:51:58 +0200 Subject: [PATCH] add Linux target support to NTFC Add a per-core "os" option (default "nuttx") that selects the target shell abstraction, and implement the Linux shell contract used by serial and QEMU devices. Linux images are pre-built, so ELF parsing and NuttX core discovery are skipped for them. This allows running the same test suites against Linux and NuttX, which is useful for comparing the two systems (e.g. benchmarks). Signed-off-by: raiden00pl Assisted-by: Claude Code --- Documentation/config-yaml.rst | 39 ++++++++++++++++ src/ntfc/builder.py | 44 +++++++++++++++++- src/ntfc/core.py | 2 +- src/ntfc/coreconfig.py | 15 +++++- src/ntfc/device/common.py | 4 +- src/ntfc/device/getos.py | 10 +++- src/ntfc/device/linux.py | 87 ++++++++++++++++++++++++++++++++++ tests/device/test_common.py | 26 ++++++++++- tests/device/test_linux.py | 56 ++++++++++++++++++++++ tests/device/test_qemu.py | 2 + tests/device/test_sim.py | 8 ++++ tests/test_builder.py | 88 ++++++++++++++++++++++++++++++++++- tests/test_coreconfig.py | 30 ++++++++++++ 13 files changed, 402 insertions(+), 9 deletions(-) create mode 100644 src/ntfc/device/linux.py create mode 100644 tests/device/test_linux.py diff --git a/Documentation/config-yaml.rst b/Documentation/config-yaml.rst index 294f1ba..3b66322 100644 --- a/Documentation/config-yaml.rst +++ b/Documentation/config-yaml.rst @@ -310,6 +310,8 @@ configuration section: config: cwd: './external' build_dir: './build' # Build output directory + nuttx_dir: './external/nuttx' # Optional explicit NuttX tree + apps_dir: './external/nuttx-apps' # Optional explicit apps tree build_env: # Optional env vars for cmake configure/build CC: gcc-14 CXX: g++-14 @@ -350,6 +352,7 @@ Flash command can use special tags that are handled by NTFC: - ``$IMAGE_BIN`` is replaced by path to ``nuttx.bin``. - ``$IMAGE_HEX`` is replaced by path to ``nuttx.hex``. +- ``$IMAGE_ELF`` is replaced by path to the core image (``elf_path``). Example usage with ``st-flash`` tool: @@ -414,6 +417,8 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`. - Human-readable core name * - ``device`` - Device type: ``sim``, ``qemu``, or ``serial`` + * - ``os`` + - Target shell type: ``nuttx`` (default) or ``linux`` * - ``exec_path`` - QEMU executable name or serial port device (``/dev/ttyACM0``, ``COM1``, etc.) @@ -426,6 +431,9 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`. * - ``boot_timeout`` - (Optional) Seconds to wait for the first shell prompt after device start. Defaults to ``5`` + * - ``read_poll_interval`` + - (Optional) Console polling interval in seconds used when reading + device output. Must be positive. Defaults to ``0.1`` * - ``app_bindir`` - (Optional) Directory with kernel-mode application binaries. Defaults to the ``bin/`` directory next to the NuttX ELF for kernel-mode @@ -455,3 +463,34 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`. * - ``kv`` - Per-core Kconfig overrides applied before build. Overrides matching keys from global ``config.kv`` + +Linux Targets +============= + +Setting ``os: linux`` on a core switches the shell abstraction from NuttX +to Linux: shell prompt (``#`` by default), command-not-found marker, +``poweroff``/``reboot``/``uname`` commands and kernel crash signatures. +This allows running the same test suites against Linux and NuttX, which is +useful for comparing the two systems (e.g. benchmarks). + +Linux images are pre-built, so NTFC does not build them: point ``elf_path`` +at the kernel image and pass boot arguments via ``exec_args`` (with the +``$IMAGE_ELF`` placeholder) or a ``flash`` command. ELF symbol parsing and +NuttX core topology discovery are skipped for Linux cores, which also means +the ``cmd_check`` pytest marker is not supported on them. + +Example QEMU Linux core: + +.. code-block:: yaml + + cores: + core0: + name: 'linux' + os: 'linux' + device: 'qemu' + exec_path: 'qemu-system-x86_64' + exec_args: '-M q35 -m 2G -nographic -kernel $IMAGE_ELF + -initrd ./initramfs.img -append "console=ttyS0"' + elf_path: './bzImage' + prompt: '# ' + boot_timeout: 60 diff --git a/src/ntfc/builder.py b/src/ntfc/builder.py index ace5eeb..6e104f5 100644 --- a/src/ntfc/builder.py +++ b/src/ntfc/builder.py @@ -39,6 +39,7 @@ class NuttXBuilder: IMAGE_BIN_STR = "$IMAGE_BIN" IMAGE_HEX_STR = "$IMAGE_HEX" + IMAGE_ELF_STR = "$IMAGE_ELF" _KCONFIG_DISABLED_RE = re.compile( r"^#\s+(CONFIG_[A-Za-z0-9_]+)\s+is not set" ) @@ -100,6 +101,12 @@ def _get_cmake_defines( {str(key): str(val) for key, val in custom_defines.items()} ) + apps_dir = self._cfg_values.get("config", {}).get("apps_dir") + if apps_dir: + defines["NUTTX_APPS_DIR"] = os.path.abspath( + os.path.expanduser(os.path.expandvars(str(apps_dir))) + ) + return defines def _get_kconfig_overrides( @@ -348,6 +355,20 @@ def _run_build( self._run_command(cmd, env=run_env) + def _run_build_target( + self, + build: str, + target: str, + env: Optional[Dict[str, str]] = None, + ) -> None: + """Run one named CMake build target.""" + cmd = ["cmake", "--build", str(Path(build)), "--target", target] + run_env = os.environ.copy() + if env: + run_env.update(env) + + self._run_command(cmd, env=run_env) + def _build_core( self, core: str, cores: Dict[str, Any], product: str ) -> None: @@ -371,13 +392,24 @@ def _build_core( if not cfg_cwd: # pragma: no cover raise BuilderConfigError("not found cwd in YAML configuration") + cfg_build_dir = os.path.expanduser( + os.path.expandvars(str(cfg_build_dir)) + ) build_path = os.path.join(cfg_build_dir, build_dir) build_cfg = cores[core]["defconfig"] logger.info( f"build image " f"conf: {build_cfg}, out: {build_path}" ) - nuttx_dir = os.path.join(cfg_cwd, "nuttx") + cfg_cwd = os.path.expanduser(os.path.expandvars(str(cfg_cwd))) + configured_nuttx_dir = self._cfg_values["config"].get("nuttx_dir") + nuttx_dir = ( + os.path.expanduser( + os.path.expandvars(str(configured_nuttx_dir)) + ) + if configured_nuttx_dir + else os.path.join(cfg_cwd, "nuttx") + ) nuttx_elf_path = os.path.join(build_path, "nuttx") nuttx_conf_path = os.path.join(build_path, ".config") @@ -410,6 +442,15 @@ def _build_core( nuttx_conf_path, kv_overrides, cfg_cwd ) + # Regenerate include/nuttx/config.h after changing .config. + # Otherwise CMake can relink an image built with stale Kconfig + # values while the saved .config claims the override applied. + + if kv_overrides: + self._run_build_target( + build_path, "olddefconfig", env=build_env + ) + # build self._run_build(build_path, env=build_env) @@ -439,6 +480,7 @@ def _flash_core( flash_cmd = flash_cmd.replace(self.IMAGE_BIN_STR, image_bin) flash_cmd = flash_cmd.replace(self.IMAGE_HEX_STR, image_hex) + flash_cmd = flash_cmd.replace(self.IMAGE_ELF_STR, str(img_path)) cmd = flash_cmd.split() diff --git a/src/ntfc/core.py b/src/ntfc/core.py index e51376a..239b541 100644 --- a/src/ntfc/core.py +++ b/src/ntfc/core.py @@ -334,7 +334,7 @@ def get_core_info(self) -> Tuple[str, ...]: def init(self) -> None: """Finish product initialization.""" - cores = self.get_core_info() + cores = self.get_core_info() if self._conf.os == "nuttx" else () self._core0 = cores[0] if cores else "core0" self._cur_core = self._core0 self._cores = cores if cores else ("core0",) diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index 9f7f99d..263725a 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -42,7 +42,7 @@ def __init__(self, cfg: Dict[str, Any]) -> None: self._load_core_config() elf_path = self._config.get("elf_path", None) - if elf_path: + if elf_path and self.os == "nuttx": # load ELF self._elf = ElfParser(elf_path) @@ -86,11 +86,24 @@ def uptime(self) -> Any: """Return core uptime.""" return self._config.get("uptime", 3) + @property + def read_poll_interval(self) -> float: + """Return console polling interval in seconds.""" + value = float(self._config.get("read_poll_interval", 0.1)) + if value <= 0: + raise ValueError("read_poll_interval must be positive") + return value + @property def device(self) -> Any: """Return core device.""" return self._config.get("device", None) + @property + def os(self) -> str: + """Return target operating system name.""" + return str(self._config.get("os", "nuttx")).lower() + @property def name(self) -> Any: """Return core name.""" diff --git a/src/ntfc/device/common.py b/src/ntfc/device/common.py index 1c714b4..d4d3dfc 100644 --- a/src/ntfc/device/common.py +++ b/src/ntfc/device/common.py @@ -72,7 +72,7 @@ def __init__(self, conf: "CoreConfig", echo: bool = True): ) self.clear_fault_flags() - self._read_all_sleep = 0.1 + self._read_all_sleep = conf.read_poll_interval self._has_echo = echo self._start_time: Optional[float] = None self._output_tail_buf = bytearray() @@ -273,7 +273,7 @@ def _read_until_pattern_loop( # noqa: C901 ret = CmdStatus.TIMEOUT while True: - chunk = self._read_all(0.1) + chunk = self._read_all(self._read_all_sleep) output += chunk self._console_log(chunk) diff --git a/src/ntfc/device/getos.py b/src/ntfc/device/getos.py index f387721..a2f621d 100644 --- a/src/ntfc/device/getos.py +++ b/src/ntfc/device/getos.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING +from .linux import DeviceLinux from .nuttx import DeviceNuttx if TYPE_CHECKING: @@ -36,4 +37,11 @@ def get_os(conf: "CoreConfig") -> "OSCommon": """Get OS abstraction.""" - return DeviceNuttx(conf) # only NuttX supported now + operating_systems = { + "linux": DeviceLinux, + "nuttx": DeviceNuttx, + } + factory = operating_systems.get(conf.os) + if factory is None: + raise ValueError(f"unsupported operating system: {conf.os}") + return factory(conf) diff --git a/src/ntfc/device/linux.py b/src/ntfc/device/linux.py new file mode 100644 index 0000000..c9312f9 --- /dev/null +++ b/src/ntfc/device/linux.py @@ -0,0 +1,87 @@ +############################################################################ +# 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. +# +############################################################################ + +"""Linux shell abstraction for serial and QEMU test devices.""" + +from typing import TYPE_CHECKING, Dict, List + +from ntfc.device.state import CrashType + +from .oscommon import OSCommon + +if TYPE_CHECKING: + from ntfc.coreconfig import CoreConfig + + +class DeviceLinux(OSCommon): + """Describe the Linux shell contract used by NTFC devices.""" + + _PROMPT = b"#" + _CRASH_SIGNATURES: Dict[CrashType, List[bytes]] = { + CrashType.PANIC: [ + b"Kernel panic - not syncing", + b"not syncing: Fatal exception", + ], + CrashType.ASSERTION: [b"BUG: unable to handle kernel"], + } + + def __init__(self, conf: "CoreConfig") -> None: + """Initialize the Linux shell abstraction.""" + self._prompt = conf.prompt.encode() if conf.prompt else self._PROMPT + + @property + def prompt(self) -> bytes: + """Get shell prompt.""" + return self._prompt + + @property + def no_cmd(self) -> str: + """Get command-not-found marker.""" + return "not found" + + @property + def help_cmd(self) -> bytes: + """Get shell help command.""" + return b"help" + + @property + def poweroff_cmd(self) -> bytes: + """Get shell poweroff command.""" + return b"poweroff" + + @property + def reboot_cmd(self) -> bytes: + """Get shell reboot command.""" + return b"reboot" + + @property + def uname_cmd(self) -> bytes: + """Get operating-system identification command.""" + return b"uname -s" + + @property + def crash_signatures(self) -> Dict[CrashType, List[bytes]]: + """Get Linux kernel crash signatures.""" + return self._CRASH_SIGNATURES + + @property + def panic_char(self) -> str: + """Linux does not define a console panic character here.""" + return "" diff --git a/tests/device/test_common.py b/tests/device/test_common.py index a8f848b..80c810e 100644 --- a/tests/device/test_common.py +++ b/tests/device/test_common.py @@ -33,10 +33,14 @@ class DeviceMock(DeviceCommon): - def __init__(self, _): + def __init__(self, config): """Mock.""" - DeviceCommon.__init__(self, _) + if not isinstance(config.os, str): + config.os = "nuttx" + if not isinstance(config.read_poll_interval, (int, float)): + config.read_poll_interval = 0.1 + DeviceCommon.__init__(self, config) def _read(self, _=0): """Mock.""" @@ -101,6 +105,11 @@ def test_device_common_init(): assert d.busyloop is False assert d.flood is False + config.os = "nuttx" + config.read_poll_interval = 0.01 + explicit = DeviceMock(config) + assert explicit._read_all_sleep == 0.01 + def test_device_common_send_cmd_pattern(): @@ -223,6 +232,19 @@ def test_device_common_read_until_pattern(): dev.read_until_pattern("PASS", 10) +def test_device_common_read_until_pattern_uses_configured_poll_interval(): + with patch("ntfc.envconfig.EnvConfig") as mockdevice: + config = mockdevice.return_value + config.read_poll_interval = 0.001 + dev = DeviceMock(config) + + with patch.object(dev, "_read_all", return_value=b"PASS") as read_all: + ret = dev.read_until_pattern(b"PASS", 1) + + assert ret.status == CmdStatus.SUCCESS + read_all.assert_called_once_with(0.001) + + def test_device_common_panic_char(): with patch("ntfc.device.common.get_os") as mock_get_os: diff --git a/tests/device/test_linux.py b/tests/device/test_linux.py new file mode 100644 index 0000000..d6a3a3b --- /dev/null +++ b/tests/device/test_linux.py @@ -0,0 +1,56 @@ +############################################################################ +# 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 pytest + +from ntfc.coreconfig import CoreConfig +from ntfc.device.getos import get_os +from ntfc.device.linux import DeviceLinux +from ntfc.device.nuttx import DeviceNuttx +from ntfc.device.state import CrashType + + +def test_linux_shell_contract() -> None: + device = DeviceLinux( + CoreConfig({"name": "linux", "os": "linux", "prompt": "rtbench# "}) + ) + + assert device.prompt == b"rtbench# " + assert device.no_cmd == "not found" + assert device.help_cmd == b"help" + assert device.poweroff_cmd == b"poweroff" + assert device.reboot_cmd == b"reboot" + assert device.uname_cmd == b"uname -s" + assert device.panic_char == "" + assert CrashType.PANIC in device.crash_signatures + + +def test_get_os_defaults_to_nuttx_and_selects_linux() -> None: + assert isinstance(get_os(CoreConfig({"name": "default"})), DeviceNuttx) + assert isinstance( + get_os(CoreConfig({"name": "linux", "os": "linux"})), DeviceLinux + ) + + +def test_get_os_rejects_unknown_operating_system() -> None: + with pytest.raises( + ValueError, match="unsupported operating system: plan9" + ): + get_os(CoreConfig({"name": "unknown", "os": "plan9"})) diff --git a/tests/device/test_qemu.py b/tests/device/test_qemu.py index aed9d1c..ac062fb 100644 --- a/tests/device/test_qemu.py +++ b/tests/device/test_qemu.py @@ -30,6 +30,8 @@ def test_device_qemu_open(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value + config.os = "nuttx" + config.read_poll_interval = 0.1 config.exec_path = "" config.exec_args = "" config.elf_path = "" diff --git a/tests/device/test_sim.py b/tests/device/test_sim.py index d722058..f9523c3 100644 --- a/tests/device/test_sim.py +++ b/tests/device/test_sim.py @@ -30,6 +30,8 @@ def test_device_sim_init(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value + config.os = "nuttx" + config.read_poll_interval = 0.1 config.elf_path = "" sim = DeviceSim(config) @@ -42,6 +44,8 @@ def test_device_sim_init(): def test_device_sim_start_opens_host(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value + config.os = "nuttx" + config.read_poll_interval = 0.1 config.elf_path = "/tmp/nuttx-sim" config.uptime = 3 sim = DeviceSim(config) @@ -63,6 +67,8 @@ def fake_host_open(cmd, uptime): def test_device_sim_write_adds_newline(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value + config.os = "nuttx" + config.read_poll_interval = 0.1 config.elf_path = "/tmp/nuttx-sim" sim = DeviceSim(config) @@ -85,6 +91,8 @@ def send(self, data): def test_device_sim_write_no_extra_newline(): with patch("ntfc.coreconfig.CoreConfig") as mockdevice: config = mockdevice.return_value + config.os = "nuttx" + config.read_poll_interval = 0.1 config.elf_path = "/tmp/nuttx-sim" sim = DeviceSim(config) diff --git a/tests/test_builder.py b/tests/test_builder.py index 0ed6d65..1fb4ab8 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -105,6 +105,47 @@ def run_command_capture(cmd, env): assert calls[1][1]["CXX"] == "g++-14" +def test_builder_regenerates_config_after_kconfig_overrides() -> None: + config = copy.deepcopy(conf_dir) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + config["product"]["cores"]["core0"]["kv"] = { + "CONFIG_ARCH_X86_64_IDLE_NOP": True + } + calls = [] + + builder = NuttXBuilder(config) + builder._run_command = lambda command, env: calls.append(command) + builder._make_dir = builder_make_dir_dummy + builder._apply_kconfig_overrides = lambda *_args: None + builder.build_all() + + assert calls[1] == [ + "cmake", + "--build", + "bbb/product-xxx-dummy", + "--target", + "olddefconfig", + ] + assert calls[2] == ["cmake", "--build", "bbb/product-xxx-dummy"] + + +def test_builder_run_build_target_passes_env() -> None: + builder = NuttXBuilder(copy.deepcopy(conf_dir)) + calls = [] + builder._run_command = lambda command, env: calls.append((command, env)) + + builder._run_build_target("build-dir", "olddefconfig", {"CC": "gcc-13"}) + + assert calls[0][0] == [ + "cmake", + "--build", + "build-dir", + "--target", + "olddefconfig", + ] + assert calls[0][1]["CC"] == "gcc-13" + + def test_builder_ignores_invalid_build_env_types() -> None: config = copy.deepcopy(conf_dir) config["config"]["build_env"] = "CC=gcc-14 CXX=g++-14" @@ -147,6 +188,33 @@ def test_builder_get_cmake_defines_ignores_invalid_type() -> None: } +def test_builder_accepts_explicit_nuttx_and_apps_directories(tmp_path) -> None: + config = copy.deepcopy(conf_dir) + build_dir = tmp_path / "build" + nuttx_dir = tmp_path / "source" / "nuttx-custom" + apps_dir = tmp_path / "source" / "apps-custom" + nuttx_dir.mkdir(parents=True) + apps_dir.mkdir(parents=True) + config["config"].update( + { + "build_dir": str(build_dir), + "nuttx_dir": str(nuttx_dir), + "apps_dir": str(apps_dir), + } + ) + config["product"]["cores"]["core0"]["defconfig"] = "board:test" + calls = [] + + builder = NuttXBuilder(config) + builder._run_command = lambda command, env: calls.append(command) + builder._make_dir = lambda _path: None + builder.build_all() + + configure = calls[0] + assert f"-S{nuttx_dir}" in configure + assert f"-DNUTTX_APPS_DIR={apps_dir}" in configure + + def test_builder_kconfig_helpers() -> None: b = NuttXBuilder(copy.deepcopy(conf_dir)) core_cfg = {"kv": {"CONFIG_CORE": "m"}} @@ -454,9 +522,11 @@ def fake_apply_with_tool(conf_path, overrides, _cfg_cwd): with patch("ntfc.builder.logger.info", side_effect=logs.append): b.build_all() - assert len(calls) == 2 + assert len(calls) == 3 assert calls[0][0] == "cmake" assert calls[1][:2] == ["cmake", "--build"] + assert calls[1][-2:] == ["--target", "olddefconfig"] + assert calls[2][:2] == ["cmake", "--build"] assert any( "Applying Kconfig overrides before build:" == msg for msg in logs ) @@ -484,3 +554,19 @@ def test_builder_raises_when_cwd_missing() -> None: BuilderConfigError, match="not found cwd in YAML configuration" ): b.build_all() + + +def test_builder_flash_supports_elf_placeholder(tmp_path) -> None: + image = tmp_path / "nuttx" + image.write_bytes(b"elf") + core = { + "elf_path": str(image), + "flash": "pxe-stage $IMAGE_ELF", + } + commands = [] + builder = NuttXBuilder(copy.deepcopy(conf_dir)) + builder._run_command = lambda command, env: commands.append(command) + + builder._flash_core("core0", {"core0": core}) + + assert commands == [["pxe-stage", str(image)]] diff --git a/tests/test_coreconfig.py b/tests/test_coreconfig.py index f4e80f2..082d3f0 100644 --- a/tests/test_coreconfig.py +++ b/tests/test_coreconfig.py @@ -165,6 +165,36 @@ def test_core_config_app_bindir(tmp_path): assert CoreConfig(conf).app_bindir is None +def test_core_config_read_poll_interval() -> None: + assert CoreConfig({"name": "test"}).read_poll_interval == 0.1 + assert ( + CoreConfig( + {"name": "test", "read_poll_interval": 0.001} + ).read_poll_interval + == 0.001 + ) + with pytest.raises(ValueError, match="must be positive"): + _ = CoreConfig( + {"name": "test", "read_poll_interval": 0} + ).read_poll_interval + + +def test_core_config_os_defaults_to_nuttx() -> None: + assert CoreConfig({"name": "test"}).os == "nuttx" + assert CoreConfig({"name": "test", "os": "Linux"}).os == "linux" + + +def test_linux_kernel_image_does_not_require_elf_symbols(tmp_path) -> None: + image = tmp_path / "bzImage" + image.write_bytes(b"not-an-elf") + + core = CoreConfig({"name": "linux", "os": "linux", "elf_path": str(image)}) + + assert core.elf_path == str(image) + with pytest.raises(AttributeError): + core.cmd_check("rtbench") + + def test_core_config_prompt(): # Test with explicit prompt in YAML config conf = {