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 <[email protected]> 7# Author: Brendan Higgins <[email protected]> 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']) 38 except OSError as e: 39 raise ConfigError('Could not call make command: ' + e) 40 except subprocess.CalledProcessError as e: 41 raise ConfigError(e.output) 42 43 def make_olddefconfig(self, build_dir): 44 command = ['make', 'ARCH=um', 'olddefconfig'] 45 if build_dir: 46 command += ['O=' + build_dir] 47 try: 48 subprocess.check_output(command, stderr=subprocess.PIPE) 49 except OSError as e: 50 raise ConfigError('Could not call make command: ' + e) 51 except subprocess.CalledProcessError as e: 52 raise ConfigError(e.output) 53 54 def make_allyesconfig(self): 55 kunit_parser.print_with_timestamp( 56 'Enabling all CONFIGs for UML...') 57 process = subprocess.Popen( 58 ['make', 'ARCH=um', 'allyesconfig'], 59 stdout=subprocess.DEVNULL, 60 stderr=subprocess.STDOUT) 61 process.wait() 62 kunit_parser.print_with_timestamp( 63 'Disabling broken configs to run KUnit tests...') 64 with ExitStack() as es: 65 config = open(KCONFIG_PATH, 'a') 66 disable = open(BROKEN_ALLCONFIG_PATH, 'r').read() 67 config.write(disable) 68 kunit_parser.print_with_timestamp( 69 'Starting Kernel with all configs takes a few minutes...') 70 71 def make(self, jobs, build_dir): 72 command = ['make', 'ARCH=um', '--jobs=' + str(jobs)] 73 if build_dir: 74 command += ['O=' + build_dir] 75 try: 76 subprocess.check_output(command) 77 except OSError as e: 78 raise BuildError('Could not call execute make: ' + e) 79 except subprocess.CalledProcessError as e: 80 raise BuildError(e.output) 81 82 def linux_bin(self, params, timeout, build_dir, outfile): 83 """Runs the Linux UML binary. Must be named 'linux'.""" 84 linux_bin = './linux' 85 if build_dir: 86 linux_bin = os.path.join(build_dir, 'linux') 87 with open(outfile, 'w') as output: 88 process = subprocess.Popen([linux_bin] + params, 89 stdout=output, 90 stderr=subprocess.STDOUT) 91 process.wait(timeout) 92 93 94def get_kconfig_path(build_dir): 95 kconfig_path = KCONFIG_PATH 96 if build_dir: 97 kconfig_path = os.path.join(build_dir, KCONFIG_PATH) 98 return kconfig_path 99 100class LinuxSourceTree(object): 101 """Represents a Linux kernel source tree with KUnit tests.""" 102 103 def __init__(self): 104 self._kconfig = kunit_config.Kconfig() 105 self._kconfig.read_from_file(kunitconfig_path) 106 self._ops = LinuxSourceTreeOperations() 107 signal.signal(signal.SIGINT, self.signal_handler) 108 109 def clean(self): 110 try: 111 self._ops.make_mrproper() 112 except ConfigError as e: 113 logging.error(e) 114 return False 115 return True 116 117 def validate_config(self, build_dir): 118 kconfig_path = get_kconfig_path(build_dir) 119 validated_kconfig = kunit_config.Kconfig() 120 validated_kconfig.read_from_file(kconfig_path) 121 if not self._kconfig.is_subset_of(validated_kconfig): 122 invalid = self._kconfig.entries() - validated_kconfig.entries() 123 message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 124 'but not in .config: %s' % ( 125 ', '.join([str(e) for e in invalid]) 126 ) 127 logging.error(message) 128 return False 129 return True 130 131 def build_config(self, build_dir): 132 kconfig_path = get_kconfig_path(build_dir) 133 if build_dir and not os.path.exists(build_dir): 134 os.mkdir(build_dir) 135 self._kconfig.write_to_file(kconfig_path) 136 try: 137 self._ops.make_olddefconfig(build_dir) 138 except ConfigError as e: 139 logging.error(e) 140 return False 141 return self.validate_config(build_dir) 142 143 def build_reconfig(self, build_dir): 144 """Creates a new .config if it is not a subset of the .kunitconfig.""" 145 kconfig_path = get_kconfig_path(build_dir) 146 if os.path.exists(kconfig_path): 147 existing_kconfig = kunit_config.Kconfig() 148 existing_kconfig.read_from_file(kconfig_path) 149 if not self._kconfig.is_subset_of(existing_kconfig): 150 print('Regenerating .config ...') 151 os.remove(kconfig_path) 152 return self.build_config(build_dir) 153 else: 154 return True 155 else: 156 print('Generating .config ...') 157 return self.build_config(build_dir) 158 159 def build_um_kernel(self, alltests, jobs, build_dir): 160 if alltests: 161 self._ops.make_allyesconfig() 162 try: 163 self._ops.make_olddefconfig(build_dir) 164 self._ops.make(jobs, build_dir) 165 except (ConfigError, BuildError) as e: 166 logging.error(e) 167 return False 168 return self.validate_config(build_dir) 169 170 def run_kernel(self, args=[], build_dir='', timeout=None): 171 args.extend(['mem=1G']) 172 outfile = 'test.log' 173 self._ops.linux_bin(args, timeout, build_dir, outfile) 174 subprocess.call(['stty', 'sane']) 175 with open(outfile, 'r') as file: 176 for line in file: 177 yield line 178 179 def signal_handler(self, sig, frame): 180 logging.error('Build interruption occurred. Cleaning console.') 181 subprocess.call(['stty', 'sane']) 182