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