• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
9import importlib.abc
10import importlib.util
11import logging
12import subprocess
13import os
14import shutil
15import signal
16from typing import Iterator, Optional, Tuple
17
18from contextlib import ExitStack
19
20from collections import namedtuple
21
22import kunit_config
23import kunit_parser
24import qemu_config
25
26KCONFIG_PATH = '.config'
27KUNITCONFIG_PATH = '.kunitconfig'
28DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
29BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
30OUTFILE_PATH = 'test.log'
31ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
32QEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
33
34def get_file_path(build_dir, default):
35	if build_dir:
36		default = os.path.join(build_dir, default)
37	return default
38
39class ConfigError(Exception):
40	"""Represents an error trying to configure the Linux kernel."""
41
42
43class BuildError(Exception):
44	"""Represents an error trying to build the Linux kernel."""
45
46
47class 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
111class 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
151class 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
191def get_kconfig_path(build_dir) -> str:
192	return get_file_path(build_dir, KCONFIG_PATH)
193
194def get_kunitconfig_path(build_dir) -> str:
195	return get_file_path(build_dir, KUNITCONFIG_PATH)
196
197def get_outfile_path(build_dir) -> str:
198	return get_file_path(build_dir, OUTFILE_PATH)
199
200def 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
209def 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
230class 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'])
344