GNU Linux-libre 5.15.137-gnu
[releases.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 shutil
15 import signal
16 from typing import Iterator, Optional, Tuple
17
18 from contextlib import ExitStack
19
20 from collections import namedtuple
21
22 import kunit_config
23 import kunit_parser
24 import qemu_config
25
26 KCONFIG_PATH = '.config'
27 KUNITCONFIG_PATH = '.kunitconfig'
28 DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
29 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_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 def get_file_path(build_dir, default):
35         if build_dir:
36                 default = os.path.join(build_dir, default)
37         return default
38
39 class ConfigError(Exception):
40         """Represents an error trying to configure the Linux kernel."""
41
42
43 class BuildError(Exception):
44         """Represents an error trying to build the Linux kernel."""
45
46
47 class LinuxSourceTreeOperations(object):
48         """An abstraction over command line operations performed on a source tree."""
49
50         def __init__(self, linux_arch: str, cross_compile: Optional[str]):
51                 self._linux_arch = linux_arch
52                 self._cross_compile = cross_compile
53
54         def make_mrproper(self) -> None:
55                 try:
56                         subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
57                 except OSError as e:
58                         raise ConfigError('Could not call make command: ' + str(e))
59                 except subprocess.CalledProcessError as e:
60                         raise ConfigError(e.output.decode())
61
62         def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None:
63                 pass
64
65         def make_allyesconfig(self, build_dir, make_options) -> None:
66                 raise ConfigError('Only the "um" arch is supported for alltests')
67
68         def make_olddefconfig(self, build_dir, make_options) -> None:
69                 command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig']
70                 if self._cross_compile:
71                         command += ['CROSS_COMPILE=' + self._cross_compile]
72                 if make_options:
73                         command.extend(make_options)
74                 if build_dir:
75                         command += ['O=' + build_dir]
76                 print('Populating config with:\n$', ' '.join(command))
77                 try:
78                         subprocess.check_output(command, stderr=subprocess.STDOUT)
79                 except OSError as e:
80                         raise ConfigError('Could not call make command: ' + str(e))
81                 except subprocess.CalledProcessError as e:
82                         raise ConfigError(e.output.decode())
83
84         def make(self, jobs, build_dir, make_options) -> None:
85                 command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)]
86                 if make_options:
87                         command.extend(make_options)
88                 if self._cross_compile:
89                         command += ['CROSS_COMPILE=' + self._cross_compile]
90                 if build_dir:
91                         command += ['O=' + build_dir]
92                 print('Building with:\n$', ' '.join(command))
93                 try:
94                         proc = subprocess.Popen(command,
95                                                 stderr=subprocess.PIPE,
96                                                 stdout=subprocess.DEVNULL)
97                 except OSError as e:
98                         raise BuildError('Could not call execute make: ' + str(e))
99                 except subprocess.CalledProcessError as e:
100                         raise BuildError(e.output)
101                 _, stderr = proc.communicate()
102                 if proc.returncode != 0:
103                         raise BuildError(stderr.decode())
104                 if stderr:  # likely only due to build warnings
105                         print(stderr.decode())
106
107         def run(self, params, timeout, build_dir, outfile) -> None:
108                 pass
109
110
111 class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
112
113         def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
114                 super().__init__(linux_arch=qemu_arch_params.linux_arch,
115                                  cross_compile=cross_compile)
116                 self._kconfig = qemu_arch_params.kconfig
117                 self._qemu_arch = qemu_arch_params.qemu_arch
118                 self._kernel_path = qemu_arch_params.kernel_path
119                 self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
120                 self._extra_qemu_params = qemu_arch_params.extra_qemu_params
121
122         def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
123                 kconfig = kunit_config.Kconfig()
124                 kconfig.parse_from_string(self._kconfig)
125                 base_kunitconfig.merge_in_entries(kconfig)
126
127         def run(self, params, timeout, build_dir, outfile):
128                 kernel_path = os.path.join(build_dir, self._kernel_path)
129                 qemu_command = ['qemu-system-' + self._qemu_arch,
130                                 '-nodefaults',
131                                 '-m', '1024',
132                                 '-kernel', kernel_path,
133                                 '-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'',
134                                 '-no-reboot',
135                                 '-nographic',
136                                 '-serial stdio'] + self._extra_qemu_params
137                 print('Running tests with:\n$', ' '.join(qemu_command))
138                 with open(outfile, 'w') as output:
139                         process = subprocess.Popen(' '.join(qemu_command),
140                                                    stdin=subprocess.PIPE,
141                                                    stdout=output,
142                                                    stderr=subprocess.STDOUT,
143                                                    text=True, shell=True)
144                 try:
145                         process.wait(timeout=timeout)
146                 except Exception as e:
147                         print(e)
148                         process.terminate()
149                 return process
150
151 class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
152         """An abstraction over command line operations performed on a source tree."""
153
154         def __init__(self, cross_compile=None):
155                 super().__init__(linux_arch='um', cross_compile=cross_compile)
156
157         def make_allyesconfig(self, build_dir, make_options) -> None:
158                 kunit_parser.print_with_timestamp(
159                         'Enabling all CONFIGs for UML...')
160                 command = ['make', 'ARCH=um', 'allyesconfig']
161                 if make_options:
162                         command.extend(make_options)
163                 if build_dir:
164                         command += ['O=' + build_dir]
165                 process = subprocess.Popen(
166                         command,
167                         stdout=subprocess.DEVNULL,
168                         stderr=subprocess.STDOUT)
169                 process.wait()
170                 kunit_parser.print_with_timestamp(
171                         'Disabling broken configs to run KUnit tests...')
172                 with ExitStack() as es:
173                         config = open(get_kconfig_path(build_dir), 'a')
174                         disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
175                         config.write(disable)
176                 kunit_parser.print_with_timestamp(
177                         'Starting Kernel with all configs takes a few minutes...')
178
179         def run(self, params, timeout, build_dir, outfile):
180                 """Runs the Linux UML binary. Must be named 'linux'."""
181                 linux_bin = get_file_path(build_dir, 'linux')
182                 outfile = get_outfile_path(build_dir)
183                 with open(outfile, 'w') as output:
184                         process = subprocess.Popen([linux_bin] + params,
185                                                    stdin=subprocess.PIPE,
186                                                    stdout=output,
187                                                    stderr=subprocess.STDOUT,
188                                                    text=True)
189                         process.wait(timeout)
190
191 def get_kconfig_path(build_dir) -> str:
192         return get_file_path(build_dir, KCONFIG_PATH)
193
194 def get_kunitconfig_path(build_dir) -> str:
195         return get_file_path(build_dir, KUNITCONFIG_PATH)
196
197 def get_outfile_path(build_dir) -> str:
198         return get_file_path(build_dir, OUTFILE_PATH)
199
200 def get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations:
201         config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
202         if arch == 'um':
203                 return LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
204         elif os.path.isfile(config_path):
205                 return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1]
206         else:
207                 raise ConfigError(arch + ' is not a valid arch')
208
209 def get_source_tree_ops_from_qemu_config(config_path: str,
210                                          cross_compile: Optional[str]) -> Tuple[
211                                                          str, LinuxSourceTreeOperations]:
212         # The module name/path has very little to do with where the actual file
213         # exists (I learned this through experimentation and could not find it
214         # anywhere in the Python documentation).
215         #
216         # Bascially, we completely ignore the actual file location of the config
217         # we are loading and just tell Python that the module lives in the
218         # QEMU_CONFIGS_DIR for import purposes regardless of where it actually
219         # exists as a file.
220         module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
221         spec = importlib.util.spec_from_file_location(module_path, config_path)
222         config = importlib.util.module_from_spec(spec)
223         # TODO(brendanhiggins@google.com): I looked this up and apparently other
224         # Python projects have noted that pytype complains that "No attribute
225         # 'exec_module' on _importlib_modulespec._Loader". Disabling for now.
226         spec.loader.exec_module(config) # pytype: disable=attribute-error
227         return config.QEMU_ARCH.linux_arch, LinuxSourceTreeOperationsQemu(
228                         config.QEMU_ARCH, cross_compile=cross_compile)
229
230 class LinuxSourceTree(object):
231         """Represents a Linux kernel source tree with KUnit tests."""
232
233         def __init__(
234               self,
235               build_dir: str,
236               load_config=True,
237               kunitconfig_path='',
238               arch=None,
239               cross_compile=None,
240               qemu_config_path=None) -> None:
241                 signal.signal(signal.SIGINT, self.signal_handler)
242                 if qemu_config_path:
243                         self._arch, self._ops = get_source_tree_ops_from_qemu_config(
244                                         qemu_config_path, cross_compile)
245                 else:
246                         self._arch = 'um' if arch is None else arch
247                         self._ops = get_source_tree_ops(self._arch, cross_compile)
248
249                 if not load_config:
250                         return
251
252                 if kunitconfig_path:
253                         if os.path.isdir(kunitconfig_path):
254                                 kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH)
255                         if not os.path.exists(kunitconfig_path):
256                                 raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
257                 else:
258                         kunitconfig_path = get_kunitconfig_path(build_dir)
259                         if not os.path.exists(kunitconfig_path):
260                                 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
261
262                 self._kconfig = kunit_config.Kconfig()
263                 self._kconfig.read_from_file(kunitconfig_path)
264
265         def clean(self) -> bool:
266                 try:
267                         self._ops.make_mrproper()
268                 except ConfigError as e:
269                         logging.error(e)
270                         return False
271                 return True
272
273         def validate_config(self, build_dir) -> bool:
274                 kconfig_path = get_kconfig_path(build_dir)
275                 validated_kconfig = kunit_config.Kconfig()
276                 validated_kconfig.read_from_file(kconfig_path)
277                 if not self._kconfig.is_subset_of(validated_kconfig):
278                         invalid = self._kconfig.entries() - validated_kconfig.entries()
279                         message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
280                                           'but not in .config: %s' % (
281                                         ', '.join([str(e) for e in invalid])
282                         )
283                         logging.error(message)
284                         return False
285                 return True
286
287         def build_config(self, build_dir, make_options) -> bool:
288                 kconfig_path = get_kconfig_path(build_dir)
289                 if build_dir and not os.path.exists(build_dir):
290                         os.mkdir(build_dir)
291                 try:
292                         self._ops.make_arch_qemuconfig(self._kconfig)
293                         self._kconfig.write_to_file(kconfig_path)
294                         self._ops.make_olddefconfig(build_dir, make_options)
295                 except ConfigError as e:
296                         logging.error(e)
297                         return False
298                 return self.validate_config(build_dir)
299
300         def build_reconfig(self, build_dir, make_options) -> bool:
301                 """Creates a new .config if it is not a subset of the .kunitconfig."""
302                 kconfig_path = get_kconfig_path(build_dir)
303                 if os.path.exists(kconfig_path):
304                         existing_kconfig = kunit_config.Kconfig()
305                         existing_kconfig.read_from_file(kconfig_path)
306                         self._ops.make_arch_qemuconfig(self._kconfig)
307                         if not self._kconfig.is_subset_of(existing_kconfig):
308                                 print('Regenerating .config ...')
309                                 os.remove(kconfig_path)
310                                 return self.build_config(build_dir, make_options)
311                         else:
312                                 return True
313                 else:
314                         print('Generating .config ...')
315                         return self.build_config(build_dir, make_options)
316
317         def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
318                 try:
319                         if alltests:
320                                 self._ops.make_allyesconfig(build_dir, make_options)
321                         self._ops.make_olddefconfig(build_dir, make_options)
322                         self._ops.make(jobs, build_dir, make_options)
323                 except (ConfigError, BuildError) as e:
324                         logging.error(e)
325                         return False
326                 return self.validate_config(build_dir)
327
328         def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
329                 if not args:
330                         args = []
331                 args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
332                 if filter_glob:
333                         args.append('kunit.filter_glob='+filter_glob)
334                 outfile = get_outfile_path(build_dir)
335                 self._ops.run(args, timeout, build_dir, outfile)
336                 subprocess.call(['stty', 'sane'])
337                 with open(outfile, 'r') as file:
338                         for line in file:
339                                 yield line
340
341         def signal_handler(self, sig, frame) -> None:
342                 logging.error('Build interruption occurred. Cleaning console.')
343                 subprocess.call(['stty', 'sane'])