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