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
commit 860168994728e66a775bd85a52ef38571f0b316c Author: raiden00pl <[email protected]> AuthorDate: Tue Aug 4 18:04:32 2026 +0200 core.py: runtime command discovery fallback via target PATH listing Prebuilt kernel-mode images have no host application directory, so check_cmd could not resolve commands. List CONFIG_PATH_INITIAL (default /system/bin) once on the running target, cache it, and match cmd_check patterns against it with the matcher shared with AppBinDir. A failed listing is not cached and is retried. Signed-off-by: raiden00pl <[email protected]> Assisted-by: Claude Code --- src/ntfc/core.py | 68 +++++++++++++++++++++++++++++-------------- src/ntfc/coreconfig.py | 5 ++++ tests/test_core.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/ntfc/core.py b/src/ntfc/core.py index bc58dd1..9de18fc 100644 --- a/src/ntfc/core.py +++ b/src/ntfc/core.py @@ -28,7 +28,6 @@ from typing import ( Any, List, Optional, - Pattern, Tuple, Union, ) @@ -36,6 +35,7 @@ from typing import ( from ntfc.command_builder import CommandBuilder from ntfc.coreconfig import CoreConfig from ntfc.device.common import CmdReturn, CmdStatus +from ntfc.lib.elf.app_bindir import match_command, symbol_patterns from ntfc.log.logger import logger if TYPE_CHECKING: @@ -73,6 +73,8 @@ class CoreStatus(_Enum): class ProductCore: """This class implements product core under test.""" + _PATH_NAME_RE = re.compile(r"[A-Za-z0-9_.+-]+") + def __init__( self, device: "DeviceCommon", @@ -100,6 +102,7 @@ class ProductCore: list(ignored_cores) if ignored_cores is not None else ["dsp"] ) self._builder = CommandBuilder(device.prompt, device.no_cmd) + self._runtime_cmds: Optional[List[str]] = None self._prompt = device.prompt self._main_prompt = self._prompt @@ -528,17 +531,47 @@ class ProductCore: """Start device.""" self._device.start() + def _check_cmd_runtime(self, cmd_pattern: str) -> bool: + """Check a command against the target PATH listing. + + The listing is fetched once from the running target and cached; + a failed listing is not cached so it is retried on next use. + """ + if self._runtime_cmds is None: + result = self.sendCommandReadUntilPattern( + f"ls {self._conf.path_initial}", timeout=5 + ) + if result.status != CmdStatus.SUCCESS: + return False + + output_lower = result.output.lower() + no_cmd = str(self._device.no_cmd).lower() + if no_cmd in output_lower or any( + line.lstrip().lower().startswith("ls:") + for line in result.output.splitlines() + ): + return False + + # drop the command echo line; path headers and the prompt + # do not match the name pattern + tokens = " ".join(result.output.splitlines()[1:]).split() + self._runtime_cmds = [ + token + for token in tokens + if self._PATH_NAME_RE.fullmatch(token) + ] + + return match_command(cmd_pattern, self._runtime_cmds) + def check_cmd(self, cmd_pattern: str) -> bool: """Check if a command pattern is available on the core. - Commands are resolved from the core configuration when it knows - the application binaries, from the device ELF parser or the - shell help output otherwise. It supports both single commands - and alternative command patterns separated by '|'. + Commands are resolved from the application binaries, the running + target, the device ELF parser or the shell help output, in that + order. - :param cmd_pattern: Command pattern to check for. Can contain - alternatives separated by '|' - (e.g., 'test1|test2') + :param cmd_pattern: Command pattern, may contain alternatives + separated by '|' (e.g. 'test1|test2') :return: True if the command pattern is found, False otherwise """ # Kernel-mode: resolve against the application binary directory @@ -546,24 +579,17 @@ class ProductCore: if self._conf.has_app_bindir: return self._conf.cmd_check(cmd_pattern) + if self._conf.is_kernel_build: + # prebuilt image without host binaries: discover the + # command set once from the running target + return self._check_cmd_runtime(cmd_pattern) + # Devices that expose their own ELF parser resolve the command # from its symbols if hasattr(self._device, "elf_parser") and self._device.elf_parser: logger.debug(f"Checking command pattern: {cmd_pattern}") - for pattern in cmd_pattern.split("|"): - # For cmocka tests, append _main to symbol name - symbol_str = ( - f"{pattern}_main" if "cmocka" in pattern else pattern - ) - - # Support regex wildcards - symbol: Union[str, Pattern[str]] = ( - re.compile(symbol_str) - if ".*" in symbol_str - else symbol_str - ) - + for symbol in symbol_patterns(cmd_pattern): if self._device.elf_parser.has_symbol(symbol): return True diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index bb88e50..ed06769 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -227,6 +227,11 @@ class CoreConfig: """Return True when kernel-mode application binaries are found.""" return self._appbin is not None + @property + def path_initial(self) -> str: + """Return the initial target PATH, see CONFIG_PATH_INITIAL.""" + return str(self.kv_check("CONFIG_PATH_INITIAL") or "/system/bin") + def cmd_check(self, cmd: str, core: int = 0) -> bool: """Check if command is available in binary. diff --git a/tests/test_core.py b/tests/test_core.py index ef67281..7439831 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -627,3 +627,81 @@ def test_core_check_cmd_kernel_mode(tmp_path): assert p.check_cmd("hello_main") is True assert p.check_cmd("nonexistent") is False mockdevice.return_value.send_cmd_read_until_pattern.assert_not_called() + + +def test_core_check_cmd_kernel_runtime_fallback(tmp_path, monkeypatch): + """Test check_cmd discovers commands from the running target.""" + from ntfc.coreconfig import CoreConfig + + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text( + 'CONFIG_BUILD_KERNEL=y\nCONFIG_PATH_INITIAL="/system/bin"\n' + ) + # a kernel core whose application binaries are not on the host: + # the command set has to come from the running target + conf = CoreConfig( + { + "name": "t", + "conf_path": str(kernel_cfg), + "elf_path": "./tests/resources/nuttx/sim/nuttx", + } + ) + assert conf.has_app_bindir is False + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + p = ProductCore(mockdevice.return_value, conf) + + calls = [] + status = [CmdStatus.TIMEOUT] + + def fake_send(cmd, pattern=None, args=None, timeout=30): + calls.append(cmd) + output = "ls /system/bin\n/system/bin:\n hello\n init\n sh\nnsh> " + return CmdReturn(status[0], output=output) + + monkeypatch.setattr(p, "sendCommandReadUntilPattern", fake_send) + + # listing failure is not cached + assert p.check_cmd("hello") is False + assert len(calls) == 1 + + status[0] = CmdStatus.SUCCESS + assert p.check_cmd("hello") is True + assert p.check_cmd("hello_main") is True + assert p.check_cmd("missing|sh") is True + assert p.check_cmd("missing") is False + + # target was listed once after the failed attempt + assert calls == ["ls /system/bin", "ls /system/bin"] + + +def test_core_check_cmd_kernel_runtime_does_not_cache_ls_error( + tmp_path, monkeypatch +): + """Do not interpret words from an ls error as target commands.""" + from ntfc.coreconfig import CoreConfig + + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text("CONFIG_BUILD_KERNEL=y\n") + conf = CoreConfig({"name": "t", "conf_path": str(kernel_cfg)}) + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + p = ProductCore(mockdevice.return_value, conf) + calls = [] + + def fake_send(cmd, pattern=None, args=None, timeout=30): + calls.append(cmd) + return CmdReturn( + CmdStatus.SUCCESS, + output=( + "ls /system/bin\n" + "ls: /system/bin: No such file or directory\n" + "nsh> " + ), + ) + + monkeypatch.setattr(p, "sendCommandReadUntilPattern", fake_send) + + assert p.check_cmd("No") is False + assert p.check_cmd("file") is False + assert calls == ["ls /system/bin", "ls /system/bin"]
