aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/kconfig/tests/conftest.py
diff options
context:
space:
mode:
authorMasahiro Yamada <yamada.masahiro@socionext.com>2018-03-13 05:12:03 -0400
committerMasahiro Yamada <yamada.masahiro@socionext.com>2018-03-25 13:04:01 -0400
commit022a4bf6b59dfdb192ca8aef291c7346f984e511 (patch)
tree905f21979636165739b0ac87851b85cde66ec3a3 /scripts/kconfig/tests/conftest.py
parente9781b52d4e0e3381351d3483cfd173a968dcbe6 (diff)
kconfig: tests: add framework for Kconfig unit testing
Many parts in Kconfig are so cryptic and need refactoring. However, its complexity prevents us from moving forward. There are several naive corner cases where it is difficult to notice breakage. If those are covered by unit tests, we will be able to touch the code with more confidence. Here is a simple test framework based on pytest. The conftest.py provides a fixture useful to run commands such as 'oldaskconfig' etc. and to compare the resulted .config, stdout, stderr with expectations. How to add test cases? ---------------------- For each test case, you should create a subdirectory under scripts/kconfig/tests/ (so test cases are separated from each other). Every test case directory should contain the following files: - __init__.py: describes test functions - Kconfig: the top level Kconfig file for the test To do a useful job, test cases generally need additional data like input .config and information about expected results. How to run tests? ----------------- You need python3 and pytest. Then, run "make testconfig". O= option is supported. If V=1 is given, detailed logs captured during tests are displayed. Signed-off-by: Masahiro Yamada <yamada.masahiro@socionext.com> Reviewed-by: Ulf Magnusson <ulfalizer@gmail.com>
Diffstat (limited to 'scripts/kconfig/tests/conftest.py')
-rw-r--r--scripts/kconfig/tests/conftest.py291
1 files changed, 291 insertions, 0 deletions
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"""
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 """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")
289def conf(request):
290 """Create a Conf instance and provide it to test functions."""
291 return Conf(request)