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