162306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
262306a36Sopenharmony_ci#
362306a36Sopenharmony_ci# Copyright (C) 2018 Masahiro Yamada <yamada.masahiro@socionext.com>
462306a36Sopenharmony_ci#
562306a36Sopenharmony_ci
662306a36Sopenharmony_ci"""
762306a36Sopenharmony_ciKconfig unit testing framework.
862306a36Sopenharmony_ci
962306a36Sopenharmony_ciThis provides fixture functions commonly used from test files.
1062306a36Sopenharmony_ci"""
1162306a36Sopenharmony_ci
1262306a36Sopenharmony_ciimport os
1362306a36Sopenharmony_ciimport pytest
1462306a36Sopenharmony_ciimport shutil
1562306a36Sopenharmony_ciimport subprocess
1662306a36Sopenharmony_ciimport tempfile
1762306a36Sopenharmony_ci
1862306a36Sopenharmony_ciCONF_PATH = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf'))
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_ci
2162306a36Sopenharmony_ciclass Conf:
2262306a36Sopenharmony_ci    """Kconfig runner and result checker.
2362306a36Sopenharmony_ci
2462306a36Sopenharmony_ci    This class provides methods to run text-based interface of Kconfig
2562306a36Sopenharmony_ci    (scripts/kconfig/conf) and retrieve the resulted configuration,
2662306a36Sopenharmony_ci    stdout, and stderr.  It also provides methods to compare those
2762306a36Sopenharmony_ci    results with expectations.
2862306a36Sopenharmony_ci    """
2962306a36Sopenharmony_ci
3062306a36Sopenharmony_ci    def __init__(self, request):
3162306a36Sopenharmony_ci        """Create a new Conf instance.
3262306a36Sopenharmony_ci
3362306a36Sopenharmony_ci        request: object to introspect the requesting test module
3462306a36Sopenharmony_ci        """
3562306a36Sopenharmony_ci        # the directory of the test being run
3662306a36Sopenharmony_ci        self._test_dir = os.path.dirname(str(request.fspath))
3762306a36Sopenharmony_ci
3862306a36Sopenharmony_ci    # runners
3962306a36Sopenharmony_ci    def _run_conf(self, mode, dot_config=None, out_file='.config',
4062306a36Sopenharmony_ci                  interactive=False, in_keys=None, extra_env={}):
4162306a36Sopenharmony_ci        """Run text-based Kconfig executable and save the result.
4262306a36Sopenharmony_ci
4362306a36Sopenharmony_ci        mode: input mode option (--oldaskconfig, --defconfig=<file> etc.)
4462306a36Sopenharmony_ci        dot_config: .config file to use for configuration base
4562306a36Sopenharmony_ci        out_file: file name to contain the output config data
4662306a36Sopenharmony_ci        interactive: flag to specify the interactive mode
4762306a36Sopenharmony_ci        in_keys: key inputs for interactive modes
4862306a36Sopenharmony_ci        extra_env: additional environments
4962306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
5062306a36Sopenharmony_ci        """
5162306a36Sopenharmony_ci        command = [CONF_PATH, mode, 'Kconfig']
5262306a36Sopenharmony_ci
5362306a36Sopenharmony_ci        # Override 'srctree' environment to make the test as the top directory
5462306a36Sopenharmony_ci        extra_env['srctree'] = self._test_dir
5562306a36Sopenharmony_ci
5662306a36Sopenharmony_ci        # Clear KCONFIG_DEFCONFIG_LIST to keep unit tests from being affected
5762306a36Sopenharmony_ci        # by the user's environment.
5862306a36Sopenharmony_ci        extra_env['KCONFIG_DEFCONFIG_LIST'] = ''
5962306a36Sopenharmony_ci
6062306a36Sopenharmony_ci        # Run Kconfig in a temporary directory.
6162306a36Sopenharmony_ci        # This directory is automatically removed when done.
6262306a36Sopenharmony_ci        with tempfile.TemporaryDirectory() as temp_dir:
6362306a36Sopenharmony_ci
6462306a36Sopenharmony_ci            # if .config is given, copy it to the working directory
6562306a36Sopenharmony_ci            if dot_config:
6662306a36Sopenharmony_ci                shutil.copyfile(os.path.join(self._test_dir, dot_config),
6762306a36Sopenharmony_ci                                os.path.join(temp_dir, '.config'))
6862306a36Sopenharmony_ci
6962306a36Sopenharmony_ci            ps = subprocess.Popen(command,
7062306a36Sopenharmony_ci                                  stdin=subprocess.PIPE,
7162306a36Sopenharmony_ci                                  stdout=subprocess.PIPE,
7262306a36Sopenharmony_ci                                  stderr=subprocess.PIPE,
7362306a36Sopenharmony_ci                                  cwd=temp_dir,
7462306a36Sopenharmony_ci                                  env=dict(os.environ, **extra_env))
7562306a36Sopenharmony_ci
7662306a36Sopenharmony_ci            # If input key sequence is given, feed it to stdin.
7762306a36Sopenharmony_ci            if in_keys:
7862306a36Sopenharmony_ci                ps.stdin.write(in_keys.encode('utf-8'))
7962306a36Sopenharmony_ci
8062306a36Sopenharmony_ci            while ps.poll() is None:
8162306a36Sopenharmony_ci                # For interactive modes such as oldaskconfig, oldconfig,
8262306a36Sopenharmony_ci                # send 'Enter' key until the program finishes.
8362306a36Sopenharmony_ci                if interactive:
8462306a36Sopenharmony_ci                    ps.stdin.write(b'\n')
8562306a36Sopenharmony_ci
8662306a36Sopenharmony_ci            self.retcode = ps.returncode
8762306a36Sopenharmony_ci            self.stdout = ps.stdout.read().decode()
8862306a36Sopenharmony_ci            self.stderr = ps.stderr.read().decode()
8962306a36Sopenharmony_ci
9062306a36Sopenharmony_ci            # Retrieve the resulted config data only when .config is supposed
9162306a36Sopenharmony_ci            # to exist.  If the command fails, the .config does not exist.
9262306a36Sopenharmony_ci            # 'listnewconfig' does not produce .config in the first place.
9362306a36Sopenharmony_ci            if self.retcode == 0 and out_file:
9462306a36Sopenharmony_ci                with open(os.path.join(temp_dir, out_file)) as f:
9562306a36Sopenharmony_ci                    self.config = f.read()
9662306a36Sopenharmony_ci            else:
9762306a36Sopenharmony_ci                self.config = None
9862306a36Sopenharmony_ci
9962306a36Sopenharmony_ci        # Logging:
10062306a36Sopenharmony_ci        # Pytest captures the following information by default.  In failure
10162306a36Sopenharmony_ci        # of tests, the captured log will be displayed.  This will be useful to
10262306a36Sopenharmony_ci        # figure out what has happened.
10362306a36Sopenharmony_ci
10462306a36Sopenharmony_ci        print("[command]\n{}\n".format(' '.join(command)))
10562306a36Sopenharmony_ci
10662306a36Sopenharmony_ci        print("[retcode]\n{}\n".format(self.retcode))
10762306a36Sopenharmony_ci
10862306a36Sopenharmony_ci        print("[stdout]")
10962306a36Sopenharmony_ci        print(self.stdout)
11062306a36Sopenharmony_ci
11162306a36Sopenharmony_ci        print("[stderr]")
11262306a36Sopenharmony_ci        print(self.stderr)
11362306a36Sopenharmony_ci
11462306a36Sopenharmony_ci        if self.config is not None:
11562306a36Sopenharmony_ci            print("[output for '{}']".format(out_file))
11662306a36Sopenharmony_ci            print(self.config)
11762306a36Sopenharmony_ci
11862306a36Sopenharmony_ci        return self.retcode
11962306a36Sopenharmony_ci
12062306a36Sopenharmony_ci    def oldaskconfig(self, dot_config=None, in_keys=None):
12162306a36Sopenharmony_ci        """Run oldaskconfig.
12262306a36Sopenharmony_ci
12362306a36Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
12462306a36Sopenharmony_ci        in_key: key inputs (optional)
12562306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
12662306a36Sopenharmony_ci        """
12762306a36Sopenharmony_ci        return self._run_conf('--oldaskconfig', dot_config=dot_config,
12862306a36Sopenharmony_ci                              interactive=True, in_keys=in_keys)
12962306a36Sopenharmony_ci
13062306a36Sopenharmony_ci    def oldconfig(self, dot_config=None, in_keys=None):
13162306a36Sopenharmony_ci        """Run oldconfig.
13262306a36Sopenharmony_ci
13362306a36Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
13462306a36Sopenharmony_ci        in_key: key inputs (optional)
13562306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
13662306a36Sopenharmony_ci        """
13762306a36Sopenharmony_ci        return self._run_conf('--oldconfig', dot_config=dot_config,
13862306a36Sopenharmony_ci                              interactive=True, in_keys=in_keys)
13962306a36Sopenharmony_ci
14062306a36Sopenharmony_ci    def olddefconfig(self, dot_config=None):
14162306a36Sopenharmony_ci        """Run olddefconfig.
14262306a36Sopenharmony_ci
14362306a36Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
14462306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
14562306a36Sopenharmony_ci        """
14662306a36Sopenharmony_ci        return self._run_conf('--olddefconfig', dot_config=dot_config)
14762306a36Sopenharmony_ci
14862306a36Sopenharmony_ci    def defconfig(self, defconfig):
14962306a36Sopenharmony_ci        """Run defconfig.
15062306a36Sopenharmony_ci
15162306a36Sopenharmony_ci        defconfig: defconfig file for input
15262306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
15362306a36Sopenharmony_ci        """
15462306a36Sopenharmony_ci        defconfig_path = os.path.join(self._test_dir, defconfig)
15562306a36Sopenharmony_ci        return self._run_conf('--defconfig={}'.format(defconfig_path))
15662306a36Sopenharmony_ci
15762306a36Sopenharmony_ci    def _allconfig(self, mode, all_config):
15862306a36Sopenharmony_ci        if all_config:
15962306a36Sopenharmony_ci            all_config_path = os.path.join(self._test_dir, all_config)
16062306a36Sopenharmony_ci            extra_env = {'KCONFIG_ALLCONFIG': all_config_path}
16162306a36Sopenharmony_ci        else:
16262306a36Sopenharmony_ci            extra_env = {}
16362306a36Sopenharmony_ci
16462306a36Sopenharmony_ci        return self._run_conf('--{}config'.format(mode), extra_env=extra_env)
16562306a36Sopenharmony_ci
16662306a36Sopenharmony_ci    def allyesconfig(self, all_config=None):
16762306a36Sopenharmony_ci        """Run allyesconfig.
16862306a36Sopenharmony_ci
16962306a36Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
17062306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
17162306a36Sopenharmony_ci        """
17262306a36Sopenharmony_ci        return self._allconfig('allyes', all_config)
17362306a36Sopenharmony_ci
17462306a36Sopenharmony_ci    def allmodconfig(self, all_config=None):
17562306a36Sopenharmony_ci        """Run allmodconfig.
17662306a36Sopenharmony_ci
17762306a36Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
17862306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
17962306a36Sopenharmony_ci        """
18062306a36Sopenharmony_ci        return self._allconfig('allmod', all_config)
18162306a36Sopenharmony_ci
18262306a36Sopenharmony_ci    def allnoconfig(self, all_config=None):
18362306a36Sopenharmony_ci        """Run allnoconfig.
18462306a36Sopenharmony_ci
18562306a36Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
18662306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
18762306a36Sopenharmony_ci        """
18862306a36Sopenharmony_ci        return self._allconfig('allno', all_config)
18962306a36Sopenharmony_ci
19062306a36Sopenharmony_ci    def alldefconfig(self, all_config=None):
19162306a36Sopenharmony_ci        """Run alldefconfig.
19262306a36Sopenharmony_ci
19362306a36Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
19462306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
19562306a36Sopenharmony_ci        """
19662306a36Sopenharmony_ci        return self._allconfig('alldef', all_config)
19762306a36Sopenharmony_ci
19862306a36Sopenharmony_ci    def randconfig(self, all_config=None):
19962306a36Sopenharmony_ci        """Run randconfig.
20062306a36Sopenharmony_ci
20162306a36Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
20262306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
20362306a36Sopenharmony_ci        """
20462306a36Sopenharmony_ci        return self._allconfig('rand', all_config)
20562306a36Sopenharmony_ci
20662306a36Sopenharmony_ci    def savedefconfig(self, dot_config):
20762306a36Sopenharmony_ci        """Run savedefconfig.
20862306a36Sopenharmony_ci
20962306a36Sopenharmony_ci        dot_config: .config file for input
21062306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
21162306a36Sopenharmony_ci        """
21262306a36Sopenharmony_ci        return self._run_conf('--savedefconfig', out_file='defconfig')
21362306a36Sopenharmony_ci
21462306a36Sopenharmony_ci    def listnewconfig(self, dot_config=None):
21562306a36Sopenharmony_ci        """Run listnewconfig.
21662306a36Sopenharmony_ci
21762306a36Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
21862306a36Sopenharmony_ci        returncode: exit status of the Kconfig executable
21962306a36Sopenharmony_ci        """
22062306a36Sopenharmony_ci        return self._run_conf('--listnewconfig', dot_config=dot_config,
22162306a36Sopenharmony_ci                              out_file=None)
22262306a36Sopenharmony_ci
22362306a36Sopenharmony_ci    # checkers
22462306a36Sopenharmony_ci    def _read_and_compare(self, compare, expected):
22562306a36Sopenharmony_ci        """Compare the result with expectation.
22662306a36Sopenharmony_ci
22762306a36Sopenharmony_ci        compare: function to compare the result with expectation
22862306a36Sopenharmony_ci        expected: file that contains the expected data
22962306a36Sopenharmony_ci        """
23062306a36Sopenharmony_ci        with open(os.path.join(self._test_dir, expected)) as f:
23162306a36Sopenharmony_ci            expected_data = f.read()
23262306a36Sopenharmony_ci        return compare(self, expected_data)
23362306a36Sopenharmony_ci
23462306a36Sopenharmony_ci    def _contains(self, attr, expected):
23562306a36Sopenharmony_ci        return self._read_and_compare(
23662306a36Sopenharmony_ci                                    lambda s, e: getattr(s, attr).find(e) >= 0,
23762306a36Sopenharmony_ci                                    expected)
23862306a36Sopenharmony_ci
23962306a36Sopenharmony_ci    def _matches(self, attr, expected):
24062306a36Sopenharmony_ci        return self._read_and_compare(lambda s, e: getattr(s, attr) == e,
24162306a36Sopenharmony_ci                                      expected)
24262306a36Sopenharmony_ci
24362306a36Sopenharmony_ci    def config_contains(self, expected):
24462306a36Sopenharmony_ci        """Check if resulted configuration contains expected data.
24562306a36Sopenharmony_ci
24662306a36Sopenharmony_ci        expected: file that contains the expected data
24762306a36Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
24862306a36Sopenharmony_ci        """
24962306a36Sopenharmony_ci        return self._contains('config', expected)
25062306a36Sopenharmony_ci
25162306a36Sopenharmony_ci    def config_matches(self, expected):
25262306a36Sopenharmony_ci        """Check if resulted configuration exactly matches expected data.
25362306a36Sopenharmony_ci
25462306a36Sopenharmony_ci        expected: file that contains the expected data
25562306a36Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
25662306a36Sopenharmony_ci        """
25762306a36Sopenharmony_ci        return self._matches('config', expected)
25862306a36Sopenharmony_ci
25962306a36Sopenharmony_ci    def stdout_contains(self, expected):
26062306a36Sopenharmony_ci        """Check if resulted stdout contains expected data.
26162306a36Sopenharmony_ci
26262306a36Sopenharmony_ci        expected: file that contains the expected data
26362306a36Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
26462306a36Sopenharmony_ci        """
26562306a36Sopenharmony_ci        return self._contains('stdout', expected)
26662306a36Sopenharmony_ci
26762306a36Sopenharmony_ci    def stdout_matches(self, expected):
26862306a36Sopenharmony_ci        """Check if resulted stdout exactly matches expected data.
26962306a36Sopenharmony_ci
27062306a36Sopenharmony_ci        expected: file that contains the expected data
27162306a36Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
27262306a36Sopenharmony_ci        """
27362306a36Sopenharmony_ci        return self._matches('stdout', expected)
27462306a36Sopenharmony_ci
27562306a36Sopenharmony_ci    def stderr_contains(self, expected):
27662306a36Sopenharmony_ci        """Check if resulted stderr contains expected data.
27762306a36Sopenharmony_ci
27862306a36Sopenharmony_ci        expected: file that contains the expected data
27962306a36Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
28062306a36Sopenharmony_ci        """
28162306a36Sopenharmony_ci        return self._contains('stderr', expected)
28262306a36Sopenharmony_ci
28362306a36Sopenharmony_ci    def stderr_matches(self, expected):
28462306a36Sopenharmony_ci        """Check if resulted stderr exactly matches expected data.
28562306a36Sopenharmony_ci
28662306a36Sopenharmony_ci        expected: file that contains the expected data
28762306a36Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
28862306a36Sopenharmony_ci        """
28962306a36Sopenharmony_ci        return self._matches('stderr', expected)
29062306a36Sopenharmony_ci
29162306a36Sopenharmony_ci
29262306a36Sopenharmony_ci@pytest.fixture(scope="module")
29362306a36Sopenharmony_cidef conf(request):
29462306a36Sopenharmony_ci    """Create a Conf instance and provide it to test functions."""
29562306a36Sopenharmony_ci    return Conf(request)
296