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
143f0a50f3SDaniel Latypovimport shlex
15fcdb0bc0SAndy Shevchenkoimport shutil
16021ed9f5SHeidi Fahimimport signal
177d7c48dfSDaniel Latypovimport threading
187d7c48dfSDaniel Latypovfrom typing import Iterator, List, Optional, Tuple
191da2e622SDaniel Latypovfrom types import FrameType
20021ed9f5SHeidi Fahim
216ebf5866SFelix Guoimport kunit_config
2287c9c163SBrendan Higginsimport qemu_config
236ebf5866SFelix Guo
246ebf5866SFelix GuoKCONFIG_PATH = '.config'
25fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
264c2911f1SDaniel LatypovOLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
27d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
28980ac3adSDaniel LatypovALL_TESTS_CONFIG_PATH = 'tools/testing/kunit/configs/all_tests.config'
296fc3a863SDavid GowUML_KCONFIG_PATH = 'tools/testing/kunit/configs/arch_uml.config'
30128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
3187c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
3287c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
336ebf5866SFelix Guo
346ebf5866SFelix Guoclass ConfigError(Exception):
356ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
366ebf5866SFelix Guo
376ebf5866SFelix Guo
386ebf5866SFelix Guoclass BuildError(Exception):
396ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
406ebf5866SFelix Guo
416ebf5866SFelix Guo
420453f984SDaniel Latypovclass LinuxSourceTreeOperations:
436ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
446ebf5866SFelix Guo
4587c9c163SBrendan Higgins	def __init__(self, linux_arch: str, cross_compile: Optional[str]):
4687c9c163SBrendan Higgins		self._linux_arch = linux_arch
4787c9c163SBrendan Higgins		self._cross_compile = cross_compile
4887c9c163SBrendan Higgins
4909641f7cSDaniel Latypov	def make_mrproper(self) -> None:
506ebf5866SFelix Guo		try:
515a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
526ebf5866SFelix Guo		except OSError as e:
531abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
546ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
551abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
566ebf5866SFelix Guo
576fc3a863SDavid Gow	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
588a7c6f85SDaniel Latypov		return base_kunitconfig
5987c9c163SBrendan Higgins
601da2e622SDaniel Latypov	def make_olddefconfig(self, build_dir: str, make_options: Optional[List[str]]) -> None:
61aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
6287c9c163SBrendan Higgins		if self._cross_compile:
6387c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
640476e69fSGreg Thelen		if make_options:
650476e69fSGreg Thelen			command.extend(make_options)
6687c9c163SBrendan Higgins		print('Populating config with:\n$', ' '.join(command))
676ebf5866SFelix Guo		try:
685a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
696ebf5866SFelix Guo		except OSError as e:
701abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
716ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
721abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
736ebf5866SFelix Guo
741da2e622SDaniel Latypov	def make(self, jobs: int, build_dir: str, make_options: Optional[List[str]]) -> None:
75aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
7687c9c163SBrendan Higgins		if make_options:
7787c9c163SBrendan Higgins			command.extend(make_options)
7887c9c163SBrendan Higgins		if self._cross_compile:
7987c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
8087c9c163SBrendan Higgins		print('Building with:\n$', ' '.join(command))
8187c9c163SBrendan Higgins		try:
8287c9c163SBrendan Higgins			proc = subprocess.Popen(command,
8387c9c163SBrendan Higgins						stderr=subprocess.PIPE,
8487c9c163SBrendan Higgins						stdout=subprocess.DEVNULL)
8587c9c163SBrendan Higgins		except OSError as e:
8687c9c163SBrendan Higgins			raise BuildError('Could not call execute make: ' + str(e))
8787c9c163SBrendan Higgins		except subprocess.CalledProcessError as e:
8887c9c163SBrendan Higgins			raise BuildError(e.output)
8987c9c163SBrendan Higgins		_, stderr = proc.communicate()
9087c9c163SBrendan Higgins		if proc.returncode != 0:
9187c9c163SBrendan Higgins			raise BuildError(stderr.decode())
9287c9c163SBrendan Higgins		if stderr:  # likely only due to build warnings
9387c9c163SBrendan Higgins			print(stderr.decode())
9487c9c163SBrendan Higgins
95e30f65c4SDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
967d7c48dfSDaniel Latypov		raise RuntimeError('not implemented!')
9787c9c163SBrendan Higgins
9887c9c163SBrendan Higgins
9987c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
10087c9c163SBrendan Higgins
10187c9c163SBrendan Higgins	def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
10287c9c163SBrendan Higgins		super().__init__(linux_arch=qemu_arch_params.linux_arch,
10387c9c163SBrendan Higgins				 cross_compile=cross_compile)
10487c9c163SBrendan Higgins		self._kconfig = qemu_arch_params.kconfig
10587c9c163SBrendan Higgins		self._qemu_arch = qemu_arch_params.qemu_arch
10687c9c163SBrendan Higgins		self._kernel_path = qemu_arch_params.kernel_path
10787c9c163SBrendan Higgins		self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
10887c9c163SBrendan Higgins		self._extra_qemu_params = qemu_arch_params.extra_qemu_params
1095ffb8629SGeert Uytterhoeven		self._serial = qemu_arch_params.serial
11087c9c163SBrendan Higgins
1116fc3a863SDavid Gow	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
11298978490SDaniel Latypov		kconfig = kunit_config.parse_from_string(self._kconfig)
1138a7c6f85SDaniel Latypov		kconfig.merge_in_entries(base_kunitconfig)
1148a7c6f85SDaniel Latypov		return kconfig
11587c9c163SBrendan Higgins
116e30f65c4SDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
11787c9c163SBrendan Higgins		kernel_path = os.path.join(build_dir, self._kernel_path)
11887c9c163SBrendan Higgins		qemu_command = ['qemu-system-' + self._qemu_arch,
11987c9c163SBrendan Higgins				'-nodefaults',
12087c9c163SBrendan Higgins				'-m', '1024',
12187c9c163SBrendan Higgins				'-kernel', kernel_path,
1223f0a50f3SDaniel Latypov				'-append', ' '.join(params + [self._kernel_command_line]),
12387c9c163SBrendan Higgins				'-no-reboot',
12487c9c163SBrendan Higgins				'-nographic',
1255ffb8629SGeert Uytterhoeven				'-serial', self._serial] + self._extra_qemu_params
1263f0a50f3SDaniel Latypov		# Note: shlex.join() does what we want, but requires python 3.8+.
1273f0a50f3SDaniel Latypov		print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
1283f0a50f3SDaniel Latypov		return subprocess.Popen(qemu_command,
12987c9c163SBrendan Higgins					stdin=subprocess.PIPE,
1307d7c48dfSDaniel Latypov					stdout=subprocess.PIPE,
13187c9c163SBrendan Higgins					stderr=subprocess.STDOUT,
1323f0a50f3SDaniel Latypov					text=True, errors='backslashreplace')
13387c9c163SBrendan Higgins
13487c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
13587c9c163SBrendan Higgins	"""An abstraction over command line operations performed on a source tree."""
13687c9c163SBrendan Higgins
1371da2e622SDaniel Latypov	def __init__(self, cross_compile: Optional[str]=None):
13887c9c163SBrendan Higgins		super().__init__(linux_arch='um', cross_compile=cross_compile)
13987c9c163SBrendan Higgins
1406fc3a863SDavid Gow	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
1416fc3a863SDavid Gow		kconfig = kunit_config.parse_file(UML_KCONFIG_PATH)
1426fc3a863SDavid Gow		kconfig.merge_in_entries(base_kunitconfig)
1436fc3a863SDavid Gow		return kconfig
1446fc3a863SDavid Gow
145e30f65c4SDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
1466ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
147aa1c0555SDaniel Latypov		linux_bin = os.path.join(build_dir, 'linux')
1489241bc81SDaniel Latypov		params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
1497d7c48dfSDaniel Latypov		return subprocess.Popen([linux_bin] + params,
15087c9c163SBrendan Higgins					   stdin=subprocess.PIPE,
1517d7c48dfSDaniel Latypov					   stdout=subprocess.PIPE,
15287c9c163SBrendan Higgins					   stderr=subprocess.STDOUT,
1532ab5d5e6SDaniel Latypov					   text=True, errors='backslashreplace')
1546ebf5866SFelix Guo
155aa1c0555SDaniel Latypovdef get_kconfig_path(build_dir: str) -> str:
156aa1c0555SDaniel Latypov	return os.path.join(build_dir, KCONFIG_PATH)
1576ebf5866SFelix Guo
158aa1c0555SDaniel Latypovdef get_kunitconfig_path(build_dir: str) -> str:
159aa1c0555SDaniel Latypov	return os.path.join(build_dir, KUNITCONFIG_PATH)
160fcdb0bc0SAndy Shevchenko
161aa1c0555SDaniel Latypovdef get_old_kunitconfig_path(build_dir: str) -> str:
162aa1c0555SDaniel Latypov	return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
1634c2911f1SDaniel Latypov
16453b46621SDaniel Latypovdef get_parsed_kunitconfig(build_dir: str,
16553b46621SDaniel Latypov			   kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig:
16653b46621SDaniel Latypov	if not kunitconfig_paths:
16753b46621SDaniel Latypov		path = get_kunitconfig_path(build_dir)
16853b46621SDaniel Latypov		if not os.path.exists(path):
16953b46621SDaniel Latypov			shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path)
17053b46621SDaniel Latypov		return kunit_config.parse_file(path)
17153b46621SDaniel Latypov
17253b46621SDaniel Latypov	merged = kunit_config.Kconfig()
17353b46621SDaniel Latypov
17453b46621SDaniel Latypov	for path in kunitconfig_paths:
17553b46621SDaniel Latypov		if os.path.isdir(path):
17653b46621SDaniel Latypov			path = os.path.join(path, KUNITCONFIG_PATH)
17753b46621SDaniel Latypov		if not os.path.exists(path):
17853b46621SDaniel Latypov			raise ConfigError(f'Specified kunitconfig ({path}) does not exist')
17953b46621SDaniel Latypov
18053b46621SDaniel Latypov		partial = kunit_config.parse_file(path)
18153b46621SDaniel Latypov		diff = merged.conflicting_options(partial)
18253b46621SDaniel Latypov		if diff:
18353b46621SDaniel Latypov			diff_str = '\n\n'.join(f'{a}\n  vs from {path}\n{b}' for a, b in diff)
18453b46621SDaniel Latypov			raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}')
18553b46621SDaniel Latypov		merged.merge_in_entries(partial)
18653b46621SDaniel Latypov	return merged
18753b46621SDaniel Latypov
188aa1c0555SDaniel Latypovdef get_outfile_path(build_dir: str) -> str:
189aa1c0555SDaniel Latypov	return os.path.join(build_dir, OUTFILE_PATH)
190128dc4bcSAndy Shevchenko
1918c278d97SDaniel Latypovdef _default_qemu_config_path(arch: str) -> str:
19287c9c163SBrendan Higgins	config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
1930453f984SDaniel Latypov	if os.path.isfile(config_path):
1948c278d97SDaniel Latypov		return config_path
195fe678fedSDaniel Latypov
196fe678fedSDaniel Latypov	options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
197fe678fedSDaniel Latypov	raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
19887c9c163SBrendan Higgins
1998c278d97SDaniel Latypovdef _get_qemu_ops(config_path: str,
200a9333bd3SDaniel Latypov		  extra_qemu_args: Optional[List[str]],
2018c278d97SDaniel Latypov		  cross_compile: Optional[str]) -> Tuple[str, LinuxSourceTreeOperations]:
20287c9c163SBrendan Higgins	# The module name/path has very little to do with where the actual file
20387c9c163SBrendan Higgins	# exists (I learned this through experimentation and could not find it
20487c9c163SBrendan Higgins	# anywhere in the Python documentation).
20587c9c163SBrendan Higgins	#
20687c9c163SBrendan Higgins	# Bascially, we completely ignore the actual file location of the config
20787c9c163SBrendan Higgins	# we are loading and just tell Python that the module lives in the
20887c9c163SBrendan Higgins	# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
20987c9c163SBrendan Higgins	# exists as a file.
21087c9c163SBrendan Higgins	module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
21187c9c163SBrendan Higgins	spec = importlib.util.spec_from_file_location(module_path, config_path)
21285310a62SDaniel Latypov	assert spec is not None
21387c9c163SBrendan Higgins	config = importlib.util.module_from_spec(spec)
21452a5d80aSDaniel Latypov	# See https://github.com/python/typeshed/pull/2626 for context.
21552a5d80aSDaniel Latypov	assert isinstance(spec.loader, importlib.abc.Loader)
21652a5d80aSDaniel Latypov	spec.loader.exec_module(config)
21752a5d80aSDaniel Latypov
21852a5d80aSDaniel Latypov	if not hasattr(config, 'QEMU_ARCH'):
21952a5d80aSDaniel Latypov		raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
2201da2e622SDaniel Latypov	params: qemu_config.QemuArchParams = config.QEMU_ARCH
221a9333bd3SDaniel Latypov	if extra_qemu_args:
222a9333bd3SDaniel Latypov		params.extra_qemu_params.extend(extra_qemu_args)
22352a5d80aSDaniel Latypov	return params.linux_arch, LinuxSourceTreeOperationsQemu(
22452a5d80aSDaniel Latypov			params, cross_compile=cross_compile)
22587c9c163SBrendan Higgins
2260453f984SDaniel Latypovclass LinuxSourceTree:
2276ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
2286ebf5866SFelix Guo
22987c9c163SBrendan Higgins	def __init__(
23087c9c163SBrendan Higgins	      self,
23187c9c163SBrendan Higgins	      build_dir: str,
23253b46621SDaniel Latypov	      kunitconfig_paths: Optional[List[str]]=None,
2339f57cc76SDaniel Latypov	      kconfig_add: Optional[List[str]]=None,
2341da2e622SDaniel Latypov	      arch: Optional[str]=None,
2351da2e622SDaniel Latypov	      cross_compile: Optional[str]=None,
2361da2e622SDaniel Latypov	      qemu_config_path: Optional[str]=None,
2371da2e622SDaniel Latypov	      extra_qemu_args: Optional[List[str]]=None) -> None:
238021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
23987c9c163SBrendan Higgins		if qemu_config_path:
240a9333bd3SDaniel Latypov			self._arch, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
24187c9c163SBrendan Higgins		else:
24287c9c163SBrendan Higgins			self._arch = 'um' if arch is None else arch
2438c278d97SDaniel Latypov			if self._arch == 'um':
2448c278d97SDaniel Latypov				self._ops = LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
2458c278d97SDaniel Latypov			else:
2468c278d97SDaniel Latypov				qemu_config_path = _default_qemu_config_path(self._arch)
247a9333bd3SDaniel Latypov				_, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
2482b8fdbbfSDaniel Latypov
24953b46621SDaniel Latypov		self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths)
2509f57cc76SDaniel Latypov		if kconfig_add:
2519f57cc76SDaniel Latypov			kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
2529f57cc76SDaniel Latypov			self._kconfig.merge_in_entries(kconfig)
2539f57cc76SDaniel Latypov
254885210d3SDaniel Latypov	def arch(self) -> str:
255885210d3SDaniel Latypov		return self._arch
2562b8fdbbfSDaniel Latypov
25709641f7cSDaniel Latypov	def clean(self) -> bool:
2586ebf5866SFelix Guo		try:
2596ebf5866SFelix Guo			self._ops.make_mrproper()
2606ebf5866SFelix Guo		except ConfigError as e:
2616ebf5866SFelix Guo			logging.error(e)
2626ebf5866SFelix Guo			return False
2636ebf5866SFelix Guo		return True
2646ebf5866SFelix Guo
265aa1c0555SDaniel Latypov	def validate_config(self, build_dir: str) -> bool:
266dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
26798978490SDaniel Latypov		validated_kconfig = kunit_config.parse_file(kconfig_path)
268c44895b6SDaniel Latypov		if self._kconfig.is_subset_of(validated_kconfig):
269c44895b6SDaniel Latypov			return True
2708a7c6f85SDaniel Latypov		missing = set(self._kconfig.as_entries()) - set(validated_kconfig.as_entries())
271c44895b6SDaniel Latypov		message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
272c44895b6SDaniel Latypov			  'This is probably due to unsatisfied dependencies.\n' \
2738a7c6f85SDaniel Latypov			  'Missing: ' + ', '.join(str(e) for e in missing)
274c44895b6SDaniel Latypov		if self._arch == 'um':
275c44895b6SDaniel Latypov			message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
276c44895b6SDaniel Latypov				   'on a different architecture with something like "--arch=x86_64".'
277dde54b94SHeidi Fahim		logging.error(message)
278dde54b94SHeidi Fahim		return False
279dde54b94SHeidi Fahim
2801da2e622SDaniel Latypov	def build_config(self, build_dir: str, make_options: Optional[List[str]]) -> bool:
2816ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
2826ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
2836ebf5866SFelix Guo			os.mkdir(build_dir)
2846ebf5866SFelix Guo		try:
2856fc3a863SDavid Gow			self._kconfig = self._ops.make_arch_config(self._kconfig)
28687c9c163SBrendan Higgins			self._kconfig.write_to_file(kconfig_path)
2870476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
2886ebf5866SFelix Guo		except ConfigError as e:
2896ebf5866SFelix Guo			logging.error(e)
2906ebf5866SFelix Guo			return False
2914c2911f1SDaniel Latypov		if not self.validate_config(build_dir):
2924c2911f1SDaniel Latypov			return False
2934c2911f1SDaniel Latypov
2944c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
2954c2911f1SDaniel Latypov		if os.path.exists(old_path):
2964c2911f1SDaniel Latypov			os.remove(old_path)  # write_to_file appends to the file
2974c2911f1SDaniel Latypov		self._kconfig.write_to_file(old_path)
2984c2911f1SDaniel Latypov		return True
2994c2911f1SDaniel Latypov
3004c2911f1SDaniel Latypov	def _kunitconfig_changed(self, build_dir: str) -> bool:
3014c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
3024c2911f1SDaniel Latypov		if not os.path.exists(old_path):
3034c2911f1SDaniel Latypov			return True
3044c2911f1SDaniel Latypov
3054c2911f1SDaniel Latypov		old_kconfig = kunit_config.parse_file(old_path)
3068a7c6f85SDaniel Latypov		return old_kconfig != self._kconfig
3076ebf5866SFelix Guo
3081da2e622SDaniel Latypov	def build_reconfig(self, build_dir: str, make_options: Optional[List[str]]) -> bool:
30914ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
3106ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
3114c2911f1SDaniel Latypov		if not os.path.exists(kconfig_path):
3124c2911f1SDaniel Latypov			print('Generating .config ...')
3134c2911f1SDaniel Latypov			return self.build_config(build_dir, make_options)
3144c2911f1SDaniel Latypov
31598978490SDaniel Latypov		existing_kconfig = kunit_config.parse_file(kconfig_path)
3166fc3a863SDavid Gow		self._kconfig = self._ops.make_arch_config(self._kconfig)
3178a7c6f85SDaniel Latypov
3184c2911f1SDaniel Latypov		if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
3194c2911f1SDaniel Latypov			return True
3206ebf5866SFelix Guo		print('Regenerating .config ...')
3216ebf5866SFelix Guo		os.remove(kconfig_path)
3220476e69fSGreg Thelen		return self.build_config(build_dir, make_options)
3236ebf5866SFelix Guo
3241da2e622SDaniel Latypov	def build_kernel(self, jobs: int, build_dir: str, make_options: Optional[List[str]]) -> bool:
3256ebf5866SFelix Guo		try:
3260476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
3270476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
3286ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
3296ebf5866SFelix Guo			logging.error(e)
3306ebf5866SFelix Guo			return False
331dde54b94SHeidi Fahim		return self.validate_config(build_dir)
3326ebf5866SFelix Guo
333*723c8258SRae Moar	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]:
3347af29141SDaniel Latypov		if not args:
3357af29141SDaniel Latypov			args = []
336d992880bSDaniel Latypov		if filter_glob:
337d992880bSDaniel Latypov			args.append('kunit.filter_glob=' + filter_glob)
338*723c8258SRae Moar		if filter:
339*723c8258SRae Moar			args.append('kunit.filter="' + filter + '"')
340*723c8258SRae Moar		if filter_action:
341*723c8258SRae Moar			args.append('kunit.filter_action=' + filter_action)
342d20a6ba5SJoe Fradley		args.append('kunit.enable=1')
3437d7c48dfSDaniel Latypov
3447d7c48dfSDaniel Latypov		process = self._ops.start(args, build_dir)
3457d7c48dfSDaniel Latypov		assert process.stdout is not None  # tell mypy it's set
3467d7c48dfSDaniel Latypov
3477d7c48dfSDaniel Latypov		# Enforce the timeout in a background thread.
3481da2e622SDaniel Latypov		def _wait_proc() -> None:
3497d7c48dfSDaniel Latypov			try:
3507d7c48dfSDaniel Latypov				process.wait(timeout=timeout)
3517d7c48dfSDaniel Latypov			except Exception as e:
3527d7c48dfSDaniel Latypov				print(e)
3537d7c48dfSDaniel Latypov				process.terminate()
3547d7c48dfSDaniel Latypov				process.wait()
3557d7c48dfSDaniel Latypov		waiter = threading.Thread(target=_wait_proc)
3567d7c48dfSDaniel Latypov		waiter.start()
3577d7c48dfSDaniel Latypov
3587d7c48dfSDaniel Latypov		output = open(get_outfile_path(build_dir), 'w')
3597d7c48dfSDaniel Latypov		try:
3607d7c48dfSDaniel Latypov			# Tee the output to the file and to our caller in real time.
3617d7c48dfSDaniel Latypov			for line in process.stdout:
3627d7c48dfSDaniel Latypov				output.write(line)
363021ed9f5SHeidi Fahim				yield line
3647d7c48dfSDaniel Latypov		# This runs even if our caller doesn't consume every line.
3657d7c48dfSDaniel Latypov		finally:
3667d7c48dfSDaniel Latypov			# Flush any leftover output to the file
3677d7c48dfSDaniel Latypov			output.write(process.stdout.read())
3687d7c48dfSDaniel Latypov			output.close()
3697d7c48dfSDaniel Latypov			process.stdout.close()
3707d7c48dfSDaniel Latypov
3717d7c48dfSDaniel Latypov			waiter.join()
3727d7c48dfSDaniel Latypov			subprocess.call(['stty', 'sane'])
373021ed9f5SHeidi Fahim
3741da2e622SDaniel Latypov	def signal_handler(self, unused_sig: int, unused_frame: Optional[FrameType]) -> None:
375021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
376021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
377