| @@ -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) |