18c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
28c2ecf20Sopenharmony_ci#
38c2ecf20Sopenharmony_ci# Copyright (C) 2018 Masahiro Yamada <yamada.masahiro@socionext.com>
48c2ecf20Sopenharmony_ci#
58c2ecf20Sopenharmony_ci
68c2ecf20Sopenharmony_ci"""
78c2ecf20Sopenharmony_ciKconfig unit testing framework.
88c2ecf20Sopenharmony_ci
98c2ecf20Sopenharmony_ciThis provides fixture functions commonly used from test files.
108c2ecf20Sopenharmony_ci"""
118c2ecf20Sopenharmony_ci
128c2ecf20Sopenharmony_ciimport os
138c2ecf20Sopenharmony_ciimport pytest
148c2ecf20Sopenharmony_ciimport shutil
158c2ecf20Sopenharmony_ciimport subprocess
168c2ecf20Sopenharmony_ciimport tempfile
178c2ecf20Sopenharmony_ci
188c2ecf20Sopenharmony_ciCONF_PATH = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf'))
198c2ecf20Sopenharmony_ci
208c2ecf20Sopenharmony_ci
218c2ecf20Sopenharmony_ciclass Conf:
228c2ecf20Sopenharmony_ci    """Kconfig runner and result checker.
238c2ecf20Sopenharmony_ci
248c2ecf20Sopenharmony_ci    This class provides methods to run text-based interface of Kconfig
258c2ecf20Sopenharmony_ci    (scripts/kconfig/conf) and retrieve the resulted configuration,
268c2ecf20Sopenharmony_ci    stdout, and stderr.  It also provides methods to compare those
278c2ecf20Sopenharmony_ci    results with expectations.
288c2ecf20Sopenharmony_ci    """
298c2ecf20Sopenharmony_ci
308c2ecf20Sopenharmony_ci    def __init__(self, request):
318c2ecf20Sopenharmony_ci        """Create a new Conf instance.
328c2ecf20Sopenharmony_ci
338c2ecf20Sopenharmony_ci        request: object to introspect the requesting test module
348c2ecf20Sopenharmony_ci        """
358c2ecf20Sopenharmony_ci        # the directory of the test being run
368c2ecf20Sopenharmony_ci        self._test_dir = os.path.dirname(str(request.fspath))
378c2ecf20Sopenharmony_ci
388c2ecf20Sopenharmony_ci    # runners
398c2ecf20Sopenharmony_ci    def _run_conf(self, mode, dot_config=None, out_file='.config',
408c2ecf20Sopenharmony_ci                  interactive=False, in_keys=None, extra_env={}):
418c2ecf20Sopenharmony_ci        """Run text-based Kconfig executable and save the result.
428c2ecf20Sopenharmony_ci
438c2ecf20Sopenharmony_ci        mode: input mode option (--oldaskconfig, --defconfig=<file> etc.)
448c2ecf20Sopenharmony_ci        dot_config: .config file to use for configuration base
458c2ecf20Sopenharmony_ci        out_file: file name to contain the output config data
468c2ecf20Sopenharmony_ci        interactive: flag to specify the interactive mode
478c2ecf20Sopenharmony_ci        in_keys: key inputs for interactive modes
488c2ecf20Sopenharmony_ci        extra_env: additional environments
498c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
508c2ecf20Sopenharmony_ci        """
518c2ecf20Sopenharmony_ci        command = [CONF_PATH, mode, 'Kconfig']
528c2ecf20Sopenharmony_ci
538c2ecf20Sopenharmony_ci        # Override 'srctree' environment to make the test as the top directory
548c2ecf20Sopenharmony_ci        extra_env['srctree'] = self._test_dir
558c2ecf20Sopenharmony_ci
568c2ecf20Sopenharmony_ci        # Run Kconfig in a temporary directory.
578c2ecf20Sopenharmony_ci        # This directory is automatically removed when done.
588c2ecf20Sopenharmony_ci        with tempfile.TemporaryDirectory() as temp_dir:
598c2ecf20Sopenharmony_ci
608c2ecf20Sopenharmony_ci            # if .config is given, copy it to the working directory
618c2ecf20Sopenharmony_ci            if dot_config:
628c2ecf20Sopenharmony_ci                shutil.copyfile(os.path.join(self._test_dir, dot_config),
638c2ecf20Sopenharmony_ci                                os.path.join(temp_dir, '.config'))
648c2ecf20Sopenharmony_ci
658c2ecf20Sopenharmony_ci            ps = subprocess.Popen(command,
668c2ecf20Sopenharmony_ci                                  stdin=subprocess.PIPE,
678c2ecf20Sopenharmony_ci                                  stdout=subprocess.PIPE,
688c2ecf20Sopenharmony_ci                                  stderr=subprocess.PIPE,
698c2ecf20Sopenharmony_ci                                  cwd=temp_dir,
708c2ecf20Sopenharmony_ci                                  env=dict(os.environ, **extra_env))
718c2ecf20Sopenharmony_ci
728c2ecf20Sopenharmony_ci            # If input key sequence is given, feed it to stdin.
738c2ecf20Sopenharmony_ci            if in_keys:
748c2ecf20Sopenharmony_ci                ps.stdin.write(in_keys.encode('utf-8'))
758c2ecf20Sopenharmony_ci
768c2ecf20Sopenharmony_ci            while ps.poll() is None:
778c2ecf20Sopenharmony_ci                # For interactive modes such as oldaskconfig, oldconfig,
788c2ecf20Sopenharmony_ci                # send 'Enter' key until the program finishes.
798c2ecf20Sopenharmony_ci                if interactive:
808c2ecf20Sopenharmony_ci                    ps.stdin.write(b'\n')
818c2ecf20Sopenharmony_ci
828c2ecf20Sopenharmony_ci            self.retcode = ps.returncode
838c2ecf20Sopenharmony_ci            self.stdout = ps.stdout.read().decode()
848c2ecf20Sopenharmony_ci            self.stderr = ps.stderr.read().decode()
858c2ecf20Sopenharmony_ci
868c2ecf20Sopenharmony_ci            # Retrieve the resulted config data only when .config is supposed
878c2ecf20Sopenharmony_ci            # to exist.  If the command fails, the .config does not exist.
888c2ecf20Sopenharmony_ci            # 'listnewconfig' does not produce .config in the first place.
898c2ecf20Sopenharmony_ci            if self.retcode == 0 and out_file:
908c2ecf20Sopenharmony_ci                with open(os.path.join(temp_dir, out_file)) as f:
918c2ecf20Sopenharmony_ci                    self.config = f.read()
928c2ecf20Sopenharmony_ci            else:
938c2ecf20Sopenharmony_ci                self.config = None
948c2ecf20Sopenharmony_ci
958c2ecf20Sopenharmony_ci        # Logging:
968c2ecf20Sopenharmony_ci        # Pytest captures the following information by default.  In failure
978c2ecf20Sopenharmony_ci        # of tests, the captured log will be displayed.  This will be useful to
988c2ecf20Sopenharmony_ci        # figure out what has happened.
998c2ecf20Sopenharmony_ci
1008c2ecf20Sopenharmony_ci        print("[command]\n{}\n".format(' '.join(command)))
1018c2ecf20Sopenharmony_ci
1028c2ecf20Sopenharmony_ci        print("[retcode]\n{}\n".format(self.retcode))
1038c2ecf20Sopenharmony_ci
1048c2ecf20Sopenharmony_ci        print("[stdout]")
1058c2ecf20Sopenharmony_ci        print(self.stdout)
1068c2ecf20Sopenharmony_ci
1078c2ecf20Sopenharmony_ci        print("[stderr]")
1088c2ecf20Sopenharmony_ci        print(self.stderr)
1098c2ecf20Sopenharmony_ci
1108c2ecf20Sopenharmony_ci        if self.config is not None:
1118c2ecf20Sopenharmony_ci            print("[output for '{}']".format(out_file))
1128c2ecf20Sopenharmony_ci            print(self.config)
1138c2ecf20Sopenharmony_ci
1148c2ecf20Sopenharmony_ci        return self.retcode
1158c2ecf20Sopenharmony_ci
1168c2ecf20Sopenharmony_ci    def oldaskconfig(self, dot_config=None, in_keys=None):
1178c2ecf20Sopenharmony_ci        """Run oldaskconfig.
1188c2ecf20Sopenharmony_ci
1198c2ecf20Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
1208c2ecf20Sopenharmony_ci        in_key: key inputs (optional)
1218c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1228c2ecf20Sopenharmony_ci        """
1238c2ecf20Sopenharmony_ci        return self._run_conf('--oldaskconfig', dot_config=dot_config,
1248c2ecf20Sopenharmony_ci                              interactive=True, in_keys=in_keys)
1258c2ecf20Sopenharmony_ci
1268c2ecf20Sopenharmony_ci    def oldconfig(self, dot_config=None, in_keys=None):
1278c2ecf20Sopenharmony_ci        """Run oldconfig.
1288c2ecf20Sopenharmony_ci
1298c2ecf20Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
1308c2ecf20Sopenharmony_ci        in_key: key inputs (optional)
1318c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1328c2ecf20Sopenharmony_ci        """
1338c2ecf20Sopenharmony_ci        return self._run_conf('--oldconfig', dot_config=dot_config,
1348c2ecf20Sopenharmony_ci                              interactive=True, in_keys=in_keys)
1358c2ecf20Sopenharmony_ci
1368c2ecf20Sopenharmony_ci    def olddefconfig(self, dot_config=None):
1378c2ecf20Sopenharmony_ci        """Run olddefconfig.
1388c2ecf20Sopenharmony_ci
1398c2ecf20Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
1408c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1418c2ecf20Sopenharmony_ci        """
1428c2ecf20Sopenharmony_ci        return self._run_conf('--olddefconfig', dot_config=dot_config)
1438c2ecf20Sopenharmony_ci
1448c2ecf20Sopenharmony_ci    def defconfig(self, defconfig):
1458c2ecf20Sopenharmony_ci        """Run defconfig.
1468c2ecf20Sopenharmony_ci
1478c2ecf20Sopenharmony_ci        defconfig: defconfig file for input
1488c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1498c2ecf20Sopenharmony_ci        """
1508c2ecf20Sopenharmony_ci        defconfig_path = os.path.join(self._test_dir, defconfig)
1518c2ecf20Sopenharmony_ci        return self._run_conf('--defconfig={}'.format(defconfig_path))
1528c2ecf20Sopenharmony_ci
1538c2ecf20Sopenharmony_ci    def _allconfig(self, mode, all_config):
1548c2ecf20Sopenharmony_ci        if all_config:
1558c2ecf20Sopenharmony_ci            all_config_path = os.path.join(self._test_dir, all_config)
1568c2ecf20Sopenharmony_ci            extra_env = {'KCONFIG_ALLCONFIG': all_config_path}
1578c2ecf20Sopenharmony_ci        else:
1588c2ecf20Sopenharmony_ci            extra_env = {}
1598c2ecf20Sopenharmony_ci
1608c2ecf20Sopenharmony_ci        return self._run_conf('--{}config'.format(mode), extra_env=extra_env)
1618c2ecf20Sopenharmony_ci
1628c2ecf20Sopenharmony_ci    def allyesconfig(self, all_config=None):
1638c2ecf20Sopenharmony_ci        """Run allyesconfig.
1648c2ecf20Sopenharmony_ci
1658c2ecf20Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
1668c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1678c2ecf20Sopenharmony_ci        """
1688c2ecf20Sopenharmony_ci        return self._allconfig('allyes', all_config)
1698c2ecf20Sopenharmony_ci
1708c2ecf20Sopenharmony_ci    def allmodconfig(self, all_config=None):
1718c2ecf20Sopenharmony_ci        """Run allmodconfig.
1728c2ecf20Sopenharmony_ci
1738c2ecf20Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
1748c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1758c2ecf20Sopenharmony_ci        """
1768c2ecf20Sopenharmony_ci        return self._allconfig('allmod', all_config)
1778c2ecf20Sopenharmony_ci
1788c2ecf20Sopenharmony_ci    def allnoconfig(self, all_config=None):
1798c2ecf20Sopenharmony_ci        """Run allnoconfig.
1808c2ecf20Sopenharmony_ci
1818c2ecf20Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
1828c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1838c2ecf20Sopenharmony_ci        """
1848c2ecf20Sopenharmony_ci        return self._allconfig('allno', all_config)
1858c2ecf20Sopenharmony_ci
1868c2ecf20Sopenharmony_ci    def alldefconfig(self, all_config=None):
1878c2ecf20Sopenharmony_ci        """Run alldefconfig.
1888c2ecf20Sopenharmony_ci
1898c2ecf20Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
1908c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1918c2ecf20Sopenharmony_ci        """
1928c2ecf20Sopenharmony_ci        return self._allconfig('alldef', all_config)
1938c2ecf20Sopenharmony_ci
1948c2ecf20Sopenharmony_ci    def randconfig(self, all_config=None):
1958c2ecf20Sopenharmony_ci        """Run randconfig.
1968c2ecf20Sopenharmony_ci
1978c2ecf20Sopenharmony_ci        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
1988c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
1998c2ecf20Sopenharmony_ci        """
2008c2ecf20Sopenharmony_ci        return self._allconfig('rand', all_config)
2018c2ecf20Sopenharmony_ci
2028c2ecf20Sopenharmony_ci    def savedefconfig(self, dot_config):
2038c2ecf20Sopenharmony_ci        """Run savedefconfig.
2048c2ecf20Sopenharmony_ci
2058c2ecf20Sopenharmony_ci        dot_config: .config file for input
2068c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
2078c2ecf20Sopenharmony_ci        """
2088c2ecf20Sopenharmony_ci        return self._run_conf('--savedefconfig', out_file='defconfig')
2098c2ecf20Sopenharmony_ci
2108c2ecf20Sopenharmony_ci    def listnewconfig(self, dot_config=None):
2118c2ecf20Sopenharmony_ci        """Run listnewconfig.
2128c2ecf20Sopenharmony_ci
2138c2ecf20Sopenharmony_ci        dot_config: .config file to use for configuration base (optional)
2148c2ecf20Sopenharmony_ci        returncode: exit status of the Kconfig executable
2158c2ecf20Sopenharmony_ci        """
2168c2ecf20Sopenharmony_ci        return self._run_conf('--listnewconfig', dot_config=dot_config,
2178c2ecf20Sopenharmony_ci                              out_file=None)
2188c2ecf20Sopenharmony_ci
2198c2ecf20Sopenharmony_ci    # checkers
2208c2ecf20Sopenharmony_ci    def _read_and_compare(self, compare, expected):
2218c2ecf20Sopenharmony_ci        """Compare the result with expectation.
2228c2ecf20Sopenharmony_ci
2238c2ecf20Sopenharmony_ci        compare: function to compare the result with expectation
2248c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2258c2ecf20Sopenharmony_ci        """
2268c2ecf20Sopenharmony_ci        with open(os.path.join(self._test_dir, expected)) as f:
2278c2ecf20Sopenharmony_ci            expected_data = f.read()
2288c2ecf20Sopenharmony_ci        return compare(self, expected_data)
2298c2ecf20Sopenharmony_ci
2308c2ecf20Sopenharmony_ci    def _contains(self, attr, expected):
2318c2ecf20Sopenharmony_ci        return self._read_and_compare(
2328c2ecf20Sopenharmony_ci                                    lambda s, e: getattr(s, attr).find(e) >= 0,
2338c2ecf20Sopenharmony_ci                                    expected)
2348c2ecf20Sopenharmony_ci
2358c2ecf20Sopenharmony_ci    def _matches(self, attr, expected):
2368c2ecf20Sopenharmony_ci        return self._read_and_compare(lambda s, e: getattr(s, attr) == e,
2378c2ecf20Sopenharmony_ci                                      expected)
2388c2ecf20Sopenharmony_ci
2398c2ecf20Sopenharmony_ci    def config_contains(self, expected):
2408c2ecf20Sopenharmony_ci        """Check if resulted configuration contains expected data.
2418c2ecf20Sopenharmony_ci
2428c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2438c2ecf20Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
2448c2ecf20Sopenharmony_ci        """
2458c2ecf20Sopenharmony_ci        return self._contains('config', expected)
2468c2ecf20Sopenharmony_ci
2478c2ecf20Sopenharmony_ci    def config_matches(self, expected):
2488c2ecf20Sopenharmony_ci        """Check if resulted configuration exactly matches expected data.
2498c2ecf20Sopenharmony_ci
2508c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2518c2ecf20Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
2528c2ecf20Sopenharmony_ci        """
2538c2ecf20Sopenharmony_ci        return self._matches('config', expected)
2548c2ecf20Sopenharmony_ci
2558c2ecf20Sopenharmony_ci    def stdout_contains(self, expected):
2568c2ecf20Sopenharmony_ci        """Check if resulted stdout contains expected data.
2578c2ecf20Sopenharmony_ci
2588c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2598c2ecf20Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
2608c2ecf20Sopenharmony_ci        """
2618c2ecf20Sopenharmony_ci        return self._contains('stdout', expected)
2628c2ecf20Sopenharmony_ci
2638c2ecf20Sopenharmony_ci    def stdout_matches(self, expected):
2648c2ecf20Sopenharmony_ci        """Check if resulted stdout exactly matches expected data.
2658c2ecf20Sopenharmony_ci
2668c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2678c2ecf20Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
2688c2ecf20Sopenharmony_ci        """
2698c2ecf20Sopenharmony_ci        return self._matches('stdout', expected)
2708c2ecf20Sopenharmony_ci
2718c2ecf20Sopenharmony_ci    def stderr_contains(self, expected):
2728c2ecf20Sopenharmony_ci        """Check if resulted stderr contains expected data.
2738c2ecf20Sopenharmony_ci
2748c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2758c2ecf20Sopenharmony_ci        returncode: True if result contains the expected data, False otherwise
2768c2ecf20Sopenharmony_ci        """
2778c2ecf20Sopenharmony_ci        return self._contains('stderr', expected)
2788c2ecf20Sopenharmony_ci
2798c2ecf20Sopenharmony_ci    def stderr_matches(self, expected):
2808c2ecf20Sopenharmony_ci        """Check if resulted stderr exactly matches expected data.
2818c2ecf20Sopenharmony_ci
2828c2ecf20Sopenharmony_ci        expected: file that contains the expected data
2838c2ecf20Sopenharmony_ci        returncode: True if result matches the expected data, False otherwise
2848c2ecf20Sopenharmony_ci        """
2858c2ecf20Sopenharmony_ci        return self._matches('stderr', expected)
2868c2ecf20Sopenharmony_ci
2878c2ecf20Sopenharmony_ci
2888c2ecf20Sopenharmony_ci@pytest.fixture(scope="module")
2898c2ecf20Sopenharmony_cidef conf(request):
2908c2ecf20Sopenharmony_ci    """Create a Conf instance and provide it to test functions."""
2918c2ecf20Sopenharmony_ci    return Conf(request)
292