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