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