Merge tag 'pull-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs
[sfrench/cifs-2.6.git] / tools / testing / kunit / kunit_kernel.py
1 # SPDX-License-Identifier: GPL-2.0
2 #
3 # Runs UML kernel, collects output, and handles errors.
4 #
5 # Copyright (C) 2019, Google LLC.
6 # Author: Felix Guo <felixguoxiuping@gmail.com>
7 # Author: Brendan Higgins <brendanhiggins@google.com>
8
9 import importlib.abc
10 import importlib.util
11 import logging
12 import subprocess
13 import os
14 import shlex
15 import shutil
16 import signal
17 import threading
18 from typing import Iterator, List, Optional, Tuple
19
20 import kunit_config
21 from kunit_printer import stdout
22 import qemu_config
23
24 KCONFIG_PATH = '.config'
25 KUNITCONFIG_PATH = '.kunitconfig'
26 OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
27 DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
28 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
29 UML_KCONFIG_PATH = 'tools/testing/kunit/configs/arch_uml.config'
30 OUTFILE_PATH = 'test.log'
31 ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
32 QEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
33
34 class ConfigError(Exception):
35         """Represents an error trying to configure the Linux kernel."""
36
37
38 class BuildError(Exception):
39         """Represents an error trying to build the Linux kernel."""
40
41
42 class LinuxSourceTreeOperations:
43         """An abstraction over command line operations performed on a source tree."""
44
45         def __init__(self, linux_arch: str, cross_compile: Optional[str]):
46                 self._linux_arch = linux_arch
47                 self._cross_compile = cross_compile
48
49         def make_mrproper(self) -> None:
50                 try:
51                         subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
52                 except OSError as e:
53                         raise ConfigError('Could not call make command: ' + str(e))
54                 except subprocess.CalledProcessError as e:
55                         raise ConfigError(e.output.decode())
56
57         def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
58                 return base_kunitconfig
59
60         def make_allyesconfig(self, build_dir: str, make_options) -> None:
61                 raise ConfigError('Only the "um" arch is supported for alltests')
62
63         def make_olddefconfig(self, build_dir: str, make_options) -> None:
64                 command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
65                 if self._cross_compile:
66                         command += ['CROSS_COMPILE=' + self._cross_compile]
67                 if make_options:
68                         command.extend(make_options)
69                 print('Populating config with:\n$', ' '.join(command))
70                 try:
71                         subprocess.check_output(command, stderr=subprocess.STDOUT)
72                 except OSError as e:
73                         raise ConfigError('Could not call make command: ' + str(e))
74                 except subprocess.CalledProcessError as e:
75                         raise ConfigError(e.output.decode())
76
77         def make(self, jobs, build_dir: str, make_options) -> None:
78                 command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
79                 if make_options:
80                         command.extend(make_options)
81                 if self._cross_compile:
82                         command += ['CROSS_COMPILE=' + self._cross_compile]
83                 print('Building with:\n$', ' '.join(command))
84                 try:
85                         proc = subprocess.Popen(command,
86                                                 stderr=subprocess.PIPE,
87                                                 stdout=subprocess.DEVNULL)
88                 except OSError as e:
89                         raise BuildError('Could not call execute make: ' + str(e))
90                 except subprocess.CalledProcessError as e:
91                         raise BuildError(e.output)
92                 _, stderr = proc.communicate()
93                 if proc.returncode != 0:
94                         raise BuildError(stderr.decode())
95                 if stderr:  # likely only due to build warnings
96                         print(stderr.decode())
97
98         def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
99                 raise RuntimeError('not implemented!')
100
101
102 class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
103
104         def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
105                 super().__init__(linux_arch=qemu_arch_params.linux_arch,
106                                  cross_compile=cross_compile)
107                 self._kconfig = qemu_arch_params.kconfig
108                 self._qemu_arch = qemu_arch_params.qemu_arch
109                 self._kernel_path = qemu_arch_params.kernel_path
110                 self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
111                 self._extra_qemu_params = qemu_arch_params.extra_qemu_params
112
113         def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
114                 kconfig = kunit_config.parse_from_string(self._kconfig)
115                 kconfig.merge_in_entries(base_kunitconfig)
116                 return kconfig
117
118         def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
119                 kernel_path = os.path.join(build_dir, self._kernel_path)
120                 qemu_command = ['qemu-system-' + self._qemu_arch,
121                                 '-nodefaults',
122                                 '-m', '1024',
123                                 '-kernel', kernel_path,
124                                 '-append', ' '.join(params + [self._kernel_command_line]),
125                                 '-no-reboot',
126                                 '-nographic',
127                                 '-serial', 'stdio'] + self._extra_qemu_params
128                 # Note: shlex.join() does what we want, but requires python 3.8+.
129                 print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
130                 return subprocess.Popen(qemu_command,
131                                         stdin=subprocess.PIPE,
132                                         stdout=subprocess.PIPE,
133                                         stderr=subprocess.STDOUT,
134                                         text=True, errors='backslashreplace')
135
136 class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
137         """An abstraction over command line operations performed on a source tree."""
138
139         def __init__(self, cross_compile=None):
140                 super().__init__(linux_arch='um', cross_compile=cross_compile)
141
142         def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
143                 kconfig = kunit_config.parse_file(UML_KCONFIG_PATH)
144                 kconfig.merge_in_entries(base_kunitconfig)
145                 return kconfig
146
147         def make_allyesconfig(self, build_dir: str, make_options) -> None:
148                 stdout.print_with_timestamp(
149                         'Enabling all CONFIGs for UML...')
150                 command = ['make', 'ARCH=um', 'O=' + build_dir, 'allyesconfig']
151                 if make_options:
152                         command.extend(make_options)
153                 process = subprocess.Popen(
154                         command,
155                         stdout=subprocess.DEVNULL,
156                         stderr=subprocess.STDOUT)
157                 process.wait()
158                 stdout.print_with_timestamp(
159                         'Disabling broken configs to run KUnit tests...')
160
161                 with open(get_kconfig_path(build_dir), 'a') as config:
162                         with open(BROKEN_ALLCONFIG_PATH, 'r') as disable:
163                                 config.write(disable.read())
164                 stdout.print_with_timestamp(
165                         'Starting Kernel with all configs takes a few minutes...')
166
167         def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
168                 """Runs the Linux UML binary. Must be named 'linux'."""
169                 linux_bin = os.path.join(build_dir, 'linux')
170                 params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
171                 return subprocess.Popen([linux_bin] + params,
172                                            stdin=subprocess.PIPE,
173                                            stdout=subprocess.PIPE,
174                                            stderr=subprocess.STDOUT,
175                                            text=True, errors='backslashreplace')
176
177 def get_kconfig_path(build_dir: str) -> str:
178         return os.path.join(build_dir, KCONFIG_PATH)
179
180 def get_kunitconfig_path(build_dir: str) -> str:
181         return os.path.join(build_dir, KUNITCONFIG_PATH)
182
183 def get_old_kunitconfig_path(build_dir: str) -> str:
184         return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
185
186 def get_parsed_kunitconfig(build_dir: str,
187                            kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig:
188         if not kunitconfig_paths:
189                 path = get_kunitconfig_path(build_dir)
190                 if not os.path.exists(path):
191                         shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path)
192                 return kunit_config.parse_file(path)
193
194         merged = kunit_config.Kconfig()
195
196         for path in kunitconfig_paths:
197                 if os.path.isdir(path):
198                         path = os.path.join(path, KUNITCONFIG_PATH)
199                 if not os.path.exists(path):
200                         raise ConfigError(f'Specified kunitconfig ({path}) does not exist')
201
202                 partial = kunit_config.parse_file(path)
203                 diff = merged.conflicting_options(partial)
204                 if diff:
205                         diff_str = '\n\n'.join(f'{a}\n  vs from {path}\n{b}' for a, b in diff)
206                         raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}')
207                 merged.merge_in_entries(partial)
208         return merged
209
210 def get_outfile_path(build_dir: str) -> str:
211         return os.path.join(build_dir, OUTFILE_PATH)
212
213 def _default_qemu_config_path(arch: str) -> str:
214         config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
215         if os.path.isfile(config_path):
216                 return config_path
217
218         options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
219         raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
220
221 def _get_qemu_ops(config_path: str,
222                   extra_qemu_args: Optional[List[str]],
223                   cross_compile: Optional[str]) -> Tuple[str, LinuxSourceTreeOperations]:
224         # The module name/path has very little to do with where the actual file
225         # exists (I learned this through experimentation and could not find it
226         # anywhere in the Python documentation).
227         #
228         # Bascially, we completely ignore the actual file location of the config
229         # we are loading and just tell Python that the module lives in the
230         # QEMU_CONFIGS_DIR for import purposes regardless of where it actually
231         # exists as a file.
232         module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
233         spec = importlib.util.spec_from_file_location(module_path, config_path)
234         assert spec is not None
235         config = importlib.util.module_from_spec(spec)
236         # See https://github.com/python/typeshed/pull/2626 for context.
237         assert isinstance(spec.loader, importlib.abc.Loader)
238         spec.loader.exec_module(config)
239
240         if not hasattr(config, 'QEMU_ARCH'):
241                 raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
242         params: qemu_config.QemuArchParams = config.QEMU_ARCH  # type: ignore
243         if extra_qemu_args:
244                 params.extra_qemu_params.extend(extra_qemu_args)
245         return params.linux_arch, LinuxSourceTreeOperationsQemu(
246                         params, cross_compile=cross_compile)
247
248 class LinuxSourceTree:
249         """Represents a Linux kernel source tree with KUnit tests."""
250
251         def __init__(
252               self,
253               build_dir: str,
254               kunitconfig_paths: Optional[List[str]]=None,
255               kconfig_add: Optional[List[str]]=None,
256               arch=None,
257               cross_compile=None,
258               qemu_config_path=None,
259               extra_qemu_args=None) -> None:
260                 signal.signal(signal.SIGINT, self.signal_handler)
261                 if qemu_config_path:
262                         self._arch, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
263                 else:
264                         self._arch = 'um' if arch is None else arch
265                         if self._arch == 'um':
266                                 self._ops = LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
267                         else:
268                                 qemu_config_path = _default_qemu_config_path(self._arch)
269                                 _, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
270
271                 self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths)
272                 if kconfig_add:
273                         kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
274                         self._kconfig.merge_in_entries(kconfig)
275
276         def arch(self) -> str:
277                 return self._arch
278
279         def clean(self) -> bool:
280                 try:
281                         self._ops.make_mrproper()
282                 except ConfigError as e:
283                         logging.error(e)
284                         return False
285                 return True
286
287         def validate_config(self, build_dir: str) -> bool:
288                 kconfig_path = get_kconfig_path(build_dir)
289                 validated_kconfig = kunit_config.parse_file(kconfig_path)
290                 if self._kconfig.is_subset_of(validated_kconfig):
291                         return True
292                 missing = set(self._kconfig.as_entries()) - set(validated_kconfig.as_entries())
293                 message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
294                           'This is probably due to unsatisfied dependencies.\n' \
295                           'Missing: ' + ', '.join(str(e) for e in missing)
296                 if self._arch == 'um':
297                         message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
298                                    'on a different architecture with something like "--arch=x86_64".'
299                 logging.error(message)
300                 return False
301
302         def build_config(self, build_dir: str, make_options) -> bool:
303                 kconfig_path = get_kconfig_path(build_dir)
304                 if build_dir and not os.path.exists(build_dir):
305                         os.mkdir(build_dir)
306                 try:
307                         self._kconfig = self._ops.make_arch_config(self._kconfig)
308                         self._kconfig.write_to_file(kconfig_path)
309                         self._ops.make_olddefconfig(build_dir, make_options)
310                 except ConfigError as e:
311                         logging.error(e)
312                         return False
313                 if not self.validate_config(build_dir):
314                         return False
315
316                 old_path = get_old_kunitconfig_path(build_dir)
317                 if os.path.exists(old_path):
318                         os.remove(old_path)  # write_to_file appends to the file
319                 self._kconfig.write_to_file(old_path)
320                 return True
321
322         def _kunitconfig_changed(self, build_dir: str) -> bool:
323                 old_path = get_old_kunitconfig_path(build_dir)
324                 if not os.path.exists(old_path):
325                         return True
326
327                 old_kconfig = kunit_config.parse_file(old_path)
328                 return old_kconfig != self._kconfig
329
330         def build_reconfig(self, build_dir: str, make_options) -> bool:
331                 """Creates a new .config if it is not a subset of the .kunitconfig."""
332                 kconfig_path = get_kconfig_path(build_dir)
333                 if not os.path.exists(kconfig_path):
334                         print('Generating .config ...')
335                         return self.build_config(build_dir, make_options)
336
337                 existing_kconfig = kunit_config.parse_file(kconfig_path)
338                 self._kconfig = self._ops.make_arch_config(self._kconfig)
339
340                 if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
341                         return True
342                 print('Regenerating .config ...')
343                 os.remove(kconfig_path)
344                 return self.build_config(build_dir, make_options)
345
346         def build_kernel(self, alltests, jobs, build_dir: str, make_options) -> bool:
347                 try:
348                         if alltests:
349                                 self._ops.make_allyesconfig(build_dir, make_options)
350                         self._ops.make_olddefconfig(build_dir, make_options)
351                         self._ops.make(jobs, build_dir, make_options)
352                 except (ConfigError, BuildError) as e:
353                         logging.error(e)
354                         return False
355                 return self.validate_config(build_dir)
356
357         def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
358                 if not args:
359                         args = []
360                 if filter_glob:
361                         args.append('kunit.filter_glob='+filter_glob)
362
363                 process = self._ops.start(args, build_dir)
364                 assert process.stdout is not None  # tell mypy it's set
365
366                 # Enforce the timeout in a background thread.
367                 def _wait_proc():
368                         try:
369                                 process.wait(timeout=timeout)
370                         except Exception as e:
371                                 print(e)
372                                 process.terminate()
373                                 process.wait()
374                 waiter = threading.Thread(target=_wait_proc)
375                 waiter.start()
376
377                 output = open(get_outfile_path(build_dir), 'w')
378                 try:
379                         # Tee the output to the file and to our caller in real time.
380                         for line in process.stdout:
381                                 output.write(line)
382                                 yield line
383                 # This runs even if our caller doesn't consume every line.
384                 finally:
385                         # Flush any leftover output to the file
386                         output.write(process.stdout.read())
387                         output.close()
388                         process.stdout.close()
389
390                         waiter.join()
391                         subprocess.call(['stty', 'sane'])
392
393         def signal_handler(self, unused_sig, unused_frame) -> None:
394                 logging.error('Build interruption occurred. Cleaning console.')
395                 subprocess.call(['stty', 'sane'])