From 7aca75738fcde9940b6c2e355b772c685e3417f7 Mon Sep 17 00:00:00 2001 From: MrKevinWeiss Date: Tue, 2 May 2023 15:13:26 +0200 Subject: [PATCH 1/5] chore: .gitignore standard dev things --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4a18d79f..f576307f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ build/ *.egg-info/ dist/ +.venv/ +.vscode +*.egg +*.eggs/ From 6cd77d89e14705eeb872074cd79b7568137b22a3 Mon Sep 17 00:00:00 2001 From: MrKevinWeiss Date: Tue, 2 May 2023 15:13:58 +0200 Subject: [PATCH 2/5] feat(kconfiglib): Add .py based config files --- kconfiglib.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index c67895ce..5db93182 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -570,6 +570,11 @@ def my_other_fn(kconf, name, arg_1, arg_2, ...): # # Public classes # +class KconfigException(Exception): + def __init__(self, msg, file, line): + super().__init__(msg) + self.file = file + self.line = line class Kconfig(object): @@ -845,6 +850,7 @@ class Kconfig(object): "warn_assign_undef", "warn_to_stderr", "warnings", + "configured_syms", "y", # Parsing-related @@ -960,7 +966,7 @@ def _init(self, filename, warn, warn_to_stderr, encoding): # See __init__() self._encoding = encoding - + self.configured_syms = set() self.srctree = os.getenv("srctree", "") # A prefix we can reliably strip from glob() results to get a filename # relative to $srctree. relpath() can cause issues for symlinks, @@ -1236,7 +1242,10 @@ def load_config(self, filename=None, replace=True, verbose=None): # This stub only exists to make sure _warn_assign_no_prompt gets # reenabled try: - self._load_config(filename, replace) + if filename.endswith(".py"): + self._load_config_py(filename) + else: + self._load_config(filename, replace) except UnicodeDecodeError as e: _decoding_error(e, filename) finally: @@ -1244,6 +1253,50 @@ def load_config(self, filename=None, replace=True, verbose=None): return ("Loaded" if replace else "Merged") + msg + def _load_config_py(self, filename): + + assert filename.endswith(".py") + # Get directory from filename, if relative directory then take use . + base_dir = os.path.dirname(filename) or "." + + config = """ +import os +import traceback + +def kconfig_import(rel_path): + conf_globals=globals() + global _rel_dir + full_path = os.path.join(_rel_dir, rel_path) + _rel_dir = os.path.dirname(os.path.realpath(full_path)) + with open(full_path, "r") as f: + config = f.read() + try: + exec(config, conf_globals) + except KconfigException as exc: + raise exc + except Exception as exc: + tb = exc.__traceback__ + lineno = traceback.extract_tb(tb)[1].lineno + exc.file = full_path + exc.line = lineno + raise KconfigException(str(exc), full_path, lineno) + +""" + config += f"kconfig_import(\"{filename}\")" + + conf_globals = self.syms + conf_globals["_rel_dir"] = base_dir + conf_globals.update(self.named_choices) + conf_globals['KconfigException'] = KconfigException + conf_globals["kconfig_import"] = self._load_config_py + + try: + exec(config, conf_globals) + except KconfigException as exc: + self._warn(str(exc), exc.file, exc.line) + raise exc + + def _load_config(self, filename, replace): with self._open_config(filename) as f: if replace: @@ -4264,6 +4317,7 @@ class Symbol(object): "selects", "user_value", "weak_rev_dep", + "_attempted_value" ) # @@ -4283,6 +4337,40 @@ def type(self): return self.orig_type + @property + def val(self): + if self.type == BOOL or self.type == TRISTATE: + if self.tri_value == 2: + return True + elif self.tri_value == 0: + return False + elif self.type == INT or self.type == HEX: + return int(self.str_value) + else: + return self.str_value + + @val.setter + def val(self, value): + self.kconfig.configured_syms.add(self) + self._attempted_value = value + if self.type == BOOL or self.type == TRISTATE: + if value: + self.set_value(2) + else: + self.set_value(0) + else: + self.set_value(str(value)) + self.check_val() + + def check_val(self): + for ref_sym in self.kconfig.configured_syms: + if ref_sym._attempted_value: + if ref_sym.val != ref_sym._attempted_value: + if ref_sym == self: + raise ValueError("Could not set {} to {} from {}".format(ref_sym.name, ref_sym._attempted_value, ref_sym.val)) + else: + raise ValueError("Could not set {} to {} from {} due to {}: {}".format(ref_sym.name, ref_sym._attempted_value, ref_sym.val, self.name, self._attempted_value)) + @property def str_value(self): """ @@ -4785,8 +4873,9 @@ def __init__(self): # - UNKNOWN == 0 # - _visited is used during tree iteration and dep. loop detection - self.orig_type = self._visited = 0 + self._attempted_value = None + self.orig_type = self._visited = 0 self.nodes = [] self.defaults = [] From 9dbe334649ce5d64fe8980c0fa9ed62c03337e8b Mon Sep 17 00:00:00 2001 From: MrKevinWeiss Date: Tue, 2 May 2023 15:14:25 +0200 Subject: [PATCH 3/5] test: initial pytest for testing .py configs --- pytest/test_simple_dep/Kconfig | 9 +++++ pytest/test_simple_dep/config.conditional.py | 4 +++ pytest/test_simple_dep/config.import.py | 1 + pytest/test_simple_dep/config.import_dir.py | 2 ++ pytest/test_simple_dep/config.py | 2 ++ .../test_simple_dep/fail.config.exception.py | 1 + .../fail.config.missing_dep.py | 1 + .../fail.config.removed_dep.py | 4 +++ pytest/test_simple_dep/import/config.py | 1 + pytest/test_simple_dep/test_simple_dep.py | 34 +++++++++++++++++++ pytest/test_types/Kconfig | 14 ++++++++ pytest/test_types/config.bool.py | 5 +++ pytest/test_types/config.hex.py | 6 ++++ pytest/test_types/config.int.py | 10 ++++++ pytest/test_types/config.py | 19 +++++++++++ pytest/test_types/config.string.py | 6 ++++ pytest/test_types/config.tristate.py | 10 ++++++ pytest/test_types/fail.config.bool.py | 1 + pytest/test_types/test_types.py | 34 +++++++++++++++++++ 19 files changed, 164 insertions(+) create mode 100644 pytest/test_simple_dep/Kconfig create mode 100644 pytest/test_simple_dep/config.conditional.py create mode 100644 pytest/test_simple_dep/config.import.py create mode 100644 pytest/test_simple_dep/config.import_dir.py create mode 100644 pytest/test_simple_dep/config.py create mode 100644 pytest/test_simple_dep/fail.config.exception.py create mode 100644 pytest/test_simple_dep/fail.config.missing_dep.py create mode 100644 pytest/test_simple_dep/fail.config.removed_dep.py create mode 100644 pytest/test_simple_dep/import/config.py create mode 100644 pytest/test_simple_dep/test_simple_dep.py create mode 100644 pytest/test_types/Kconfig create mode 100644 pytest/test_types/config.bool.py create mode 100644 pytest/test_types/config.hex.py create mode 100644 pytest/test_types/config.int.py create mode 100644 pytest/test_types/config.py create mode 100644 pytest/test_types/config.string.py create mode 100644 pytest/test_types/config.tristate.py create mode 100644 pytest/test_types/fail.config.bool.py create mode 100644 pytest/test_types/test_types.py diff --git a/pytest/test_simple_dep/Kconfig b/pytest/test_simple_dep/Kconfig new file mode 100644 index 00000000..12044895 --- /dev/null +++ b/pytest/test_simple_dep/Kconfig @@ -0,0 +1,9 @@ +config FOO + bool "FOO prompt" + +config BAR + bool "BAR prompt" + depends on FOO + +config BAZ + bool "BAZ prompt" diff --git a/pytest/test_simple_dep/config.conditional.py b/pytest/test_simple_dep/config.conditional.py new file mode 100644 index 00000000..f5e17c36 --- /dev/null +++ b/pytest/test_simple_dep/config.conditional.py @@ -0,0 +1,4 @@ +if FOO.val: + BAR.val = True +else: + FOO.val = True diff --git a/pytest/test_simple_dep/config.import.py b/pytest/test_simple_dep/config.import.py new file mode 100644 index 00000000..d332ce32 --- /dev/null +++ b/pytest/test_simple_dep/config.import.py @@ -0,0 +1 @@ +kconfig_import("config.py") diff --git a/pytest/test_simple_dep/config.import_dir.py b/pytest/test_simple_dep/config.import_dir.py new file mode 100644 index 00000000..2467dc5f --- /dev/null +++ b/pytest/test_simple_dep/config.import_dir.py @@ -0,0 +1,2 @@ +kconfig_import("config.py") +kconfig_import("import/config.py") diff --git a/pytest/test_simple_dep/config.py b/pytest/test_simple_dep/config.py new file mode 100644 index 00000000..63869e5b --- /dev/null +++ b/pytest/test_simple_dep/config.py @@ -0,0 +1,2 @@ +FOO.val = True +BAR.val = True diff --git a/pytest/test_simple_dep/fail.config.exception.py b/pytest/test_simple_dep/fail.config.exception.py new file mode 100644 index 00000000..b6f8c5f2 --- /dev/null +++ b/pytest/test_simple_dep/fail.config.exception.py @@ -0,0 +1 @@ +1/0 diff --git a/pytest/test_simple_dep/fail.config.missing_dep.py b/pytest/test_simple_dep/fail.config.missing_dep.py new file mode 100644 index 00000000..02f90146 --- /dev/null +++ b/pytest/test_simple_dep/fail.config.missing_dep.py @@ -0,0 +1 @@ +BAR.val = True diff --git a/pytest/test_simple_dep/fail.config.removed_dep.py b/pytest/test_simple_dep/fail.config.removed_dep.py new file mode 100644 index 00000000..9a34102e --- /dev/null +++ b/pytest/test_simple_dep/fail.config.removed_dep.py @@ -0,0 +1,4 @@ +FOO.val = True +BAR.val = True +# Expect the following to fail since BAR is True and depends on FOO. +FOO.val = False diff --git a/pytest/test_simple_dep/import/config.py b/pytest/test_simple_dep/import/config.py new file mode 100644 index 00000000..401dc86a --- /dev/null +++ b/pytest/test_simple_dep/import/config.py @@ -0,0 +1 @@ +BAR.val = False diff --git a/pytest/test_simple_dep/test_simple_dep.py b/pytest/test_simple_dep/test_simple_dep.py new file mode 100644 index 00000000..be6ab06d --- /dev/null +++ b/pytest/test_simple_dep/test_simple_dep.py @@ -0,0 +1,34 @@ +import os +import glob +import kconfiglib +import pytest + +CUR_DIR = os.path.dirname(os.path.realpath(__file__)) + +@pytest.mark.parametrize("config_file", + sorted(glob.glob(CUR_DIR + '/config*.py', + recursive=True))) +def test_configs_should_not_crash(config_file): + """Test config files that should not crash. + + No asserts are used as we are just looking for exceptions. + """ + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + kconf.load_config(filename=config_file) + + +@pytest.mark.parametrize("config_file", + sorted(glob.glob(CUR_DIR + '/fail.config*.py', + recursive=True))) +def test_configs_should_crash(config_file): + """Test config files that should not crash. + + No asserts are used as we are just looking for exceptions. + """ + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path, warn=False) + with pytest.raises(Exception): + kconf.load_config(filename=config_file) diff --git a/pytest/test_types/Kconfig b/pytest/test_types/Kconfig new file mode 100644 index 00000000..e73c0313 --- /dev/null +++ b/pytest/test_types/Kconfig @@ -0,0 +1,14 @@ +config TEST_BOOL + bool "TEST_BOOL prompt" + +config TEST_TRISTATE + tristate "TEST_TRISTATE prompt" + +config TEST_STRING + string "TEST_STRING prompt" + +config TEST_INT + int "TEST_INT prompt" + +config TEST_HEX + hex "TEST_HEX prompt" diff --git a/pytest/test_types/config.bool.py b/pytest/test_types/config.bool.py new file mode 100644 index 00000000..73d1fc0c --- /dev/null +++ b/pytest/test_types/config.bool.py @@ -0,0 +1,5 @@ +TEST_BOOL.val = True +assert TEST_BOOL.val == True + +TEST_BOOL.val = False +assert TEST_BOOL.val == False diff --git a/pytest/test_types/config.hex.py b/pytest/test_types/config.hex.py new file mode 100644 index 00000000..47da27b4 --- /dev/null +++ b/pytest/test_types/config.hex.py @@ -0,0 +1,6 @@ +TEST_HEX.val = 0x80000000 +assert TEST_HEX.val == 0x80000000 +TEST_HEX.val = 0 +assert TEST_HEX.val == 0 +TEST_HEX.val = 0x12345678 +assert TEST_HEX.val == 0x12345678 diff --git a/pytest/test_types/config.int.py b/pytest/test_types/config.int.py new file mode 100644 index 00000000..17447db3 --- /dev/null +++ b/pytest/test_types/config.int.py @@ -0,0 +1,10 @@ +TEST_INT.val = -2147483648 +assert TEST_INT.val == -2147483648 +TEST_INT.val = -1 +assert TEST_INT.val == -1 +TEST_INT.val = 0 +assert TEST_INT.val == 0 +TEST_INT.val = 1 +assert TEST_INT.val == 1 +TEST_INT.val = 2147483647 +assert TEST_INT.val == 2147483647 diff --git a/pytest/test_types/config.py b/pytest/test_types/config.py new file mode 100644 index 00000000..77a0cf4a --- /dev/null +++ b/pytest/test_types/config.py @@ -0,0 +1,19 @@ +TEST_BOOL.val = True +TEST_BOOL.val = False + +TEST_TRISTATE.val = False +TEST_TRISTATE.val = None +TEST_TRISTATE.val = True + +TEST_STRING.val = "" +TEST_STRING.val = "FOO" + +TEST_INT.val = -2147483648 +TEST_INT.val = -1 +TEST_INT.val = 0 +TEST_INT.val = 1 +TEST_INT.val = 2147483647 + +TEST_HEX.val = 0x80000000 +TEST_HEX.val = 0 +TEST_HEX.val = 0x12345678 diff --git a/pytest/test_types/config.string.py b/pytest/test_types/config.string.py new file mode 100644 index 00000000..688aea09 --- /dev/null +++ b/pytest/test_types/config.string.py @@ -0,0 +1,6 @@ +TEST_STRING.val = "" +assert TEST_STRING.val == "" +TEST_STRING.val = "FOO" +assert TEST_STRING.val == "FOO" +TEST_STRING.val = "FOO BAR" +assert TEST_STRING.val == "FOO BAR" diff --git a/pytest/test_types/config.tristate.py b/pytest/test_types/config.tristate.py new file mode 100644 index 00000000..add0206e --- /dev/null +++ b/pytest/test_types/config.tristate.py @@ -0,0 +1,10 @@ +TEST_TRISTATE.val = False +assert TEST_TRISTATE.val == False + +# Tristates will default to False if nothing sets it to True +# This means we cannot read None... +TEST_TRISTATE.val = None +assert TEST_TRISTATE.val == False + +TEST_TRISTATE.val = True +assert TEST_TRISTATE.val == True diff --git a/pytest/test_types/fail.config.bool.py b/pytest/test_types/fail.config.bool.py new file mode 100644 index 00000000..8a327831 --- /dev/null +++ b/pytest/test_types/fail.config.bool.py @@ -0,0 +1 @@ +TEST_BOOL.val = "asdf" diff --git a/pytest/test_types/test_types.py b/pytest/test_types/test_types.py new file mode 100644 index 00000000..95a15727 --- /dev/null +++ b/pytest/test_types/test_types.py @@ -0,0 +1,34 @@ +import os +import glob +import kconfiglib +import pytest + +CUR_DIR = os.path.dirname(os.path.realpath(__file__)) + +@pytest.mark.parametrize("config_file", + sorted(glob.glob(CUR_DIR + '/config*.py', + recursive=True))) +def test_configs_should_not_crash(config_file): + """Test config files that should not crash. + + No asserts are used as we are just looking for exceptions. + """ + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path, suppress_traceback=True) + kconf.load_config(filename=config_file) + + +@pytest.mark.parametrize("config_file", + sorted(glob.glob(CUR_DIR + '/fail.config*.py', + recursive=True))) +def test_configs_should_crash(config_file): + """Test config files that should not crash. + + No asserts are used as we are just looking for exceptions. + """ + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path, warn=False) + with pytest.raises(Exception): + kconf.load_config(filename=config_file) From cd30ee844704f0c79c1d2490afac0523c3d61233 Mon Sep 17 00:00:00 2001 From: MrKevinWeiss Date: Tue, 2 May 2023 15:14:41 +0200 Subject: [PATCH 4/5] ci: Add pytest to CI --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..43526823 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: Run Pytest + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + run_pytest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install . + + - name: Run pytest + run: pytest pytest/ -v From 4da93437c8c9cdd73a1f4f390b22b7dc625effa3 Mon Sep 17 00:00:00 2001 From: MrKevinWeiss Date: Wed, 3 May 2023 08:42:57 +0200 Subject: [PATCH 5/5] test: add benchmarks --- .github/workflows/ci.yml | 1 + pytest/test_simple_dep/app.config | 2 + pytest/test_simple_dep/config.import100.py | 1 + pytest/test_simple_dep/config1.py | 2 + pytest/test_simple_dep/config100.py | 2 + .../test_simple_dep/test_simple_dep_bench.py | 54 +++++++++++++++++++ 6 files changed, 62 insertions(+) create mode 100644 pytest/test_simple_dep/app.config create mode 100644 pytest/test_simple_dep/config.import100.py create mode 100644 pytest/test_simple_dep/config1.py create mode 100644 pytest/test_simple_dep/config100.py create mode 100644 pytest/test_simple_dep/test_simple_dep_bench.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43526823..7f0206a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest + pip install pytest-benchmark pip install . - name: Run pytest diff --git a/pytest/test_simple_dep/app.config b/pytest/test_simple_dep/app.config new file mode 100644 index 00000000..7f9c9f44 --- /dev/null +++ b/pytest/test_simple_dep/app.config @@ -0,0 +1,2 @@ +FOO = y +BAR = y diff --git a/pytest/test_simple_dep/config.import100.py b/pytest/test_simple_dep/config.import100.py new file mode 100644 index 00000000..b2580565 --- /dev/null +++ b/pytest/test_simple_dep/config.import100.py @@ -0,0 +1 @@ +[kconfig_import("config.py") for _ in range(100)] diff --git a/pytest/test_simple_dep/config1.py b/pytest/test_simple_dep/config1.py new file mode 100644 index 00000000..a931a517 --- /dev/null +++ b/pytest/test_simple_dep/config1.py @@ -0,0 +1,2 @@ +for _ in range(1): + FOO.val = not FOO.val diff --git a/pytest/test_simple_dep/config100.py b/pytest/test_simple_dep/config100.py new file mode 100644 index 00000000..50b307b5 --- /dev/null +++ b/pytest/test_simple_dep/config100.py @@ -0,0 +1,2 @@ +for _ in range(100): + FOO.val = not FOO.val diff --git a/pytest/test_simple_dep/test_simple_dep_bench.py b/pytest/test_simple_dep/test_simple_dep_bench.py new file mode 100644 index 00000000..05dec9a4 --- /dev/null +++ b/pytest/test_simple_dep/test_simple_dep_bench.py @@ -0,0 +1,54 @@ +import os +import glob +import kconfiglib +import pytest + +CUR_DIR = os.path.dirname(os.path.realpath(__file__)) + + +def test_bench_config(benchmark): + """Evaluate the performance standard config file.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/app.config') + + +def test_bench_configpy(benchmark): + """Evaluate the performance python config.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/config.py') + + +def test_bench_import_configpy(benchmark): + """Evaluate the performance of importing.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/config.import.py') + + +def test_bench_import100_configpy(benchmark): + """Evaluate the performance of importing foo 100 times.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/config.import100.py') + + +def test_bench_config1_configpy(benchmark): + """Evaluate the performance of setting foo 1 time.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/config1.py') + + +def test_bench_config100_configpy(benchmark): + """Evaluate the performance of setting foo 100 times.""" + kconfig_path = CUR_DIR + '/Kconfig' + + kconf = kconfiglib.Kconfig(kconfig_path) + benchmark(kconf.load_config, filename=CUR_DIR + '/config100.py')