18c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
28c2ecf20Sopenharmony_ci#
38c2ecf20Sopenharmony_ci# Runs UML kernel, collects output, and handles errors.
48c2ecf20Sopenharmony_ci#
58c2ecf20Sopenharmony_ci# Copyright (C) 2019, Google LLC.
68c2ecf20Sopenharmony_ci# Author: Felix Guo <felixguoxiuping@gmail.com>
78c2ecf20Sopenharmony_ci# Author: Brendan Higgins <brendanhiggins@google.com>
88c2ecf20Sopenharmony_ci
98c2ecf20Sopenharmony_ciimport logging
108c2ecf20Sopenharmony_ciimport subprocess
118c2ecf20Sopenharmony_ciimport os
128c2ecf20Sopenharmony_ciimport shutil
138c2ecf20Sopenharmony_ciimport signal
148c2ecf20Sopenharmony_ci
158c2ecf20Sopenharmony_cifrom contextlib import ExitStack
168c2ecf20Sopenharmony_ci
178c2ecf20Sopenharmony_ciimport kunit_config
188c2ecf20Sopenharmony_ciimport kunit_parser
198c2ecf20Sopenharmony_ci
208c2ecf20Sopenharmony_ciKCONFIG_PATH = '.config'
218c2ecf20Sopenharmony_ciKUNITCONFIG_PATH = '.kunitconfig'
228c2ecf20Sopenharmony_ciDEFAULT_KUNITCONFIG_PATH = 'arch/um/configs/kunit_defconfig'
238c2ecf20Sopenharmony_ciBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
248c2ecf20Sopenharmony_ciOUTFILE_PATH = 'test.log'
258c2ecf20Sopenharmony_ci
268c2ecf20Sopenharmony_ciclass ConfigError(Exception):
278c2ecf20Sopenharmony_ci	"""Represents an error trying to configure the Linux kernel."""
288c2ecf20Sopenharmony_ci
298c2ecf20Sopenharmony_ci
308c2ecf20Sopenharmony_ciclass BuildError(Exception):
318c2ecf20Sopenharmony_ci	"""Represents an error trying to build the Linux kernel."""
328c2ecf20Sopenharmony_ci
338c2ecf20Sopenharmony_ci
348c2ecf20Sopenharmony_ciclass LinuxSourceTreeOperations(object):
358c2ecf20Sopenharmony_ci	"""An abstraction over command line operations performed on a source tree."""
368c2ecf20Sopenharmony_ci
378c2ecf20Sopenharmony_ci	def make_mrproper(self):
388c2ecf20Sopenharmony_ci		try:
398c2ecf20Sopenharmony_ci			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
408c2ecf20Sopenharmony_ci		except OSError as e:
418c2ecf20Sopenharmony_ci			raise ConfigError('Could not call make command: ' + str(e))
428c2ecf20Sopenharmony_ci		except subprocess.CalledProcessError as e:
438c2ecf20Sopenharmony_ci			raise ConfigError(e.output.decode())
448c2ecf20Sopenharmony_ci
458c2ecf20Sopenharmony_ci	def make_olddefconfig(self, build_dir, make_options):
468c2ecf20Sopenharmony_ci		command = ['make', 'ARCH=um', 'olddefconfig']
478c2ecf20Sopenharmony_ci		if make_options:
488c2ecf20Sopenharmony_ci			command.extend(make_options)
498c2ecf20Sopenharmony_ci		if build_dir:
508c2ecf20Sopenharmony_ci			command += ['O=' + build_dir]
518c2ecf20Sopenharmony_ci		try:
528c2ecf20Sopenharmony_ci			subprocess.check_output(command, stderr=subprocess.STDOUT)
538c2ecf20Sopenharmony_ci		except OSError as e:
548c2ecf20Sopenharmony_ci			raise ConfigError('Could not call make command: ' + str(e))
558c2ecf20Sopenharmony_ci		except subprocess.CalledProcessError as e:
568c2ecf20Sopenharmony_ci			raise ConfigError(e.output.decode())
578c2ecf20Sopenharmony_ci
588c2ecf20Sopenharmony_ci	def make_allyesconfig(self, build_dir, make_options):
598c2ecf20Sopenharmony_ci		kunit_parser.print_with_timestamp(
608c2ecf20Sopenharmony_ci			'Enabling all CONFIGs for UML...')
618c2ecf20Sopenharmony_ci		command = ['make', 'ARCH=um', 'allyesconfig']
628c2ecf20Sopenharmony_ci		if make_options:
638c2ecf20Sopenharmony_ci			command.extend(make_options)
648c2ecf20Sopenharmony_ci		if build_dir:
658c2ecf20Sopenharmony_ci			command += ['O=' + build_dir]
668c2ecf20Sopenharmony_ci		process = subprocess.Popen(
678c2ecf20Sopenharmony_ci			command,
688c2ecf20Sopenharmony_ci			stdout=subprocess.DEVNULL,
698c2ecf20Sopenharmony_ci			stderr=subprocess.STDOUT)
708c2ecf20Sopenharmony_ci		process.wait()
718c2ecf20Sopenharmony_ci		kunit_parser.print_with_timestamp(
728c2ecf20Sopenharmony_ci			'Disabling broken configs to run KUnit tests...')
738c2ecf20Sopenharmony_ci		with ExitStack() as es:
748c2ecf20Sopenharmony_ci			config = open(get_kconfig_path(build_dir), 'a')
758c2ecf20Sopenharmony_ci			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
768c2ecf20Sopenharmony_ci			config.write(disable)
778c2ecf20Sopenharmony_ci		kunit_parser.print_with_timestamp(
788c2ecf20Sopenharmony_ci			'Starting Kernel with all configs takes a few minutes...')
798c2ecf20Sopenharmony_ci
808c2ecf20Sopenharmony_ci	def make(self, jobs, build_dir, make_options):
818c2ecf20Sopenharmony_ci		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
828c2ecf20Sopenharmony_ci		if make_options:
838c2ecf20Sopenharmony_ci			command.extend(make_options)
848c2ecf20Sopenharmony_ci		if build_dir:
858c2ecf20Sopenharmony_ci			command += ['O=' + build_dir]
868c2ecf20Sopenharmony_ci		try:
878c2ecf20Sopenharmony_ci			proc = subprocess.Popen(command,
888c2ecf20Sopenharmony_ci						stderr=subprocess.PIPE,
898c2ecf20Sopenharmony_ci						stdout=subprocess.DEVNULL)
908c2ecf20Sopenharmony_ci		except OSError as e:
918c2ecf20Sopenharmony_ci			raise BuildError('Could not call make command: ' + str(e))
928c2ecf20Sopenharmony_ci		_, stderr = proc.communicate()
938c2ecf20Sopenharmony_ci		if proc.returncode != 0:
948c2ecf20Sopenharmony_ci			raise BuildError(stderr.decode())
958c2ecf20Sopenharmony_ci		if stderr:  # likely only due to build warnings
968c2ecf20Sopenharmony_ci			print(stderr.decode())
978c2ecf20Sopenharmony_ci
988c2ecf20Sopenharmony_ci	def linux_bin(self, params, timeout, build_dir):
998c2ecf20Sopenharmony_ci		"""Runs the Linux UML binary. Must be named 'linux'."""
1008c2ecf20Sopenharmony_ci		linux_bin = './linux'
1018c2ecf20Sopenharmony_ci		if build_dir:
1028c2ecf20Sopenharmony_ci			linux_bin = os.path.join(build_dir, 'linux')
1038c2ecf20Sopenharmony_ci		outfile = get_outfile_path(build_dir)
1048c2ecf20Sopenharmony_ci		with open(outfile, 'w') as output:
1058c2ecf20Sopenharmony_ci			process = subprocess.Popen([linux_bin] + params,
1068c2ecf20Sopenharmony_ci						   stdout=output,
1078c2ecf20Sopenharmony_ci						   stderr=subprocess.STDOUT)
1088c2ecf20Sopenharmony_ci			process.wait(timeout)
1098c2ecf20Sopenharmony_ci
1108c2ecf20Sopenharmony_cidef get_kconfig_path(build_dir):
1118c2ecf20Sopenharmony_ci	kconfig_path = KCONFIG_PATH
1128c2ecf20Sopenharmony_ci	if build_dir:
1138c2ecf20Sopenharmony_ci		kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
1148c2ecf20Sopenharmony_ci	return kconfig_path
1158c2ecf20Sopenharmony_ci
1168c2ecf20Sopenharmony_cidef get_kunitconfig_path(build_dir):
1178c2ecf20Sopenharmony_ci	kunitconfig_path = KUNITCONFIG_PATH
1188c2ecf20Sopenharmony_ci	if build_dir:
1198c2ecf20Sopenharmony_ci		kunitconfig_path = os.path.join(build_dir, KUNITCONFIG_PATH)
1208c2ecf20Sopenharmony_ci	return kunitconfig_path
1218c2ecf20Sopenharmony_ci
1228c2ecf20Sopenharmony_cidef get_outfile_path(build_dir):
1238c2ecf20Sopenharmony_ci	outfile_path = OUTFILE_PATH
1248c2ecf20Sopenharmony_ci	if build_dir:
1258c2ecf20Sopenharmony_ci		outfile_path = os.path.join(build_dir, OUTFILE_PATH)
1268c2ecf20Sopenharmony_ci	return outfile_path
1278c2ecf20Sopenharmony_ci
1288c2ecf20Sopenharmony_ciclass LinuxSourceTree(object):
1298c2ecf20Sopenharmony_ci	"""Represents a Linux kernel source tree with KUnit tests."""
1308c2ecf20Sopenharmony_ci
1318c2ecf20Sopenharmony_ci	def __init__(self):
1328c2ecf20Sopenharmony_ci		self._ops = LinuxSourceTreeOperations()
1338c2ecf20Sopenharmony_ci		signal.signal(signal.SIGINT, self.signal_handler)
1348c2ecf20Sopenharmony_ci
1358c2ecf20Sopenharmony_ci	def clean(self):
1368c2ecf20Sopenharmony_ci		try:
1378c2ecf20Sopenharmony_ci			self._ops.make_mrproper()
1388c2ecf20Sopenharmony_ci		except ConfigError as e:
1398c2ecf20Sopenharmony_ci			logging.error(e)
1408c2ecf20Sopenharmony_ci			return False
1418c2ecf20Sopenharmony_ci		return True
1428c2ecf20Sopenharmony_ci
1438c2ecf20Sopenharmony_ci	def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH):
1448c2ecf20Sopenharmony_ci		kunitconfig_path = get_kunitconfig_path(build_dir)
1458c2ecf20Sopenharmony_ci		if not os.path.exists(kunitconfig_path):
1468c2ecf20Sopenharmony_ci			shutil.copyfile(defconfig, kunitconfig_path)
1478c2ecf20Sopenharmony_ci
1488c2ecf20Sopenharmony_ci	def read_kunitconfig(self, build_dir):
1498c2ecf20Sopenharmony_ci		kunitconfig_path = get_kunitconfig_path(build_dir)
1508c2ecf20Sopenharmony_ci		self._kconfig = kunit_config.Kconfig()
1518c2ecf20Sopenharmony_ci		self._kconfig.read_from_file(kunitconfig_path)
1528c2ecf20Sopenharmony_ci
1538c2ecf20Sopenharmony_ci	def validate_config(self, build_dir):
1548c2ecf20Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
1558c2ecf20Sopenharmony_ci		validated_kconfig = kunit_config.Kconfig()
1568c2ecf20Sopenharmony_ci		validated_kconfig.read_from_file(kconfig_path)
1578c2ecf20Sopenharmony_ci		if not self._kconfig.is_subset_of(validated_kconfig):
1588c2ecf20Sopenharmony_ci			invalid = self._kconfig.entries() - validated_kconfig.entries()
1598c2ecf20Sopenharmony_ci			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
1608c2ecf20Sopenharmony_ci					  'but not in .config: %s' % (
1618c2ecf20Sopenharmony_ci					', '.join([str(e) for e in invalid])
1628c2ecf20Sopenharmony_ci			)
1638c2ecf20Sopenharmony_ci			logging.error(message)
1648c2ecf20Sopenharmony_ci			return False
1658c2ecf20Sopenharmony_ci		return True
1668c2ecf20Sopenharmony_ci
1678c2ecf20Sopenharmony_ci	def build_config(self, build_dir, make_options):
1688c2ecf20Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
1698c2ecf20Sopenharmony_ci		if build_dir and not os.path.exists(build_dir):
1708c2ecf20Sopenharmony_ci			os.mkdir(build_dir)
1718c2ecf20Sopenharmony_ci		self._kconfig.write_to_file(kconfig_path)
1728c2ecf20Sopenharmony_ci		try:
1738c2ecf20Sopenharmony_ci			self._ops.make_olddefconfig(build_dir, make_options)
1748c2ecf20Sopenharmony_ci		except ConfigError as e:
1758c2ecf20Sopenharmony_ci			logging.error(e)
1768c2ecf20Sopenharmony_ci			return False
1778c2ecf20Sopenharmony_ci		return self.validate_config(build_dir)
1788c2ecf20Sopenharmony_ci
1798c2ecf20Sopenharmony_ci	def build_reconfig(self, build_dir, make_options):
1808c2ecf20Sopenharmony_ci		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1818c2ecf20Sopenharmony_ci		kconfig_path = get_kconfig_path(build_dir)
1828c2ecf20Sopenharmony_ci		if os.path.exists(kconfig_path):
1838c2ecf20Sopenharmony_ci			existing_kconfig = kunit_config.Kconfig()
1848c2ecf20Sopenharmony_ci			existing_kconfig.read_from_file(kconfig_path)
1858c2ecf20Sopenharmony_ci			if not self._kconfig.is_subset_of(existing_kconfig):
1868c2ecf20Sopenharmony_ci				print('Regenerating .config ...')
1878c2ecf20Sopenharmony_ci				os.remove(kconfig_path)
1888c2ecf20Sopenharmony_ci				return self.build_config(build_dir, make_options)
1898c2ecf20Sopenharmony_ci			else:
1908c2ecf20Sopenharmony_ci				return True
1918c2ecf20Sopenharmony_ci		else:
1928c2ecf20Sopenharmony_ci			print('Generating .config ...')
1938c2ecf20Sopenharmony_ci			return self.build_config(build_dir, make_options)
1948c2ecf20Sopenharmony_ci
1958c2ecf20Sopenharmony_ci	def build_um_kernel(self, alltests, jobs, build_dir, make_options):
1968c2ecf20Sopenharmony_ci		try:
1978c2ecf20Sopenharmony_ci			if alltests:
1988c2ecf20Sopenharmony_ci				self._ops.make_allyesconfig(build_dir, make_options)
1998c2ecf20Sopenharmony_ci			self._ops.make_olddefconfig(build_dir, make_options)
2008c2ecf20Sopenharmony_ci			self._ops.make(jobs, build_dir, make_options)
2018c2ecf20Sopenharmony_ci		except (ConfigError, BuildError) as e:
2028c2ecf20Sopenharmony_ci			logging.error(e)
2038c2ecf20Sopenharmony_ci			return False
2048c2ecf20Sopenharmony_ci		return self.validate_config(build_dir)
2058c2ecf20Sopenharmony_ci
2068c2ecf20Sopenharmony_ci	def run_kernel(self, args=[], build_dir='', timeout=None):
2078c2ecf20Sopenharmony_ci		args.extend(['mem=1G'])
2088c2ecf20Sopenharmony_ci		self._ops.linux_bin(args, timeout, build_dir)
2098c2ecf20Sopenharmony_ci		outfile = get_outfile_path(build_dir)
2108c2ecf20Sopenharmony_ci		subprocess.call(['stty', 'sane'])
2118c2ecf20Sopenharmony_ci		with open(outfile, 'r') as file:
2128c2ecf20Sopenharmony_ci			for line in file:
2138c2ecf20Sopenharmony_ci				yield line
2148c2ecf20Sopenharmony_ci
2158c2ecf20Sopenharmony_ci	def signal_handler(self, sig, frame):
2168c2ecf20Sopenharmony_ci		logging.error('Build interruption occurred. Cleaning console.')
2178c2ecf20Sopenharmony_ci		subprocess.call(['stty', 'sane'])
218