1022a4bf6SMasahiro Yamada# SPDX-License-Identifier: GPL-2.0
2022a4bf6SMasahiro Yamada#
3022a4bf6SMasahiro Yamada# Copyright (C) 2018 Masahiro Yamada <[email protected]>
4022a4bf6SMasahiro Yamada#
5022a4bf6SMasahiro Yamada
6022a4bf6SMasahiro Yamada"""
7022a4bf6SMasahiro YamadaKconfig unit testing framework.
8022a4bf6SMasahiro Yamada
9022a4bf6SMasahiro YamadaThis provides fixture functions commonly used from test files.
10022a4bf6SMasahiro Yamada"""
11022a4bf6SMasahiro Yamada
12022a4bf6SMasahiro Yamadaimport os
13022a4bf6SMasahiro Yamadaimport pytest
14022a4bf6SMasahiro Yamadaimport shutil
15022a4bf6SMasahiro Yamadaimport subprocess
16022a4bf6SMasahiro Yamadaimport tempfile
17022a4bf6SMasahiro Yamada
18022a4bf6SMasahiro YamadaCONF_PATH = os.path.abspath(os.path.join('scripts', 'kconfig', 'conf'))
19022a4bf6SMasahiro Yamada
20022a4bf6SMasahiro Yamada
21022a4bf6SMasahiro Yamadaclass Conf:
22022a4bf6SMasahiro Yamada    """Kconfig runner and result checker.
23022a4bf6SMasahiro Yamada
24022a4bf6SMasahiro Yamada    This class provides methods to run text-based interface of Kconfig
25022a4bf6SMasahiro Yamada    (scripts/kconfig/conf) and retrieve the resulted configuration,
26022a4bf6SMasahiro Yamada    stdout, and stderr.  It also provides methods to compare those
27022a4bf6SMasahiro Yamada    results with expectations.
28022a4bf6SMasahiro Yamada    """
29022a4bf6SMasahiro Yamada
30022a4bf6SMasahiro Yamada    def __init__(self, request):
31022a4bf6SMasahiro Yamada        """Create a new Conf instance.
32022a4bf6SMasahiro Yamada
33022a4bf6SMasahiro Yamada        request: object to introspect the requesting test module
34022a4bf6SMasahiro Yamada        """
35022a4bf6SMasahiro Yamada        # the directory of the test being run
36022a4bf6SMasahiro Yamada        self._test_dir = os.path.dirname(str(request.fspath))
37022a4bf6SMasahiro Yamada
38022a4bf6SMasahiro Yamada    # runners
39022a4bf6SMasahiro Yamada    def _run_conf(self, mode, dot_config=None, out_file='.config',
40022a4bf6SMasahiro Yamada                  interactive=False, in_keys=None, extra_env={}):
41022a4bf6SMasahiro Yamada        """Run text-based Kconfig executable and save the result.
42022a4bf6SMasahiro Yamada
43022a4bf6SMasahiro Yamada        mode: input mode option (--oldaskconfig, --defconfig=<file> etc.)
44022a4bf6SMasahiro Yamada        dot_config: .config file to use for configuration base
45022a4bf6SMasahiro Yamada        out_file: file name to contain the output config data
46022a4bf6SMasahiro Yamada        interactive: flag to specify the interactive mode
47022a4bf6SMasahiro Yamada        in_keys: key inputs for interactive modes
48022a4bf6SMasahiro Yamada        extra_env: additional environments
49022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
50022a4bf6SMasahiro Yamada        """
51022a4bf6SMasahiro Yamada        command = [CONF_PATH, mode, 'Kconfig']
52022a4bf6SMasahiro Yamada
53022a4bf6SMasahiro Yamada        # Override 'srctree' environment to make the test as the top directory
54022a4bf6SMasahiro Yamada        extra_env['srctree'] = self._test_dir
55022a4bf6SMasahiro Yamada
56b75b0a81SMasahiro Yamada        # Clear KCONFIG_DEFCONFIG_LIST to keep unit tests from being affected
57b75b0a81SMasahiro Yamada        # by the user's environment.
58b75b0a81SMasahiro Yamada        extra_env['KCONFIG_DEFCONFIG_LIST'] = ''
59b75b0a81SMasahiro Yamada
60022a4bf6SMasahiro Yamada        # Run Kconfig in a temporary directory.
61022a4bf6SMasahiro Yamada        # This directory is automatically removed when done.
62022a4bf6SMasahiro Yamada        with tempfile.TemporaryDirectory() as temp_dir:
63022a4bf6SMasahiro Yamada
64022a4bf6SMasahiro Yamada            # if .config is given, copy it to the working directory
65022a4bf6SMasahiro Yamada            if dot_config:
66022a4bf6SMasahiro Yamada                shutil.copyfile(os.path.join(self._test_dir, dot_config),
67022a4bf6SMasahiro Yamada                                os.path.join(temp_dir, '.config'))
68022a4bf6SMasahiro Yamada
69022a4bf6SMasahiro Yamada            ps = subprocess.Popen(command,
70022a4bf6SMasahiro Yamada                                  stdin=subprocess.PIPE,
71022a4bf6SMasahiro Yamada                                  stdout=subprocess.PIPE,
72022a4bf6SMasahiro Yamada                                  stderr=subprocess.PIPE,
73022a4bf6SMasahiro Yamada                                  cwd=temp_dir,
74022a4bf6SMasahiro Yamada                                  env=dict(os.environ, **extra_env))
75022a4bf6SMasahiro Yamada
76022a4bf6SMasahiro Yamada            # If input key sequence is given, feed it to stdin.
77022a4bf6SMasahiro Yamada            if in_keys:
78022a4bf6SMasahiro Yamada                ps.stdin.write(in_keys.encode('utf-8'))
79022a4bf6SMasahiro Yamada
80022a4bf6SMasahiro Yamada            while ps.poll() is None:
81022a4bf6SMasahiro Yamada                # For interactive modes such as oldaskconfig, oldconfig,
82022a4bf6SMasahiro Yamada                # send 'Enter' key until the program finishes.
83022a4bf6SMasahiro Yamada                if interactive:
84022a4bf6SMasahiro Yamada                    ps.stdin.write(b'\n')
85022a4bf6SMasahiro Yamada
86022a4bf6SMasahiro Yamada            self.retcode = ps.returncode
87022a4bf6SMasahiro Yamada            self.stdout = ps.stdout.read().decode()
88022a4bf6SMasahiro Yamada            self.stderr = ps.stderr.read().decode()
89022a4bf6SMasahiro Yamada
90022a4bf6SMasahiro Yamada            # Retrieve the resulted config data only when .config is supposed
91022a4bf6SMasahiro Yamada            # to exist.  If the command fails, the .config does not exist.
92022a4bf6SMasahiro Yamada            # 'listnewconfig' does not produce .config in the first place.
93022a4bf6SMasahiro Yamada            if self.retcode == 0 and out_file:
94022a4bf6SMasahiro Yamada                with open(os.path.join(temp_dir, out_file)) as f:
95022a4bf6SMasahiro Yamada                    self.config = f.read()
96022a4bf6SMasahiro Yamada            else:
97022a4bf6SMasahiro Yamada                self.config = None
98022a4bf6SMasahiro Yamada
99022a4bf6SMasahiro Yamada        # Logging:
100022a4bf6SMasahiro Yamada        # Pytest captures the following information by default.  In failure
101022a4bf6SMasahiro Yamada        # of tests, the captured log will be displayed.  This will be useful to
102022a4bf6SMasahiro Yamada        # figure out what has happened.
103022a4bf6SMasahiro Yamada
104022a4bf6SMasahiro Yamada        print("[command]\n{}\n".format(' '.join(command)))
105022a4bf6SMasahiro Yamada
106022a4bf6SMasahiro Yamada        print("[retcode]\n{}\n".format(self.retcode))
107022a4bf6SMasahiro Yamada
108022a4bf6SMasahiro Yamada        print("[stdout]")
109022a4bf6SMasahiro Yamada        print(self.stdout)
110022a4bf6SMasahiro Yamada
111022a4bf6SMasahiro Yamada        print("[stderr]")
112022a4bf6SMasahiro Yamada        print(self.stderr)
113022a4bf6SMasahiro Yamada
114022a4bf6SMasahiro Yamada        if self.config is not None:
115022a4bf6SMasahiro Yamada            print("[output for '{}']".format(out_file))
116022a4bf6SMasahiro Yamada            print(self.config)
117022a4bf6SMasahiro Yamada
118022a4bf6SMasahiro Yamada        return self.retcode
119022a4bf6SMasahiro Yamada
120022a4bf6SMasahiro Yamada    def oldaskconfig(self, dot_config=None, in_keys=None):
121022a4bf6SMasahiro Yamada        """Run oldaskconfig.
122022a4bf6SMasahiro Yamada
123022a4bf6SMasahiro Yamada        dot_config: .config file to use for configuration base (optional)
124022a4bf6SMasahiro Yamada        in_key: key inputs (optional)
125022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
126022a4bf6SMasahiro Yamada        """
127022a4bf6SMasahiro Yamada        return self._run_conf('--oldaskconfig', dot_config=dot_config,
128022a4bf6SMasahiro Yamada                              interactive=True, in_keys=in_keys)
129022a4bf6SMasahiro Yamada
130022a4bf6SMasahiro Yamada    def oldconfig(self, dot_config=None, in_keys=None):
131022a4bf6SMasahiro Yamada        """Run oldconfig.
132022a4bf6SMasahiro Yamada
133022a4bf6SMasahiro Yamada        dot_config: .config file to use for configuration base (optional)
134022a4bf6SMasahiro Yamada        in_key: key inputs (optional)
135022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
136022a4bf6SMasahiro Yamada        """
137022a4bf6SMasahiro Yamada        return self._run_conf('--oldconfig', dot_config=dot_config,
138022a4bf6SMasahiro Yamada                              interactive=True, in_keys=in_keys)
139022a4bf6SMasahiro Yamada
140022a4bf6SMasahiro Yamada    def olddefconfig(self, dot_config=None):
141022a4bf6SMasahiro Yamada        """Run olddefconfig.
142022a4bf6SMasahiro Yamada
143022a4bf6SMasahiro Yamada        dot_config: .config file to use for configuration base (optional)
144022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
145022a4bf6SMasahiro Yamada        """
146022a4bf6SMasahiro Yamada        return self._run_conf('--olddefconfig', dot_config=dot_config)
147022a4bf6SMasahiro Yamada
148022a4bf6SMasahiro Yamada    def defconfig(self, defconfig):
149022a4bf6SMasahiro Yamada        """Run defconfig.
150022a4bf6SMasahiro Yamada
151022a4bf6SMasahiro Yamada        defconfig: defconfig file for input
152022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
153022a4bf6SMasahiro Yamada        """
154022a4bf6SMasahiro Yamada        defconfig_path = os.path.join(self._test_dir, defconfig)
155022a4bf6SMasahiro Yamada        return self._run_conf('--defconfig={}'.format(defconfig_path))
156022a4bf6SMasahiro Yamada
157*c9aa7d86SMasahiro Yamada    def _allconfig(self, mode, all_config, extra_env={}):
158022a4bf6SMasahiro Yamada        if all_config:
159022a4bf6SMasahiro Yamada            all_config_path = os.path.join(self._test_dir, all_config)
160*c9aa7d86SMasahiro Yamada            extra_env['KCONFIG_ALLCONFIG'] = all_config_path
161022a4bf6SMasahiro Yamada
162022a4bf6SMasahiro Yamada        return self._run_conf('--{}config'.format(mode), extra_env=extra_env)
163022a4bf6SMasahiro Yamada
164022a4bf6SMasahiro Yamada    def allyesconfig(self, all_config=None):
165022a4bf6SMasahiro Yamada        """Run allyesconfig.
166022a4bf6SMasahiro Yamada
167022a4bf6SMasahiro Yamada        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
168022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
169022a4bf6SMasahiro Yamada        """
170022a4bf6SMasahiro Yamada        return self._allconfig('allyes', all_config)
171022a4bf6SMasahiro Yamada
172022a4bf6SMasahiro Yamada    def allmodconfig(self, all_config=None):
173022a4bf6SMasahiro Yamada        """Run allmodconfig.
174022a4bf6SMasahiro Yamada
175022a4bf6SMasahiro Yamada        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
176022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
177022a4bf6SMasahiro Yamada        """
178022a4bf6SMasahiro Yamada        return self._allconfig('allmod', all_config)
179022a4bf6SMasahiro Yamada
180022a4bf6SMasahiro Yamada    def allnoconfig(self, all_config=None):
181022a4bf6SMasahiro Yamada        """Run allnoconfig.
182022a4bf6SMasahiro Yamada
183022a4bf6SMasahiro Yamada        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
184022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
185022a4bf6SMasahiro Yamada        """
186022a4bf6SMasahiro Yamada        return self._allconfig('allno', all_config)
187022a4bf6SMasahiro Yamada
188022a4bf6SMasahiro Yamada    def alldefconfig(self, all_config=None):
189022a4bf6SMasahiro Yamada        """Run alldefconfig.
190022a4bf6SMasahiro Yamada
191022a4bf6SMasahiro Yamada        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
192022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
193022a4bf6SMasahiro Yamada        """
194022a4bf6SMasahiro Yamada        return self._allconfig('alldef', all_config)
195022a4bf6SMasahiro Yamada
196*c9aa7d86SMasahiro Yamada    def randconfig(self, all_config=None, seed=None):
197022a4bf6SMasahiro Yamada        """Run randconfig.
198022a4bf6SMasahiro Yamada
199022a4bf6SMasahiro Yamada        all_config: fragment config file for KCONFIG_ALLCONFIG (optional)
200*c9aa7d86SMasahiro Yamada        seed: the seed for randconfig (optional)
201022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
202022a4bf6SMasahiro Yamada        """
203*c9aa7d86SMasahiro Yamada        if seed is not None:
204*c9aa7d86SMasahiro Yamada            extra_env = {'KCONFIG_SEED': hex(seed)}
205*c9aa7d86SMasahiro Yamada        else:
206*c9aa7d86SMasahiro Yamada            extra_env = {}
207*c9aa7d86SMasahiro Yamada
208*c9aa7d86SMasahiro Yamada        return self._allconfig('rand', all_config, extra_env=extra_env)
209022a4bf6SMasahiro Yamada
210022a4bf6SMasahiro Yamada    def savedefconfig(self, dot_config):
211022a4bf6SMasahiro Yamada        """Run savedefconfig.
212022a4bf6SMasahiro Yamada
213022a4bf6SMasahiro Yamada        dot_config: .config file for input
214022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
215022a4bf6SMasahiro Yamada        """
216022a4bf6SMasahiro Yamada        return self._run_conf('--savedefconfig', out_file='defconfig')
217022a4bf6SMasahiro Yamada
218022a4bf6SMasahiro Yamada    def listnewconfig(self, dot_config=None):
219022a4bf6SMasahiro Yamada        """Run listnewconfig.
220022a4bf6SMasahiro Yamada
221022a4bf6SMasahiro Yamada        dot_config: .config file to use for configuration base (optional)
222022a4bf6SMasahiro Yamada        returncode: exit status of the Kconfig executable
223022a4bf6SMasahiro Yamada        """
224022a4bf6SMasahiro Yamada        return self._run_conf('--listnewconfig', dot_config=dot_config,
225022a4bf6SMasahiro Yamada                              out_file=None)
226022a4bf6SMasahiro Yamada
227022a4bf6SMasahiro Yamada    # checkers
228022a4bf6SMasahiro Yamada    def _read_and_compare(self, compare, expected):
229022a4bf6SMasahiro Yamada        """Compare the result with expectation.
230022a4bf6SMasahiro Yamada
231022a4bf6SMasahiro Yamada        compare: function to compare the result with expectation
232022a4bf6SMasahiro Yamada        expected: file that contains the expected data
233022a4bf6SMasahiro Yamada        """
234022a4bf6SMasahiro Yamada        with open(os.path.join(self._test_dir, expected)) as f:
235022a4bf6SMasahiro Yamada            expected_data = f.read()
236022a4bf6SMasahiro Yamada        return compare(self, expected_data)
237022a4bf6SMasahiro Yamada
238022a4bf6SMasahiro Yamada    def _contains(self, attr, expected):
239022a4bf6SMasahiro Yamada        return self._read_and_compare(
240022a4bf6SMasahiro Yamada                                    lambda s, e: getattr(s, attr).find(e) >= 0,
241022a4bf6SMasahiro Yamada                                    expected)
242022a4bf6SMasahiro Yamada
243022a4bf6SMasahiro Yamada    def _matches(self, attr, expected):
244022a4bf6SMasahiro Yamada        return self._read_and_compare(lambda s, e: getattr(s, attr) == e,
245022a4bf6SMasahiro Yamada                                      expected)
246022a4bf6SMasahiro Yamada
247022a4bf6SMasahiro Yamada    def config_contains(self, expected):
248022a4bf6SMasahiro Yamada        """Check if resulted configuration contains expected data.
249022a4bf6SMasahiro Yamada
250022a4bf6SMasahiro Yamada        expected: file that contains the expected data
251022a4bf6SMasahiro Yamada        returncode: True if result contains the expected data, False otherwise
252022a4bf6SMasahiro Yamada        """
253022a4bf6SMasahiro Yamada        return self._contains('config', expected)
254022a4bf6SMasahiro Yamada
255022a4bf6SMasahiro Yamada    def config_matches(self, expected):
256022a4bf6SMasahiro Yamada        """Check if resulted configuration exactly matches expected data.
257022a4bf6SMasahiro Yamada
258022a4bf6SMasahiro Yamada        expected: file that contains the expected data
259022a4bf6SMasahiro Yamada        returncode: True if result matches the expected data, False otherwise
260022a4bf6SMasahiro Yamada        """
261022a4bf6SMasahiro Yamada        return self._matches('config', expected)
262022a4bf6SMasahiro Yamada
263022a4bf6SMasahiro Yamada    def stdout_contains(self, expected):
264022a4bf6SMasahiro Yamada        """Check if resulted stdout contains expected data.
265022a4bf6SMasahiro Yamada
266022a4bf6SMasahiro Yamada        expected: file that contains the expected data
267022a4bf6SMasahiro Yamada        returncode: True if result contains the expected data, False otherwise
268022a4bf6SMasahiro Yamada        """
269022a4bf6SMasahiro Yamada        return self._contains('stdout', expected)
270022a4bf6SMasahiro Yamada
271022a4bf6SMasahiro Yamada    def stdout_matches(self, expected):
272022a4bf6SMasahiro Yamada        """Check if resulted stdout exactly matches expected data.
273022a4bf6SMasahiro Yamada
274022a4bf6SMasahiro Yamada        expected: file that contains the expected data
275022a4bf6SMasahiro Yamada        returncode: True if result matches the expected data, False otherwise
276022a4bf6SMasahiro Yamada        """
277022a4bf6SMasahiro Yamada        return self._matches('stdout', expected)
278022a4bf6SMasahiro Yamada
279022a4bf6SMasahiro Yamada    def stderr_contains(self, expected):
280022a4bf6SMasahiro Yamada        """Check if resulted stderr contains expected data.
281022a4bf6SMasahiro Yamada
282022a4bf6SMasahiro Yamada        expected: file that contains the expected data
283022a4bf6SMasahiro Yamada        returncode: True if result contains the expected data, False otherwise
284022a4bf6SMasahiro Yamada        """
285022a4bf6SMasahiro Yamada        return self._contains('stderr', expected)
286022a4bf6SMasahiro Yamada
287022a4bf6SMasahiro Yamada    def stderr_matches(self, expected):
288022a4bf6SMasahiro Yamada        """Check if resulted stderr exactly matches expected data.
289022a4bf6SMasahiro Yamada
290022a4bf6SMasahiro Yamada        expected: file that contains the expected data
291022a4bf6SMasahiro Yamada        returncode: True if result matches the expected data, False otherwise
292022a4bf6SMasahiro Yamada        """
293022a4bf6SMasahiro Yamada        return self._matches('stderr', expected)
294022a4bf6SMasahiro Yamada
295022a4bf6SMasahiro Yamada
296022a4bf6SMasahiro Yamada@pytest.fixture(scope="module")
297022a4bf6SMasahiro Yamadadef conf(request):
298022a4bf6SMasahiro Yamada    """Create a Conf instance and provide it to test functions."""
299022a4bf6SMasahiro Yamada    return Conf(request)
300