16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0
26ebf5866SFelix Guo#
36ebf5866SFelix Guo# Runs UML kernel, collects output, and handles errors.
46ebf5866SFelix Guo#
56ebf5866SFelix Guo# Copyright (C) 2019, Google LLC.
66ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com>
76ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com>
86ebf5866SFelix Guo
923552807SMichał Winiarskiimport importlib.abc
1087c9c163SBrendan Higginsimport importlib.util
116ebf5866SFelix Guoimport logging
126ebf5866SFelix Guoimport subprocess
136ebf5866SFelix Guoimport os
14fcdb0bc0SAndy Shevchenkoimport shutil
15021ed9f5SHeidi Fahimimport signal
167d7c48dfSDaniel Latypovimport threading
177d7c48dfSDaniel Latypovfrom typing import Iterator, List, Optional, Tuple
18021ed9f5SHeidi Fahim
196ebf5866SFelix Guoimport kunit_config
20021ed9f5SHeidi Fahimimport kunit_parser
2187c9c163SBrendan Higginsimport qemu_config
226ebf5866SFelix Guo
236ebf5866SFelix GuoKCONFIG_PATH = '.config'
24fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
254c2911f1SDaniel LatypovOLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
26d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
27021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
28128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
2987c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
3087c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
316ebf5866SFelix Guo
326ebf5866SFelix Guoclass ConfigError(Exception):
336ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
346ebf5866SFelix Guo
356ebf5866SFelix Guo
366ebf5866SFelix Guoclass BuildError(Exception):
376ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
386ebf5866SFelix Guo
396ebf5866SFelix Guo
406ebf5866SFelix Guoclass LinuxSourceTreeOperations(object):
416ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
426ebf5866SFelix Guo
4387c9c163SBrendan Higgins	def __init__(self, linux_arch: str, cross_compile: Optional[str]):
4487c9c163SBrendan Higgins		self._linux_arch = linux_arch
4587c9c163SBrendan Higgins		self._cross_compile = cross_compile
4687c9c163SBrendan Higgins
4709641f7cSDaniel Latypov	def make_mrproper(self) -> None:
486ebf5866SFelix Guo		try:
495a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
506ebf5866SFelix Guo		except OSError as e:
511abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
526ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
531abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
546ebf5866SFelix Guo
5587c9c163SBrendan Higgins	def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None:
5687c9c163SBrendan Higgins		pass
5787c9c163SBrendan Higgins
58aa1c0555SDaniel Latypov	def make_allyesconfig(self, build_dir: str, make_options) -> None:
5987c9c163SBrendan Higgins		raise ConfigError('Only the "um" arch is supported for alltests')
6087c9c163SBrendan Higgins
61aa1c0555SDaniel Latypov	def make_olddefconfig(self, build_dir: str, make_options) -> None:
62aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
6387c9c163SBrendan Higgins		if self._cross_compile:
6487c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
650476e69fSGreg Thelen		if make_options:
660476e69fSGreg Thelen			command.extend(make_options)
6787c9c163SBrendan Higgins		print('Populating config with:\n$', ' '.join(command))
686ebf5866SFelix Guo		try:
695a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
706ebf5866SFelix Guo		except OSError as e:
711abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
726ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
731abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
746ebf5866SFelix Guo
75aa1c0555SDaniel Latypov	def make(self, jobs, build_dir: str, make_options) -> None:
76aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
7787c9c163SBrendan Higgins		if make_options:
7887c9c163SBrendan Higgins			command.extend(make_options)
7987c9c163SBrendan Higgins		if self._cross_compile:
8087c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
8187c9c163SBrendan Higgins		print('Building with:\n$', ' '.join(command))
8287c9c163SBrendan Higgins		try:
8387c9c163SBrendan Higgins			proc = subprocess.Popen(command,
8487c9c163SBrendan Higgins						stderr=subprocess.PIPE,
8587c9c163SBrendan Higgins						stdout=subprocess.DEVNULL)
8687c9c163SBrendan Higgins		except OSError as e:
8787c9c163SBrendan Higgins			raise BuildError('Could not call execute make: ' + str(e))
8887c9c163SBrendan Higgins		except subprocess.CalledProcessError as e:
8987c9c163SBrendan Higgins			raise BuildError(e.output)
9087c9c163SBrendan Higgins		_, stderr = proc.communicate()
9187c9c163SBrendan Higgins		if proc.returncode != 0:
9287c9c163SBrendan Higgins			raise BuildError(stderr.decode())
9387c9c163SBrendan Higgins		if stderr:  # likely only due to build warnings
9487c9c163SBrendan Higgins			print(stderr.decode())
9587c9c163SBrendan Higgins
967d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
977d7c48dfSDaniel Latypov		raise RuntimeError('not implemented!')
9887c9c163SBrendan Higgins
9987c9c163SBrendan Higgins
10087c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
10187c9c163SBrendan Higgins
10287c9c163SBrendan Higgins	def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
10387c9c163SBrendan Higgins		super().__init__(linux_arch=qemu_arch_params.linux_arch,
10487c9c163SBrendan Higgins				 cross_compile=cross_compile)
10587c9c163SBrendan Higgins		self._kconfig = qemu_arch_params.kconfig
10687c9c163SBrendan Higgins		self._qemu_arch = qemu_arch_params.qemu_arch
10787c9c163SBrendan Higgins		self._kernel_path = qemu_arch_params.kernel_path
10887c9c163SBrendan Higgins		self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
10987c9c163SBrendan Higgins		self._extra_qemu_params = qemu_arch_params.extra_qemu_params
11087c9c163SBrendan Higgins
11187c9c163SBrendan Higgins	def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
11298978490SDaniel Latypov		kconfig = kunit_config.parse_from_string(self._kconfig)
11387c9c163SBrendan Higgins		base_kunitconfig.merge_in_entries(kconfig)
11487c9c163SBrendan Higgins
1157d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
11687c9c163SBrendan Higgins		kernel_path = os.path.join(build_dir, self._kernel_path)
11787c9c163SBrendan Higgins		qemu_command = ['qemu-system-' + self._qemu_arch,
11887c9c163SBrendan Higgins				'-nodefaults',
11987c9c163SBrendan Higgins				'-m', '1024',
12087c9c163SBrendan Higgins				'-kernel', kernel_path,
12187c9c163SBrendan Higgins				'-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'',
12287c9c163SBrendan Higgins				'-no-reboot',
12387c9c163SBrendan Higgins				'-nographic',
12487c9c163SBrendan Higgins				'-serial stdio'] + self._extra_qemu_params
12587c9c163SBrendan Higgins		print('Running tests with:\n$', ' '.join(qemu_command))
1267d7c48dfSDaniel Latypov		return subprocess.Popen(' '.join(qemu_command),
12787c9c163SBrendan Higgins					   stdin=subprocess.PIPE,
1287d7c48dfSDaniel Latypov					   stdout=subprocess.PIPE,
12987c9c163SBrendan Higgins					   stderr=subprocess.STDOUT,
1302ab5d5e6SDaniel Latypov					   text=True, shell=True, errors='backslashreplace')
13187c9c163SBrendan Higgins
13287c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
13387c9c163SBrendan Higgins	"""An abstraction over command line operations performed on a source tree."""
13487c9c163SBrendan Higgins
13587c9c163SBrendan Higgins	def __init__(self, cross_compile=None):
13687c9c163SBrendan Higgins		super().__init__(linux_arch='um', cross_compile=cross_compile)
13787c9c163SBrendan Higgins
138aa1c0555SDaniel Latypov	def make_allyesconfig(self, build_dir: str, make_options) -> None:
139021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
140021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
141aa1c0555SDaniel Latypov		command = ['make', 'ARCH=um', 'O=' + build_dir, 'allyesconfig']
14267e2fae3SBrendan Higgins		if make_options:
14367e2fae3SBrendan Higgins			command.extend(make_options)
144021ed9f5SHeidi Fahim		process = subprocess.Popen(
14567e2fae3SBrendan Higgins			command,
146021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
147021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
148021ed9f5SHeidi Fahim		process.wait()
149021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
150021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
151a54ea2e0SDaniel Latypov
152a54ea2e0SDaniel Latypov		with open(get_kconfig_path(build_dir), 'a') as config:
153a54ea2e0SDaniel Latypov			with open(BROKEN_ALLCONFIG_PATH, 'r') as disable:
154a54ea2e0SDaniel Latypov				config.write(disable.read())
155021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
156021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
157021ed9f5SHeidi Fahim
1587d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
1596ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
160aa1c0555SDaniel Latypov		linux_bin = os.path.join(build_dir, 'linux')
1617d7c48dfSDaniel Latypov		return subprocess.Popen([linux_bin] + params,
16287c9c163SBrendan Higgins					   stdin=subprocess.PIPE,
1637d7c48dfSDaniel Latypov					   stdout=subprocess.PIPE,
16487c9c163SBrendan Higgins					   stderr=subprocess.STDOUT,
1652ab5d5e6SDaniel Latypov					   text=True, errors='backslashreplace')
1666ebf5866SFelix Guo
167aa1c0555SDaniel Latypovdef get_kconfig_path(build_dir: str) -> str:
168aa1c0555SDaniel Latypov	return os.path.join(build_dir, KCONFIG_PATH)
1696ebf5866SFelix Guo
170aa1c0555SDaniel Latypovdef get_kunitconfig_path(build_dir: str) -> str:
171aa1c0555SDaniel Latypov	return os.path.join(build_dir, KUNITCONFIG_PATH)
172fcdb0bc0SAndy Shevchenko
173aa1c0555SDaniel Latypovdef get_old_kunitconfig_path(build_dir: str) -> str:
174aa1c0555SDaniel Latypov	return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
1754c2911f1SDaniel Latypov
176aa1c0555SDaniel Latypovdef get_outfile_path(build_dir: str) -> str:
177aa1c0555SDaniel Latypov	return os.path.join(build_dir, OUTFILE_PATH)
178128dc4bcSAndy Shevchenko
17987c9c163SBrendan Higginsdef get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations:
18087c9c163SBrendan Higgins	config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
18187c9c163SBrendan Higgins	if arch == 'um':
18287c9c163SBrendan Higgins		return LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
18387c9c163SBrendan Higgins	elif os.path.isfile(config_path):
18487c9c163SBrendan Higgins		return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1]
185fe678fedSDaniel Latypov
186fe678fedSDaniel Latypov	options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
187fe678fedSDaniel Latypov	raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
18887c9c163SBrendan Higgins
18987c9c163SBrendan Higginsdef get_source_tree_ops_from_qemu_config(config_path: str,
19058c965d8SDaniel Latypov					 cross_compile: Optional[str]) -> Tuple[
19187c9c163SBrendan Higgins							 str, LinuxSourceTreeOperations]:
19287c9c163SBrendan Higgins	# The module name/path has very little to do with where the actual file
19387c9c163SBrendan Higgins	# exists (I learned this through experimentation and could not find it
19487c9c163SBrendan Higgins	# anywhere in the Python documentation).
19587c9c163SBrendan Higgins	#
19687c9c163SBrendan Higgins	# Bascially, we completely ignore the actual file location of the config
19787c9c163SBrendan Higgins	# we are loading and just tell Python that the module lives in the
19887c9c163SBrendan Higgins	# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
19987c9c163SBrendan Higgins	# exists as a file.
20087c9c163SBrendan Higgins	module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
20187c9c163SBrendan Higgins	spec = importlib.util.spec_from_file_location(module_path, config_path)
20285310a62SDaniel Latypov	assert spec is not None
20387c9c163SBrendan Higgins	config = importlib.util.module_from_spec(spec)
20452a5d80aSDaniel Latypov	# See https://github.com/python/typeshed/pull/2626 for context.
20552a5d80aSDaniel Latypov	assert isinstance(spec.loader, importlib.abc.Loader)
20652a5d80aSDaniel Latypov	spec.loader.exec_module(config)
20752a5d80aSDaniel Latypov
20852a5d80aSDaniel Latypov	if not hasattr(config, 'QEMU_ARCH'):
20952a5d80aSDaniel Latypov		raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
21052a5d80aSDaniel Latypov	params: qemu_config.QemuArchParams = config.QEMU_ARCH  # type: ignore
21152a5d80aSDaniel Latypov	return params.linux_arch, LinuxSourceTreeOperationsQemu(
21252a5d80aSDaniel Latypov			params, cross_compile=cross_compile)
21387c9c163SBrendan Higgins
2146ebf5866SFelix Guoclass LinuxSourceTree(object):
2156ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
2166ebf5866SFelix Guo
21787c9c163SBrendan Higgins	def __init__(
21887c9c163SBrendan Higgins	      self,
21987c9c163SBrendan Higgins	      build_dir: str,
22087c9c163SBrendan Higgins	      load_config=True,
22187c9c163SBrendan Higgins	      kunitconfig_path='',
2229f57cc76SDaniel Latypov	      kconfig_add: Optional[List[str]]=None,
22387c9c163SBrendan Higgins	      arch=None,
22487c9c163SBrendan Higgins	      cross_compile=None,
22587c9c163SBrendan Higgins	      qemu_config_path=None) -> None:
226021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
22787c9c163SBrendan Higgins		if qemu_config_path:
22887c9c163SBrendan Higgins			self._arch, self._ops = get_source_tree_ops_from_qemu_config(
22987c9c163SBrendan Higgins					qemu_config_path, cross_compile)
23087c9c163SBrendan Higgins		else:
23187c9c163SBrendan Higgins			self._arch = 'um' if arch is None else arch
23287c9c163SBrendan Higgins			self._ops = get_source_tree_ops(self._arch, cross_compile)
2332b8fdbbfSDaniel Latypov
2342b8fdbbfSDaniel Latypov		if not load_config:
2352b8fdbbfSDaniel Latypov			return
2362b8fdbbfSDaniel Latypov
237243180f5SDaniel Latypov		if kunitconfig_path:
2389854781dSDaniel Latypov			if os.path.isdir(kunitconfig_path):
2399854781dSDaniel Latypov				kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH)
240243180f5SDaniel Latypov			if not os.path.exists(kunitconfig_path):
241243180f5SDaniel Latypov				raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
242243180f5SDaniel Latypov		else:
2432b8fdbbfSDaniel Latypov			kunitconfig_path = get_kunitconfig_path(build_dir)
2442b8fdbbfSDaniel Latypov			if not os.path.exists(kunitconfig_path):
245243180f5SDaniel Latypov				shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
2462b8fdbbfSDaniel Latypov
24798978490SDaniel Latypov		self._kconfig = kunit_config.parse_file(kunitconfig_path)
2489f57cc76SDaniel Latypov		if kconfig_add:
2499f57cc76SDaniel Latypov			kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
2509f57cc76SDaniel Latypov			self._kconfig.merge_in_entries(kconfig)
2519f57cc76SDaniel Latypov
252*885210d3SDaniel Latypov	def arch(self) -> str:
253*885210d3SDaniel Latypov		return self._arch
2542b8fdbbfSDaniel Latypov
25509641f7cSDaniel Latypov	def clean(self) -> bool:
2566ebf5866SFelix Guo		try:
2576ebf5866SFelix Guo			self._ops.make_mrproper()
2586ebf5866SFelix Guo		except ConfigError as e:
2596ebf5866SFelix Guo			logging.error(e)
2606ebf5866SFelix Guo			return False
2616ebf5866SFelix Guo		return True
2626ebf5866SFelix Guo
263aa1c0555SDaniel Latypov	def validate_config(self, build_dir: str) -> bool:
264dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
26598978490SDaniel Latypov		validated_kconfig = kunit_config.parse_file(kconfig_path)
266c44895b6SDaniel Latypov		if self._kconfig.is_subset_of(validated_kconfig):
267c44895b6SDaniel Latypov			return True
268dde54b94SHeidi Fahim		invalid = self._kconfig.entries() - validated_kconfig.entries()
269c44895b6SDaniel Latypov		message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
270c44895b6SDaniel Latypov			  'This is probably due to unsatisfied dependencies.\n' \
271c44895b6SDaniel Latypov			  'Missing: ' + ', '.join([str(e) for e in invalid])
272c44895b6SDaniel Latypov		if self._arch == 'um':
273c44895b6SDaniel Latypov			message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
274c44895b6SDaniel Latypov				   'on a different architecture with something like "--arch=x86_64".'
275dde54b94SHeidi Fahim		logging.error(message)
276dde54b94SHeidi Fahim		return False
277dde54b94SHeidi Fahim
278aa1c0555SDaniel Latypov	def build_config(self, build_dir: str, make_options) -> bool:
2796ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
2806ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
2816ebf5866SFelix Guo			os.mkdir(build_dir)
2826ebf5866SFelix Guo		try:
28387c9c163SBrendan Higgins			self._ops.make_arch_qemuconfig(self._kconfig)
28487c9c163SBrendan Higgins			self._kconfig.write_to_file(kconfig_path)
2850476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
2866ebf5866SFelix Guo		except ConfigError as e:
2876ebf5866SFelix Guo			logging.error(e)
2886ebf5866SFelix Guo			return False
2894c2911f1SDaniel Latypov		if not self.validate_config(build_dir):
2904c2911f1SDaniel Latypov			return False
2914c2911f1SDaniel Latypov
2924c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
2934c2911f1SDaniel Latypov		if os.path.exists(old_path):
2944c2911f1SDaniel Latypov			os.remove(old_path)  # write_to_file appends to the file
2954c2911f1SDaniel Latypov		self._kconfig.write_to_file(old_path)
2964c2911f1SDaniel Latypov		return True
2974c2911f1SDaniel Latypov
2984c2911f1SDaniel Latypov	def _kunitconfig_changed(self, build_dir: str) -> bool:
2994c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
3004c2911f1SDaniel Latypov		if not os.path.exists(old_path):
3014c2911f1SDaniel Latypov			return True
3024c2911f1SDaniel Latypov
3034c2911f1SDaniel Latypov		old_kconfig = kunit_config.parse_file(old_path)
3044c2911f1SDaniel Latypov		return old_kconfig.entries() != self._kconfig.entries()
3056ebf5866SFelix Guo
306aa1c0555SDaniel Latypov	def build_reconfig(self, build_dir: str, make_options) -> bool:
30714ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
3086ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
3094c2911f1SDaniel Latypov		if not os.path.exists(kconfig_path):
3104c2911f1SDaniel Latypov			print('Generating .config ...')
3114c2911f1SDaniel Latypov			return self.build_config(build_dir, make_options)
3124c2911f1SDaniel Latypov
31398978490SDaniel Latypov		existing_kconfig = kunit_config.parse_file(kconfig_path)
31487c9c163SBrendan Higgins		self._ops.make_arch_qemuconfig(self._kconfig)
3154c2911f1SDaniel Latypov		if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
3164c2911f1SDaniel Latypov			return True
3176ebf5866SFelix Guo		print('Regenerating .config ...')
3186ebf5866SFelix Guo		os.remove(kconfig_path)
3190476e69fSGreg Thelen		return self.build_config(build_dir, make_options)
3206ebf5866SFelix Guo
321aa1c0555SDaniel Latypov	def build_kernel(self, alltests, jobs, build_dir: str, make_options) -> bool:
3226ebf5866SFelix Guo		try:
32367e2fae3SBrendan Higgins			if alltests:
32467e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
3250476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
3260476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
3276ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
3286ebf5866SFelix Guo			logging.error(e)
3296ebf5866SFelix Guo			return False
330dde54b94SHeidi Fahim		return self.validate_config(build_dir)
3316ebf5866SFelix Guo
3327af29141SDaniel Latypov	def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
3337af29141SDaniel Latypov		if not args:
3347af29141SDaniel Latypov			args = []
335b6d5799bSDavid Gow		args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
336d992880bSDaniel Latypov		if filter_glob:
337d992880bSDaniel Latypov			args.append('kunit.filter_glob='+filter_glob)
3387d7c48dfSDaniel Latypov
3397d7c48dfSDaniel Latypov		process = self._ops.start(args, build_dir)
3407d7c48dfSDaniel Latypov		assert process.stdout is not None  # tell mypy it's set
3417d7c48dfSDaniel Latypov
3427d7c48dfSDaniel Latypov		# Enforce the timeout in a background thread.
3437d7c48dfSDaniel Latypov		def _wait_proc():
3447d7c48dfSDaniel Latypov			try:
3457d7c48dfSDaniel Latypov				process.wait(timeout=timeout)
3467d7c48dfSDaniel Latypov			except Exception as e:
3477d7c48dfSDaniel Latypov				print(e)
3487d7c48dfSDaniel Latypov				process.terminate()
3497d7c48dfSDaniel Latypov				process.wait()
3507d7c48dfSDaniel Latypov		waiter = threading.Thread(target=_wait_proc)
3517d7c48dfSDaniel Latypov		waiter.start()
3527d7c48dfSDaniel Latypov
3537d7c48dfSDaniel Latypov		output = open(get_outfile_path(build_dir), 'w')
3547d7c48dfSDaniel Latypov		try:
3557d7c48dfSDaniel Latypov			# Tee the output to the file and to our caller in real time.
3567d7c48dfSDaniel Latypov			for line in process.stdout:
3577d7c48dfSDaniel Latypov				output.write(line)
358021ed9f5SHeidi Fahim				yield line
3597d7c48dfSDaniel Latypov		# This runs even if our caller doesn't consume every line.
3607d7c48dfSDaniel Latypov		finally:
3617d7c48dfSDaniel Latypov			# Flush any leftover output to the file
3627d7c48dfSDaniel Latypov			output.write(process.stdout.read())
3637d7c48dfSDaniel Latypov			output.close()
3647d7c48dfSDaniel Latypov			process.stdout.close()
3657d7c48dfSDaniel Latypov
3667d7c48dfSDaniel Latypov			waiter.join()
3677d7c48dfSDaniel Latypov			subprocess.call(['stty', 'sane'])
368021ed9f5SHeidi Fahim
36909641f7cSDaniel Latypov	def signal_handler(self, sig, frame) -> None:
370021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
371021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
372