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
14021ed9f5SHeidi Fahim
15021ed9f5SHeidi Fahimfrom contextlib import ExitStack
166ebf5866SFelix Guo
176ebf5866SFelix Guoimport kunit_config
18021ed9f5SHeidi Fahimimport kunit_parser
196ebf5866SFelix Guo
206ebf5866SFelix GuoKCONFIG_PATH = '.config'
21fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
22fcdb0bc0SAndy ShevchenkoDEFAULT_KUNITCONFIG_PATH = 'arch/um/configs/kunit_defconfig'
23021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
24128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
256ebf5866SFelix Guo
26f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default):
27f3ed003eSAndy Shevchenko	if build_dir:
28f3ed003eSAndy Shevchenko		default = os.path.join(build_dir, default)
29f3ed003eSAndy Shevchenko	return default
30f3ed003eSAndy Shevchenko
316ebf5866SFelix Guoclass ConfigError(Exception):
326ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
336ebf5866SFelix Guo
346ebf5866SFelix Guo
356ebf5866SFelix Guoclass BuildError(Exception):
366ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
376ebf5866SFelix Guo
386ebf5866SFelix Guo
396ebf5866SFelix Guoclass LinuxSourceTreeOperations(object):
406ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
416ebf5866SFelix Guo
426ebf5866SFelix Guo	def make_mrproper(self):
436ebf5866SFelix Guo		try:
445a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
456ebf5866SFelix Guo		except OSError as e:
461abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
476ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
481abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
496ebf5866SFelix Guo
500476e69fSGreg Thelen	def make_olddefconfig(self, build_dir, make_options):
516ebf5866SFelix Guo		command = ['make', 'ARCH=um', 'olddefconfig']
520476e69fSGreg Thelen		if make_options:
530476e69fSGreg Thelen			command.extend(make_options)
546ebf5866SFelix Guo		if build_dir:
556ebf5866SFelix Guo			command += ['O=' + build_dir]
566ebf5866SFelix Guo		try:
575a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
586ebf5866SFelix Guo		except OSError as e:
591abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
606ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
611abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
626ebf5866SFelix Guo
6367e2fae3SBrendan Higgins	def make_allyesconfig(self, build_dir, make_options):
64021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
65021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
6667e2fae3SBrendan Higgins		command = ['make', 'ARCH=um', 'allyesconfig']
6767e2fae3SBrendan Higgins		if make_options:
6867e2fae3SBrendan Higgins			command.extend(make_options)
6967e2fae3SBrendan Higgins		if build_dir:
7067e2fae3SBrendan Higgins			command += ['O=' + build_dir]
71021ed9f5SHeidi Fahim		process = subprocess.Popen(
7267e2fae3SBrendan Higgins			command,
73021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
74021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
75021ed9f5SHeidi Fahim		process.wait()
76021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
77021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
78021ed9f5SHeidi Fahim		with ExitStack() as es:
7967e2fae3SBrendan Higgins			config = open(get_kconfig_path(build_dir), 'a')
80021ed9f5SHeidi Fahim			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
81021ed9f5SHeidi Fahim			config.write(disable)
82021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
83021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
84021ed9f5SHeidi Fahim
850476e69fSGreg Thelen	def make(self, jobs, build_dir, make_options):
866ebf5866SFelix Guo		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
870476e69fSGreg Thelen		if make_options:
880476e69fSGreg Thelen			command.extend(make_options)
896ebf5866SFelix Guo		if build_dir:
906ebf5866SFelix Guo			command += ['O=' + build_dir]
916ebf5866SFelix Guo		try:
9239088144SDaniel Latypov			proc = subprocess.Popen(command,
9339088144SDaniel Latypov						stderr=subprocess.PIPE,
9439088144SDaniel Latypov						stdout=subprocess.DEVNULL)
956ebf5866SFelix Guo		except OSError as e:
9639088144SDaniel Latypov			raise BuildError('Could not call make command: ' + str(e))
9739088144SDaniel Latypov		_, stderr = proc.communicate()
9839088144SDaniel Latypov		if proc.returncode != 0:
9939088144SDaniel Latypov			raise BuildError(stderr.decode())
10039088144SDaniel Latypov		if stderr:  # likely only due to build warnings
10139088144SDaniel Latypov			print(stderr.decode())
1026ebf5866SFelix Guo
103128dc4bcSAndy Shevchenko	def linux_bin(self, params, timeout, build_dir):
1046ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
105f3ed003eSAndy Shevchenko		linux_bin = get_file_path(build_dir, 'linux')
106128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
107021ed9f5SHeidi Fahim		with open(outfile, 'w') as output:
108021ed9f5SHeidi Fahim			process = subprocess.Popen([linux_bin] + params,
109021ed9f5SHeidi Fahim						   stdout=output,
110021ed9f5SHeidi Fahim						   stderr=subprocess.STDOUT)
111021ed9f5SHeidi Fahim			process.wait(timeout)
1126ebf5866SFelix Guo
1136ebf5866SFelix Guodef get_kconfig_path(build_dir):
114f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KCONFIG_PATH)
1156ebf5866SFelix Guo
116fcdb0bc0SAndy Shevchenkodef get_kunitconfig_path(build_dir):
117f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KUNITCONFIG_PATH)
118fcdb0bc0SAndy Shevchenko
119128dc4bcSAndy Shevchenkodef get_outfile_path(build_dir):
120f3ed003eSAndy Shevchenko	return get_file_path(build_dir, OUTFILE_PATH)
121128dc4bcSAndy Shevchenko
1226ebf5866SFelix Guoclass LinuxSourceTree(object):
1236ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
1246ebf5866SFelix Guo
1256ebf5866SFelix Guo	def __init__(self):
1266ebf5866SFelix Guo		self._ops = LinuxSourceTreeOperations()
127021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
1286ebf5866SFelix Guo
1296ebf5866SFelix Guo	def clean(self):
1306ebf5866SFelix Guo		try:
1316ebf5866SFelix Guo			self._ops.make_mrproper()
1326ebf5866SFelix Guo		except ConfigError as e:
1336ebf5866SFelix Guo			logging.error(e)
1346ebf5866SFelix Guo			return False
1356ebf5866SFelix Guo		return True
1366ebf5866SFelix Guo
137fcdb0bc0SAndy Shevchenko	def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH):
138fcdb0bc0SAndy Shevchenko		kunitconfig_path = get_kunitconfig_path(build_dir)
139fcdb0bc0SAndy Shevchenko		if not os.path.exists(kunitconfig_path):
140fcdb0bc0SAndy Shevchenko			shutil.copyfile(defconfig, kunitconfig_path)
141fcdb0bc0SAndy Shevchenko
142fcdb0bc0SAndy Shevchenko	def read_kunitconfig(self, build_dir):
143fcdb0bc0SAndy Shevchenko		kunitconfig_path = get_kunitconfig_path(build_dir)
144fcdb0bc0SAndy Shevchenko		self._kconfig = kunit_config.Kconfig()
145fcdb0bc0SAndy Shevchenko		self._kconfig.read_from_file(kunitconfig_path)
146fcdb0bc0SAndy Shevchenko
147dde54b94SHeidi Fahim	def validate_config(self, build_dir):
148dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
149dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
150dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
151dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
152dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
153dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
154dde54b94SHeidi Fahim					  'but not in .config: %s' % (
155dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
156dde54b94SHeidi Fahim			)
157dde54b94SHeidi Fahim			logging.error(message)
158dde54b94SHeidi Fahim			return False
159dde54b94SHeidi Fahim		return True
160dde54b94SHeidi Fahim
1610476e69fSGreg Thelen	def build_config(self, build_dir, make_options):
1626ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1636ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
1646ebf5866SFelix Guo			os.mkdir(build_dir)
1656ebf5866SFelix Guo		self._kconfig.write_to_file(kconfig_path)
1666ebf5866SFelix Guo		try:
1670476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1686ebf5866SFelix Guo		except ConfigError as e:
1696ebf5866SFelix Guo			logging.error(e)
1706ebf5866SFelix Guo			return False
171dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1726ebf5866SFelix Guo
1730476e69fSGreg Thelen	def build_reconfig(self, build_dir, make_options):
17414ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
1756ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
1766ebf5866SFelix Guo		if os.path.exists(kconfig_path):
1776ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
1786ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
1796ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
1806ebf5866SFelix Guo				print('Regenerating .config ...')
1816ebf5866SFelix Guo				os.remove(kconfig_path)
1820476e69fSGreg Thelen				return self.build_config(build_dir, make_options)
1836ebf5866SFelix Guo			else:
1846ebf5866SFelix Guo				return True
1856ebf5866SFelix Guo		else:
1866ebf5866SFelix Guo			print('Generating .config ...')
1870476e69fSGreg Thelen			return self.build_config(build_dir, make_options)
1886ebf5866SFelix Guo
1890476e69fSGreg Thelen	def build_um_kernel(self, alltests, jobs, build_dir, make_options):
1906ebf5866SFelix Guo		try:
19167e2fae3SBrendan Higgins			if alltests:
19267e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
1930476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
1940476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
1956ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
1966ebf5866SFelix Guo			logging.error(e)
1976ebf5866SFelix Guo			return False
198dde54b94SHeidi Fahim		return self.validate_config(build_dir)
1996ebf5866SFelix Guo
200021ed9f5SHeidi Fahim	def run_kernel(self, args=[], build_dir='', timeout=None):
201*65a4e529SDavid Gow		args.extend(['mem=1G', 'console=tty'])
202128dc4bcSAndy Shevchenko		self._ops.linux_bin(args, timeout, build_dir)
203128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
204021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
205021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
206021ed9f5SHeidi Fahim			for line in file:
207021ed9f5SHeidi Fahim				yield line
208021ed9f5SHeidi Fahim
209021ed9f5SHeidi Fahim	def signal_handler(self, sig, frame):
210021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
211021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
212