Re: [PATCH 07/14] kconfig: test: add framework for Kconfig unit-tests

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Tue, Feb 6, 2018 at 1:34 AM, Masahiro Yamada
<yamada.masahiro@xxxxxxxxxxxxx> wrote:
> I admit various parts in Kconfig are cryptic and need refactoring,
> but at the same time, I fear regressions.
>
> There are several subtle corner cases where it is difficult to notice
> breakage.  It is time to add unit-tests.
>
> Here is a simple framework based on pytest.  The conftest.py provides
> a fixture useful to run commands such as 'oldaskconfig' etc. and
> to compare the resulted .config, stdout, stderr with expectations.
>
> How to add test cases?
> ----------------------
>
> For each test case, you should create a subdirectory under
> scripts/kconfig/tests/ (so test cases are seperated from each other).
> Every test case directory must contain the following files:
>
>  - __init__.py: describes test functions
>  - Kconfig: the top level Kconfig file for this test
>
> To do a useful job, test cases generally need additional data like
> input .config and information about expected results.
>
> How to run tests?
> -----------------
>
> You need python3 and pytest.  Then, run "make testconfig".
> O= option is supported.  If V=1 is given, details logs during tests
> are displayed.
>
> Signed-off-by: Masahiro Yamada <yamada.masahiro@xxxxxxxxxxxxx>
> ---
>
>  scripts/kconfig/Makefile          |   8 ++
>  scripts/kconfig/tests/conftest.py | 255 ++++++++++++++++++++++++++++++++++++++
>  scripts/kconfig/tests/pytest.ini  |   6 +
>  3 files changed, 269 insertions(+)
>  create mode 100644 scripts/kconfig/tests/conftest.py
>  create mode 100644 scripts/kconfig/tests/pytest.ini
>
> diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile
> index cb3ec53..c5d1d1a 100644
> --- a/scripts/kconfig/Makefile
> +++ b/scripts/kconfig/Makefile
> @@ -135,6 +135,14 @@ PHONY += tinyconfig
>  tinyconfig:
>         $(Q)$(MAKE) -f $(srctree)/Makefile allnoconfig tiny.config
>
> +# CHECK: -o cache_dir=<path> working?
> +PHONY += testconfig
> +testconfig: $(obj)/conf
> +       $(PYTHON3) -B -m pytest $(srctree)/$(src)/tests \
> +       -o cache_dir=$(abspath $(obj)/tests/.cache) \
> +       $(if $(findstring 1,$(KBUILD_VERBOSE)),--capture=no)
> +clean-dirs += tests/.cache
> +
>  # Help text used by make help
>  help:
>         @echo  '  config          - Update current config utilising a line-oriented program'
> diff --git a/scripts/kconfig/tests/conftest.py b/scripts/kconfig/tests/conftest.py
> new file mode 100644
> index 0000000..f0f3237
> --- /dev/null
> +++ b/scripts/kconfig/tests/conftest.py
> @@ -0,0 +1,255 @@
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Copyright (C) 2018 Masahiro Yamada <yamada.masahiro@xxxxxxxxxxxxx>
> +#
> +
> +import os
> +import pytest
> +import shutil
> +import subprocess
> +import tempfile
> +
> +conf_path = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf'))
> +
> +class Conf:
> +
> +    def __init__(self, request):
> +        """Create a new Conf object, which is a scripts/kconfig/conf
> +        runner and result checker.
> +
> +        Arguments:
> +        request - object to introspect the requesting test module
> +        """
> +
> +        # the directory of the test being run
> +        self.test_dir = os.path.dirname(str(request.fspath))
> +
> +    def __run_conf(self, mode, dot_config=None, out_file='.config',
> +                   interactive=False, in_keys=None, extra_env={}):
> +        """Run scripts/kconfig/conf
> +
> +        mode: input mode option (--oldaskconfig, --defconfig=<file> etc.)
> +        dot_config: the .config file for input.
> +        out_file: file name to contain the output config data.
> +        interactive: flag to specify the interactive mode.
> +        in_keys: key inputs for interactive modes.
> +        extra_env: additional environment.
> +        """
> +
> +        command = [conf_path, mode, 'Kconfig']
> +
> +        # Override 'srctree' environment to make the test as the top directory
> +        extra_env['srctree'] = self.test_dir
> +
> +        # scripts/kconfig/conf is run in a temporary directory.
> +        # This directory is automatically removed when done.
> +        with tempfile.TemporaryDirectory() as temp_dir:
> +
> +            # if .config is given, copy it to the working directory
> +            if dot_config:
> +                shutil.copyfile(os.path.join(self.test_dir, dot_config),
> +                                os.path.join(temp_dir, '.config'))
> +
> +            ps = subprocess.Popen(command,
> +                                  stdin=subprocess.PIPE,
> +                                  stdout=subprocess.PIPE,
> +                                  stderr=subprocess.PIPE,
> +                                  cwd=temp_dir,
> +                                  env=dict(os.environ, **extra_env))
> +
> +            # If user key input is specified, feed it into stdin.
> +            if in_keys:
> +                ps.stdin.write(in_keys.encode('utf-8'))
> +
> +            while ps.poll() == None:
> +                # For interactive modes such as 'make config', 'make oldconfig',
> +                # send 'Enter' key until the program finishes.
> +                if interactive:
> +                    ps.stdin.write(b'\n')
> +
> +            self.retcode = ps.returncode
> +            self.stdout = ps.stdout.read().decode()
> +            self.stderr = ps.stderr.read().decode()
> +
> +            # Retrieve the resulted config data only when .config is supposed
> +            # to exist.  If the command fails, the .config does not exist.
> +            # 'make listnewconfig' does not produce .config in the first place.
> +            if self.retcode == 0 and out_file:
> +                with open(os.path.join(temp_dir, out_file)) as f:
> +                    self.config = f.read()
> +            else:
> +                self.config = None
> +
> +        # Logging:
> +        # Pytest captures the following information by default.  In failure
> +        # of tests, the captured log will be displayed.  This will be useful to
> +        # figure out what has happened.
> +
> +        print("command: {}\n".format(' '.join(command)))
> +        print("retcode: {}\n".format(self.retcode))
> +
> +        if dot_config:
> +            print("input .config:".format(dot_config))
> +
> +        print("stdout:")
> +        print(self.stdout)
> +        print("stderr:")
> +        print(self.stderr)
> +
> +        if self.config is not None:
> +            print("output of {}:".format(out_file))
> +            print(self.config)
> +
> +        return self.retcode
> +
> +    def oldaskconfig(self, dot_config=None, in_keys=None):
> +        """Run oldaskconfig (make config)
> +
> +        dot_config: the .config file for input (optional).
> +        in_key: key inputs (optional).
> +        """
> +        return self.__run_conf('--oldaskconfig', dot_config=dot_config,
> +                             interactive=True, in_keys=in_keys)
> +
> +    def oldconfig(self, dot_config=None, in_keys=None):
> +        """Run oldconfig
> +
> +        dot_config: the .config file for input (optional).
> +        in_key: key inputs (optional).
> +        """
> +        return self.__run_conf('--oldconfig', dot_config=dot_config,
> +                             interactive=True, in_keys=in_keys)
> +
> +    def defconfig(self, defconfig):
> +        """Run defconfig
> +
> +        defconfig: the defconfig file for input.
> +        """
> +        defconfig_path = os.path.join(self.test_dir, defconfig)
> +        return self.__run_conf('--defconfig={}'.format(defconfig_path))
> +
> +    def olddefconfig(self, dot_config=None):
> +        """Run olddefconfig
> +
> +        dot_config: the .config file for input (optional).
> +        """
> +        return self.__run_conf('--olddefconfig', dot_config=dot_config)
> +
> +    def __allconfig(self, foo, all_config):
> +        """Run all*config
> +
> +        all_config: fragment config file for KCONFIG_ALLCONFIG (optional).
> +        """
> +        if all_config:
> +            all_config_path = os.path.join(self.test_dir, all_config)
> +            extra_env = {'KCONFIG_ALLCONFIG': all_config_path}
> +        else:
> +            extra_env = {}
> +
> +        return self.__run_conf('--all{}config'.format(foo), extra_env=extra_env)
> +
> +    def allyesconfig(self, all_config=None):
> +        """Run allyesconfig
> +        """
> +        return self.__allconfig('yes', all_config)
> +
> +    def allmodconfig(self, all_config=None):
> +        """Run allmodconfig
> +        """
> +        return self.__allconfig('mod', all_config)
> +
> +    def allnoconfig(self, all_config=None):
> +        """Run allnoconfig
> +        """
> +        return self.__allconfig('no', all_config)
> +
> +    def alldefconfig(self, all_config=None):
> +        """Run alldefconfig
> +        """
> +        return self.__allconfig('def', all_config)
> +
> +    def savedefconfig(self, dot_config):
> +        """Run savedefconfig
> +        """
> +        return self.__run_conf('--savedefconfig', out_file='defconfig')
> +
> +    def listnewconfig(self, dot_config=None):
> +        """Run listnewconfig
> +        """
> +        return self.__run_conf('--listnewconfig', dot_config=dot_config,
> +                               out_file=None)
> +
> +    # checkers
> +    def __read_and_compare(self, compare, expected):
> +        """Compare the result with expectation.
> +
> +        Arguments:
> +        compare: function to compare the result with expectation
> +        expected: file that contains the expected data
> +        """
> +        with open(os.path.join(self.test_dir, expected)) as f:
> +            expected_data = f.read()
> +        print(expected_data)
> +        return compare(self, expected_data)
> +
> +    def __contains(self, attr, expected):
> +        print("{0} is expected to contain '{1}':".format(attr, expected))
> +        return self.__read_and_compare(lambda s, e: getattr(s, attr).find(e) >= 0,
> +                                       expected)
> +
> +    def __matches(self, attr, expected):
> +        print("{0} is expected to match '{1}':".format(attr, expected))
> +        return self.__read_and_compare(lambda s, e: getattr(s, attr) == e,
> +                                       expected)
> +
> +    def config_contains(self, expected):
> +        """Check if resulted configuration contains expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__contains('config', expected)
> +
> +    def config_matches(self, expected):
> +        """Check if resulted configuration exactly matches expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__matches('config', expected)
> +
> +    def stdout_contains(self, expected):
> +        """Check if resulted stdout contains expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__contains('stdout', expected)
> +
> +    def stdout_matches(self, cmp_file):
> +        """Check if resulted stdout exactly matches expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__matches('stdout', expected)
> +
> +    def stderr_contains(self, expected):
> +        """Check if resulted stderr contains expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__contains('stderr', expected)
> +
> +    def stderr_matches(self, cmp_file):
> +        """Check if resulted stderr exactly matches expected data.
> +
> +        Arguments:
> +        expected: file that contains the expected data.
> +        """
> +        return self.__matches('stderr', expected)
> +
> +@pytest.fixture(scope="module")
> +def conf(request):
> +    return Conf(request)
> diff --git a/scripts/kconfig/tests/pytest.ini b/scripts/kconfig/tests/pytest.ini
> new file mode 100644
> index 0000000..07b94e0
> --- /dev/null
> +++ b/scripts/kconfig/tests/pytest.ini
> @@ -0,0 +1,6 @@
> +[pytest]
> +addopts = --verbose
> +# Pytest requires that test files have unique names, because pytest imports
> +# them as top-level modules.  It is silly to prefix or suffix a test file with
> +# the directory name that contains it.  Use __init__.py for all test files.
> +python_files = __init__.py
> --
> 2.7.4
>

Reviewed-by: Ulf Magnusson <ulfalizer@xxxxxxxxx>
--
To unsubscribe from this list: send the line "unsubscribe linux-kbuild" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Linux&nblp;USB Development]     [Linux Media]     [Video for Linux]     [Linux Audio Users]     [Yosemite Secrets]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux