1# SPDX-License-Identifier: GPL-2.0 2# 3# Copyright (C) 2018 Masahiro Yamada <yamada.masahiro@socionext.com> 4# 5 6""" 7Kconfig unit testing framework. 8 9This provides fixture functions commonly used from test files. 10""" 11 12import os 13import pytest 14import shutil 15import subprocess 16import tempfile 17 18CONF_PATH = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf')) 19 20 21class Conf: 22 """Kconfig runner and result checker. 23 24 This class provides methods to run text-based interface of Kconfig 25 (scripts/kconfig/conf) and retrieve the resulted configuration, 26 stdout, and stderr. It also provides methods to compare those 27 results with expectations. 28 """ 29 30 def __init__(self, request): 31 """Create a new Conf instance. 32 33 request: object to introspect the requesting test module 34 """ 35 # the directory of the test being run 36 self._test_dir = os.path.dirname(str(request.fspath)) 37 38 # runners 39 def _run_conf(self, mode, dot_config=None, out_file='.config', 40 interactive=False, in_keys=None, extra_env={}, 41 silent=False): 42 """Run text-based Kconfig executable and save the result. 43 44 mode: input mode option (--oldaskconfig, --defconfig=<file> etc.) 45 dot_config: .config file to use for configuration base 46 out_file: file name to contain the output config data 47 interactive: flag to specify the interactive mode 48 in_keys: key inputs for interactive modes 49 extra_env: additional environments 50 returncode: exit status of the Kconfig executable 51 """ 52 command = [CONF_PATH] 53 if silent: 54 command.append('-s') 55 command += [mode, 'Kconfig'] 56 57 # Override 'srctree' environment to make the test as the top directory 58 extra_env['srctree'] = self._test_dir 59 60 # Clear KCONFIG_DEFCONFIG_LIST to keep unit tests from being affected 61 # by the user's environment. 62 extra_env['KCONFIG_DEFCONFIG_LIST'] = '' 63 64 # Run Kconfig in a temporary directory. 65 # This directory is automatically removed when done. 66 with tempfile.TemporaryDirectory() as temp_dir: 67 68 # if .config is given, copy it to the working directory 69 if dot_config: 70 shutil.copyfile(os.path.join(self._test_dir, dot_config), 71 os.path.join(temp_dir, '.config')) 72 73 ps = subprocess.Popen(command, 74 stdin=subprocess.PIPE, 75 stdout=subprocess.PIPE, 76 stderr=subprocess.PIPE, 77 cwd=temp_dir, 78 env=dict(os.environ, **extra_env)) 79 80 # If input key sequence is given, feed it to stdin. 81 if in_keys: 82 ps.stdin.write(in_keys.encode('utf-8')) 83 84 while ps.poll() is None: 85 # For interactive modes such as oldaskconfig, oldconfig, 86 # send 'Enter' key until the program finishes. 87 if interactive: 88 try: 89 ps.stdin.write(b'\n') 90 ps.stdin.flush() 91 except (BrokenPipeError, OSError): 92 # Process has exited, stop sending input 93 break 94 95 # Close stdin gracefully 96 try: 97 ps.stdin.close() 98 except (BrokenPipeError, OSError): 99 # Ignore broken pipe on close 100 pass 101 102 # Wait for process to complete 103 ps.wait() 104 105 self.retcode = ps.returncode 106 self.stdout = ps.stdout.read().decode() 107 self.stderr = ps.stderr.read().decode() 108 109 # Retrieve the resulted config data only when .config is supposed 110 # to exist. If the command fails, the .config does not exist. 111 # 'listnewconfig' does not produce .config in the first place. 112 if self.retcode == 0 and out_file: 113 with open(os.path.join(temp_dir, out_file)) as f: 114 self.config = f.read() 115 else: 116 self.config = None 117 118 # Logging: 119 # Pytest captures the following information by default. In failure 120 # of tests, the captured log will be displayed. This will be useful to 121 # figure out what has happened. 122 123 print("[command]\n{}\n".format(' '.join(command))) 124 125 print("[retcode]\n{}\n".format(self.retcode)) 126 127 print("[stdout]") 128 print(self.stdout) 129 130 print("[stderr]") 131 print(self.stderr) 132 133 if self.config is not None: 134 print("[output for '{}']".format(out_file)) 135 print(self.config) 136 137 return self.retcode 138 139 def oldaskconfig(self, dot_config=None, in_keys=None): 140 """Run oldaskconfig. 141 142 dot_config: .config file to use for configuration base (optional) 143 in_key: key inputs (optional) 144 returncode: exit status of the Kconfig executable 145 """ 146 return self._run_conf('--oldaskconfig', dot_config=dot_config, 147 interactive=True, in_keys=in_keys) 148 149 def oldconfig(self, dot_config=None, in_keys=None): 150 """Run oldconfig. 151 152 dot_config: .config file to use for configuration base (optional) 153 in_key: key inputs (optional) 154 returncode: exit status of the Kconfig executable 155 """ 156 return self._run_conf('--oldconfig', dot_config=dot_config, 157 interactive=True, in_keys=in_keys) 158 159 def olddefconfig(self, dot_config=None): 160 """Run olddefconfig. 161 162 dot_config: .config file to use for configuration base (optional) 163 returncode: exit status of the Kconfig executable 164 """ 165 return self._run_conf('--olddefconfig', dot_config=dot_config) 166 167 def defconfig(self, defconfig): 168 """Run defconfig. 169 170 defconfig: defconfig file for input 171 returncode: exit status of the Kconfig executable 172 """ 173 defconfig_path = os.path.join(self._test_dir, defconfig) 174 return self._run_conf('--defconfig={}'.format(defconfig_path)) 175 176 def _allconfig(self, mode, all_config, extra_env={}): 177 if all_config: 178 all_config_path = os.path.join(self._test_dir, all_config) 179 extra_env['KCONFIG_ALLCONFIG'] = all_config_path 180 181 return self._run_conf('--{}config'.format(mode), extra_env=extra_env) 182 183 def allyesconfig(self, all_config=None): 184 """Run allyesconfig. 185 186 all_config: fragment config file for KCONFIG_ALLCONFIG (optional) 187 returncode: exit status of the Kconfig executable 188 """ 189 return self._allconfig('allyes', all_config) 190 191 def allmodconfig(self, all_config=None): 192 """Run allmodconfig. 193 194 all_config: fragment config file for KCONFIG_ALLCONFIG (optional) 195 returncode: exit status of the Kconfig executable 196 """ 197 return self._allconfig('allmod', all_config) 198 199 def allnoconfig(self, all_config=None): 200 """Run allnoconfig. 201 202 all_config: fragment config file for KCONFIG_ALLCONFIG (optional) 203 returncode: exit status of the Kconfig executable 204 """ 205 return self._allconfig('allno', all_config) 206 207 def alldefconfig(self, all_config=None): 208 """Run alldefconfig. 209 210 all_config: fragment config file for KCONFIG_ALLCONFIG (optional) 211 returncode: exit status of the Kconfig executable 212 """ 213 return self._allconfig('alldef', all_config) 214 215 def randconfig(self, all_config=None, seed=None): 216 """Run randconfig. 217 218 all_config: fragment config file for KCONFIG_ALLCONFIG (optional) 219 seed: the seed for randconfig (optional) 220 returncode: exit status of the Kconfig executable 221 """ 222 if seed is not None: 223 extra_env = {'KCONFIG_SEED': hex(seed)} 224 else: 225 extra_env = {} 226 227 return self._allconfig('rand', all_config, extra_env=extra_env) 228 229 def savedefconfig(self, dot_config): 230 """Run savedefconfig. 231 232 dot_config: .config file for input 233 returncode: exit status of the Kconfig executable 234 """ 235 return self._run_conf('--savedefconfig', out_file='defconfig') 236 237 def listnewconfig(self, dot_config=None): 238 """Run listnewconfig. 239 240 dot_config: .config file to use for configuration base (optional) 241 returncode: exit status of the Kconfig executable 242 """ 243 return self._run_conf('--listnewconfig', dot_config=dot_config, 244 out_file=None) 245 246 # checkers 247 def _read_and_compare(self, compare, expected): 248 """Compare the result with expectation. 249 250 compare: function to compare the result with expectation 251 expected: file that contains the expected data 252 """ 253 with open(os.path.join(self._test_dir, expected)) as f: 254 expected_data = f.read() 255 return compare(self, expected_data) 256 257 def _contains(self, attr, expected): 258 return self._read_and_compare( 259 lambda s, e: getattr(s, attr).find(e) >= 0, 260 expected) 261 262 def _matches(self, attr, expected): 263 return self._read_and_compare(lambda s, e: getattr(s, attr) == e, 264 expected) 265 266 def config_contains(self, expected): 267 """Check if resulted configuration contains expected data. 268 269 expected: file that contains the expected data 270 returncode: True if result contains the expected data, False otherwise 271 """ 272 return self._contains('config', expected) 273 274 def config_matches(self, expected): 275 """Check if resulted configuration exactly matches expected data. 276 277 expected: file that contains the expected data 278 returncode: True if result matches the expected data, False otherwise 279 """ 280 return self._matches('config', expected) 281 282 def stdout_contains(self, expected): 283 """Check if resulted stdout contains expected data. 284 285 expected: file that contains the expected data 286 returncode: True if result contains the expected data, False otherwise 287 """ 288 return self._contains('stdout', expected) 289 290 def stdout_matches(self, expected): 291 """Check if resulted stdout exactly matches expected data. 292 293 expected: file that contains the expected data 294 returncode: True if result matches the expected data, False otherwise 295 """ 296 return self._matches('stdout', expected) 297 298 def stderr_contains(self, expected): 299 """Check if resulted stderr contains expected data. 300 301 expected: file that contains the expected data 302 returncode: True if result contains the expected data, False otherwise 303 """ 304 return self._contains('stderr', expected) 305 306 def stderr_matches(self, expected): 307 """Check if resulted stderr exactly matches expected data. 308 309 expected: file that contains the expected data 310 returncode: True if result matches the expected data, False otherwise 311 """ 312 return self._matches('stderr', expected) 313 314 315@pytest.fixture(scope="module") 316def conf(request): 317 """Create a Conf instance and provide it to test functions.""" 318 return Conf(request) 319