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