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
96ebf5866SFelix Guo
106ebf5866SFelix Guoimport logging
116ebf5866SFelix Guoimport subprocess
126ebf5866SFelix Guoimport os
13021ed9f5SHeidi Fahimimport signal
14021ed9f5SHeidi Fahim
15021ed9f5SHeidi Fahimfrom contextlib import ExitStack
166ebf5866SFelix Guo
176ebf5866SFelix Guoimport kunit_config
18021ed9f5SHeidi Fahimimport kunit_parser
196ebf5866SFelix Guo
206ebf5866SFelix GuoKCONFIG_PATH = '.config'
2114ee5cfdSSeongJae Parkkunitconfig_path = '.kunitconfig'
22021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
236ebf5866SFelix Guo
246ebf5866SFelix Guoclass ConfigError(Exception):
256ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
266ebf5866SFelix Guo
276ebf5866SFelix Guo
286ebf5866SFelix Guoclass BuildError(Exception):
296ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
306ebf5866SFelix Guo
316ebf5866SFelix Guo
326ebf5866SFelix Guoclass LinuxSourceTreeOperations(object):
336ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
346ebf5866SFelix Guo
356ebf5866SFelix Guo	def make_mrproper(self):
366ebf5866SFelix Guo		try:
376ebf5866SFelix Guo			subprocess.check_output(['make', 'mrproper'])
386ebf5866SFelix Guo		except OSError as e:
396ebf5866SFelix Guo			raise ConfigError('Could not call make command: ' + e)
406ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
416ebf5866SFelix Guo			raise ConfigError(e.output)
426ebf5866SFelix Guo
436ebf5866SFelix Guo	def make_olddefconfig(self, build_dir):
446ebf5866SFelix Guo		command = ['make', 'ARCH=um', 'olddefconfig']
456ebf5866SFelix Guo		if build_dir:
466ebf5866SFelix Guo			command += ['O=' + build_dir]
476ebf5866SFelix Guo		try:
48021ed9f5SHeidi Fahim			subprocess.check_output(command, stderr=subprocess.PIPE)
496ebf5866SFelix Guo		except OSError as e:
506ebf5866SFelix Guo			raise ConfigError('Could not call make command: ' + e)
516ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
526ebf5866SFelix Guo			raise ConfigError(e.output)
536ebf5866SFelix Guo
54021ed9f5SHeidi Fahim	def make_allyesconfig(self):
55021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
56021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
57021ed9f5SHeidi Fahim		process = subprocess.Popen(
58021ed9f5SHeidi Fahim			['make', 'ARCH=um', 'allyesconfig'],
59021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
60021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
61021ed9f5SHeidi Fahim		process.wait()
62021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
63021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
64021ed9f5SHeidi Fahim		with ExitStack() as es:
65021ed9f5SHeidi Fahim			config = open(KCONFIG_PATH, 'a')
66021ed9f5SHeidi Fahim			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
67021ed9f5SHeidi Fahim			config.write(disable)
68021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
69021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
70021ed9f5SHeidi Fahim
716ebf5866SFelix Guo	def make(self, jobs, build_dir):
726ebf5866SFelix Guo		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
736ebf5866SFelix Guo		if build_dir:
746ebf5866SFelix Guo			command += ['O=' + build_dir]
756ebf5866SFelix Guo		try:
766ebf5866SFelix Guo			subprocess.check_output(command)
776ebf5866SFelix Guo		except OSError as e:
786ebf5866SFelix Guo			raise BuildError('Could not call execute make: ' + e)
796ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
806ebf5866SFelix Guo			raise BuildError(e.output)
816ebf5866SFelix Guo
82021ed9f5SHeidi Fahim	def linux_bin(self, params, timeout, build_dir, outfile):
836ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
846ebf5866SFelix Guo		linux_bin = './linux'
856ebf5866SFelix Guo		if build_dir:
866ebf5866SFelix Guo			linux_bin = os.path.join(build_dir, 'linux')
87021ed9f5SHeidi Fahim		with open(outfile, 'w') as output:
88021ed9f5SHeidi Fahim			process = subprocess.Popen([linux_bin] + params,
89021ed9f5SHeidi Fahim						   stdout=output,
90021ed9f5SHeidi Fahim						   stderr=subprocess.STDOUT)
91021ed9f5SHeidi Fahim			process.wait(timeout)
926ebf5866SFelix Guo
936ebf5866SFelix Guo
946ebf5866SFelix Guodef get_kconfig_path(build_dir):
956ebf5866SFelix Guo	kconfig_path = KCONFIG_PATH
966ebf5866SFelix Guo	if build_dir:
976ebf5866SFelix Guo		kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
986ebf5866SFelix Guo	return kconfig_path
996ebf5866SFelix Guo
1006ebf5866SFelix Guoclass LinuxSourceTree(object):
1016ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
1026ebf5866SFelix Guo
1036ebf5866SFelix Guo	def __init__(self):
1046ebf5866SFelix Guo		self._kconfig = kunit_config.Kconfig()
105e3212513SSeongJae Park		self._kconfig.read_from_file(kunitconfig_path)
1066ebf5866SFelix Guo		self._ops = LinuxSourceTreeOperations()
107021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
1086ebf5866SFelix Guo
1096ebf5866SFelix Guo	def clean(self):
1106ebf5866SFelix Guo		try:
1116ebf5866SFelix Guo			self._ops.make_mrproper()
1126ebf5866SFelix Guo		except ConfigError as e:
1136ebf5866SFelix Guo			logging.error(e)
1146ebf5866SFelix Guo			return False
1156ebf5866SFelix Guo		return True
1166ebf5866SFelix Guo
117dde54b94SHeidi Fahim	def validate_config(self, build_dir):
118dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
119dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
120dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
121dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
122dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
123dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
124dde54b94SHeidi Fahim					  'but not in .config: %s' % (
125dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
126dde54b94SHeidi Fahim			)
127dde54b94SHeidi Fahim			logging.error(message)
128dde54b94SHeidi Fahim			return False
129dde54b94SHeidi Fahim		return True
130dde54b94SHeidi Fahim
1316ebf5866SFelix Guo	def build_config(self, build_dir):
1326ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1336ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
1346ebf5866SFelix Guo			os.mkdir(build_dir)
1356ebf5866SFelix Guo		self._kconfig.write_to_file(kconfig_path)
1366ebf5866SFelix Guo		try:
1376ebf5866SFelix Guo			self._ops.make_olddefconfig(build_dir)
1386ebf5866SFelix Guo		except ConfigError as e:
1396ebf5866SFelix Guo			logging.error(e)
1406ebf5866SFelix Guo			return False
141dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1426ebf5866SFelix Guo
1436ebf5866SFelix Guo	def build_reconfig(self, build_dir):
14414ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1456ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1466ebf5866SFelix Guo		if os.path.exists(kconfig_path):
1476ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
1486ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
1496ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
1506ebf5866SFelix Guo				print('Regenerating .config ...')
1516ebf5866SFelix Guo				os.remove(kconfig_path)
1526ebf5866SFelix Guo				return self.build_config(build_dir)
1536ebf5866SFelix Guo			else:
1546ebf5866SFelix Guo				return True
1556ebf5866SFelix Guo		else:
1566ebf5866SFelix Guo			print('Generating .config ...')
1576ebf5866SFelix Guo			return self.build_config(build_dir)
1586ebf5866SFelix Guo
159021ed9f5SHeidi Fahim	def build_um_kernel(self, alltests, jobs, build_dir):
160021ed9f5SHeidi Fahim		if alltests:
161021ed9f5SHeidi Fahim			self._ops.make_allyesconfig()
1626ebf5866SFelix Guo		try:
1636ebf5866SFelix Guo			self._ops.make_olddefconfig(build_dir)
1646ebf5866SFelix Guo			self._ops.make(jobs, build_dir)
1656ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
1666ebf5866SFelix Guo			logging.error(e)
1676ebf5866SFelix Guo			return False
168dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1696ebf5866SFelix Guo
170021ed9f5SHeidi Fahim	def run_kernel(self, args=[], build_dir='', timeout=None):
171021ed9f5SHeidi Fahim		args.extend(['mem=1G'])
172021ed9f5SHeidi Fahim		outfile = 'test.log'
173021ed9f5SHeidi Fahim		self._ops.linux_bin(args, timeout, build_dir, outfile)
174021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
175021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
176021ed9f5SHeidi Fahim			for line in file:
177021ed9f5SHeidi Fahim				yield line
178021ed9f5SHeidi Fahim
179021ed9f5SHeidi Fahim	def signal_handler(self, sig, frame):
180021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
181021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
182