~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

TOMOYO Linux Cross Reference
Linux/tools/testing/kunit/kunit_kernel.py

Version: ~ [ linux-6.12-rc7 ] ~ [ linux-6.11.7 ] ~ [ linux-6.10.14 ] ~ [ linux-6.9.12 ] ~ [ linux-6.8.12 ] ~ [ linux-6.7.12 ] ~ [ linux-6.6.60 ] ~ [ linux-6.5.13 ] ~ [ linux-6.4.16 ] ~ [ linux-6.3.13 ] ~ [ linux-6.2.16 ] ~ [ linux-6.1.116 ] ~ [ linux-6.0.19 ] ~ [ linux-5.19.17 ] ~ [ linux-5.18.19 ] ~ [ linux-5.17.15 ] ~ [ linux-5.16.20 ] ~ [ linux-5.15.171 ] ~ [ linux-5.14.21 ] ~ [ linux-5.13.19 ] ~ [ linux-5.12.19 ] ~ [ linux-5.11.22 ] ~ [ linux-5.10.229 ] ~ [ linux-5.9.16 ] ~ [ linux-5.8.18 ] ~ [ linux-5.7.19 ] ~ [ linux-5.6.19 ] ~ [ linux-5.5.19 ] ~ [ linux-5.4.285 ] ~ [ linux-5.3.18 ] ~ [ linux-5.2.21 ] ~ [ linux-5.1.21 ] ~ [ linux-5.0.21 ] ~ [ linux-4.20.17 ] ~ [ linux-4.19.323 ] ~ [ linux-4.18.20 ] ~ [ linux-4.17.19 ] ~ [ linux-4.16.18 ] ~ [ linux-4.15.18 ] ~ [ linux-4.14.336 ] ~ [ linux-4.13.16 ] ~ [ linux-4.12.14 ] ~ [ linux-4.11.12 ] ~ [ linux-4.10.17 ] ~ [ linux-4.9.337 ] ~ [ linux-4.4.302 ] ~ [ linux-3.10.108 ] ~ [ linux-2.6.32.71 ] ~ [ linux-2.6.0 ] ~ [ linux-2.4.37.11 ] ~ [ unix-v6-master ] ~ [ ccs-tools-1.8.12 ] ~ [ policy-sample ] ~
Architecture: ~ [ i386 ] ~ [ alpha ] ~ [ m68k ] ~ [ mips ] ~ [ ppc ] ~ [ sparc ] ~ [ sparc64 ] ~

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

~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

kernel.org | git.kernel.org | LWN.net | Project Home | SVN repository | Mail admin

Linux® is a registered trademark of Linus Torvalds in the United States and other countries.
TOMOYO® is a registered trademark of NTT DATA CORPORATION.

sflogo.php