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