1# SPDX-License-Identifier: GPL-2.0
2#
3# Runs UML kernel, collects output, and handles errors.
4#
5# Copyright (C) 2019, Google LLC.
6# Author: Felix Guo <[email protected]>
7# Author: Brendan Higgins <[email protected]>
8
9
10import logging
11import subprocess
12import os
13import signal
14
15from contextlib import ExitStack
16
17import kunit_config
18import kunit_parser
19
20KCONFIG_PATH = '.config'
21kunitconfig_path = '.kunitconfig'
22BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
23
24class ConfigError(Exception):
25	"""Represents an error trying to configure the Linux kernel."""
26
27
28class BuildError(Exception):
29	"""Represents an error trying to build the Linux kernel."""
30
31
32class LinuxSourceTreeOperations(object):
33	"""An abstraction over command line operations performed on a source tree."""
34
35	def make_mrproper(self):
36		try:
37			subprocess.check_output(['make', 'mrproper'])
38		except OSError as e:
39			raise ConfigError('Could not call make command: ' + e)
40		except subprocess.CalledProcessError as e:
41			raise ConfigError(e.output)
42
43	def make_olddefconfig(self, build_dir):
44		command = ['make', 'ARCH=um', 'olddefconfig']
45		if build_dir:
46			command += ['O=' + build_dir]
47		try:
48			subprocess.check_output(command, stderr=subprocess.PIPE)
49		except OSError as e:
50			raise ConfigError('Could not call make command: ' + e)
51		except subprocess.CalledProcessError as e:
52			raise ConfigError(e.output)
53
54	def make_allyesconfig(self):
55		kunit_parser.print_with_timestamp(
56			'Enabling all CONFIGs for UML...')
57		process = subprocess.Popen(
58			['make', 'ARCH=um', 'allyesconfig'],
59			stdout=subprocess.DEVNULL,
60			stderr=subprocess.STDOUT)
61		process.wait()
62		kunit_parser.print_with_timestamp(
63			'Disabling broken configs to run KUnit tests...')
64		with ExitStack() as es:
65			config = open(KCONFIG_PATH, 'a')
66			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
67			config.write(disable)
68		kunit_parser.print_with_timestamp(
69			'Starting Kernel with all configs takes a few minutes...')
70
71	def make(self, jobs, build_dir):
72		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
73		if build_dir:
74			command += ['O=' + build_dir]
75		try:
76			subprocess.check_output(command)
77		except OSError as e:
78			raise BuildError('Could not call execute make: ' + e)
79		except subprocess.CalledProcessError as e:
80			raise BuildError(e.output)
81
82	def linux_bin(self, params, timeout, build_dir, outfile):
83		"""Runs the Linux UML binary. Must be named 'linux'."""
84		linux_bin = './linux'
85		if build_dir:
86			linux_bin = os.path.join(build_dir, 'linux')
87		with open(outfile, 'w') as output:
88			process = subprocess.Popen([linux_bin] + params,
89						   stdout=output,
90						   stderr=subprocess.STDOUT)
91			process.wait(timeout)
92
93
94def get_kconfig_path(build_dir):
95	kconfig_path = KCONFIG_PATH
96	if build_dir:
97		kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
98	return kconfig_path
99
100class LinuxSourceTree(object):
101	"""Represents a Linux kernel source tree with KUnit tests."""
102
103	def __init__(self):
104		self._kconfig = kunit_config.Kconfig()
105		self._kconfig.read_from_file(kunitconfig_path)
106		self._ops = LinuxSourceTreeOperations()
107		signal.signal(signal.SIGINT, self.signal_handler)
108
109	def clean(self):
110		try:
111			self._ops.make_mrproper()
112		except ConfigError as e:
113			logging.error(e)
114			return False
115		return True
116
117	def validate_config(self, build_dir):
118		kconfig_path = get_kconfig_path(build_dir)
119		validated_kconfig = kunit_config.Kconfig()
120		validated_kconfig.read_from_file(kconfig_path)
121		if not self._kconfig.is_subset_of(validated_kconfig):
122			invalid = self._kconfig.entries() - validated_kconfig.entries()
123			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
124					  'but not in .config: %s' % (
125					', '.join([str(e) for e in invalid])
126			)
127			logging.error(message)
128			return False
129		return True
130
131	def build_config(self, build_dir):
132		kconfig_path = get_kconfig_path(build_dir)
133		if build_dir and not os.path.exists(build_dir):
134			os.mkdir(build_dir)
135		self._kconfig.write_to_file(kconfig_path)
136		try:
137			self._ops.make_olddefconfig(build_dir)
138		except ConfigError as e:
139			logging.error(e)
140			return False
141		return self.validate_config(build_dir)
142
143	def build_reconfig(self, build_dir):
144		"""Creates a new .config if it is not a subset of the .kunitconfig."""
145		kconfig_path = get_kconfig_path(build_dir)
146		if os.path.exists(kconfig_path):
147			existing_kconfig = kunit_config.Kconfig()
148			existing_kconfig.read_from_file(kconfig_path)
149			if not self._kconfig.is_subset_of(existing_kconfig):
150				print('Regenerating .config ...')
151				os.remove(kconfig_path)
152				return self.build_config(build_dir)
153			else:
154				return True
155		else:
156			print('Generating .config ...')
157			return self.build_config(build_dir)
158
159	def build_um_kernel(self, alltests, jobs, build_dir):
160		if alltests:
161			self._ops.make_allyesconfig()
162		try:
163			self._ops.make_olddefconfig(build_dir)
164			self._ops.make(jobs, build_dir)
165		except (ConfigError, BuildError) as e:
166			logging.error(e)
167			return False
168		return self.validate_config(build_dir)
169
170	def run_kernel(self, args=[], build_dir='', timeout=None):
171		args.extend(['mem=1G'])
172		outfile = 'test.log'
173		self._ops.linux_bin(args, timeout, build_dir, outfile)
174		subprocess.call(['stty', 'sane'])
175		with open(outfile, 'r') as file:
176			for line in file:
177				yield line
178
179	def signal_handler(self, sig, frame):
180		logging.error('Build interruption occurred. Cleaning console.')
181		subprocess.call(['stty', 'sane'])
182