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