diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7f0206a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +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 pytest-benchmark + pip install . + + - name: Run pytest + run: pytest pytest/ -v diff --git a/.gitignore b/.gitignore index 4a18d79..f576307 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ build/ *.egg-info/ dist/ +.venv/ +.vscode +*.egg +*.eggs/ diff --git a/kconfiglib.py b/kconfiglib.py index c67895c..5db9318 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 = [] diff --git a/pytest/test_simple_dep/Kconfig b/pytest/test_simple_dep/Kconfig new file mode 100644 index 0000000..1204489 --- /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/app.config b/pytest/test_simple_dep/app.config new file mode 100644 index 0000000..7f9c9f4 --- /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.conditional.py b/pytest/test_simple_dep/config.conditional.py new file mode 100644 index 0000000..f5e17c3 --- /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 0000000..d332ce3 --- /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.import100.py b/pytest/test_simple_dep/config.import100.py new file mode 100644 index 0000000..b258056 --- /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/config.import_dir.py b/pytest/test_simple_dep/config.import_dir.py new file mode 100644 index 0000000..2467dc5 --- /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 0000000..63869e5 --- /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/config1.py b/pytest/test_simple_dep/config1.py new file mode 100644 index 0000000..a931a51 --- /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 0000000..50b307b --- /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/fail.config.exception.py b/pytest/test_simple_dep/fail.config.exception.py new file mode 100644 index 0000000..b6f8c5f --- /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 0000000..02f9014 --- /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 0000000..9a34102 --- /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 0000000..401dc86 --- /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 0000000..be6ab06 --- /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_simple_dep/test_simple_dep_bench.py b/pytest/test_simple_dep/test_simple_dep_bench.py new file mode 100644 index 0000000..05dec9a --- /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') diff --git a/pytest/test_types/Kconfig b/pytest/test_types/Kconfig new file mode 100644 index 0000000..e73c031 --- /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 0000000..73d1fc0 --- /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 0000000..47da27b --- /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 0000000..17447db --- /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 0000000..77a0cf4 --- /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 0000000..688aea0 --- /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 0000000..add0206 --- /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 0000000..8a32783 --- /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 0000000..95a1572 --- /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)