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 <[email protected]>
76ebf5866SFelix Guo# Author: Brendan Higgins <[email protected]>
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*2b8fdbbfSDaniel Latypov	def __init__(self, build_dir: str, load_config=True, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None:
127021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
1286ebf5866SFelix Guo
129*2b8fdbbfSDaniel Latypov		self._ops = LinuxSourceTreeOperations()
130*2b8fdbbfSDaniel Latypov
131*2b8fdbbfSDaniel Latypov		if not load_config:
132*2b8fdbbfSDaniel Latypov			return
133*2b8fdbbfSDaniel Latypov
134*2b8fdbbfSDaniel Latypov		kunitconfig_path = get_kunitconfig_path(build_dir)
135*2b8fdbbfSDaniel Latypov		if not os.path.exists(kunitconfig_path):
136*2b8fdbbfSDaniel Latypov			shutil.copyfile(defconfig, kunitconfig_path)
137*2b8fdbbfSDaniel Latypov
138*2b8fdbbfSDaniel Latypov		self._kconfig = kunit_config.Kconfig()
139*2b8fdbbfSDaniel Latypov		self._kconfig.read_from_file(kunitconfig_path)
140*2b8fdbbfSDaniel Latypov
14109641f7cSDaniel Latypov	def clean(self) -> bool:
1426ebf5866SFelix Guo		try:
1436ebf5866SFelix Guo			self._ops.make_mrproper()
1446ebf5866SFelix Guo		except ConfigError as e:
1456ebf5866SFelix Guo			logging.error(e)
1466ebf5866SFelix Guo			return False
1476ebf5866SFelix Guo		return True
1486ebf5866SFelix Guo
14909641f7cSDaniel Latypov	def validate_config(self, build_dir) -> bool:
150dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
151dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
152dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
153dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
154dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
155dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
156dde54b94SHeidi Fahim					  'but not in .config: %s' % (
157dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
158dde54b94SHeidi Fahim			)
159dde54b94SHeidi Fahim			logging.error(message)
160dde54b94SHeidi Fahim			return False
161dde54b94SHeidi Fahim		return True
162dde54b94SHeidi Fahim
16309641f7cSDaniel Latypov	def build_config(self, build_dir, make_options) -> bool:
1646ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1656ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
1666ebf5866SFelix Guo			os.mkdir(build_dir)
1676ebf5866SFelix Guo		self._kconfig.write_to_file(kconfig_path)
1686ebf5866SFelix Guo		try:
1690476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1706ebf5866SFelix Guo		except ConfigError as e:
1716ebf5866SFelix Guo			logging.error(e)
1726ebf5866SFelix Guo			return False
173dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1746ebf5866SFelix Guo
17509641f7cSDaniel Latypov	def build_reconfig(self, build_dir, make_options) -> bool:
17614ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1776ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1786ebf5866SFelix Guo		if os.path.exists(kconfig_path):
1796ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
1806ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
1816ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
1826ebf5866SFelix Guo				print('Regenerating .config ...')
1836ebf5866SFelix Guo				os.remove(kconfig_path)
1840476e69fSGreg Thelen				return self.build_config(build_dir, make_options)
1856ebf5866SFelix Guo			else:
1866ebf5866SFelix Guo				return True
1876ebf5866SFelix Guo		else:
1886ebf5866SFelix Guo			print('Generating .config ...')
1890476e69fSGreg Thelen			return self.build_config(build_dir, make_options)
1906ebf5866SFelix Guo
19109641f7cSDaniel Latypov	def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
1926ebf5866SFelix Guo		try:
19367e2fae3SBrendan Higgins			if alltests:
19467e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
1950476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1960476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
1976ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
1986ebf5866SFelix Guo			logging.error(e)
1996ebf5866SFelix Guo			return False
200dde54b94SHeidi Fahim		return self.validate_config(build_dir)
2016ebf5866SFelix Guo
20209641f7cSDaniel Latypov	def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]:
20365a4e529SDavid Gow		args.extend(['mem=1G', 'console=tty'])
204128dc4bcSAndy Shevchenko		self._ops.linux_bin(args, timeout, build_dir)
205128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
206021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
207021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
208021ed9f5SHeidi Fahim			for line in file:
209021ed9f5SHeidi Fahim				yield line
210021ed9f5SHeidi Fahim
21109641f7cSDaniel Latypov	def signal_handler(self, sig, frame) -> None:
212021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
213021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
214