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 b7206c7  builder.py: check that Kconfig overrides are applied
b7206c7 is described below

commit b7206c7d2d8bc39cf9b098e29a9f808b77045a71
Author: raiden00pl <[email protected]>
AuthorDate: Wed Oct 7 21:11:07 2026 +0200

    builder.py: check that Kconfig overrides are applied
    
    kconfig-tweak upper-cases symbol names unless --keep-case is given, so
    a 'kv' override of a mixed-case symbol (e.g. CONFIG_NET_IPv6) was
    written under a wrong name and silently dropped by olddefconfig. The
    same happened to overrides of unknown symbols, with unmet dependencies
    or with invalid values: the build used a configuration nobody asked
    for.
    
    * Pass --keep-case to kconfig-tweak.
    * After olddefconfig, compare every override with the final .config
      and fail the build with the list of overrides that were not applied.
    
    Assisted-by: Claude:claude-opus-5-5
    Signed-off-by: raiden00pl <[email protected]>
---
 src/ntfc/builder.py   | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 tests/test_builder.py | 70 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 160 insertions(+), 1 deletion(-)

diff --git a/src/ntfc/builder.py b/src/ntfc/builder.py
index 22d10b1..3a18fb4 100644
--- a/src/ntfc/builder.py
+++ b/src/ntfc/builder.py
@@ -47,6 +47,7 @@ class NuttXBuilder:
     _KCONFIG_DISABLED_RE = re.compile(
         r"^#\s+(CONFIG_[A-Za-z0-9_]+)\s+is not set"
     )
+    _KCONFIG_VALUE_RE = re.compile(r"^(CONFIG_[A-Za-z0-9_]+)=(.*)$")
 
     def __init__(self, config: Dict[str, Any], rebuild: bool = True):
         """Initialize NuttX builder."""
@@ -173,7 +174,10 @@ class NuttXBuilder:
         self, tool: str, conf_path: str, key: str, value: Any
     ) -> List[str]:
         """Build one ``kconfig-tweak`` command for a Kconfig override."""
-        cmd = [tool, "--file", conf_path]
+        # Kconfig symbols are case-sensitive (e.g. CONFIG_NET_IPv6) and
+        # kconfig-tweak upper-cases them unless asked not to
+
+        cmd = [tool, "--file", conf_path, "--keep-case"]
         if value is False or value == "n":
             cmd.extend(["--disable", key])
         elif value is True or value == "y":
@@ -242,6 +246,85 @@ class NuttXBuilder:
 
         return f'{key}="{value}"\n'
 
+    def _read_kconfig_values(self, conf_path: str) -> Dict[str, str]:
+        """Read symbol values from a ``.config`` file.
+
+        :param conf_path: path to ``.config``
+        :return: map of symbol to value; ``n`` for "is not set" symbols
+        """
+        values: Dict[str, str] = {}
+        with open(conf_path, "r", encoding="utf-8") as f:
+            for line in f:
+                line = line.rstrip("\n")
+                disabled = self._KCONFIG_DISABLED_RE.match(line)
+                if disabled:
+                    values[disabled.group(1)] = "n"
+                    continue
+                match = self._KCONFIG_VALUE_RE.match(line)
+                if match:
+                    values[match.group(1)] = match.group(2)
+
+        return values
+
+    @staticmethod
+    def _kconfig_values_equal(requested: str, actual: str) -> bool:
+        """Compare two Kconfig values, numbers by value.
+
+        :param requested: value as written by the override
+        :param actual: value found in ``.config``
+        :return: True if the values are the same
+        """
+        if requested == actual:
+            return True
+
+        try:
+            return int(requested, 0) == int(actual, 0)
+        except ValueError:
+            return False
+
+    def _check_kconfig_overrides(
+        self, conf_path: str, overrides: Dict[str, Any]
+    ) -> None:
+        """Check that every Kconfig override is in the final ``.config``.
+
+        Kconfig drops or resets an override silently: an unknown symbol,
+        unmet dependencies or an invalid value.  Building anyway would test
+        a configuration nobody asked for.
+
+        :param conf_path: path to ``.config`` after ``olddefconfig``
+        :param overrides: requested Kconfig overrides
+        :raises AssertionError: if any override is not applied
+        """
+        values = self._read_kconfig_values(conf_path)
+        failed: List[str] = []
+
+        for key, value in overrides.items():
+            line = self._format_kconfig_line(key, value).rstrip("\n")
+            match = self._KCONFIG_VALUE_RE.match(line)
+            requested = match.group(2) if match else "n"
+            actual = values.get(key)
+
+            if actual is None and requested == "n":
+                continue
+
+            if actual is None:
+                failed.append(
+                    f"{key}: requested {requested}, not in .config "
+                    "(unknown symbol or unmet dependencies)"
+                )
+            elif not self._kconfig_values_equal(requested, actual):
+                failed.append(
+                    f"{key}: requested {requested}, got {actual} "
+                    "(unmet dependencies or invalid value)"
+                )
+
+        if failed:
+            for item in failed:
+                logger.error(f"Kconfig override not applied: {item}")
+            raise AssertionError(
+                "'kv' Kconfig overrides not applied: " + "; ".join(failed)
+            )
+
     def _apply_kconfig_overrides(
         self, conf_path: str, overrides: Dict[str, Any], cfg_cwd: str = ""
     ) -> None:
