diff --git a/Documentation/ntfc.yaml b/Documentation/ntfc.yaml index 55558a1..a338833 100644 --- a/Documentation/ntfc.yaml +++ b/Documentation/ntfc.yaml @@ -5,4 +5,6 @@ dependencies: ["toml"] # python dependencies for test cases module requirements: # nuttx config requirements - ["CONFIG_DEBUG_SYMBOLS", True] - ["CONFIG_SYSTEM_NSH", True] - - ["CONFIG_INIT_ENTRYPOINT", "nsh_main"] + # alternatives: any one entry satisfies the requirement; + # "*" accepts any set value + - [["CONFIG_INIT_ENTRYPOINT", "nsh_main"], ["CONFIG_INIT_FILEPATH", "*"]] diff --git a/Documentation/writing-test-cases.rst b/Documentation/writing-test-cases.rst index b13a59b..7637292 100644 --- a/Documentation/writing-test-cases.rst +++ b/Documentation/writing-test-cases.rst @@ -70,6 +70,20 @@ String/Value Requirements: - ["CONFIG_INIT_ENTRYPOINT", "nsh_main"] # CONFIG must equal value - ["CONFIG_TASK_NAME_SIZE", "32"] # CONFIG must equal value +Wildcard Requirements: + +.. code-block:: yaml + + requirements: + - ["CONFIG_INIT_FILEPATH", "*"] # CONFIG must be set (any value) + +Alternative Requirements (any one entry satisfies the requirement): + +.. code-block:: yaml + + requirements: + - [["CONFIG_INIT_ENTRYPOINT", "nsh_main"], ["CONFIG_INIT_FILEPATH", "*"]] + How Requirements Work: 1. NTFC reads NuttX ``.config`` file from configuration diff --git a/src/ntfc/pytest/mypytest.py b/src/ntfc/pytest/mypytest.py index b85f695..5a1c15d 100644 --- a/src/ntfc/pytest/mypytest.py +++ b/src/ntfc/pytest/mypytest.py @@ -124,14 +124,31 @@ def _write_session_config(self, result_dir: str) -> None: json.dump(self._config.config, f, indent=2, sort_keys=True) f.write("\n") + def _req_satisfied(self, product: Product, core: int, req: Any) -> bool: + """Check a single ntfc.yaml requirement entry. + + Supported entry forms: + + - ``[key, value]``: config value must equal ``value`` + - ``[key, "*"]``: any truthy config value satisfies + - ``[[key, value], ...]``: alternatives, any one satisfies + """ + if isinstance(req[0], list): + return any(self._req_satisfied(product, core, r) for r in req) + + value = product.conf.kv_check(req[0], core) + if req[1] == "*": + return bool(value) + return bool(value == req[1]) + def _kv_validate( self, product: Product, core: int - ) -> Tuple[bool, Optional[Any]]: # pragma: no cover + ) -> Tuple[bool, Optional[Any]]: """Check if configuration can be used with this tool.""" requirements = pytest.ntfcyaml.get("requirements", {}) for req in requirements: - if product.conf.kv_check(req[0], core) != req[1]: + if not self._req_satisfied(product, core, req): return False, req return True, None @@ -143,9 +160,12 @@ def _create_products(self, config: "EnvConfig") -> List[Product]: # 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]}") + ok, req = self._kv_validate(p, core) + if not ok: + raise IOError( + f"product '{p.conf.name}' core {core}: " + f"missing kconfig requirement: {req}" + ) tmp.append(p) diff --git a/tests/pytest/test_mypytest.py b/tests/pytest/test_mypytest.py index c8affd5..6686b4f 100644 --- a/tests/pytest/test_mypytest.py +++ b/tests/pytest/test_mypytest.py @@ -212,6 +212,76 @@ def test_create_products_skips_requirements_for_flash_only_core( assert products[0].cores == ["cpuapp"] +def test_create_products_requirement_forms(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", + }, + }, + }, + } + + monkeypatch.setattr( + pytest_module, + "ntfcyaml", + { + "requirements": [ + # plain form: exact match + ["CONFIG_HOST_LINUX", True], + # alternatives: first unset, second matches + [ + ["CONFIG_INIT_FILEPATH", "*"], + ["CONFIG_INIT_ENTRYPOINT", "nsh_main"], + ], + # wildcard: any truthy value + ["CONFIG_NSH_PROMPT_STRING", "*"], + ] + }, + raising=False, + ) + with patch("ntfc.cores.get_device", return_value=device_dummy): + products = MyPytest(config)._create_products(EnvConfig(config)) + assert len(products) == 1 + + +def test_create_products_requirement_unmet(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", + }, + }, + }, + } + + monkeypatch.setattr( + pytest_module, + "ntfcyaml", + {"requirements": [["CONFIG_INIT_FILEPATH", "*"]]}, + raising=False, + ) + with patch("ntfc.cores.get_device", return_value=device_dummy): + with pytest.raises(IOError, match="product.*core 0.*INIT_FILEPATH"): + MyPytest(config)._create_products(EnvConfig(config)) + + 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):