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