Eugeniu Rosca | e91610d | 2018-05-19 14:13:50 +0200 | [diff] [blame] | 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) |