blob: 2e3cc0fac726369266414404a6fce81c151b7234 [file] [log] [blame]
Olivier Deprez157378f2022-04-04 15:47:50 +02001# 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'])