@@ -462,6 +545,12 @@ class NuttXBuilder:
                     # that enable new applications take effect
                     configure()
 
+                    # fail instead of building a configuration that
+                    # silently lost some overrides
+                    self._check_kconfig_overrides(
+                        nuttx_conf_path, kv_overrides
+                    )
+
                 # build
                 self._run_build(build_path, env=build_env)
 
diff --git a/tests/test_builder.py b/tests/test_builder.py
index cd5f7a0..b62461f 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -159,6 +159,9 @@ def 
test_builder_reconfigures_after_kv_overrides(monkeypatch) -> None:
     monkeypatch.setattr(
         b, "_apply_kconfig_overrides", lambda *args, **kwargs: None
     )
+    monkeypatch.setattr(
+        b, "_check_kconfig_overrides", lambda *args, **kwargs: None
+    )
 
     b.build_all()
 
@@ -303,6 +306,7 @@ def 
test_builder_regenerates_config_after_kconfig_overrides() -> None:
     builder._run_command = lambda command, env: calls.append(command)
     builder._make_dir = builder_make_dir_dummy
     builder._apply_kconfig_overrides = lambda *_args: None
+    builder._check_kconfig_overrides = lambda *_args: None
     builder.build_all()
 
     assert calls[1] == [
@@ -488,6 +492,7 @@ def test_builder_apply_kconfig_overrides_kconfig_tweak() -> 
None:
         "/usr/bin/kconfig-tweak",
         "--file",
         "/tmp/.config",
+        "--keep-case",
         "--disable",
         "CONFIG_FALSE",
     ]
@@ -761,3 +766,68 @@ def test_builder_flash_supports_elf_placeholder(tmp_path) 
-> None:
     builder._flash_core("core0", {"core0": core})
 
     assert commands == [["pxe-stage", str(image)]]
+
+
+def test_builder_check_kconfig_overrides(tmp_path) -> None:
+    b = NuttXBuilder(copy.deepcopy(conf_dir))
+    conf = tmp_path / ".config"
+    conf.write_text(
+        "# comment\n"
+        "CONFIG_NET_IPv6=y\n"
+        "# CONFIG_OFF is not set\n"
+        "CONFIG_HEX=0x10\n"
+        "CONFIG_INT=8192\n"
+        'CONFIG_STR="abc"\n'
+        "CONFIG_MOD=m\n",
+        encoding="utf-8",
+    )
+
+    assert b._read_kconfig_values(str(conf)) == {
+        "CONFIG_NET_IPv6": "y",
+        "CONFIG_OFF": "n",
+        "CONFIG_HEX": "0x10",
+        "CONFIG_INT": "8192",
+        "CONFIG_STR": '"abc"',
+        "CONFIG_MOD": "m",
+    }
+
+    # everything applied: mixed case, disabled and absent "n", numbers by
+    # value, strings with and without quotes
+    b._check_kconfig_overrides(
+        str(conf),
+        {
+            "CONFIG_NET_IPv6": True,
+            "CONFIG_OFF": "n",
+            "CONFIG_ABSENT": False,
+            "CONFIG_HEX": "0x0010",
+            "CONFIG_INT": 8192,
+            "CONFIG_STR": "abc",
+            "CONFIG_MOD": "m",
+        },
+    )
+
+    with patch("ntfc.builder.logger.error") as error_mock:
+        with pytest.raises(AssertionError) as excinfo:
+            b._check_kconfig_overrides(
+                str(conf),
+                {
+                    "CONFIG_NET_IPV6": "y",
+                    "CONFIG_OFF": "y",
+                    "CONFIG_HEX": "0x20",
+                    "CONFIG_STR": "xyz",
+                },
+            )
+    msg = str(excinfo.value)
+    assert "CONFIG_NET_IPV6: requested y, not in .config" in msg
+    assert "CONFIG_OFF: requested y, got n" in msg
+    assert "CONFIG_HEX: requested 0x20, got 0x10" in msg
+    assert 'CONFIG_STR: requested "xyz", got "abc"' in msg
+    assert error_mock.call_count == 4
+
+
+def test_builder_kconfig_values_equal() -> None:
+    assert NuttXBuilder._kconfig_values_equal("y", "y")
+    assert NuttXBuilder._kconfig_values_equal("0x0010", "0x10")
+    assert NuttXBuilder._kconfig_values_equal("16", "0x10")
+    assert not NuttXBuilder._kconfig_values_equal("y", "n")
+    assert not NuttXBuilder._kconfig_values_equal('"a"', '"b"')

Reply via email to