This is an automated email from the ASF dual-hosted git repository.
xiaoxiang781216 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nuttx-ntfc.git
The following commit(s) were added to refs/heads/main by this push:
new a0ba968 add Linux target support to NTFC
a0ba968 is described below
commit a0ba968941c666e455c748ad9805fc78ec557100
Author: raiden00pl <[email protected]>
AuthorDate: Sat Aug 22 15:51:58 2026 +0200
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 <[email protected]>
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(-)
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 @@ class NuttXBuilder:
{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 @@ class NuttXBuilder:
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 @@ class NuttXBuilder:
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 @@ class NuttXBuilder:
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 @@ class NuttXBuilder:
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 @@ class ProductCore:
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 @@ class CoreConfig:
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 @@ class CoreConfig:
"""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 @@ class DeviceCommon(ABC):
)
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 @@ class DeviceCommon(ABC):
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 @@ if TYPE_CHECKING:
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 @@ g_mock_read = b""
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 test_device_sim_start_opens_host():
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 test_device_sim_write_adds_newline():
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 test_builder_passes_build_env() -> None:
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 test_builder_applies_kv_before_build() -> None:
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 = {