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
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'], stderr=subprocess.STDOUT)
38		except OSError as e:
39			raise ConfigError('Could not call make command: ' + str(e))
40		except subprocess.CalledProcessError as e:
41			raise ConfigError(e.output.decode())
42
43	def make_olddefconfig(self, build_dir, make_options):
44		command = ['make', 'ARCH=um', 'olddefconfig']
45		if make_options:
46			command.extend(make_options)
47		if build_dir:
48			command += ['O=' + build_dir]
49		try:
50			subprocess.check_output(command, stderr=subprocess.STDOUT)
51		except OSError as e:
52			raise ConfigError('Could not call make command: ' + str(e))
53		except subprocess.CalledProcessError as e:
54			raise ConfigError(e.output.decode())
55
56	def make_allyesconfig(self, build_dir, make_options):
57		kunit_parser.print_with_timestamp(
58			'Enabling all CONFIGs for UML...')
59		command = ['make', 'ARCH=um', 'allyesconfig']
60		if make_options:
61			command.extend(make_options)
62		if build_dir:
63			command += ['O=' + build_dir]
64		process = subprocess.Popen(
65			command,
66			stdout=subprocess.DEVNULL,
67			stderr=subprocess.STDOUT)
68		process.wait()
69		kunit_parser.print_with_timestamp(
70			'Disabling broken configs to run KUnit tests...')
71		with ExitStack() as es:
72			config = open(get_kconfig_path(build_dir), 'a')
73			disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
74			config.write(disable)
75		kunit_parser.print_with_timestamp(
76			'Starting Kernel with all configs takes a few minutes...')
77
78	def make(self, jobs, build_dir, make_options):
79		command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
80		if make_options:
81			command.extend(make_options)
82		if build_dir:
83			command += ['O=' + build_dir]
84		try:
85			subprocess.check_output(command, stderr=subprocess.STDOUT)
86		except OSError as e:
87			raise BuildError('Could not call execute make: ' + str(e))
88		except subprocess.CalledProcessError as e:
89			raise BuildError(e.output.decode())
90
91	def linux_bin(self, params, timeout, build_dir, outfile):
92		"""Runs the Linux UML binary. Must be named 'linux'."""
93		linux_bin = './linux'
94		if build_dir:
95			linux_bin = os.path.join(build_dir, 'linux')
96		with open(outfile, 'w') as output:
97			process = subprocess.Popen([linux_bin] + params,
98						   stdout=output,
99						   stderr=subprocess.STDOUT)
100			process.wait(timeout)
101
102
103def get_kconfig_path(build_dir):
104	kconfig_path = KCONFIG_PATH
105	if build_dir:
106		kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
107	return kconfig_path
108
109class LinuxSourceTree(object):
110	"""Represents a Linux kernel source tree with KUnit tests."""
111
112	def __init__(self):
113		self._kconfig = kunit_config.Kconfig()
114		self._kconfig.read_from_file(kunitconfig_path)
115		self._ops = LinuxSourceTreeOperations()
116		signal.signal(signal.SIGINT, self.signal_handler)
117
118	def clean(self):
119		try:
120			self._ops.make_mrproper()
121		except ConfigError as e:
122			logging.error(e)
123			return False
124		return True
125
126	def validate_config(self, build_dir):
127		kconfig_path = get_kconfig_path(build_dir)
128		validated_kconfig = kunit_config.Kconfig()
129		validated_kconfig.read_from_file(kconfig_path)
130		if not self._kconfig.is_subset_of(validated_kconfig):
131			invalid = self._kconfig.entries() - validated_kconfig.entries()
132			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
133					  'but not in .config: %s' % (
134					', '.join([str(e) for e in invalid])
135			)
136			logging.error(message)
137			return False
138		return True
139
140	def build_config(self, build_dir, make_options):
141		kconfig_path = get_kconfig_path(build_dir)
142		if build_dir and not os.path.exists(build_dir):
143			os.mkdir(build_dir)
144		self._kconfig.write_to_file(kconfig_path)
145		try:
146			self._ops.make_olddefconfig(build_dir, make_options)
147		except ConfigError as e:
148			logging.error(e)
149			return False
150		return self.validate_config(build_dir)
151
152	def build_reconfig(self, build_dir, make_options):
153		"""Creates a new .config if it is not a subset of the .kunitconfig."""
154		kconfig_path = get_kconfig_path(build_dir)
155		if os.path.exists(kconfig_path):
156			existing_kconfig = kunit_config.Kconfig()
157			existing_kconfig.read_from_file(kconfig_path)
158			if not self._kconfig.is_subset_of(existing_kconfig):
159				print('Regenerating .config ...')
160				os.remove(kconfig_path)
161				return self.build_config(build_dir, make_options)
162			else:
163				return True
164		else:
165			print('Generating .config ...')
166			return self.build_config(build_dir, make_options)
167
168	def build_um_kernel(self, alltests, jobs, build_dir, make_options):
169		try:
170			if alltests:
171				self._ops.make_allyesconfig(build_dir, make_options)
172			self._ops.make_olddefconfig(build_dir, make_options)
173			self._ops.make(jobs, build_dir, make_options)
174		except (ConfigError, BuildError) as e:
175			logging.error(e)
176			return False
177		return self.validate_config(build_dir)
178
179	def run_kernel(self, args=[], build_dir='', timeout=None):
180		args.extend(['mem=1G'])
181		outfile = 'test.log'
182		self._ops.linux_bin(args, timeout, build_dir, outfile)
183		subprocess.call(['stty', 'sane'])
184		with open(outfile, 'r') as file:
185			for line in file:
186				yield line
187
188	def signal_handler(self, sig, frame):
189		logging.error('Build interruption occurred. Cleaning console.')
190		subprocess.call(['stty', 'sane'])
191