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
14*09641f7cSDaniel 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
43*09641f7cSDaniel 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
51*09641f7cSDaniel 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
64*09641f7cSDaniel 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
86*09641f7cSDaniel 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
104*09641f7cSDaniel 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
114*09641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str:
115f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KCONFIG_PATH)
1166ebf5866SFelix Guo
117*09641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str:
118f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KUNITCONFIG_PATH)
119fcdb0bc0SAndy Shevchenko
120*09641f7cSDaniel 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*09641f7cSDaniel Latypov	def __init__(self) -> None:
1276ebf5866SFelix Guo		self._ops = LinuxSourceTreeOperations()
128021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
1296ebf5866SFelix Guo
130*09641f7cSDaniel Latypov	def clean(self) -> bool:
1316ebf5866SFelix Guo		try:
1326ebf5866SFelix Guo			self._ops.make_mrproper()
1336ebf5866SFelix Guo		except ConfigError as e:
1346ebf5866SFelix Guo			logging.error(e)
1356ebf5866SFelix Guo			return False
1366ebf5866SFelix Guo		return True
1376ebf5866SFelix Guo
138*09641f7cSDaniel Latypov	def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None:
139fcdb0bc0SAndy Shevchenko		kunitconfig_path = get_kunitconfig_path(build_dir)
140fcdb0bc0SAndy Shevchenko		if not os.path.exists(kunitconfig_path):
141fcdb0bc0SAndy Shevchenko			shutil.copyfile(defconfig, kunitconfig_path)
142fcdb0bc0SAndy Shevchenko
143*09641f7cSDaniel Latypov	def read_kunitconfig(self, build_dir) -> None:
144fcdb0bc0SAndy Shevchenko		kunitconfig_path = get_kunitconfig_path(build_dir)
145fcdb0bc0SAndy Shevchenko		self._kconfig = kunit_config.Kconfig()
146fcdb0bc0SAndy Shevchenko		self._kconfig.read_from_file(kunitconfig_path)
147fcdb0bc0SAndy Shevchenko
148*09641f7cSDaniel Latypov	def validate_config(self, build_dir) -> bool:
149dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
150dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
151dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
152dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
153dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
154dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
155dde54b94SHeidi Fahim					  'but not in .config: %s' % (
156dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
157dde54b94SHeidi Fahim			)
158dde54b94SHeidi Fahim			logging.error(message)
159dde54b94SHeidi Fahim			return False
160dde54b94SHeidi Fahim		return True
161dde54b94SHeidi Fahim
162*09641f7cSDaniel Latypov	def build_config(self, build_dir, make_options) -> bool:
1636ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1646ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
1656ebf5866SFelix Guo			os.mkdir(build_dir)
1666ebf5866SFelix Guo		self._kconfig.write_to_file(kconfig_path)
1676ebf5866SFelix Guo		try:
1680476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1696ebf5866SFelix Guo		except ConfigError as e:
1706ebf5866SFelix Guo			logging.error(e)
1716ebf5866SFelix Guo			return False
172dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1736ebf5866SFelix Guo
174*09641f7cSDaniel Latypov	def build_reconfig(self, build_dir, make_options) -> bool:
17514ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1766ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1776ebf5866SFelix Guo		if os.path.exists(kconfig_path):
1786ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
1796ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
1806ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
1816ebf5866SFelix Guo				print('Regenerating .config ...')
1826ebf5866SFelix Guo				os.remove(kconfig_path)
1830476e69fSGreg Thelen				return self.build_config(build_dir, make_options)
1846ebf5866SFelix Guo			else:
1856ebf5866SFelix Guo				return True
1866ebf5866SFelix Guo		else:
1876ebf5866SFelix Guo			print('Generating .config ...')
1880476e69fSGreg Thelen			return self.build_config(build_dir, make_options)
1896ebf5866SFelix Guo
190*09641f7cSDaniel Latypov	def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
1916ebf5866SFelix Guo		try:
19267e2fae3SBrendan Higgins			if alltests:
19367e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
1940476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1950476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
1966ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
1976ebf5866SFelix Guo			logging.error(e)
1986ebf5866SFelix Guo			return False
199dde54b94SHeidi Fahim		return self.validate_config(build_dir)
2006ebf5866SFelix Guo
201*09641f7cSDaniel Latypov	def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]:
20265a4e529SDavid Gow		args.extend(['mem=1G', 'console=tty'])
203128dc4bcSAndy Shevchenko		self._ops.linux_bin(args, timeout, build_dir)
204128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
205021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
206021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
207021ed9f5SHeidi Fahim			for line in file:
208021ed9f5SHeidi Fahim				yield line
209021ed9f5SHeidi Fahim
210*09641f7cSDaniel Latypov	def signal_handler(self, sig, frame) -> None:
211021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
212021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
213