diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/kconfig/Makefile | 8 | ||||
| -rw-r--r-- | scripts/kconfig/tests/conftest.py | 291 | ||||
| -rw-r--r-- | scripts/kconfig/tests/pytest.ini | 7 |
3 files changed, 306 insertions, 0 deletions
diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile index a9f325ada4cb..78c96aabd00f 100644 --- a/scripts/kconfig/Makefile +++ b/scripts/kconfig/Makefile | |||
| @@ -142,6 +142,14 @@ PHONY += tinyconfig | |||
| 142 | tinyconfig: | 142 | tinyconfig: |
| 143 | $(Q)$(MAKE) -f $(srctree)/Makefile allnoconfig tiny.config | 143 | $(Q)$(MAKE) -f $(srctree)/Makefile allnoconfig tiny.config |
| 144 | 144 | ||
| 145 | # CHECK: -o cache_dir=<path> working? | ||
| 146 | PHONY += testconfig | ||
| 147 | testconfig: $(obj)/conf | ||
| 148 | $(PYTHON3) -B -m pytest $(srctree)/$(src)/tests \ | ||
| 149 | -o cache_dir=$(abspath $(obj)/tests/.cache) \ | ||
| 150 | $(if $(findstring 1,$(KBUILD_VERBOSE)),--capture=no) | ||
| 151 | clean-dirs += tests/.cache | ||
| 152 | |||
| 145 | # Help text used by make help | 153 | # Help text used by make help |
| 146 | help: | 154 | help: |
| 147 | @echo ' config - Update current config utilising a line-oriented program' | 155 | @echo ' config - Update current config utilising a line-oriented program' |
diff --git a/scripts/kconfig/tests/conftest.py b/scripts/kconfig/tests/conftest.py new file mode 100644 index 000000000000..0345ef6e3273 --- /dev/null +++ b/scripts/kconfig/tests/conftest.py | |||
| @@ -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) | ||
diff --git a/scripts/kconfig/tests/pytest.ini b/scripts/kconfig/tests/pytest.ini new file mode 100644 index 000000000000..85d7ce8e448b --- /dev/null +++ b/scripts/kconfig/tests/pytest.ini | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | [pytest] | ||
| 2 | addopts = --verbose | ||
| 3 | |||
| 4 | # Pytest requires that test files have unique names, because pytest imports | ||
| 5 | # them as top-level modules. It is silly to prefix or suffix a test file with | ||
| 6 | # the directory name that contains it. Use __init__.py for all test files. | ||
| 7 | python_files = __init__.py | ||
