1 # SPDX-License-Identifier: GPL-2.0
3 # Runs UML kernel, collects output, and handles errors.
5 # Copyright (C) 2019, Google LLC.
6 # Author: Felix Guo <felixguoxiuping@gmail.com>
7 # Author: Brendan Higgins <brendanhiggins@google.com>
15 from contextlib import ExitStack
20 KCONFIG_PATH = '.config'
21 kunitconfig_path = '.kunitconfig'
22 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
24 class ConfigError(Exception):
25 """Represents an error trying to configure the Linux kernel."""
28 class BuildError(Exception):
29 """Represents an error trying to build the Linux kernel."""
32 class LinuxSourceTreeOperations(object):
33 """An abstraction over command line operations performed on a source tree."""
35 def make_mrproper(self):
37 subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
39 raise ConfigError('Could not call make command: ' + str(e))
40 except subprocess.CalledProcessError as e:
41 raise ConfigError(e.output.decode())
43 def make_olddefconfig(self, build_dir, make_options):
44 command = ['make', 'ARCH=um', 'olddefconfig']
46 command.extend(make_options)
48 command += ['O=' + build_dir]
50 subprocess.check_output(command, stderr=subprocess.STDOUT)
52 raise ConfigError('Could not call make command: ' + str(e))
53 except subprocess.CalledProcessError as e:
54 raise ConfigError(e.output.decode())
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']
61 command.extend(make_options)
63 command += ['O=' + build_dir]
64 process = subprocess.Popen(
66 stdout=subprocess.DEVNULL,
67 stderr=subprocess.STDOUT)
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()
75 kunit_parser.print_with_timestamp(
76 'Starting Kernel with all configs takes a few minutes...')
78 def make(self, jobs, build_dir, make_options):
79 command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
81 command.extend(make_options)
83 command += ['O=' + build_dir]
85 subprocess.check_output(command, stderr=subprocess.STDOUT)
87 raise BuildError('Could not call execute make: ' + str(e))
88 except subprocess.CalledProcessError as e:
89 raise BuildError(e.output.decode())
91 def linux_bin(self, params, timeout, build_dir, outfile):
92 """Runs the Linux UML binary. Must be named 'linux'."""
95 linux_bin = os.path.join(build_dir, 'linux')
96 with open(outfile, 'w') as output:
97 process = subprocess.Popen([linux_bin] + params,
99 stderr=subprocess.STDOUT)
100 process.wait(timeout)
103 def get_kconfig_path(build_dir):
104 kconfig_path = KCONFIG_PATH
106 kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
109 class LinuxSourceTree(object):
110 """Represents a Linux kernel source tree with KUnit tests."""
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)
120 self._ops.make_mrproper()
121 except ConfigError as e:
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])
136 logging.error(message)
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):
144 self._kconfig.write_to_file(kconfig_path)
146 self._ops.make_olddefconfig(build_dir, make_options)
147 except ConfigError as e:
150 return self.validate_config(build_dir)
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)
165 print('Generating .config ...')
166 return self.build_config(build_dir, make_options)
168 def build_um_kernel(self, alltests, jobs, build_dir, make_options):
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:
177 return self.validate_config(build_dir)
179 def run_kernel(self, args=[], build_dir='', timeout=None):
180 args.extend(['mem=1G'])
182 self._ops.linux_bin(args, timeout, build_dir, outfile)
183 subprocess.call(['stty', 'sane'])
184 with open(outfile, 'r') as file:
188 def signal_handler(self, sig, frame):
189 logging.error('Build interruption occurred. Cleaning console.')
190 subprocess.call(['stty', 'sane'])