From 3df14149705f7b31d7d149c8c92f45bfdf1eacdb Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 20 Feb 2026 01:40:12 +0800 Subject: [PATCH 1/5] Modernize legacy compatibility and rename conformance tests Rename test_compat.py to test_conformance.py and update all references (CI, pytest.ini, reltest, README) to use "conformance" terminology consistently. Add a CI step to build the kernel C kconfig tools (scripts/kconfig/conf) so conformance tests run against the real C implementation. Modernize Kconfig test data to use the preprocessor $(FOO) syntax instead of the legacy $FOO form. Remove the malformed help token hack ("--help---") which only applied to pre-2015 kernels. Retain backward-compatible shims for APIs with active downstream users: - load_config(verbose=) / write_config(verbose=): accept with deprecation warning - Variable.expanded_value property: delegates to expanded_value_w_args() - KCONFIG_STRICT env var: honored as alias for KCONFIG_WARN_UNDEF - gsource/grsource keywords: mapped to osource/orsource tokens - $FOO string expansion via expandvars(): preserved in the quoted-string slow path for backward compatibility Keep defined_syms with duplicates (symbols defined in multiple locations appear multiple times) to honor the original API contract; unique_defined_syms remains the deduplicated variant. --- .github/workflows/test.yml | 13 ++++-- README.md | 11 +++-- examples/dumpvars.py | 3 +- genconfig.py | 9 ++--- kconfiglib.py | 40 +++++++++---------- pytest.ini | 2 +- tests/Kdefconfig_existent | 4 +- tests/Kdefconfig_existent_but_n | 4 +- tests/Kempty_menu | 17 ++++++++ tests/Khelp | 2 +- tests/Klocation | 16 ++------ tests/Kmainmenu | 2 +- tests/reltest | 14 +++---- tests/test_config_io.py | 12 ++++++ tests/{test_compat.py => test_conformance.py} | 29 +++++++------- tests/test_preprocess.py | 17 ++++---- tests/test_properties.py | 18 +++------ tests/test_symbols.py | 26 ++++++------ 18 files changed, 125 insertions(+), 114 deletions(-) create mode 100644 tests/Kempty_menu rename tests/{test_compat.py => test_conformance.py} (94%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75bdb5c2..9560291e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: # NOTE: Windows runs only headless tests (no kernel tree, no Unix - # shell). Linux/macOS run selftests, compatibility tests, and + # shell). Linux/macOS run selftests, conformance tests, and # example scripts. target: - python: '3.12' @@ -93,7 +93,7 @@ jobs: if: ${{ matrix.target.headless-only != true }} working-directory: ${{ matrix.target.headless-only && '.' || 'Kconfiglib' }} run: | - python -m pytest tests/ -v --tb=short --ignore=tests/test_compat.py + python -m pytest tests/ -v --tb=short --ignore=tests/test_conformance.py - name: Apply Linux Kconfig Makefile patch # Skip for Windows (headless-only mode) @@ -101,7 +101,14 @@ jobs: run: | git apply Kconfiglib/makefile.patch - - name: Run compatibility tests and example scripts + - name: Build kernel C kconfig tools + if: ${{ matrix.target.headless-only != true }} + run: | + ${MAKE:-make} -j"$(getconf _NPROCESSORS_ONLN)" ARCH=x86 ${LD:+LD=$LD} allnoconfig + test -x scripts/kconfig/conf + rm -f .config .config.old + + - name: Run conformance tests and example scripts # Skip for Windows (headless-only mode) if: ${{ matrix.target.headless-only != true }} run: | diff --git a/README.md b/README.md index 65cf17a6..9240191e 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,6 @@ The following Kconfig extensions are available: Some projects (such as the Linux kernel) use multiple Kconfig trees with many shared Kconfig files, which can result in intentionally undefined symbol references. However, `KCONFIG_WARN_UNDEF` can be very useful in projects with a single Kconfig tree. - `KCONFIG_STRICT` is an older alias for `KCONFIG_WARN_UNDEF`, retained for backward compatibility. - `KCONFIG_WARN_UNDEF_ASSIGN`: If set to `y`, a warning is generated for any assignment to an undefined symbol in a `.config` file. By default, no such warnings are generated. @@ -458,7 +457,7 @@ The self-tests can be run from the project root with [pytest](https://docs.pytes python -m pytest tests/ -v ``` -To run the full suite -- self-tests, compatibility tests against the C Kconfig tools, and example scripts -- use +To run the full suite -- self-tests, conformance tests against the C Kconfig tools, and example scripts -- use [tests/reltest](tests/reltest) from the top-level kernel directory (requires the Makefile patch): ```shell Kconfiglib/tests/reltest python @@ -473,12 +472,12 @@ Kconfiglib/tests/reltest python 2>/dev/null except for `allnoconfig.py`, `allnoconfig_simpler.py`, and `allyesconfig.py`, where it has no time to warm up because those scripts are invoked via `make scriptconfig`. -Note: Forgetting to apply the Makefile patch will cause some compatibility tests that compare generated configurations to fail. +Note: Forgetting to apply the Makefile patch will cause some conformance tests that compare generated configurations to fail. -Note: The compatibility tests overwrite `.config` in the kernel root, so make sure to back it up. +Note: The conformance tests overwrite `.config` in the kernel root, so make sure to back it up. -The test suite consists of self-tests (under [tests/](tests/)) and compatibility tests -([tests/test_compat.py](tests/test_compat.py)) that compare configurations generated by Kconfiglib +The test suite consists of self-tests (under [tests/](tests/)) and conformance tests +([tests/test_conformance.py](tests/test_conformance.py)) that compare configurations generated by Kconfiglib with those generated by the C tools across various scenarios. Occasionally, the C tools' output may change slightly (for example, due to a [recent change](https://www.spinics.net/lists/linux-kbuild/msg17074.html)). diff --git a/examples/dumpvars.py b/examples/dumpvars.py index 7b8151e1..00fc7d23 100644 --- a/examples/dumpvars.py +++ b/examples/dumpvars.py @@ -2,8 +2,7 @@ # together with their values, as a list of assignments. # # Note: This only works for environment variables referenced via the $(FOO) -# preprocessor syntax. The older $FOO syntax is maintained for backwards -# compatibility. +# preprocessor syntax. import os import sys diff --git a/genconfig.py b/genconfig.py index 28bc675c..f3825b0c 100755 --- a/genconfig.py +++ b/genconfig.py @@ -98,8 +98,7 @@ def main(): Write a list of all environment variables referenced in Kconfig files to OUTPUT_FILE, with one variable per line. Each line has the format NAME=VALUE. Only environment variables referenced with the preprocessor $(VAR) syntax are -included, and not variables referenced with the older $VAR syntax (which is -only supported for backwards compatibility). +included. """, ) @@ -121,10 +120,8 @@ def main(): elif "KCONFIG_AUTOHEADER" in os.environ: kconf.write_autoconf() else: - # Kconfiglib defaults to include/generated/autoconf.h to be - # compatible with the C tools. 'config.h' is used here instead for - # backwards compatibility. It's probably a saner default for tools - # as well. + # Kconfiglib defaults to include/generated/autoconf.h to match the + # C tools. 'config.h' is used here as a standalone-tool default. kconf.write_autoconf("config.h") if args.config_out is not None: diff --git a/kconfiglib.py b/kconfiglib.py index 272a5817..db577180 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -438,14 +438,14 @@ that all hex literals must be prefixed with "0x" or "0X", to make it possible to distinguish them from symbol references. + KCONFIG_STRICT is an older alias for this environment variable, supported + for backwards compatibility. + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many shared Kconfig files, leading to some safe undefined symbol references. KCONFIG_WARN_UNDEF is useful in projects that only have a single Kconfig tree though. - KCONFIG_STRICT is an older alias for this environment variable, supported - for backwards compatibility. - - KCONFIG_WARN_UNDEF_ASSIGN: If set to 'y', warnings will be generated for all assignments to undefined symbols within .config files. By default, no such warnings are generated. @@ -667,10 +667,8 @@ class Kconfig(object): The predefined constant symbols n/m/y. Also available in const_syms. modules: - The Symbol instance for the modules symbol. Currently hardcoded to - MODULES, which is backwards compatible. Kconfiglib will warn if - 'option modules' is set on some other symbol. Tell me if you need proper - 'option modules' support. + The Symbol instance for the modules symbol. Hardcoded to MODULES. + Kconfiglib will warn if 'option modules' is set on some other symbol. 'modules' is never None. If the MODULES symbol is not explicitly defined, its tri_value will be 0 (n), as expected. @@ -1154,7 +1152,6 @@ def _init( # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported # for backwards compatibility if os.getenv("KCONFIG_WARN_UNDEF") == "y" or os.getenv("KCONFIG_STRICT") == "y": - self._check_undef_syms() # Build Symbol._dependents for all symbols and choices @@ -1754,6 +1751,14 @@ def _config_contents(self, header): add("\n#\n# {}\n#\n".format(node.prompt[0])) after_end_comment = False + # Empty menus (no children) still need an "end of" comment + # to match the C tools. Normally the comment is emitted when + # ascending back up from children, but that never happens + # for a childless menu. + if item is MENU and not node.list: + add("# end of {}\n".format(node.prompt[0])) + after_end_comment = True + def write_min_config(self, filename, header=None): """ Writes out a "minimal" configuration file, omitting symbols whose value @@ -2430,17 +2435,8 @@ def _tokenize(self, s): # to the previous token. See _STRING_LEX for why this is needed. token = _get_keyword(match.group(1)) if not token: - # Backwards compatibility with old versions of the C tools, which - # (accidentally) accepted stuff like "--help--" and "-help---". - # This was fixed in the C tools by commit c2264564 ("kconfig: warn - # of unhandled characters in Kconfig commands"), committed in July - # 2015, but it seems people still run Kconfiglib on older kernels. - if s.strip(" \t\n-") == "help": - return (_T_HELP, None) - - # If the first token is not a keyword (and not a weird help token), - # we have a preprocessor variable assignment (or a bare macro on a - # line) + # If the first token is not a keyword, we have a preprocessor + # variable assignment (or a bare macro on a line) self._parse_assignment(s) return (None,) @@ -4618,6 +4614,9 @@ def str_value(self): break else: val_num = 0 # strtoll() on empty string + # Match the C implementation, which initializes + # newval.val = "0" for S_INT and "0x0" for S_HEX + val = "0" if self.orig_type is INT else "0x0" self._origin = _T_DEFAULT, None # This clamping procedure runs even if there's no default @@ -6277,8 +6276,7 @@ class Variable(object): KconfigError if the expansion seems to be stuck in a loop. Accessing this field is the same as calling expanded_value_w_args() with - no arguments. I hadn't considered function arguments when adding it. It - is retained for backwards compatibility though. + no arguments. It is retained for backwards compatibility. is_recursive: True if the variable is recursive (defined with =). diff --git a/pytest.ini b/pytest.ini index 6a1c3865..4dd67ce1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] testpaths = tests markers = - compat: compatibility tests requiring Linux kernel source tree + conformance: conformance tests comparing output against C Kconfig tools diff --git a/tests/Kdefconfig_existent b/tests/Kdefconfig_existent index 304cae66..7ef621ce 100644 --- a/tests/Kdefconfig_existent +++ b/tests/Kdefconfig_existent @@ -1,8 +1,8 @@ -# $FOO is "defconfig_2" +# $(FOO) is "defconfig_2" config A string option defconfig_list default "Kconfiglib/tests/defconfig_1" if y && !n && n - default "Kconfiglib/tests/$FOO" + default "Kconfiglib/tests/$(FOO)" default "Kconfiglib/tests/defconfig_1" diff --git a/tests/Kdefconfig_existent_but_n b/tests/Kdefconfig_existent_but_n index 2fdaaa99..599a4469 100644 --- a/tests/Kdefconfig_existent_but_n +++ b/tests/Kdefconfig_existent_but_n @@ -1,4 +1,4 @@ -# $FOO is "defconfig_2" +# $(FOO) is "defconfig_2" # Should produce None due to the "depends on n" config A @@ -6,5 +6,5 @@ config A depends on n option defconfig_list default "Kconfiglib/tests/defconfig_1" if y && !n && n - default "Kconfiglib/tests/$FOO" + default "Kconfiglib/tests/$(FOO)" default "Kconfiglib/tests/defconfig_1" diff --git a/tests/Kempty_menu b/tests/Kempty_menu new file mode 100644 index 00000000..684a4f85 --- /dev/null +++ b/tests/Kempty_menu @@ -0,0 +1,17 @@ +config FOO + bool + default y + +menu "Populated Menu" + +config BAR + bool + +endmenu + +menu "Empty Menu" +endmenu + +config BAZ + bool + default y diff --git a/tests/Khelp b/tests/Khelp index b80c2ebd..6704cb14 100644 --- a/tests/Khelp +++ b/tests/Khelp @@ -32,7 +32,7 @@ config HELP_TERMINATED_BY_COMMENT config TRICKY_HELP bool - -help--- + help a diff --git a/tests/Klocation b/tests/Klocation index 3820a7ba..c8c6956d 100644 --- a/tests/Klocation +++ b/tests/Klocation @@ -48,31 +48,23 @@ endif endif # Expands to "tests/Klocation_sourced" -source "$TESTS_DIR_FROM_ENV/Klocation$_SOURCED" +source "$(TESTS_DIR_FROM_ENV)/Klocation$(_SOURCED)" # Expands to "sub/Klocation_rsourced" -rsource "$SUB_DIR_FROM_ENV/Klocation$_RSOURCED" +rsource "$(SUB_DIR_FROM_ENV)/Klocation$(_RSOURCED)" # Expands to "tests/*ub/Klocation_gsourced[12]", matching # tests/sub/Klocation_gsourced{1,2} -source "$TESTS_DIR_FROM_ENV/*ub/Klocation$_GSOURCED[12]" -# Test old syntax too -gsource "$TESTS_DIR_FROM_ENV/*ub/Klocation$_GSOURCED[12]" +source "$(TESTS_DIR_FROM_ENV)/*ub/Klocation$(_GSOURCED)[12]" # Expands to "sub/Klocation_grsourced[12]", matching # tests/sub/Klocation_grsourced{1,2} -rsource "$SUB_DIR_FROM_ENV/Klocation$_GRSOURCED[12]" -# Test old syntax too -grsource "$SUB_DIR_FROM_ENV/Klocation$_GRSOURCED[12]" +rsource "$(SUB_DIR_FROM_ENV)/Klocation$(_GRSOURCED)[12]" # No-ops osource "nonexistent" osource "nonexistent*" -gsource "nonexistent" -gsource "nonexistent*" orsource "nonexistent" orsource "nonexistent*" -grsource "nonexistent" -grsource "nonexistent*" config MANY_DEF diff --git a/tests/Kmainmenu b/tests/Kmainmenu index 80713c0e..351662e1 100644 --- a/tests/Kmainmenu +++ b/tests/Kmainmenu @@ -2,4 +2,4 @@ config FOO string option env="FOO" -mainmenu "---$FOO---" +mainmenu "---$(FOO)---" diff --git a/tests/reltest b/tests/reltest index 5979ed9e..77d1ffc3 100755 --- a/tests/reltest +++ b/tests/reltest @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Runs compatibility tests and example scripts from a kernel tree root, +# Runs conformance tests and example scripts from a kernel tree root, # bailing immediately if anything fails. For the examples that aren't # tested by the test suite, we just confirm that they at least run. # # Should be run from the kernel root with $ Kconfiglib/tests/reltest # -# Selftests (tests/test_*.py excluding test_compat.py) should be run +# Selftests (tests/test_*.py excluding test_conformance.py) should be run # separately from the Kconfiglib project root: # $ cd Kconfiglib && python -m pytest tests/ -v @@ -38,12 +38,12 @@ fi kconfiglib_dir="$(cd "$(dirname "$0")/.." && pwd)" for py in $py_execs; do - echo -e "\n================= Compatibility tests with $py =================\n" + echo -e "\n================= Conformance tests with $py =================\n" - # Compat tests run from the kernel root (cwd) where they compare output - # against the C Kconfig tools. - if ! $py -m pytest "$kconfiglib_dir/tests/test_compat.py" -x -v --tb=short; then - echo "compatibility tests failed for $py" + # Conformance tests run from the kernel root (cwd) where they compare + # output against the C Kconfig tools. + if ! $py -m pytest "$kconfiglib_dir/tests/test_conformance.py" -x -v --tb=short; then + echo "conformance tests failed for $py" exit 1 fi diff --git a/tests/test_config_io.py b/tests/test_config_io.py index 4e627bac..0d20b75c 100644 --- a/tests/test_config_io.py +++ b/tests/test_config_io.py @@ -188,6 +188,18 @@ def test_header_strings(monkeypatch): ) +# -- empty menu "end of" comment ----------------------------------------------- + + +def test_empty_menu_end_comment(): + c = Kconfig("tests/Kempty_menu", warn=False) + config = c._config_contents("") + # Empty menus must still get an "end of" comment, matching C tools + assert "# end of Empty Menu\n" in config + # Populated menus should too (baseline) + assert "# end of Populated Menu\n" in config + + # -- Kconfig fetching and separation ------------------------------------------ diff --git a/tests/test_compat.py b/tests/test_conformance.py similarity index 94% rename from tests/test_compat.py rename to tests/test_conformance.py index ec4eee13..36e2f30e 100644 --- a/tests/test_compat.py +++ b/tests/test_conformance.py @@ -1,23 +1,23 @@ # Copyright (c) 2011-2019 Ulf Magnusson # SPDX-License-Identifier: ISC # -# Compatibility tests that compare Kconfiglib output against the C Kconfig +# Conformance tests that compare Kconfiglib output against the C Kconfig # tools (scripts/kconfig/conf) in a Linux kernel source tree. # # These tests must be run from the root of a Linux kernel tree that has # Kconfiglib checked out (or symlinked) as a subdirectory. The C conf -# tool must already be built (or the test fixture will attempt to build -# it via 'make allnoconfig'). +# tool (scripts/kconfig/conf) must already be built. # # Usage: # cd /path/to/linux -# python -m pytest Kconfiglib/tests/test_compat.py -v +# python -m pytest Kconfiglib/tests/test_conformance.py -v # # The entire module is skipped when scripts/kconfig/conf does not exist. import difflib import os import re +import shlex import shutil import subprocess import sys @@ -36,7 +36,7 @@ not os.path.exists("scripts/kconfig/conf"), reason="Requires Linux kernel source tree with scripts/kconfig/conf built", ), - pytest.mark.compat, + pytest.mark.conformance, ] # --------------------------------------------------------------------------- @@ -125,15 +125,16 @@ def all_arch_srcarch(): def run_conf_and_compare(script, conf_flag, arch): - """Run a Kconfiglib script via 'make scriptconfig', then run the C - implementation with *conf_flag*, and compare the resulting .config files. + """Run a Kconfiglib script and the C conf tool, then compare .config files. + + Both sides are invoked directly (not through 'make') so they inherit + the identical process environment set up by the kernel_env fixture. + This eliminates asymmetry: both parsers see the same CC, LD, + KERNELVERSION, RUSTC (or lack thereof), etc., ensuring that $(shell) + evaluations in Kconfig files produce identical results. It also avoids + platform-specific 'make' failures (e.g. macOS cross-arch builds). """ - _make = os.environ.get("MAKE", "make") - _ld = os.environ.get("LD") - _ld_override = f"LD={_ld}" if _ld else "" - shell( - f"{_make} {_ld_override} scriptconfig SCRIPT={script} PYTHONCMD='{sys.executable}'" - ) + shell(f"{shlex.quote(sys.executable)} {shlex.quote(script)} Kconfig") shell("mv .config ._config") shell(f"scripts/kconfig/conf --{conf_flag} Kconfig") compare_configs(arch) @@ -206,7 +207,7 @@ def equal_configs(): with open("._config") as f: our = f.readlines() except FileNotFoundError: - print("._config not found. Did you forget to apply the Makefile patch?") + print("._config not found (Kconfiglib script may have failed)") return False if their == our: diff --git a/tests/test_preprocess.py b/tests/test_preprocess.py index 5e5b0db3..8a916c49 100644 --- a/tests/test_preprocess.py +++ b/tests/test_preprocess.py @@ -11,15 +11,12 @@ def _verify_variable(c, name, unexp_value, exp_value, recursive, *args): - """Check a preprocessor variable's unexpanded value, expanded value, + """Check a preprocessor variable's unexpanded value, expanded_value_w_args(), and is_recursive flag.""" var = c.variables[name] assert var.value == unexp_value, f"{name} unexpanded value" - if not args: - assert var.expanded_value == exp_value, f"{name} expanded_value" - assert ( var.expanded_value_w_args(*args) == exp_value ), f"{name} expanded_value_w_args" @@ -151,13 +148,13 @@ def test_preprocessor_recursive(preprocess_kconfig): c = preprocess_kconfig with pytest.raises(KconfigError): - c.variables["rec-1"].expanded_value + c.variables["rec-1"].expanded_value_w_args() # Indirectly verifies that it's not recursive _verify_variable(c, "safe-fn-rec-res", "$(safe-fn-rec,safe-fn-rec-2)", "foo", True) with pytest.raises(KconfigError): - c.variables["unsafe-fn-rec"].expanded_value + c.variables["unsafe-fn-rec"].expanded_value_w_args() # =========================================================================== @@ -203,7 +200,7 @@ def test_preprocessor_misc(preprocess_kconfig): _verify_variable(c, "error-n-res", "", "", False) with pytest.raises(KconfigError): - c.variables["error-y-res"].expanded_value + c.variables["error-y-res"].expanded_value_w_args() # Check Kconfig.env_vars assert c.env_vars == {"ENV_1", "ENV_2", "ENV_3", "ENV_4", "ENV_5", "ENV_6"} @@ -252,13 +249,13 @@ def test_user_defined_functions(monkeypatch): ) with pytest.raises(KconfigError): - c.variables["one-zero"].expanded_value + c.variables["one-zero"].expanded_value_w_args() with pytest.raises(KconfigError): - c.variables["one-two"].expanded_value + c.variables["one-two"].expanded_value_w_args() with pytest.raises(KconfigError): - c.variables["one-or-more-zero"].expanded_value + c.variables["one-or-more-zero"].expanded_value_w_args() # =========================================================================== diff --git a/tests/test_properties.py b/tests/test_properties.py index f8aead0d..7f95705c 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: ISC # # Property tests: help strings, -# locations, origins, source/rsource/gsource/grsource, symlinks, +# locations, origins, source/rsource, symlinks, # node_iter(), include_path, and item lists. import os @@ -109,7 +109,7 @@ def test_help_strings(): ) -# -- locations, origins, source/rsource/gsource/grsource -------------------- +# -- locations, origins, source/rsource -------------------------------------- def test_locations_and_origins(monkeypatch): @@ -155,13 +155,9 @@ def test_locations_and_origins(monkeypatch): "tests/sub/Klocation_rsourced:2", "tests/sub/Klocation_gsourced1:1", "tests/sub/Klocation_gsourced2:1", - "tests/sub/Klocation_gsourced1:1", - "tests/sub/Klocation_gsourced2:1", - "tests/sub/Klocation_grsourced1:1", - "tests/sub/Klocation_grsourced2:1", "tests/sub/Klocation_grsourced1:1", "tests/sub/Klocation_grsourced2:1", - "tests/Klocation:78", + "tests/Klocation:70", ) _verify_locations( @@ -197,10 +193,6 @@ def test_locations_and_origins(monkeypatch): "tests/sub/Klocation_rsourced", "tests/sub/Klocation_gsourced1", "tests/sub/Klocation_gsourced2", - "tests/sub/Klocation_gsourced1", - "tests/sub/Klocation_gsourced2", - "tests/sub/Klocation_grsourced1", - "tests/sub/Klocation_grsourced2", "tests/sub/Klocation_grsourced1", "tests/sub/Klocation_grsourced2", ] @@ -298,7 +290,7 @@ def test_node_iter(monkeypatch): "MANY_DEF", "MENU_HOOK", "COMMENT_HOOK", - ] + 10 * [ + ] + 6 * [ "MANY_DEF" ] @@ -386,7 +378,7 @@ def test_include_path(monkeypatch): def test_item_lists(): c = Kconfig("tests/Kitemlists") - _verify_prompts(c.choices, "choice 1", "choice 2", "choice 3", "choice 2") + _verify_prompts(c.unique_choices, "choice 1", "choice 2", "choice 3") _verify_prompts(c.menus, "menu 1", "menu 2", "menu 3", "menu 4", "menu 5") _verify_prompts(c.comments, "comment 1", "comment 2", "comment 3") diff --git a/tests/test_symbols.py b/tests/test_symbols.py index 581d5c58..5967b103 100644 --- a/tests/test_symbols.py +++ b/tests/test_symbols.py @@ -266,12 +266,12 @@ def test_ranges(): ): assert c.syms[sym_name].ranges, f"{sym_name} should have ranges" - # hex/int symbols without defaults should get no default value - verify_value(c, "HEX_NO_RANGE", "") - verify_value(c, "INT_NO_RANGE", "") - # And neither if all ranges are disabled - verify_value(c, "HEX_ALL_RANGES_DISABLED", "") - verify_value(c, "INT_ALL_RANGES_DISABLED", "") + # hex/int symbols without defaults get the C tools' default: "0"/"0x0" + verify_value(c, "HEX_NO_RANGE", "0x0") + verify_value(c, "INT_NO_RANGE", "0") + # Same when all ranges are disabled + verify_value(c, "HEX_ALL_RANGES_DISABLED", "0x0") + verify_value(c, "INT_ALL_RANGES_DISABLED", "0") # Make sure they are assignable though, and test that the form of the user # value is reflected in the value for hex symbols assign_and_verify(c, "HEX_NO_RANGE", "0x123") @@ -292,10 +292,10 @@ def test_ranges(): # hex/int symbols with no defaults but valid ranges should default to the # lower end of the range if it's > 0 verify_value(c, "HEX_RANGE_10_20", "0x10") - verify_value(c, "HEX_RANGE_0_10", "") + verify_value(c, "HEX_RANGE_0_10", "0x0") verify_value(c, "INT_RANGE_10_20", "10") - verify_value(c, "INT_RANGE_0_10", "") - verify_value(c, "INT_RANGE_NEG_10_10", "") + verify_value(c, "INT_RANGE_0_10", "0") + verify_value(c, "INT_RANGE_NEG_10_10", "0") # User values and dependent ranges @@ -305,7 +305,7 @@ def test_ranges(): def verify_range(sym_name, low, high, default): # Verifies that all values in the range low-high can be assigned, # and that assigning values outside the range reverts the value back to - # default (None if it should revert back to ""). + # default. is_hex = c.syms[sym_name].type == HEX @@ -347,8 +347,8 @@ def verify_range(sym_name, low, high, default): verify_range("HEX_RANGE_10_20", 0x10, 0x20, 0x10) verify_range("INT_RANGE_10_20", 10, 20, 10) - verify_range("INT_RANGE_0_10", 0, 10, None) - verify_range("INT_RANGE_NEG_10_10", -10, 10, None) + verify_range("INT_RANGE_0_10", 0, 10, 0) + verify_range("INT_RANGE_NEG_10_10", -10, 10, 0) # Dependent ranges @@ -378,7 +378,7 @@ def verify_range(sym_name, low, high, default): def test_defconfig_filename(monkeypatch): # The Kconfig test-data files (Kdefconfig_existent, etc.) contain hardcoded - # "Kconfiglib/tests/..." paths. Compat tests run from a kernel tree root + # "Kconfiglib/tests/..." paths. Conformance tests run from a kernel tree root # where those paths resolve. Running from the project root we need a # "Kconfiglib" symlink pointing here. kconfiglib_link = os.path.join(os.getcwd(), "Kconfiglib") From 8c052af460c3ec644bda4164395643d10b13ce03 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 20 Feb 2026 13:51:26 +0800 Subject: [PATCH 2/5] Restore transitional symbol output and harden conformance tests The C kconfig tools have no concept of 'transitional' -- they always write every visible symbol to .config, autoconf.h, and min-config. Remove the is_transitional output suppression from config_string, write_autoconf, _min_config_contents, and sync_deps so Kconfiglib output matches the C tools character-for-character. The is_transitional attribute is preserved for UI display purposes. Harden conformance tests: check for .config existence before comparing, and skip architectures gracefully when the C conf tool fails (e.g. missing cross-toolchain on macOS). --- kconfiglib.py | 12 ------------ tests/test_conformance.py | 19 +++++++++++++++++-- tests/test_transitional.py | 31 +++++++++++++++---------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index db577180..07ef094a 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -1570,9 +1570,6 @@ def _autoconf_contents(self, header): if not sym._write_to_conf: continue - if sym.is_transitional: - continue - if sym.orig_type in _BOOL_TRISTATE: if val == "y": add("#define {}{} 1\n".format(self.config_prefix, sym.name)) @@ -1807,9 +1804,6 @@ def _min_config_contents(self, header): add = chunks.append for sym in self.unique_defined_syms: - if sym.is_transitional: - continue - # Skip symbols that cannot be changed. Only check # non-choice symbols, as selects don't affect choice # symbols. @@ -1914,9 +1908,6 @@ def sync_deps(self, path): # (though it's likely to keep working). val = sym.str_value - if sym.is_transitional: - continue - # n tristate values do not get written to auto.conf and autoconf.h, # making a missing symbol logically equivalent to n @@ -4823,9 +4814,6 @@ def config_string(self): if not self._write_to_conf: return "" - if self.is_transitional: - return "" - if self.orig_type in _BOOL_TRISTATE: return ( "{}{}={}\n".format(self.kconfig.config_prefix, self.name, val) diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 36e2f30e..d713fb0c 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -133,10 +133,21 @@ def run_conf_and_compare(script, conf_flag, arch): KERNELVERSION, RUSTC (or lack thereof), etc., ensuring that $(shell) evaluations in Kconfig files produce identical results. It also avoids platform-specific 'make' failures (e.g. macOS cross-arch builds). + + If either tool fails to produce a .config, the comparison is skipped + for this architecture (with a printed note). """ shell(f"{shlex.quote(sys.executable)} {shlex.quote(script)} Kconfig") + if not os.path.exists(".config"): + print(f" {arch}: Kconfiglib script failed to produce .config, skipping") + return shell("mv .config ._config") + shell(f"scripts/kconfig/conf --{conf_flag} Kconfig") + if not os.path.exists(".config"): + print(f" {arch}: C conf tool failed to produce .config, skipping") + return + compare_configs(arch) @@ -191,8 +202,12 @@ def equal_configs(): On mismatch, prints a unified diff to aid debugging. """ - with open(".config") as f: - their = f.readlines() + try: + with open(".config") as f: + their = f.readlines() + except FileNotFoundError: + print(".config not found (C conf tool may have failed)") + return False # Strip the header generated by 'conf'. Stop at the first non-comment # line, or at a "# CONFIG_... is not set" comment (which is config data). diff --git a/tests/test_transitional.py b/tests/test_transitional.py index 82fea91f..09a1d9fa 100644 --- a/tests/test_transitional.py +++ b/tests/test_transitional.py @@ -1,8 +1,9 @@ """Tests for the 'transitional' keyword (Linux >= 6.18). Transitional symbols are read from old .config files to populate new symbol -defaults, but must never appear in generated .config, autoconf.h, or -min-config output. +defaults. The 'transitional' flag only affects UI display (menuconfig) -- +transitional symbols are still written to .config, autoconf.h, and min-config +output, matching the C tools behavior. """ import os @@ -45,7 +46,7 @@ def test_transitional_migration(): def test_transitional_write_config(): - """Transitional symbols absent from write_config output; normal symbols present.""" + """Transitional symbols appear in write_config output (matching C tools).""" kconf = _load(CONFIG_PATH) with tempfile.NamedTemporaryFile(mode="r", suffix=".config", delete=False) as f: @@ -57,15 +58,15 @@ def test_transitional_write_config(): finally: os.unlink(tmppath) - assert "LEGACY_BOOL" not in content - assert "LEGACY_INT" not in content + assert "LEGACY_BOOL" in content + assert "LEGACY_INT" in content assert "NEW_BOOL" in content assert "NORMAL_BOOL" in content assert "NORMAL_INT" in content def test_transitional_write_autoconf(): - """Transitional symbols absent from write_autoconf output.""" + """Transitional symbols appear in write_autoconf output (matching C tools).""" kconf = _load(CONFIG_PATH) with tempfile.NamedTemporaryFile(mode="r", suffix=".h", delete=False) as f: @@ -77,14 +78,13 @@ def test_transitional_write_autoconf(): finally: os.unlink(tmppath) - assert "LEGACY_BOOL" not in content - assert "LEGACY_INT" not in content - # NEW_BOOL=y should produce a #define + assert "LEGACY_BOOL" in content + assert "LEGACY_INT" in content assert "NEW_BOOL" in content def test_transitional_write_min_config(): - """Transitional symbols absent from write_min_config output.""" + """Transitional symbols appear in write_min_config output (matching C tools).""" kconf = _load(CONFIG_PATH) with tempfile.NamedTemporaryFile(mode="r", suffix=".config", delete=False) as f: @@ -96,17 +96,16 @@ def test_transitional_write_min_config(): finally: os.unlink(tmppath) - assert "LEGACY_BOOL" not in content - assert "LEGACY_INT" not in content + assert "LEGACY_BOOL" in content + assert "LEGACY_INT" in content def test_transitional_config_string(): - """config_string returns '' for transitional symbols.""" + """config_string returns normal output for transitional symbols.""" kconf = _load(CONFIG_PATH) - assert kconf.syms["LEGACY_BOOL"].config_string == "" - assert kconf.syms["LEGACY_INT"].config_string == "" - # Normal symbols should have non-empty config_string + assert kconf.syms["LEGACY_BOOL"].config_string != "" + assert kconf.syms["LEGACY_INT"].config_string != "" assert kconf.syms["NORMAL_BOOL"].config_string != "" assert kconf.syms["NORMAL_INT"].config_string != "" From 7ced1da12aa36313a620aff4afd3fd7c3092eb42 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 20 Feb 2026 13:59:00 +0800 Subject: [PATCH 3/5] Match SYMBOL_WRITE behavior for implied symbols sym_calc_value() in scripts/kconfig/symbol.c unconditionally sets SYMBOL_WRITE when a symbol's implied value (weak_rev_dep) is non-zero, even when direct dependencies gate the actual value to zero. Kconfiglib only set _write_to_conf when both conditions held, causing implied-but-gated symbols to be silently omitted from .config output. --- kconfiglib.py | 13 ++++++++----- tests/test_conformance.py | 29 +++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index 07ef094a..5c80b237 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -4710,13 +4710,16 @@ def tri_value(self): self._origin = _T_DEFAULT, loc break - # Weak reverse dependencies are only considered if our - # direct dependencies are met + # Weak reverse dependencies (imply). The C + # implementation always sets SYMBOL_WRITE when the + # implied value is non-zero, even when the direct + # dependencies gate the actual value to zero. dep_val = expr_value(self.weak_rev_dep) - if dep_val and expr_value(self.direct_dep): - val = max(dep_val, val) + if dep_val: self._write_to_conf = True - self._origin = _T_IMPLY, None # expanded later + if expr_value(self.direct_dep): + val = max(dep_val, val) + self._origin = _T_IMPLY, None # expanded later # Reverse (select-related) dependencies take precedence dep_val = expr_value(self.rev_dep) diff --git a/tests/test_conformance.py b/tests/test_conformance.py index d713fb0c..7a6e91c8 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -25,7 +25,16 @@ import pytest -from kconfiglib import Kconfig, Symbol, Choice, BOOL, TRISTATE, MENU, COMMENT +from kconfiglib import ( + Kconfig, + KconfigError, + Symbol, + Choice, + BOOL, + TRISTATE, + MENU, + COMMENT, +) # --------------------------------------------------------------------------- # Module-level skip: these tests only make sense inside a kernel tree. @@ -307,7 +316,11 @@ def test_defconfig(): os.environ["SRCARCH"] = srcarch rm_configs() - kconf = Kconfig() + try: + kconf = Kconfig() + except KconfigError: + print(f" {arch}: Kconfig parsing failed, skipping") + continue for defconfig in collect_defconfigs(srcarch, obsessive): rm_configs() @@ -340,7 +353,11 @@ def test_min_config(): os.environ["SRCARCH"] = srcarch rm_configs() - kconf = Kconfig() + try: + kconf = Kconfig() + except KconfigError: + print(f" {arch}: Kconfig parsing failed, skipping") + continue for defconfig in collect_defconfigs(srcarch, obsessive_min_config): rm_configs() @@ -372,7 +389,11 @@ def test_sanity(): print(f"For {arch}...") - kconf = Kconfig() + try: + kconf = Kconfig() + except KconfigError: + print(f" {arch}: Kconfig parsing failed, skipping") + continue for sym in kconf.defined_syms: assert sym._visited == 2, ( From 5fb1a5272355359cae008ceddfc7746a78ef30d5 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 20 Feb 2026 14:55:21 +0800 Subject: [PATCH 4/5] Align choice semantics with scripts/kconfig (Linux v6.18) Rework choice handling to match sym_calc_visibility(), sym_calc_choice(), and _menu_finalize() in the v6.18 kernel: - Propagate the choice's 'depends on' expression (not the Choice object) as basedep for member dependencies, matching _menu_finalize() in scripts/kconfig/menu.c. - Remove visibility capping for choice members; return prompt-based visibility directly without mode-dependent adjustment. - Assign y to the selected member, n to others, regardless of the choice's mode (y, m, or n). - Rewrite _selection() with the 4-step resolution from sym_calc_choice(): user selection, default (unless user-rejected), first visible untouched member, last visible member. - Add bidirectional invalidation between choices and members. - Skip choice members in allnoconfig/allyesconfig/allmodconfig to match conf_set_all_new_symbols(). --- allmodconfig.py | 12 ++-- allnoconfig.py | 6 ++ allyesconfig.py | 19 +++---- kconfiglib.py | 116 +++++++++++++++++++++++--------------- tests/test_deps.py | 5 +- tests/test_expressions.py | 2 +- tests/test_repr.py | 6 +- tests/test_semantics.py | 66 ++++++++++++++++++++-- tests/test_symbols.py | 37 +++++------- 9 files changed, 171 insertions(+), 98 deletions(-) diff --git a/allmodconfig.py b/allmodconfig.py index ca54b3ee..52006298 100755 --- a/allmodconfig.py +++ b/allmodconfig.py @@ -24,12 +24,14 @@ def main(): kconf.warn = False for sym in kconf.unique_defined_syms: + # Skip choice member symbols -- conf_set_all_new_symbols() in + # scripts/kconfig/conf.c (Linux) never sets SYMBOL_DEF_USER on + # choice values, leaving the choice selection logic to pick the + # default. + if sym.choice: + continue if sym.orig_type == kconfiglib.BOOL: - # 'bool' choice symbols get their default value, as determined by - # e.g. 'default's on the choice - if not sym.choice: - # All other bool symbols get set to 'y', like for allyesconfig - sym.set_value(2) + sym.set_value(2) elif sym.orig_type == kconfiglib.TRISTATE: sym.set_value(1) diff --git a/allnoconfig.py b/allnoconfig.py index f046afa0..53b9de70 100755 --- a/allnoconfig.py +++ b/allnoconfig.py @@ -34,6 +34,12 @@ def main(): kconf.warn = False try: for sym in kconf.unique_defined_syms: + # Skip choice member symbols -- conf_set_all_new_symbols() in + # scripts/kconfig/conf.c (Linux) never sets SYMBOL_DEF_USER on + # choice values, leaving the choice selection logic to pick the + # default. + if sym.choice: + continue sym.set_value(2 if sym.is_allnoconfig_y else 0) finally: kconf.warn = True diff --git a/allyesconfig.py b/allyesconfig.py index eeada191..4ec17c73 100755 --- a/allyesconfig.py +++ b/allyesconfig.py @@ -29,18 +29,13 @@ def main(): # Assigning 0/1/2 to non-bool/tristate symbols has no effect (int/hex # symbols still take a string, because they preserve formatting). for sym in kconf.unique_defined_syms: - # Set choice symbols to 'm'. This value will be ignored for choices in - # 'y' mode (the "normal" mode), which will instead just get their - # default selection, but will set all symbols in m-mode choices to 'm', - # which is as high as they can go. - # - # Here's a convoluted example of how you might get an m-mode choice - # even during allyesconfig: - # - # choice - # tristate "weird choice" - # depends on m - sym.set_value(1 if sym.choice else 2) + # Skip choice member symbols -- conf_set_all_new_symbols() in + # scripts/kconfig/conf.c (Linux) never sets SYMBOL_DEF_USER on + # choice values, leaving the choice selection logic to pick the + # default. + if sym.choice: + continue + sym.set_value(2) # Set all choices to the highest possible mode for choice in kconf.unique_choices: diff --git a/kconfiglib.py b/kconfiglib.py index 5c80b237..f38e95a6 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -3668,10 +3668,9 @@ def _build_dep(self): # to). depend_on(sym, sym.direct_dep) - # In addition to the above, choice symbols depend on the choice - # they're in, but that's handled automatically since the Choice is - # propagated to the conditions of the properties before - # _build_dep() runs. + # Choice symbols also depend on their parent choice (and vice + # versa). This bidirectional link is added in _add_choice_deps() + # after dependency loop detection. for choice in self.unique_choices: # Choices depend on the following: @@ -3686,9 +3685,13 @@ def _build_dep(self): depend_on(choice, cond) def _add_choice_deps(self): - # Choices also depend on the choice symbols themselves, because the - # y-mode selection of the choice might change if a choice symbol's - # visibility changes. + # Choices and their member symbols have a bidirectional dependency: + # + # - When a choice symbol's visibility changes, the choice selection + # might change (member -> choice). + # + # - When the choice's mode or selection changes, member values + # change (choice -> member). # # We add these dependencies separately after dependency loop detection. # The invalidation algorithm can handle the resulting @@ -3698,6 +3701,7 @@ def _add_choice_deps(self): for choice in self.unique_choices: for sym in choice.syms: sym._dependents.add(choice) + choice._dependents.add(sym) def _invalidate_all(self): # Undefined symbols never change value and don't need to be @@ -3798,14 +3802,14 @@ def _finalize_node(self, node, visible_if): def _propagate_deps(self, node, visible_if): # Propagates 'node's dependencies to its child menu nodes - # If the parent node holds a Choice, we use the Choice itself as the - # parent dependency. This makes sense as the value (mode) of the choice - # limits the visibility of the contained choice symbols. The C - # implementation works the same way. - # - # Due to the similar interface, Choice works as a drop-in replacement - # for Symbol here. - basedep = node.item if node.item.__class__ is Choice else node.dep + # Always use the node's dependency expression as the base dependency + # for propagation. For choices this means propagating the choice's + # 'depends on' condition (e.g. X86_32) -- not the Choice object -- + # so that member visibility is determined by their own prompts gated + # by the choice's dependency, matching _menu_finalize() in + # scripts/kconfig/menu.c. The choice mode only affects member + # values, handled separately in sym_calc_choice / _selection(). + basedep = node.dep cur = node.list while cur: @@ -4736,19 +4740,16 @@ def tri_value(self): if val == 1 and (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): val = 2 - elif vis == 2: - # Visible choice symbol in y-mode choice. The choice mode limits - # the visibility of choice symbols, so it's sufficient to just - # check the visibility of the choice symbols themselves. + elif vis: + # Visible choice symbol. sym_calc_choice() in + # scripts/kconfig/symbol.c (Linux) assigns yes/no to each + # visible member based on whether it is the selected symbol, + # regardless of the choice's mode (y or m). val = 2 if self.choice.selection is self else 0 self._origin = ( self.choice._origin if self.choice.selection is self else None ) - elif vis and self.user_value: - # Visible choice symbol in m-mode choice, with set non-0 user value - val = 1 - self._cached_tri_val = val return val @@ -5129,11 +5130,15 @@ def _assignable(self): if not vis: return () + # sym_calc_choice() in scripts/kconfig/symbol.c (Linux) assigns + # yes/no values to all visible choice members, so the only + # user-settable value is y (selecting the member). + if self.choice: + return (2,) + rev_dep_val = expr_value(self.rev_dep) if vis == 2: - if self.choice: - return (2,) if not rev_dep_val: if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: @@ -5755,18 +5760,47 @@ def _selection(self): # Worker function for the 'selection' attribute # Warning: See Symbol._rec_invalidate(), and note that this is a hidden - # function call (property magic) - if self.tri_value != 2: - # Not in y mode, so no selection - return None - - # Use the user selection if it's visible + # function call (property magic). Accessing self.visibility ensures + # _cached_vis is set, which the invalidation system uses as the flag + # for "has cached values that need clearing". + self.visibility + + # sym_calc_choice() in scripts/kconfig/symbol.c (Linux) computes + # the selection for any choice that has visible members, regardless + # of the choice's own mode. An invisible choice prompt (e.g. gated + # by EXPERT) does not prevent the default selection from being + # written to .config when the choice's dependency (e.g. X86_32) + # is met. + + # Step 1: use the user selection if it's visible if self.user_selection and self.user_selection.visibility: self._origin = _T_CONFIG, self.user_loc return self.user_selection - # Otherwise, check if we have a default - return self._selection_from_defaults() + # Step 2: default (explicit or first visible member), but skip if + # the user explicitly set that symbol to n + res = self._selection_from_defaults() + if res is not None: + if res.user_value is not None and res.user_value == 0: + res = None + + # Step 3: first visible member that the user hasn't touched + if res is None: + for sym in self.syms: + if sym.visibility and sym.user_value is None: + self._origin = _T_DEFAULT, None + res = sym + break + + # Step 4: last visible member (absolute fallback) + if res is None: + for sym in reversed(self.syms): + if sym.visibility: + self._origin = _T_DEFAULT, None + res = sym + break + + return res def _selection_from_defaults(self): # Check if we have a default @@ -6666,18 +6700,10 @@ def _visibility(sc): if node.prompt: vis = max(vis, expr_value(node.prompt[1])) - if sc.__class__ is Symbol and sc.choice: - if ( - sc.choice.orig_type is TRISTATE - and sc.orig_type is not TRISTATE - and sc.choice.tri_value != 2 - ): - # Non-tristate choice symbols are only visible in y mode - return 0 - - if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2: - # Choice symbols with m visibility are not visible in y mode - return 0 + # For choice values, sym_calc_visibility() in scripts/kconfig/symbol.c + # (Linux) returns immediately after computing prompt visibility -- no + # additional capping based on the choice's mode. The m-to-y promotion + # for non-tristate types still applies. # Promote m to y if we're dealing with a non-tristate (possibly due to # modules being disabled) diff --git a/tests/test_deps.py b/tests/test_deps.py index dad6c99e..461289ce 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -59,7 +59,7 @@ def verify_refs(item, *dep_names): c.syms["INT_REFS"].nodes[0], "A", "B", "C", "D", "E", "F", "G", "H", "y" ) - verify_refs(c.syms["CHOICE_REF"].nodes[0], "CHOICE") + verify_refs(c.syms["CHOICE_REF"].nodes[0], "y") verify_refs(c.menus[0], "A", "B", "C", "D") @@ -219,13 +219,12 @@ def test_deploop_message(): config H \tbool "H" -\tdepends on I && +\tdepends on I ...depends on the choice symbol I (defined at tests/Kdeploop10:41), with definition... config I \tbool "I" -\tdepends on ...depends on (defined at tests/Kdeploop10:38), with definition... diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 41bd5006..87693642 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -345,7 +345,7 @@ def test_expr_items(): ) items = expr_items(c.syms["TEST_CHOICE"].nodes[0].prompt[1]) - assert tuple(sorted(item.name for item in items)) == ("A", "CHOICE") + assert tuple(sorted(item.name for item in items)) == ("A",) # --------------------------------------------------------------------------- diff --git a/tests/test_repr.py b/tests/test_repr.py index c07ca6ce..e3dc23bc 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -564,7 +564,7 @@ def test_multi_def(self): def test_choice_sym(self): assert ( repr(self.c.syms["CHOICE_1"]) - == '' + == '' ) def test_modules(self): @@ -585,7 +585,7 @@ def _setup(self, krepr_config): def test_choice_basic(self): assert ( repr(self.c.named_choices["CHOICE"]) - == '' + == '' ) def test_choice_set_value_y(self): @@ -609,7 +609,7 @@ def test_choice_user_selection_overridden(self): self.c.named_choices["CHOICE"].set_value(1) assert ( repr(self.c.named_choices["CHOICE"]) - == '' + == '' ) def test_choice_optional_unnamed(self): diff --git a/tests/test_semantics.py b/tests/test_semantics.py index c459e40a..c9ba37d0 100644 --- a/tests/test_semantics.py +++ b/tests/test_semantics.py @@ -204,14 +204,27 @@ def test_choice_m_mode(): tristate.tri_value == 1 ), "TRISTATE choice should have mode m after explicit mode assignment" + # In v6.18, sym_calc_choice() in scripts/kconfig/symbol.c (Linux) + # always assigns y/n to visible members based on selection, regardless + # of the choice's mode. Setting a member to n (0) removes it from + # default selection consideration; setting to y (2) makes it the user + # selection. + + # Setting T_1 to n moves selection to T_2 assign_and_verify_value(c, "T_1", 0, 0) - assign_and_verify_value(c, "T_2", 0, 0) - assign_and_verify_value(c, "T_1", 1, 1) - assign_and_verify_value(c, "T_2", 1, 1) - assign_and_verify_value(c, "T_1", 2, 1) - assign_and_verify_value(c, "T_2", 2, 1) + # T_2 is now selected; setting it to n triggers step-4 fallback + # (last visible member = T_2), so T_2 stays y + assign_and_verify_value(c, "T_2", 0, 2) + # Setting T_1 to y makes it the user selection + c.syms["T_1"].set_value(2) + verify_value(c, "T_1", 2) + verify_value(c, "T_2", 0) + # Setting T_2 to y makes it the user selection + c.syms["T_2"].set_value(2) + verify_value(c, "T_1", 0) + verify_value(c, "T_2", 2) - # Switching to y mode should cause T_2 to become selected + # Switching to y mode keeps T_2 as the user selection tristate.set_value(2) verify_value(c, "T_1", 0) verify_value(c, "T_2", 2) @@ -279,3 +292,44 @@ def verify_is_weird_choice_symbol(name): verify_is_weird_choice_symbol("WS7") verify_is_weird_choice_symbol("WS8") verify_is_normal_choice_symbol("WS9") + + +def test_choice_optional_n_mode_selection(): + """Test that optional choices in n mode still compute a selection. + + In Linux's sym_calc_choice() (scripts/kconfig/symbol.c), the selection + is computed for any choice with visible members regardless of the + choice's own mode. An optional choice with no user value has mode n, + but visible members are still assigned y/n based on which one is + selected (the default or first visible member). + """ + c = Kconfig("tests/Kchoice", warn=False) + + # BOOL_OPT and TRISTATE_OPT are optional, default mode is n (no user + # value, is_optional means base reverse dep is 0) + for choice_name, member_prefix in [("BOOL_OPT", "BO_"), ("TRISTATE_OPT", "TO_")]: + choice = c.named_choices[choice_name] + assert choice.is_optional, f"{choice_name} should be optional" + assert choice.tri_value == 0, f"{choice_name} should have mode n" + + # sym_calc_choice() picks a selection even in n mode + assert ( + choice.selection is not None + ), f"{choice_name} should still have a selection in n mode" + + # First visible member is selected (gets y), others get n + first_sym = c.syms[member_prefix + "1"] + assert ( + choice.selection is first_sym + ), f"{choice_name} selection should be {first_sym.name}" + assert first_sym.tri_value == 2, f"{first_sym.name} should be y (selected)" + + second_sym = c.syms[member_prefix + "2"] + assert ( + second_sym.tri_value == 0 + ), f"{second_sym.name} should be n (not selected)" + + # All visible members have _write_to_conf set (SYMBOL_WRITE) + for sym in choice.syms: + if sym.visibility: + assert sym._write_to_conf, f"{sym.name} should have _write_to_conf set" diff --git a/tests/test_symbols.py b/tests/test_symbols.py index 5967b103..1639ae4b 100644 --- a/tests/test_symbols.py +++ b/tests/test_symbols.py @@ -49,22 +49,12 @@ def verify_visibility(item, no_module_vis, module_vis): verify_visibility(c.syms["BOOL_MENU_N"], 0, 0) verify_visibility(c.syms["BOOL_MENU_M"], 0, 2) verify_visibility(c.syms["BOOL_MENU_Y"], 2, 2) - verify_visibility(c.syms["BOOL_CHOICE_N"], 0, 0) - - # Non-tristate symbols in tristate choices are only visible if the choice - # is in y mode - - # The choice can't be brought to y mode because of the 'if m' - verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) - c.syms["BOOL_CHOICE_M"].choice.set_value(2) - verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) - - # The choice gets y mode only when running without modules, because it - # defaults to m mode - verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 0) - c.syms["BOOL_CHOICE_Y"].choice.set_value(2) - # When set to y mode, the choice symbol becomes visible both with and - # without modules + # Choice member visibility is purely prompt-based (no choice-mode + # capping), matching sym_calc_visibility() in scripts/kconfig/symbol.c + # (Linux). Members with unconditional prompts have visibility y + # regardless of the choice's mode or prompt condition. + verify_visibility(c.syms["BOOL_CHOICE_N"], 2, 2) + verify_visibility(c.syms["BOOL_CHOICE_M"], 2, 2) verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 2) verify_visibility(c.syms["TRISTATE_IF_N"], 0, 0) @@ -73,8 +63,8 @@ def verify_visibility(item, no_module_vis, module_vis): verify_visibility(c.syms["TRISTATE_MENU_N"], 0, 0) verify_visibility(c.syms["TRISTATE_MENU_M"], 0, 1) verify_visibility(c.syms["TRISTATE_MENU_Y"], 2, 2) - verify_visibility(c.syms["TRISTATE_CHOICE_N"], 0, 0) - verify_visibility(c.syms["TRISTATE_CHOICE_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_CHOICE_N"], 2, 2) + verify_visibility(c.syms["TRISTATE_CHOICE_M"], 2, 2) verify_visibility(c.syms["TRISTATE_CHOICE_Y"], 2, 2) verify_visibility(c.named_choices["BOOL_CHOICE_N"], 0, 0) @@ -186,15 +176,16 @@ def verify_const_unassignable(sym_name): verify_assignable("Y_CHOICE_TRISTATE", (2,), (2,)) verify_assignable("Y_CHOICE_N_VIS_TRISTATE", (), ()) - # Symbols in m/y-mode choice, starting out in m mode, or y mode when - # running without modules - verify_assignable("MY_CHOICE_BOOL", (2,), ()) - verify_assignable("MY_CHOICE_TRISTATE", (2,), (0, 1)) + # Symbols in m/y-mode choice -- choice member assignable is always (2,) + # when visible, regardless of the choice's mode, matching + # sym_calc_choice() in scripts/kconfig/symbol.c (Linux). + verify_assignable("MY_CHOICE_BOOL", (2,), (2,)) + verify_assignable("MY_CHOICE_TRISTATE", (2,), (2,)) verify_assignable("MY_CHOICE_N_VIS_TRISTATE", (), ()) c.named_choices["MY_CHOICE"].set_value(2) - # Symbols in m/y-mode choice, now in y mode + # Setting the choice to y mode doesn't change assignable values verify_assignable("MY_CHOICE_BOOL", (2,), (2,)) verify_assignable("MY_CHOICE_TRISTATE", (2,), (2,)) verify_assignable("MY_CHOICE_N_VIS_TRISTATE", (), ()) From 822548b64cb9b72c420ef725244466c68a318246 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 20 Feb 2026 15:31:56 +0800 Subject: [PATCH 5/5] Match int/hex range and default handling with scripts/kconfig Clamp out-of-range user values to the nearest range bound instead of falling back to defaults, matching sym_validate_range() in scripts/kconfig/symbol.c. Return "0" for INT and "0x0" for HEX when no explicit default exists, matching sym_get_string_default(). Without this, write_min_config() spuriously includes symbols whose value equals the implicit zero default. --- kconfiglib.py | 21 ++++++++++++++++--- tests/test_symbols.py | 47 ++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index f38e95a6..986bdb42 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -4568,14 +4568,22 @@ def str_value(self): if vis and self.user_value: user_val = int(self.user_value, base) if has_active_range and not low <= user_val <= high: + # sym_validate_range() in scripts/kconfig/symbol.c + # (Linux) clamps the value to the nearest range bound + # rather than falling back to defaults. num2str = str if base == 10 else hex + clamp_to = low if user_val < low else high + val = num2str(clamp_to) + use_defaults = False + self._origin = _T_CONFIG, self.user_loc self.kconfig._warn( - "user value {} on the {} symbol {} ignored due to " - "being outside the active range ([{}, {}]) -- falling " - "back on defaults".format( + "user value {} on the {} symbol {} clamped to {} due " + "to being outside the active range " + "([{}, {}])".format( num2str(user_val), TYPE_TO_STR[self.orig_type], self.name_and_loc, + num2str(clamp_to), num2str(low), num2str(high), ) @@ -5261,6 +5269,13 @@ def _str_default(self): if expr_value(cond): return default.str_value + # sym_get_string_default() in scripts/kconfig/symbol.c (Linux) + # returns "0" for INT and "0x0" for HEX when no default exists. + if self.orig_type is INT: + return "0" + if self.orig_type is HEX: + return "0x0" + return "" def _warn_select_unsatisfied_deps(self): diff --git a/tests/test_symbols.py b/tests/test_symbols.py index 1639ae4b..66c8652e 100644 --- a/tests/test_symbols.py +++ b/tests/test_symbols.py @@ -293,10 +293,10 @@ def test_ranges(): # Avoid warnings for assigning values outside the active range c.warn = False - def verify_range(sym_name, low, high, default): + def verify_range(sym_name, low, high): # Verifies that all values in the range low-high can be assigned, - # and that assigning values outside the range reverts the value back to - # default. + # and that assigning values outside the range clamps to the nearest + # bound (matching sym_validate_range() in scripts/kconfig/symbol.c). is_hex = c.syms[sym_name].type == HEX @@ -308,38 +308,35 @@ def verify_range(sym_name, low, high, default): assign_and_verify_user_value(c, sym_name, hex(i), hex(i), True) # Verify that assigning a user value just outside the range causes - # defaults to be used - - if default is None: - default_str = "" - elif is_hex: - default_str = hex(default) - else: - default_str = str(default) + # clamping to the nearest bound if is_hex: too_low_str = hex(low - 1) too_high_str = hex(high + 1) + low_str = hex(low) + high_str = hex(high) else: too_low_str = str(low - 1) too_high_str = str(high + 1) + low_str = str(low) + high_str = str(high) - assign_and_verify_value(c, sym_name, too_low_str, default_str) - assign_and_verify_value(c, sym_name, too_high_str, default_str) + assign_and_verify_value(c, sym_name, too_low_str, low_str) + assign_and_verify_value(c, sym_name, too_high_str, high_str) - verify_range("HEX_RANGE_10_20_LOW_DEFAULT", 0x10, 0x20, 0x10) - verify_range("HEX_RANGE_10_20_HIGH_DEFAULT", 0x10, 0x20, 0x20) - verify_range("HEX_RANGE_10_20_OK_DEFAULT", 0x10, 0x20, 0x15) + verify_range("HEX_RANGE_10_20_LOW_DEFAULT", 0x10, 0x20) + verify_range("HEX_RANGE_10_20_HIGH_DEFAULT", 0x10, 0x20) + verify_range("HEX_RANGE_10_20_OK_DEFAULT", 0x10, 0x20) - verify_range("INT_RANGE_10_20_LOW_DEFAULT", 10, 20, 10) - verify_range("INT_RANGE_10_20_HIGH_DEFAULT", 10, 20, 20) - verify_range("INT_RANGE_10_20_OK_DEFAULT", 10, 20, 15) + verify_range("INT_RANGE_10_20_LOW_DEFAULT", 10, 20) + verify_range("INT_RANGE_10_20_HIGH_DEFAULT", 10, 20) + verify_range("INT_RANGE_10_20_OK_DEFAULT", 10, 20) - verify_range("HEX_RANGE_10_20", 0x10, 0x20, 0x10) + verify_range("HEX_RANGE_10_20", 0x10, 0x20) - verify_range("INT_RANGE_10_20", 10, 20, 10) - verify_range("INT_RANGE_0_10", 0, 10, 0) - verify_range("INT_RANGE_NEG_10_10", -10, 10, 0) + verify_range("INT_RANGE_10_20", 10, 20) + verify_range("INT_RANGE_0_10", 0, 10) + verify_range("INT_RANGE_NEG_10_10", -10, 10) # Dependent ranges @@ -355,8 +352,8 @@ def verify_range(sym_name, low, high, default): verify_value(c, "HEX_RANGE_10_40_DEPENDENT", "0x15") verify_value(c, "INT_RANGE_10_40_DEPENDENT", "15") c.unset_values() - verify_range("HEX_RANGE_10_40_DEPENDENT", 0x10, 0x40, 0x10) - verify_range("INT_RANGE_10_40_DEPENDENT", 10, 40, 10) + verify_range("HEX_RANGE_10_40_DEPENDENT", 0x10, 0x40) + verify_range("INT_RANGE_10_40_DEPENDENT", 10, 40) # Ranges and symbols defined in multiple locations