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