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 Guoimport logging
106ebf5866SFelix Guoimport subprocess
116ebf5866SFelix Guoimport os
12fcdb0bc0SAndy Shevchenkoimport shutil
13021ed9f5SHeidi Fahimimport signal
1409641f7cSDaniel Latypovfrom typing import Iterator
15021ed9f5SHeidi Fahim
16021ed9f5SHeidi Fahimfrom contextlib import ExitStack
176ebf5866SFelix Guo
186ebf5866SFelix Guoimport kunit_config
19021ed9f5SHeidi Fahimimport kunit_parser
206ebf5866SFelix Guo
216ebf5866SFelix GuoKCONFIG_PATH = '.config'
22fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
23fcdb0bc0SAndy ShevchenkoDEFAULT_KUNITCONFIG_PATH = 'arch/um/configs/kunit_defconfig'
24021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
25128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
266ebf5866SFelix Guo
27f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default):
28f3ed003eSAndy Shevchenko	if build_dir:
29f3ed003eSAndy Shevchenko		default = os.path.join(build_dir, default)
30f3ed003eSAndy Shevchenko	return default
31f3ed003eSAndy Shevchenko
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
4309641f7cSDaniel Latypov	def make_mrproper(self) -> None:
446ebf5866SFelix Guo		try:
455a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
466ebf5866SFelix Guo		except OSError as e:
471abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
486ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
491abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
506ebf5866SFelix Guo
5109641f7cSDaniel Latypov	def make_olddefconfig(self, build_dir, make_options) -> None:
526ebf5866SFelix Guo		command = ['make', 'ARCH=um', 'olddefconfig']
530476e69fSGreg Thelen		if make_options:
540476e69fSGreg Thelen			command.extend(make_options)
556ebf5866SFelix Guo		if build_dir:
566ebf5866SFelix Guo			command += ['O=' + build_dir]
576ebf5866SFelix Guo		try:
585a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
596ebf5866SFelix Guo		except OSError as e:
601abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
616ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
621abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
636ebf5866SFelix Guo
6409641f7cSDaniel Latypov	def make_allyesconfig(self, build_dir, make_options) -> None:
65021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
66021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
6767e2fae3SBrendan Higgins		command = ['make', 'ARCH=um', 'allyesconfig']
6867e2fae3SBrendan Higgins		if make_options:
6967e2fae3SBrendan Higgins			command.extend(make_options)
7067e2fae3SBrendan Higgins		if build_dir:
7167e2fae3SBrendan Higgins			command += ['O=' + build_dir]
72021ed9f5SHeidi Fahim		process = subprocess.Popen(
7367e2fae3SBrendan Higgins			command,
74021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
75021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
76021ed9f5SHeidi Fahim		process.wait()
77021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
78021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
79021ed9f5SHeidi Fahim		with ExitStack() as es:
8067e2fae3SBrendan Higgins			config = open(get_kconfig_path(build_dir), 'a')
81021ed9f5SHeidi Fahim			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
82021ed9f5SHeidi Fahim			config.write(disable)
83021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
84021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
85021ed9f5SHeidi Fahim
8609641f7cSDaniel Latypov	def make(self, jobs, build_dir, make_options) -> None:
876ebf5866SFelix Guo		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
880476e69fSGreg Thelen		if make_options:
890476e69fSGreg Thelen			command.extend(make_options)
906ebf5866SFelix Guo		if build_dir:
916ebf5866SFelix Guo			command += ['O=' + build_dir]
926ebf5866SFelix Guo		try:
9339088144SDaniel Latypov			proc = subprocess.Popen(command,
9439088144SDaniel Latypov						stderr=subprocess.PIPE,
9539088144SDaniel Latypov						stdout=subprocess.DEVNULL)
966ebf5866SFelix Guo		except OSError as e:
9739088144SDaniel Latypov			raise BuildError('Could not call make command: ' + str(e))
9839088144SDaniel Latypov		_, stderr = proc.communicate()
9939088144SDaniel Latypov		if proc.returncode != 0:
10039088144SDaniel Latypov			raise BuildError(stderr.decode())
10139088144SDaniel Latypov		if stderr:  # likely only due to build warnings
10239088144SDaniel Latypov			print(stderr.decode())
1036ebf5866SFelix Guo
10409641f7cSDaniel Latypov	def linux_bin(self, params, timeout, build_dir) -> None:
1056ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
106f3ed003eSAndy Shevchenko		linux_bin = get_file_path(build_dir, 'linux')
107128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
108021ed9f5SHeidi Fahim		with open(outfile, 'w') as output:
109021ed9f5SHeidi Fahim			process = subprocess.Popen([linux_bin] + params,
110021ed9f5SHeidi Fahim						   stdout=output,
111021ed9f5SHeidi Fahim						   stderr=subprocess.STDOUT)
112021ed9f5SHeidi Fahim			process.wait(timeout)
1136ebf5866SFelix Guo
11409641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str:
115f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KCONFIG_PATH)
1166ebf5866SFelix Guo
11709641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str:
118f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KUNITCONFIG_PATH)
119fcdb0bc0SAndy Shevchenko
12009641f7cSDaniel Latypovdef get_outfile_path(build_dir) -> str:
121f3ed003eSAndy Shevchenko	return get_file_path(build_dir, OUTFILE_PATH)
122128dc4bcSAndy Shevchenko
1236ebf5866SFelix Guoclass LinuxSourceTree(object):
1246ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
1256ebf5866SFelix Guo
126*243180f5SDaniel Latypov	def __init__(self, build_dir: str, load_config=True, kunitconfig_path='') -> None:
127021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
1286ebf5866SFelix Guo
1292b8fdbbfSDaniel Latypov		self._ops = LinuxSourceTreeOperations()
1302b8fdbbfSDaniel Latypov
1312b8fdbbfSDaniel Latypov		if not load_config:
1322b8fdbbfSDaniel Latypov			return
1332b8fdbbfSDaniel Latypov
134*243180f5SDaniel Latypov		if kunitconfig_path:
135*243180f5SDaniel Latypov			if not os.path.exists(kunitconfig_path):
136*243180f5SDaniel Latypov				raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
137*243180f5SDaniel Latypov		else:
1382b8fdbbfSDaniel Latypov			kunitconfig_path = get_kunitconfig_path(build_dir)
1392b8fdbbfSDaniel Latypov			if not os.path.exists(kunitconfig_path):
140*243180f5SDaniel Latypov				shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
1412b8fdbbfSDaniel Latypov
1422b8fdbbfSDaniel Latypov		self._kconfig = kunit_config.Kconfig()
1432b8fdbbfSDaniel Latypov		self._kconfig.read_from_file(kunitconfig_path)
1442b8fdbbfSDaniel Latypov
14509641f7cSDaniel Latypov	def clean(self) -> bool:
1466ebf5866SFelix Guo		try:
1476ebf5866SFelix Guo			self._ops.make_mrproper()
1486ebf5866SFelix Guo		except ConfigError as e:
1496ebf5866SFelix Guo			logging.error(e)
1506ebf5866SFelix Guo			return False
1516ebf5866SFelix Guo		return True
1526ebf5866SFelix Guo
15309641f7cSDaniel Latypov	def validate_config(self, build_dir) -> bool:
154dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
155dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
156dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
157dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
158dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
159dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
160dde54b94SHeidi Fahim					  'but not in .config: %s' % (
161dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
162dde54b94SHeidi Fahim			)
163dde54b94SHeidi Fahim			logging.error(message)
164dde54b94SHeidi Fahim			return False
165dde54b94SHeidi Fahim		return True
166dde54b94SHeidi Fahim
16709641f7cSDaniel Latypov	def build_config(self, build_dir, make_options) -> bool:
1686ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1696ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
1706ebf5866SFelix Guo			os.mkdir(build_dir)
1716ebf5866SFelix Guo		self._kconfig.write_to_file(kconfig_path)
1726ebf5866SFelix Guo		try:
1730476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1746ebf5866SFelix Guo		except ConfigError as e:
1756ebf5866SFelix Guo			logging.error(e)
1766ebf5866SFelix Guo			return False
177dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1786ebf5866SFelix Guo
17909641f7cSDaniel Latypov	def build_reconfig(self, build_dir, make_options) -> bool:
18014ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1816ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1826ebf5866SFelix Guo		if os.path.exists(kconfig_path):
1836ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
1846ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
1856ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
1866ebf5866SFelix Guo				print('Regenerating .config ...')
1876ebf5866SFelix Guo				os.remove(kconfig_path)
1880476e69fSGreg Thelen				return self.build_config(build_dir, make_options)
1896ebf5866SFelix Guo			else:
1906ebf5866SFelix Guo				return True
1916ebf5866SFelix Guo		else:
1926ebf5866SFelix Guo			print('Generating .config ...')
1930476e69fSGreg Thelen			return self.build_config(build_dir, make_options)
1946ebf5866SFelix Guo
19509641f7cSDaniel Latypov	def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
1966ebf5866SFelix Guo		try:
19767e2fae3SBrendan Higgins			if alltests:
19867e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
1990476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
2000476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
2016ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
2026ebf5866SFelix Guo			logging.error(e)
2036ebf5866SFelix Guo			return False
204dde54b94SHeidi Fahim		return self.validate_config(build_dir)
2056ebf5866SFelix Guo
20609641f7cSDaniel Latypov	def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]:
20765a4e529SDavid Gow		args.extend(['mem=1G', 'console=tty'])
208128dc4bcSAndy Shevchenko		self._ops.linux_bin(args, timeout, build_dir)
209128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
210021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
211021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
212021ed9f5SHeidi Fahim			for line in file:
213021ed9f5SHeidi Fahim				yield line
214021ed9f5SHeidi Fahim
21509641f7cSDaniel Latypov	def signal_handler(self, sig, frame) -> None:
216021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
217021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
218