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 26def get_file_path(build_dir, default): 27 if build_dir: 28 default = os.path.join(build_dir, default) 29 return default 30 31class ConfigError(Exception): 32 """Represents an error trying to configure the Linux kernel.""" 33 34 35class BuildError(Exception): 36 """Represents an error trying to build the Linux kernel.""" 37 38 39class LinuxSourceTreeOperations(object): 40 """An abstraction over command line operations performed on a source tree.""" 41 42 def make_mrproper(self): 43 try: 44 subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 45 except OSError as e: 46 raise ConfigError('Could not call make command: ' + str(e)) 47 except subprocess.CalledProcessError as e: 48 raise ConfigError(e.output.decode()) 49 50 def make_olddefconfig(self, build_dir, make_options): 51 command = ['make', 'ARCH=um', 'olddefconfig'] 52 if make_options: 53 command.extend(make_options) 54 if build_dir: 55 command += ['O=' + build_dir] 56 try: 57 subprocess.check_output(command, stderr=subprocess.STDOUT) 58 except OSError as e: 59 raise ConfigError('Could not call make command: ' + str(e)) 60 except subprocess.CalledProcessError as e: 61 raise ConfigError(e.output.decode()) 62 63 def make_allyesconfig(self, build_dir, make_options): 64 kunit_parser.print_with_timestamp( 65 'Enabling all CONFIGs for UML...') 66 command = ['make', 'ARCH=um', 'allyesconfig'] 67 if make_options: 68 command.extend(make_options) 69 if build_dir: 70 command += ['O=' + build_dir] 71 process = subprocess.Popen( 72 command, 73 stdout=subprocess.DEVNULL, 74 stderr=subprocess.STDOUT) 75 process.wait() 76 kunit_parser.print_with_timestamp( 77 'Disabling broken configs to run KUnit tests...') 78 with ExitStack() as es: 79 config = open(get_kconfig_path(build_dir), 'a') 80 disable = open(BROKEN_ALLCONFIG_PATH, 'r').read() 81 config.write(disable) 82 kunit_parser.print_with_timestamp( 83 'Starting Kernel with all configs takes a few minutes...') 84 85 def make(self, jobs, build_dir, make_options): 86 command = ['make', 'ARCH=um', '--jobs=' + str(jobs)] 87 if make_options: 88 command.extend(make_options) 89 if build_dir: 90 command += ['O=' + build_dir] 91 try: 92 proc = subprocess.Popen(command, 93 stderr=subprocess.PIPE, 94 stdout=subprocess.DEVNULL) 95 except OSError as e: 96 raise BuildError('Could not call make command: ' + str(e)) 97 _, stderr = proc.communicate() 98 if proc.returncode != 0: 99 raise BuildError(stderr.decode()) 100 if stderr: # likely only due to build warnings 101 print(stderr.decode()) 102 103 def linux_bin(self, params, timeout, build_dir): 104 """Runs the Linux UML binary. Must be named 'linux'.""" 105 linux_bin = get_file_path(build_dir, 'linux') 106 outfile = get_outfile_path(build_dir) 107 with open(outfile, 'w') as output: 108 process = subprocess.Popen([linux_bin] + params, 109 stdout=output, 110 stderr=subprocess.STDOUT) 111 process.wait(timeout) 112 113def get_kconfig_path(build_dir): 114 return get_file_path(build_dir, KCONFIG_PATH) 115 116def get_kunitconfig_path(build_dir): 117 return get_file_path(build_dir, KUNITCONFIG_PATH) 118 119def get_outfile_path(build_dir): 120 return get_file_path(build_dir, OUTFILE_PATH) 121 122class LinuxSourceTree(object): 123 """Represents a Linux kernel source tree with KUnit tests.""" 124 125 def __init__(self): 126 self._ops = LinuxSourceTreeOperations() 127 signal.signal(signal.SIGINT, self.signal_handler) 128 129 def clean(self): 130 try: 131 self._ops.make_mrproper() 132 except ConfigError as e: 133 logging.error(e) 134 return False 135 return True 136 137 def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH): 138 kunitconfig_path = get_kunitconfig_path(build_dir) 139 if not os.path.exists(kunitconfig_path): 140 shutil.copyfile(defconfig, kunitconfig_path) 141 142 def read_kunitconfig(self, build_dir): 143 kunitconfig_path = get_kunitconfig_path(build_dir) 144 self._kconfig = kunit_config.Kconfig() 145 self._kconfig.read_from_file(kunitconfig_path) 146 147 def validate_config(self, build_dir): 148 kconfig_path = get_kconfig_path(build_dir) 149 validated_kconfig = kunit_config.Kconfig() 150 validated_kconfig.read_from_file(kconfig_path) 151 if not self._kconfig.is_subset_of(validated_kconfig): 152 invalid = self._kconfig.entries() - validated_kconfig.entries() 153 message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 154 'but not in .config: %s' % ( 155 ', '.join([str(e) for e in invalid]) 156 ) 157 logging.error(message) 158 return False 159 return True 160 161 def build_config(self, build_dir, make_options): 162 kconfig_path = get_kconfig_path(build_dir) 163 if build_dir and not os.path.exists(build_dir): 164 os.mkdir(build_dir) 165 self._kconfig.write_to_file(kconfig_path) 166 try: 167 self._ops.make_olddefconfig(build_dir, make_options) 168 except ConfigError as e: 169 logging.error(e) 170 return False 171 return self.validate_config(build_dir) 172 173 def build_reconfig(self, build_dir, make_options): 174 """Creates a new .config if it is not a subset of the .kunitconfig.""" 175 kconfig_path = get_kconfig_path(build_dir) 176 if os.path.exists(kconfig_path): 177 existing_kconfig = kunit_config.Kconfig() 178 existing_kconfig.read_from_file(kconfig_path) 179 if not self._kconfig.is_subset_of(existing_kconfig): 180 print('Regenerating .config ...') 181 os.remove(kconfig_path) 182 return self.build_config(build_dir, make_options) 183 else: 184 return True 185 else: 186 print('Generating .config ...') 187 return self.build_config(build_dir, make_options) 188 189 def build_um_kernel(self, alltests, jobs, build_dir, make_options): 190 try: 191 if alltests: 192 self._ops.make_allyesconfig(build_dir, make_options) 193 self._ops.make_olddefconfig(build_dir, make_options) 194 self._ops.make(jobs, build_dir, make_options) 195 except (ConfigError, BuildError) as e: 196 logging.error(e) 197 return False 198 return self.validate_config(build_dir) 199 200 def run_kernel(self, args=[], build_dir='', timeout=None): 201 args.extend(['mem=1G']) 202 self._ops.linux_bin(args, timeout, build_dir) 203 outfile = get_outfile_path(build_dir) 204 subprocess.call(['stty', 'sane']) 205 with open(outfile, 'r') as file: 206 for line in file: 207 yield line 208 209 def signal_handler(self, sig, frame): 210 logging.error('Build interruption occurred. Cleaning console.') 211 subprocess.call(['stty', 'sane']) 212