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