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 9d7308031d4ae3dc47fef6387f36d47c0f7b9c4e Author: raiden00pl <[email protected]> AuthorDate: Thu May 14 13:54:43 2026 +0200 add flash_only config option for core add flash_only config option for core. This allows flash core firmware for a given code, without running NTFC checks and test cases on it. This is usefull for AMP cases where we run tests only on application core, but we have to flash network interface core Signed-off-by: raiden00pl <[email protected]> --- Documentation/config-yaml.rst | 40 +++++++++++++++++++++++++++++++++ Documentation/config.yaml | 2 ++ src/ntfc/coreconfig.py | 10 +++++++++ src/ntfc/cores.py | 27 +++++++++++++---------- src/ntfc/envconfig.py | 16 ++++++++++---- src/ntfc/productconfig.py | 27 ++++++++++++++++++++++- src/ntfc/pytest/mypytest.py | 4 ++-- tests/pytest/test_mypytest.py | 38 ++++++++++++++++++++++++++++++++ tests/test_coreconfig.py | 6 +++++ tests/test_cores.py | 51 +++++++++++++++++++++++++++++++++++++++++++ tests/test_envconfig.py | 19 ++++++++++++++++ tests/test_productconfig.py | 22 +++++++++++++++++++ 12 files changed, 243 insertions(+), 19 deletions(-) diff --git a/Documentation/config-yaml.rst b/Documentation/config-yaml.rst index c3a5a53..ce8ef9f 100644 --- a/Documentation/config-yaml.rst +++ b/Documentation/config-yaml.rst @@ -82,6 +82,42 @@ Use AMP when: - Cores run independently - Testing different firmware images on each core +**Build/flash-only auxiliary cores** + +A core can be marked as build/flash-only with ``flash_only: true``. NTFC will +still build and flash that core, but it will not: + +- wait for that core to boot, +- validate ``ntfc.yaml`` Kconfig requirements on it, +- collect logs from it, +- send test commands to it, or +- include it in reboot/heartbeat/test orchestration. + +This is useful when one tested core depends on companion firmware running on +another core, but only the tested core should participate in NTFC runtime +checks. + +.. code-block:: yaml + + product: + name: "product-name" + platform: "amp" + cores: + core0: + name: 'cpuapp' + device: 'serial' + exec_path: '/dev/ttyACM0' + exec_args: '115200,n,8,1' + # core0 is the tested core + + core1: + name: 'cpunet' + device: 'serial' + exec_path: '/dev/ttyACM1' + exec_args: '115200,n,8,1' + flash_only: true + # core1 is built/flashed only + **SMP (Symmetric Multi-Processing)** In SMP mode, all cores share the same device instance, coordinated by @@ -392,6 +428,10 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`. - System command for hardware reboot of the device (serial only) * - ``poweroff`` - System command for hardware poweroff of the device (serial only) + * - ``flash_only`` + - Build/flash this core but exclude it from runtime test orchestration: + no boot checks, no requirement validation, no logs, and no command + execution against it * - ``dcmake`` - Defines passed to CMake build (YAML mapping syntax, e.g. ``FEATURE_X: ON``) diff --git a/Documentation/config.yaml b/Documentation/config.yaml index 0641cb9..a5531e6 100644 --- a/Documentation/config.yaml +++ b/Documentation/config.yaml @@ -45,6 +45,8 @@ product: # many products can be supported in tests (pro flash: '' # (optional) flash command if NTFC is used for flashing reboot: '' # (optional) System command to reset DUT, eg 'st-flash reset' poweroff: '' # (optional) System command to power off DUT + flash_only: false # (optional) build/flash this core but skip boot checks, + # requirement validation, logs, and test execution on it dcmake: # (optional) Defines passed to CMake build DEFINE1: "VALUE1" DEFINE2: "VALUE2" diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index 6813b02..2283c55 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -134,6 +134,16 @@ class CoreConfig: """Return core poweroff command.""" return self._config.get("poweroff", "") + @property + def flash_only(self) -> bool: + """Return whether this core is build/flash-only. + + Flash-only cores are still built/flashed by NTFC, but are skipped for + runtime test orchestration: no boot checks, no log collection, no test + requirements validation, and no command execution against them. + """ + return bool(self._config.get("flash_only", False)) + def kv_check(self, cfg: str) -> Any: """Check Kconfig option and return its value. diff --git a/src/ntfc/cores.py b/src/ntfc/cores.py index 9c4159d..c5157b4 100644 --- a/src/ntfc/cores.py +++ b/src/ntfc/cores.py @@ -63,24 +63,27 @@ class CoresHandler: self._conf = conf self._cores: List[ProductCore] = [] - # For SMP mode, only create one device instance from core0 - # For AMP mode, create separate device instances for each core + # For SMP mode, only create one device instance from the first active + # core. For AMP mode, create separate device instances for active + # cores. ignored = conf.ignored_cores + active_cores = conf.active_core_indices if conf.is_smp: - # SMP: Create only core0 device, but track all cores - dev = get_device(conf.cfg_core(0)) - self._cores.append(ProductCore(dev, conf.cfg_core(0), ignored)) - - # Add logical cores for SMP (without creating new devices) - # These will be used for core switching via device commands - for core in range(1, conf.cores_num): - # Reuse the same device but with different core config + if not active_cores: + return + + first_core = active_cores[0] + dev = get_device(conf.cfg_core(first_core)) + self._cores.append( + ProductCore(dev, conf.cfg_core(first_core), ignored) + ) + + for core in active_cores[1:]: self._cores.append( ProductCore(dev, conf.cfg_core(core), ignored) ) else: - # AMP: Create separate device for each core - for core in range(conf.cores_num): + for core in active_cores: dev = get_device(conf.cfg_core(core)) self._cores.append( ProductCore(dev, conf.cfg_core(core), ignored) diff --git a/src/ntfc/envconfig.py b/src/ntfc/envconfig.py index b6e9e46..9de0f78 100644 --- a/src/ntfc/envconfig.py +++ b/src/ntfc/envconfig.py @@ -117,24 +117,32 @@ class EnvConfig: return {**default_config, **recovery_cfg} # dep_config - def kv_check(self, cfg: str, product: int = 0, core: int | str = 0) -> Any: + def kv_check( + self, cfg: str, product: int = 0, core: int | str | None = None + ) -> Any: """Check Kconfig option. :param cfg: Kconfig option name :param product: Product index (default: 0) - :param core: Core index (0, 1, 2) or name ('main', 'cpu1', 'cpu2') + :param core: Core index/name to check. When omitted, uses the primary + non-``flash_only`` core for the selected product. """ + if core is None: + core = self._products[product].primary_test_core_index return self._products[product].kv_check(cfg, core) def cmd_check( - self, cmd: str, product: int = 0, core: int | str = 0 + self, cmd: str, product: int = 0, core: int | str | None = None ) -> bool: """Check if command is available in binary. :param cmd: Command name or pattern (e.g., 'free' or 'free|ps') :param product: Product index (default: 0) - :param core: Core index (0, 1, 2) or name ('main', 'cpu1', 'cpu2') + :param core: Core index/name to check. When omitted, uses the primary + non-``flash_only`` core for the selected product. """ + if core is None: + core = self._products[product].primary_test_core_index return self._products[product].cmd_check(cmd, core) def extra_check( diff --git a/src/ntfc/productconfig.py b/src/ntfc/productconfig.py index c2c4b31..76e3402 100644 --- a/src/ntfc/productconfig.py +++ b/src/ntfc/productconfig.py @@ -150,9 +150,34 @@ class ProductConfig: @property def core_names(self) -> list[str]: - """Get list of core names.""" + """Get list of all configured core names.""" return list(self._cores.keys()) + @property + def active_core_indices(self) -> list[int]: + """Return configured core indices that participate in tests.""" + return [ + idx + for idx in sorted(self._core_index_to_name) + if not self.cfg_core(idx).flash_only + ] + + @property + def active_core_names(self) -> list[str]: + """Return core names that participate in tests.""" + return [self._get_core_name(idx) for idx in self.active_core_indices] + + @property + def active_cores_num(self) -> int: + """Get number of cores that participate in tests.""" + return len(self.active_core_indices) + + @property + def primary_test_core_index(self) -> int: + """Return the default core index used for requirement checks.""" + active = self.active_core_indices + return active[0] if active else 0 + def cfg_core(self, cpu: int) -> CoreConfig: """Get core configuration by index. diff --git a/src/ntfc/pytest/mypytest.py b/src/ntfc/pytest/mypytest.py index 6910d95..5817954 100644 --- a/src/ntfc/pytest/mypytest.py +++ b/src/ntfc/pytest/mypytest.py @@ -140,8 +140,8 @@ class MyPytest: for prod_config in config.product: p = Product(prod_config) - # check config requirements - for core in range(len(p.conf.cores)): + # check config requirements only on cores that participate in tests + for core in p.conf.active_core_indices: ret = self._kv_validate(p, core) if ret[0] is False: # pragma: no cover raise IOError(f"Missing kconfig dependency: {ret[1]}") diff --git a/tests/pytest/test_mypytest.py b/tests/pytest/test_mypytest.py index 1178e58..2f91f9b 100644 --- a/tests/pytest/test_mypytest.py +++ b/tests/pytest/test_mypytest.py @@ -23,6 +23,7 @@ from unittest.mock import MagicMock, patch import pytest +from ntfc.envconfig import EnvConfig from ntfc.pytest.mypytest import MyPytest @@ -174,6 +175,43 @@ def test_runner_run_device_starts_when_not_alive( assert p.runner(path, {}) == 0 +def test_create_products_skips_requirements_for_flash_only_core( + device_dummy, monkeypatch +): + import pytest as pytest_module + + config = { + "config": {}, + "product": { + "name": "product", + "cores": { + "core0": { + "name": "cpuapp", + "device": "sim", + "conf_path": "./tests/resources/nuttx/sim/kv_config", + "elf_path": "./tests/resources/nuttx/sim/nuttx", + }, + "core1": { + "name": "cpunet", + "device": "sim", + "flash_only": True, + }, + }, + }, + } + + monkeypatch.setattr( + pytest_module, + "ntfcyaml", + {"requirements": [["CONFIG_HOST_LINUX", True]]}, + raising=False, + ) + with patch("ntfc.cores.get_device", return_value=device_dummy): + products = MyPytest(config)._create_products(EnvConfig(config)) + assert len(products) == 1 + assert products[0].cores == ["cpuapp"] + + def test_device_stop_calls_stop(config_dummy, device_dummy): """_device_stop calls device.stop() for each core.""" with patch("ntfc.cores.get_device", return_value=device_dummy): diff --git a/tests/test_coreconfig.py b/tests/test_coreconfig.py index 4861d29..2ee0c93 100644 --- a/tests/test_coreconfig.py +++ b/tests/test_coreconfig.py @@ -91,6 +91,12 @@ def test_load_core_config_value_types(tmp_path): assert p.kv_check("CONFIG_UNQUOTED") == "plain_text" +def test_core_config_flash_only_property(): + conf = {"name": "test", "flash_only": True} + assert CoreConfig(conf).flash_only is True + assert CoreConfig({"name": "test"}).flash_only is False + + def test_core_config_prompt(): # Test with explicit prompt in YAML config conf = { diff --git a/tests/test_cores.py b/tests/test_cores.py index 875a21e..f70d289 100644 --- a/tests/test_cores.py +++ b/tests/test_cores.py @@ -98,6 +98,57 @@ def test_cores_init(envconfig_dummy): assert c.force_panic() is True +def test_cores_skip_flash_only_amp_core(): + conf = { + "name": "product", + "platform": "amp", + "cores": { + "core0": {"name": "cpuapp", "device": "sim"}, + "core1": { + "name": "cpunet", + "device": "sim", + "flash_only": True, + }, + }, + } + + from ntfc.productconfig import ProductConfig + + with ( + patch("ntfc.cores.get_device") as mock_get_device, + patch("ntfc.cores.ProductCore") as mock_product_core, + ): + core = mock_product_core.return_value + core.name = "cpuapp" + mock_get_device.return_value = object() + c = CoresHandler(ProductConfig(conf)) + c._cores[0] = core + assert c.cores == ["cpuapp"] + assert mock_get_device.call_count == 1 + + +def test_cores_smp_all_flash_only(): + """SMP product with only flash_only cores creates no core instances.""" + conf = { + "name": "product", + "platform": "smp", + "cores": { + "core0": { + "name": "cpuapp", + "device": "sim", + "flash_only": True, + }, + }, + } + + from ntfc.productconfig import ProductConfig + + with patch("ntfc.cores.get_device") as mock_get_device: + c = CoresHandler(ProductConfig(conf)) + assert c.cores == [] + mock_get_device.assert_not_called() + + def test_cores_smp_mode(envconfig_smp_dummy): """Test CoresHandler in SMP mode.""" diff --git a/tests/test_envconfig.py b/tests/test_envconfig.py index cb50e3c..9eecd44 100644 --- a/tests/test_envconfig.py +++ b/tests/test_envconfig.py @@ -140,6 +140,20 @@ def test_envconfig_extra_check(envconfig_dummy): assert envconfig_dummy.extra_check("") is False +def test_envconfig_cmd_check_defaults_to_primary_test_core(): + env = EnvConfig({"config": {}, "product": {"name": "p0", "cores": {}}}) + product = MagicMock() + product.primary_test_core_index = 2 + product.cmd_check.return_value = True + product.kv_check.return_value = True + env._products = [product] + + assert env.cmd_check("hello_main") is True + assert env.kv_check("CONFIG_SYSTEM_NSH") is True + product.cmd_check.assert_called_once_with("hello_main", 2) + product.kv_check.assert_called_once_with("CONFIG_SYSTEM_NSH", 2) + + def test_envconfig_cmd_check_respects_product_index(): """cmd_check should use the requested product index.""" env = EnvConfig({"config": {}, "product": {"name": "p0", "cores": {}}}) @@ -147,12 +161,17 @@ def test_envconfig_cmd_check_respects_product_index(): p1 = MagicMock() p0.cmd_check.return_value = False p1.cmd_check.return_value = True + p1.kv_check.return_value = True env._products = [p0, p1] assert env.cmd_check("hello_main", product=1, core=0) is True p1.cmd_check.assert_called_once_with("hello_main", 0) p0.cmd_check.assert_not_called() + assert env.kv_check("CONFIG_SYSTEM_NSH", product=1, core=0) is True + p1.kv_check.assert_called_once_with("CONFIG_SYSTEM_NSH", 0) + p0.kv_check.assert_not_called() + def test_envconfig_recovery_defaults(): """Test recovery property returns defaults when no config.""" diff --git a/tests/test_productconfig.py b/tests/test_productconfig.py index 1dd2c58..3f40abb 100644 --- a/tests/test_productconfig.py +++ b/tests/test_productconfig.py @@ -191,6 +191,28 @@ def test_product_config_core_names_property(): assert p.core_names == ["main", "cpu1", "cpu2"] +def test_product_config_active_cores(): + conf = { + "name": "product", + "cores": { + "core0": {"name": "cpuapp", "device": "sim"}, + "core1": { + "name": "cpunet", + "device": "sim", + "flash_only": True, + }, + "core2": {"name": "sensor", "device": "sim"}, + }, + } + + p = ProductConfig(conf) + assert p.core_names == ["cpuapp", "cpunet", "sensor"] + assert p.active_core_indices == [0, 2] + assert p.active_core_names == ["cpuapp", "sensor"] + assert p.active_cores_num == 2 + assert p.primary_test_core_index == 0 + + def test_product_config_cfg_core(): """Test cfg_core and cfg_core_by_name methods."""
