162306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
262306a36Sopenharmony_ci#
362306a36Sopenharmony_ci# Runs UML kernel, collects output, and handles errors.
462306a36Sopenharmony_ci#
562306a36Sopenharmony_ci# Copyright (C) 2019, Google LLC.
662306a36Sopenharmony_ci# Author: Felix Guo <felixguoxiuping@gmail.com>
762306a36Sopenharmony_ci# Author: Brendan Higgins <brendanhiggins@google.com>
862306a36Sopenharmony_ci
962306a36Sopenharmony_ciimport importlib.abc
1062306a36Sopenharmony_ciimport importlib.util
1162306a36Sopenharmony_ciimport logging
1262306a36Sopenharmony_ciimport subprocess
1362306a36Sopenharmony_ciimport os
1462306a36Sopenharmony_ciimport shlex
1562306a36Sopenharmony_ciimport shutil
1662306a36Sopenharmony_ciimport signal
1762306a36Sopenharmony_ciimport threading
1862306a36Sopenharmony_cifrom typing import Iterator, List, Optional, Tuple
1962306a36Sopenharmony_cifrom types import FrameType
2062306a36Sopenharmony_ci
2162306a36Sopenharmony_ciimport kunit_config
2262306a36Sopenharmony_ciimport qemu_config
2362306a36Sopenharmony_ci
2462306a36Sopenharmony_ciKCONFIG_PATH = '.config'
2562306a36Sopenharmony_ciKUNITCONFIG_PATH = '.kunitconfig'
2662306a36Sopenharmony_ciOLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
2762306a36Sopenharmony_ciDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
2862306a36Sopenharmony_ciALL_TESTS_CONFIG_PATH = 'tools/testing/kunit/configs/all_tests.config'
2962306a36Sopenharmony_ciUML_KCONFIG_PATH = 'tools/testing/kunit/configs/arch_uml.config'
3062306a36Sopenharmony_ciOUTFILE_PATH = 'test.log'
3162306a36Sopenharmony_ciABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
3262306a36Sopenharmony_ciQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
3362306a36Sopenharmony_ci
3462306a36Sopenharmony_ciclass ConfigError(Exception):
3562306a36Sopenharmony_ci	"""Represents an error trying to configure the Linux kernel."""
3662306a36Sopenharmony_ci
3762306a36Sopenharmony_ci
3862306a36Sopenharmony_ciclass BuildError(Exception):
3962306a36Sopenharmony_ci	"""Represents an error trying to build the Linux kernel."""
4062306a36Sopenharmony_ci
4162306a36Sopenharmony_ci
4262306a36Sopenharmony_ciclass LinuxSourceTreeOperations:
4362306a36Sopenharmony_ci	"""An abstraction over command line operations performed on a source tree."""
4462306a36Sopenharmony_ci
4562306a36Sopenharmony_ci	def __init__(self, linux_arch: str, cross_compile: Optional[str]):
4662306a36Sopenharmony_ci		self._linux_arch = linux_arch
4762306a36Sopenharmony_ci		self._cross_compile = cross_compile
4862306a36Sopenharmony_ci
4962306a36Sopenharmony_ci	def make_mrproper(self) -> None:
5062306a36Sopenharmony_ci		try:
5162306a36Sopenharmony_ci			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
5262306a36Sopenharmony_ci		except OSError as e:
5362306a36Sopenharmony_ci			raise ConfigError('Could not call make command: ' + str(e))
5462306a36Sopenharmony_ci		except subprocess.CalledProcessError as e:
5562306a36Sopenharmony_ci			raise ConfigError(e.output.decode())
5662306a36Sopenharmony_ci
5762306a36Sopenharmony_ci	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
5862306a36Sopenharmony_ci		return base_kunitconfig
5962306a36Sopenharmony_ci
6062306a36Sopenharmony_ci	def make_olddefconfig(self, build_dir: str, make_options: Optional[List[str]]) -> None:
6162306a36Sopenharmony_ci		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
6262306a36Sopenharmony_ci		if self._cross_compile:
6362306a36Sopenharmony_ci			command += ['CROSS_COMPILE=' + self._cross_compile]
6462306a36Sopenharmony_ci		if make_options:
6562306a36Sopenharmony_ci			command.extend(make_options)
6662306a36Sopenharmony_ci		print('Populating config with:\n$', ' '.join(command))
6762306a36Sopenharmony_ci		try:
6862306a36Sopenharmony_ci			subprocess.check_output(command, stderr=subprocess.STDOUT)
6962306a36Sopenharmony_ci		except OSError as e:
7062306a36Sopenharmony_ci			raise ConfigError('Could not call make command: ' + str(e))
7162306a36Sopenharmony_ci		except subprocess.CalledProcessError as e:
7262306a36Sopenharmony_ci			raise ConfigError(e.output.decode())
7362306a36Sopenharmony_ci
7462306a36Sopenharmony_ci	def make(self, jobs: int, build_dir: str, make_options: Optional[List[str]]) -> None:
7562306a36Sopenharmony_ci		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
7662306a36Sopenharmony_ci		if make_options:
7762306a36Sopenharmony_ci			command.extend(make_options)
7862306a36Sopenharmony_ci		if self._cross_compile:
7962306a36Sopenharmony_ci			command += ['CROSS_COMPILE=' + self._cross_compile]
8062306a36Sopenharmony_ci		print('Building with:\n$', ' '.join(command))
8162306a36Sopenharmony_ci		try:
8262306a36Sopenharmony_ci			proc = subprocess.Popen(command,
8362306a36Sopenharmony_ci						stderr=subprocess.PIPE,
8462306a36Sopenharmony_ci						stdout=subprocess.DEVNULL)
8562306a36Sopenharmony_ci		except OSError as e:
8662306a36Sopenharmony_ci			raise BuildError('Could not call execute make: ' + str(e))
8762306a36Sopenharmony_ci		except subprocess.CalledProcessError as e:
8862306a36Sopenharmony_ci			raise BuildError(e.output)
8962306a36Sopenharmony_ci		_, stderr = proc.communicate()
9062306a36Sopenharmony_ci		if proc.returncode != 0:
9162306a36Sopenharmony_ci			raise BuildError(stderr.decode())
9262306a36Sopenharmony_ci		if stderr:  # likely only due to build warnings
9362306a36Sopenharmony_ci			print(stderr.decode())
9462306a36Sopenharmony_ci
9562306a36Sopenharmony_ci	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
9662306a36Sopenharmony_ci		raise RuntimeError('not implemented!')
9762306a36Sopenharmony_ci
9862306a36Sopenharmony_ci
9962306a36Sopenharmony_ciclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
10062306a36Sopenharmony_ci
10162306a36Sopenharmony_ci	def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
10262306a36Sopenharmony_ci		super().__init__(linux_arch=qemu_arch_params.linux_arch,
10362306a36Sopenharmony_ci				 cross_compile=cross_compile)
10462306a36Sopenharmony_ci		self._kconfig = qemu_arch_params.kconfig
10562306a36Sopenharmony_ci		self._qemu_arch = qemu_arch_params.qemu_arch
10662306a36Sopenharmony_ci		self._kernel_path = qemu_arch_params.kernel_path
10762306a36Sopenharmony_ci		self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
10862306a36Sopenharmony_ci		self._extra_qemu_params = qemu_arch_params.extra_qemu_params
10962306a36Sopenharmony_ci		self._serial = qemu_arch_params.serial
11062306a36Sopenharmony_ci
11162306a36Sopenharmony_ci	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
11262306a36Sopenharmony_ci		kconfig = kunit_config.parse_from_string(self._kconfig)
11362306a36Sopenharmony_ci		kconfig.merge_in_entries(base_kunitconfig)
11462306a36Sopenharmony_ci		return kconfig
11562306a36Sopenharmony_ci
11662306a36Sopenharmony_ci	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
11762306a36Sopenharmony_ci		kernel_path = os.path.join(build_dir, self._kernel_path)
11862306a36Sopenharmony_ci		qemu_command = ['qemu-system-' + self._qemu_arch,
11962306a36Sopenharmony_ci				'-nodefaults',
12062306a36Sopenharmony_ci				'-m', '1024',
12162306a36Sopenharmony_ci				'-kernel', kernel_path,
12262306a36Sopenharmony_ci				'-append', ' '.join(params + [self._kernel_command_line]),
12362306a36Sopenharmony_ci				'-no-reboot',
12462306a36Sopenharmony_ci				'-nographic',
12562306a36Sopenharmony_ci				'-serial', self._serial] + self._extra_qemu_params
12662306a36Sopenharmony_ci		# Note: shlex.join() does what we want, but requires python 3.8+.
12762306a36Sopenharmony_ci		print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
12862306a36Sopenharmony_ci		return subprocess.Popen(qemu_command,
12962306a36Sopenharmony_ci					stdin=subprocess.PIPE,
13062306a36Sopenharmony_ci					stdout=subprocess.PIPE,
13162306a36Sopenharmony_ci					stderr=subprocess.STDOUT,
13262306a36Sopenharmony_ci					text=True, errors='backslashreplace')
13362306a36Sopenharmony_ci
13462306a36Sopenharmony_ciclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
13562306a36Sopenharmony_ci	"""An abstraction over command line operations performed on a source tree."""
13662306a36Sopenharmony_ci
13762306a36Sopenharmony_ci	def __init__(self, cross_compile: Optional[str]=None):
13862306a36Sopenharmony_ci		super().__init__(linux_arch='um', cross_compile=cross_compile)
13962306a36Sopenharmony_ci
14062306a36Sopenharmony_ci	def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
14162306a36Sopenharmony_ci		kconfig = kunit_config.parse_file(UML_KCONFIG_PATH)
14262306a36Sopenharmony_ci		kconfig.merge_in_entries(base_kunitconfig)
14362306a36Sopenharmony_ci		return kconfig
14462306a36Sopenharmony_ci
14562306a36Sopenharmony_ci	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
14662306a36Sopenharmony_ci		"""Runs the Linux UML binary. Must be named 'linux'."""
14762306a36Sopenharmony_ci		linux_bin = os.path.join(build_dir, 'linux')
14862306a36Sopenharmony_ci		params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
14962306a36Sopenharmony_ci		return subprocess.Popen([linux_bin] + params,
15062306a36Sopenharmony_ci					   stdin=subprocess.PIPE,
15162306a36Sopenharmony_ci					   stdout=subprocess.PIPE,
15262306a36Sopenharmony_ci					   stderr=subprocess.STDOUT,
15362306a36Sopenharmony_ci					   text=True, errors='backslashreplace')
15462306a36Sopenharmony_ci
15562306a36Sopenharmony_cidef get_kconfig_path(build_dir: str) -> str:
15662306a36Sopenharmony_ci	return os.path.join(build_dir, KCONFIG_PATH)
15762306a36Sopenharmony_ci
15862306a36Sopenharmony_cidef get_kunitconfig_path(build_dir: str) -> str:
15962306a36Sopenharmony_ci	return os.path.join(build_dir, KUNITCONFIG_PATH)
16062306a36Sopenharmony_ci
16162306a36Sopenharmony_cidef get_old_kunitconfig_path(build_dir: str) -> str:
16262306a36Sopenharmony_ci	return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
16362306a36Sopenharmony_ci
16462306a36Sopenharmony_cidef get_parsed_kunitconfig(build_dir: str,
16562306a36Sopenharmony_ci			   kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig:
16662306a36Sopenharmony_ci	if not kunitconfig_paths:
16762306a36Sopenharmony_ci		path = get_kunitconfig_path(build_dir)
16862306a36Sopenharmony_ci		if not os.path.exists(path):
16962306a36Sopenharmony_ci			shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path)
17062306a36Sopenharmony_ci		return kunit_config.parse_file(path)
17162306a36Sopenharmony_ci
17262306a36Sopenharmony_ci	merged = kunit_config.Kconfig()
17362306a36Sopenharmony_ci
17462306a36Sopenharmony_ci	for path in kunitconfig_paths:
17562306a36Sopenharmony_ci		if os.path.isdir(path):
17662306a36Sopenharmony_ci			path = os.path.join(path, KUNITCONFIG_PATH)
17762306a36Sopenharmony_ci		if not os.path.exists(path):
17862306a36Sopenharmony_ci			raise ConfigError(f'Specified kunitconfig ({path}) does not exist')
17962306a36Sopenharmony_ci
18062306a36Sopenharmony_ci		partial = kunit_config.parse_file(path)
18162306a36Sopenharmony_ci		diff = merged.conflicting_options(partial)
18262306a36Sopenharmony_ci		if diff:
18362306a36Sopenharmony_ci			diff_str = '\n\n'.join(f'{a}\n  vs from {path}\n{b}' for a, b in diff)
18462306a36Sopenharmony_ci			raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}')
18562306a36Sopenharmony_ci		merged.merge_in_entries(partial)
18662306a36Sopenharmony_ci	return merged
18762306a36Sopenharmony_ci
18862306a36Sopenharmony_cidef get_outfile_path(build_dir: str) -> str:
18962306a36Sopenharmony_ci	return os.path.join(build_dir, OUTFILE_PATH)
19062306a36Sopenharmony_ci
19162306a36Sopenharmony_cidef _default_qemu_config_path(arch: str) -> str:
19262306a36Sopenharmony_ci	config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
19362306a36Sopenharmony_ci	if os.path.isfile(config_path):
19462306a36Sopenharmony_ci		return config_path
19562306a36Sopenharmony_ci
19662306a36Sopenharmony_ci	options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
19762306a36Sopenharmony_ci	raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
19862306a36Sopenharmony_ci
19962306a36Sopenharmony_cidef _get_qemu_ops(config_path: str,
20062306a36Sopenharmony_ci		  extra_qemu_args: Optional[List[str]],
20162306a36Sopenharmony_ci		  cross_compile: Optional[str]) -> Tuple[str, LinuxSourceTreeOperations]:
20262306a36Sopenharmony_ci	# The module name/path has very little to do with where the actual file
20362306a36Sopenharmony_ci	# exists (I learned this through experimentation and could not find it
20462306a36Sopenharmony_ci	# anywhere in the Python documentation).
20562306a36Sopenharmony_ci	#
20662306a36Sopenharmony_ci	# Bascially, we completely ignore the actual file location of the config
20762306a36Sopenharmony_ci	# we are loading and just tell Python that the module lives in the
20862306a36Sopenharmony_ci	# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
20962306a36Sopenharmony_ci	# exists as a file.
21062306a36Sopenharmony_ci	module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
21162306a36Sopenharmony_ci	spec = importlib.util.spec_from_file_location(module_path, config_path)
21262306a36Sopenharmony_ci	assert spec is not None
21362306a36Sopenharmony_ci	config = importlib.util.module_from_spec(spec)
21462306a36Sopenharmony_ci	# See https://github.com/python/typeshed/pull/2626 for context.
21562306a36Sopenharmony_ci	assert isinstance(spec.loader, importlib.abc.Loader)
21662306a36Sopenharmony_ci	spec.loader.exec_module(config)
21762306a36Sopenharmony_ci
21862306a36Sopenharmony_ci	if not hasattr(config, 'QEMU_ARCH'):
21962306a36Sopenharmony_ci		raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
22062306a36Sopenharmony_ci	params: qemu_config.QemuArchParams = config.QEMU_ARCH
22162306a36Sopenharmony_ci	if extra_qemu_args:
22262306a36Sopenharmony_ci		params.extra_qemu_params.extend(extra_qemu_args)
22362306a36Sopenharmony_ci	return params.linux_arch, LinuxSourceTreeOperationsQemu(
22462306a36Sopenharmony_ci			params, cross_compile=cross_compile)
22562306a36Sopenharmony_ci
22662306a36Sopenharmony_ciclass LinuxSourceTree:
22762306a36Sopenharmony_ci	"""Represents a Linux kernel source tree with KUnit tests."""
22862306a36Sopenharmony_ci
22962306a36Sopenharmony_ci	def __init__(
23062306a36Sopenharmony_ci	      self,
23162306a36Sopenharmony_ci	      build_dir: str,
23262306a36Sopenharmony_ci	      kunitconfig_paths: Optional[List[str]]=None,
23362306a36Sopenharmony_ci	      kconfig_add: Optional[List[str]]=None,
23462306a36Sopenharmony_ci	      arch: Optional[str]=None,
23562306a36Sopenharmony_ci	      cross_compile: Optional[str]=None,
23662306a36Sopenharmony_ci	      qemu_config_path: Optional[str]=None,
23762306a36Sopenharmony_ci	      extra_qemu_args: Optional[List[str]]=None) -> None:
23862306a36Sopenharmony_ci		signal.signal(signal.SIGINT, self.signal_handler)
23962306a36Sopenharmony_ci		if qemu_config_path:
24062306a36Sopenharmony_ci			self._arch, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
24162306a36Sopenharmony_ci		else:
24262306a36Sopenharmony_ci			self._arch = 'um' if arch is None else arch
24362306a36Sopenharmony_ci			if self._arch == 'um':
24462306a36Sopenharmony_ci				self._ops = LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
24562306a36Sopenharmony_ci			else:
24662306a36Sopenharmony_ci				qemu_config_path = _default_qemu_config_path(self._arch)
24762306a36Sopenharmony_ci				_, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
24862306a36Sopenharmony_ci
24962306a36Sopenharmony_ci		self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths)
25062306a36Sopenharmony_ci		if kconfig_add:
25162306a36Sopenharmony_ci			kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
25262306a36Sopenharmony_ci			self._kconfig.merge_in_entries(kconfig)
25362306a36Sopenharmony_ci
25462306a36Sopenharmony_ci	def arch(self) -> str:
25562306a36Sopenharmony_ci		return self._arch
25662306a36Sopenharmony_ci
25762306a36Sopenharmony_ci	def clean(self) -> bool:
25862306a36Sopenharmony_ci		try:
25962306a36Sopenharmony_ci			self._ops.make_mrproper()
26062306a36Sopenharmony_ci		except ConfigError as e:
26162306a36Sopenharmony_ci			logging.error(e)
26262306a36Sopenharmony_ci			return False
26362306a36Sopenharmony_ci		return True
26462306a36Sopenharmony_ci
26562306a36Sopenharmony_ci	def validate_config(self, build_dir: str) -> bool:
26662306a36Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
26762306a36Sopenharmony_ci		validated_kconfig = kunit_config.parse_file(kconfig_path)
26862306a36Sopenharmony_ci		if self._kconfig.is_subset_of(validated_kconfig):
26962306a36Sopenharmony_ci			return True
27062306a36Sopenharmony_ci		missing = set(self._kconfig.as_entries()) - set(validated_kconfig.as_entries())
27162306a36Sopenharmony_ci		message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
27262306a36Sopenharmony_ci			  'This is probably due to unsatisfied dependencies.\n' \
27362306a36Sopenharmony_ci			  'Missing: ' + ', '.join(str(e) for e in missing)
27462306a36Sopenharmony_ci		if self._arch == 'um':
27562306a36Sopenharmony_ci			message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
27662306a36Sopenharmony_ci				   'on a different architecture with something like "--arch=x86_64".'
27762306a36Sopenharmony_ci		logging.error(message)
27862306a36Sopenharmony_ci		return False
27962306a36Sopenharmony_ci
28062306a36Sopenharmony_ci	def build_config(self, build_dir: str, make_options: Optional[List[str]]) -> bool:
28162306a36Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
28262306a36Sopenharmony_ci		if build_dir and not os.path.exists(build_dir):
28362306a36Sopenharmony_ci			os.mkdir(build_dir)
28462306a36Sopenharmony_ci		try:
28562306a36Sopenharmony_ci			self._kconfig = self._ops.make_arch_config(self._kconfig)
28662306a36Sopenharmony_ci			self._kconfig.write_to_file(kconfig_path)
28762306a36Sopenharmony_ci			self._ops.make_olddefconfig(build_dir, make_options)
28862306a36Sopenharmony_ci		except ConfigError as e:
28962306a36Sopenharmony_ci			logging.error(e)
29062306a36Sopenharmony_ci			return False
29162306a36Sopenharmony_ci		if not self.validate_config(build_dir):
29262306a36Sopenharmony_ci			return False
29362306a36Sopenharmony_ci
29462306a36Sopenharmony_ci		old_path = get_old_kunitconfig_path(build_dir)
29562306a36Sopenharmony_ci		if os.path.exists(old_path):
29662306a36Sopenharmony_ci			os.remove(old_path)  # write_to_file appends to the file
29762306a36Sopenharmony_ci		self._kconfig.write_to_file(old_path)
29862306a36Sopenharmony_ci		return True
29962306a36Sopenharmony_ci
30062306a36Sopenharmony_ci	def _kunitconfig_changed(self, build_dir: str) -> bool:
30162306a36Sopenharmony_ci		old_path = get_old_kunitconfig_path(build_dir)
30262306a36Sopenharmony_ci		if not os.path.exists(old_path):
30362306a36Sopenharmony_ci			return True
30462306a36Sopenharmony_ci
30562306a36Sopenharmony_ci		old_kconfig = kunit_config.parse_file(old_path)
30662306a36Sopenharmony_ci		return old_kconfig != self._kconfig
30762306a36Sopenharmony_ci
30862306a36Sopenharmony_ci	def build_reconfig(self, build_dir: str, make_options: Optional[List[str]]) -> bool:
30962306a36Sopenharmony_ci		"""Creates a new .config if it is not a subset of the .kunitconfig."""
31062306a36Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
31162306a36Sopenharmony_ci		if not os.path.exists(kconfig_path):
31262306a36Sopenharmony_ci			print('Generating .config ...')
31362306a36Sopenharmony_ci			return self.build_config(build_dir, make_options)
31462306a36Sopenharmony_ci
31562306a36Sopenharmony_ci		existing_kconfig = kunit_config.parse_file(kconfig_path)
31662306a36Sopenharmony_ci		self._kconfig = self._ops.make_arch_config(self._kconfig)
31762306a36Sopenharmony_ci
31862306a36Sopenharmony_ci		if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
31962306a36Sopenharmony_ci			return True
32062306a36Sopenharmony_ci		print('Regenerating .config ...')
32162306a36Sopenharmony_ci		os.remove(kconfig_path)
32262306a36Sopenharmony_ci		return self.build_config(build_dir, make_options)
32362306a36Sopenharmony_ci
32462306a36Sopenharmony_ci	def build_kernel(self, jobs: int, build_dir: str, make_options: Optional[List[str]]) -> bool:
32562306a36Sopenharmony_ci		try:
32662306a36Sopenharmony_ci			self._ops.make_olddefconfig(build_dir, make_options)
32762306a36Sopenharmony_ci			self._ops.make(jobs, build_dir, make_options)
32862306a36Sopenharmony_ci		except (ConfigError, BuildError) as e:
32962306a36Sopenharmony_ci			logging.error(e)
33062306a36Sopenharmony_ci			return False
33162306a36Sopenharmony_ci		return self.validate_config(build_dir)
33262306a36Sopenharmony_ci
33362306a36Sopenharmony_ci	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]:
33462306a36Sopenharmony_ci		if not args:
33562306a36Sopenharmony_ci			args = []
33662306a36Sopenharmony_ci		if filter_glob:
33762306a36Sopenharmony_ci			args.append('kunit.filter_glob=' + filter_glob)
33862306a36Sopenharmony_ci		if filter:
33962306a36Sopenharmony_ci			args.append('kunit.filter="' + filter + '"')
34062306a36Sopenharmony_ci		if filter_action:
34162306a36Sopenharmony_ci			args.append('kunit.filter_action=' + filter_action)
34262306a36Sopenharmony_ci		args.append('kunit.enable=1')
34362306a36Sopenharmony_ci
34462306a36Sopenharmony_ci		process = self._ops.start(args, build_dir)
34562306a36Sopenharmony_ci		assert process.stdout is not None  # tell mypy it's set
34662306a36Sopenharmony_ci
34762306a36Sopenharmony_ci		# Enforce the timeout in a background thread.
34862306a36Sopenharmony_ci		def _wait_proc() -> None:
34962306a36Sopenharmony_ci			try:
35062306a36Sopenharmony_ci				process.wait(timeout=timeout)
35162306a36Sopenharmony_ci			except Exception as e:
35262306a36Sopenharmony_ci				print(e)
35362306a36Sopenharmony_ci				process.terminate()
35462306a36Sopenharmony_ci				process.wait()
35562306a36Sopenharmony_ci		waiter = threading.Thread(target=_wait_proc)
35662306a36Sopenharmony_ci		waiter.start()
35762306a36Sopenharmony_ci
35862306a36Sopenharmony_ci		output = open(get_outfile_path(build_dir), 'w')
35962306a36Sopenharmony_ci		try:
36062306a36Sopenharmony_ci			# Tee the output to the file and to our caller in real time.
36162306a36Sopenharmony_ci			for line in process.stdout:
36262306a36Sopenharmony_ci				output.write(line)
36362306a36Sopenharmony_ci				yield line
36462306a36Sopenharmony_ci		# This runs even if our caller doesn't consume every line.
36562306a36Sopenharmony_ci		finally:
36662306a36Sopenharmony_ci			# Flush any leftover output to the file
36762306a36Sopenharmony_ci			output.write(process.stdout.read())
36862306a36Sopenharmony_ci			output.close()
36962306a36Sopenharmony_ci			process.stdout.close()
37062306a36Sopenharmony_ci
37162306a36Sopenharmony_ci			waiter.join()
37262306a36Sopenharmony_ci			subprocess.call(['stty', 'sane'])
37362306a36Sopenharmony_ci
37462306a36Sopenharmony_ci	def signal_handler(self, unused_sig: int, unused_frame: Optional[FrameType]) -> None:
37562306a36Sopenharmony_ci		logging.error('Build interruption occurred. Cleaning console.')
37662306a36Sopenharmony_ci		subprocess.call(['stty', 'sane'])
377