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"]

Reply via email to