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