18c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0 28c2ecf20Sopenharmony_ci# 38c2ecf20Sopenharmony_ci# Runs UML kernel, collects output, and handles errors. 48c2ecf20Sopenharmony_ci# 58c2ecf20Sopenharmony_ci# Copyright (C) 2019, Google LLC. 68c2ecf20Sopenharmony_ci# Author: Felix Guo <felixguoxiuping@gmail.com> 78c2ecf20Sopenharmony_ci# Author: Brendan Higgins <brendanhiggins@google.com> 88c2ecf20Sopenharmony_ci 98c2ecf20Sopenharmony_ciimport logging 108c2ecf20Sopenharmony_ciimport subprocess 118c2ecf20Sopenharmony_ciimport os 128c2ecf20Sopenharmony_ciimport shutil 138c2ecf20Sopenharmony_ciimport signal 148c2ecf20Sopenharmony_ci 158c2ecf20Sopenharmony_cifrom contextlib import ExitStack 168c2ecf20Sopenharmony_ci 178c2ecf20Sopenharmony_ciimport kunit_config 188c2ecf20Sopenharmony_ciimport kunit_parser 198c2ecf20Sopenharmony_ci 208c2ecf20Sopenharmony_ciKCONFIG_PATH = '.config' 218c2ecf20Sopenharmony_ciKUNITCONFIG_PATH = '.kunitconfig' 228c2ecf20Sopenharmony_ciDEFAULT_KUNITCONFIG_PATH = 'arch/um/configs/kunit_defconfig' 238c2ecf20Sopenharmony_ciBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 248c2ecf20Sopenharmony_ciOUTFILE_PATH = 'test.log' 258c2ecf20Sopenharmony_ci 268c2ecf20Sopenharmony_ciclass ConfigError(Exception): 278c2ecf20Sopenharmony_ci """Represents an error trying to configure the Linux kernel.""" 288c2ecf20Sopenharmony_ci 298c2ecf20Sopenharmony_ci 308c2ecf20Sopenharmony_ciclass BuildError(Exception): 318c2ecf20Sopenharmony_ci """Represents an error trying to build the Linux kernel.""" 328c2ecf20Sopenharmony_ci 338c2ecf20Sopenharmony_ci 348c2ecf20Sopenharmony_ciclass LinuxSourceTreeOperations(object): 358c2ecf20Sopenharmony_ci """An abstraction over command line operations performed on a source tree.""" 368c2ecf20Sopenharmony_ci 378c2ecf20Sopenharmony_ci def make_mrproper(self): 388c2ecf20Sopenharmony_ci try: 398c2ecf20Sopenharmony_ci subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 408c2ecf20Sopenharmony_ci except OSError as e: 418c2ecf20Sopenharmony_ci raise ConfigError('Could not call make command: ' + str(e)) 428c2ecf20Sopenharmony_ci except subprocess.CalledProcessError as e: 438c2ecf20Sopenharmony_ci raise ConfigError(e.output.decode()) 448c2ecf20Sopenharmony_ci 458c2ecf20Sopenharmony_ci def make_olddefconfig(self, build_dir, make_options): 468c2ecf20Sopenharmony_ci command = ['make', 'ARCH=um', 'olddefconfig'] 478c2ecf20Sopenharmony_ci if make_options: 488c2ecf20Sopenharmony_ci command.extend(make_options) 498c2ecf20Sopenharmony_ci if build_dir: 508c2ecf20Sopenharmony_ci command += ['O=' + build_dir] 518c2ecf20Sopenharmony_ci try: 528c2ecf20Sopenharmony_ci subprocess.check_output(command, stderr=subprocess.STDOUT) 538c2ecf20Sopenharmony_ci except OSError as e: 548c2ecf20Sopenharmony_ci raise ConfigError('Could not call make command: ' + str(e)) 558c2ecf20Sopenharmony_ci except subprocess.CalledProcessError as e: 568c2ecf20Sopenharmony_ci raise ConfigError(e.output.decode()) 578c2ecf20Sopenharmony_ci 588c2ecf20Sopenharmony_ci def make_allyesconfig(self, build_dir, make_options): 598c2ecf20Sopenharmony_ci kunit_parser.print_with_timestamp( 608c2ecf20Sopenharmony_ci 'Enabling all CONFIGs for UML...') 618c2ecf20Sopenharmony_ci command = ['make', 'ARCH=um', 'allyesconfig'] 628c2ecf20Sopenharmony_ci if make_options: 638c2ecf20Sopenharmony_ci command.extend(make_options) 648c2ecf20Sopenharmony_ci if build_dir: 658c2ecf20Sopenharmony_ci command += ['O=' + build_dir] 668c2ecf20Sopenharmony_ci process = subprocess.Popen( 678c2ecf20Sopenharmony_ci command, 688c2ecf20Sopenharmony_ci stdout=subprocess.DEVNULL, 698c2ecf20Sopenharmony_ci stderr=subprocess.STDOUT) 708c2ecf20Sopenharmony_ci process.wait() 718c2ecf20Sopenharmony_ci kunit_parser.print_with_timestamp( 728c2ecf20Sopenharmony_ci 'Disabling broken configs to run KUnit tests...') 738c2ecf20Sopenharmony_ci with ExitStack() as es: 748c2ecf20Sopenharmony_ci config = open(get_kconfig_path(build_dir), 'a') 758c2ecf20Sopenharmony_ci disable = open(BROKEN_ALLCONFIG_PATH, 'r').read() 768c2ecf20Sopenharmony_ci config.write(disable) 778c2ecf20Sopenharmony_ci kunit_parser.print_with_timestamp( 788c2ecf20Sopenharmony_ci 'Starting Kernel with all configs takes a few minutes...') 798c2ecf20Sopenharmony_ci 808c2ecf20Sopenharmony_ci def make(self, jobs, build_dir, make_options): 818c2ecf20Sopenharmony_ci command = ['make', 'ARCH=um', '--jobs=' + str(jobs)] 828c2ecf20Sopenharmony_ci if make_options: 838c2ecf20Sopenharmony_ci command.extend(make_options) 848c2ecf20Sopenharmony_ci if build_dir: 858c2ecf20Sopenharmony_ci command += ['O=' + build_dir] 868c2ecf20Sopenharmony_ci try: 878c2ecf20Sopenharmony_ci proc = subprocess.Popen(command, 888c2ecf20Sopenharmony_ci stderr=subprocess.PIPE, 898c2ecf20Sopenharmony_ci stdout=subprocess.DEVNULL) 908c2ecf20Sopenharmony_ci except OSError as e: 918c2ecf20Sopenharmony_ci raise BuildError('Could not call make command: ' + str(e)) 928c2ecf20Sopenharmony_ci _, stderr = proc.communicate() 938c2ecf20Sopenharmony_ci if proc.returncode != 0: 948c2ecf20Sopenharmony_ci raise BuildError(stderr.decode()) 958c2ecf20Sopenharmony_ci if stderr: # likely only due to build warnings 968c2ecf20Sopenharmony_ci print(stderr.decode()) 978c2ecf20Sopenharmony_ci 988c2ecf20Sopenharmony_ci def linux_bin(self, params, timeout, build_dir): 998c2ecf20Sopenharmony_ci """Runs the Linux UML binary. Must be named 'linux'.""" 1008c2ecf20Sopenharmony_ci linux_bin = './linux' 1018c2ecf20Sopenharmony_ci if build_dir: 1028c2ecf20Sopenharmony_ci linux_bin = os.path.join(build_dir, 'linux') 1038c2ecf20Sopenharmony_ci outfile = get_outfile_path(build_dir) 1048c2ecf20Sopenharmony_ci with open(outfile, 'w') as output: 1058c2ecf20Sopenharmony_ci process = subprocess.Popen([linux_bin] + params, 1068c2ecf20Sopenharmony_ci stdout=output, 1078c2ecf20Sopenharmony_ci stderr=subprocess.STDOUT) 1088c2ecf20Sopenharmony_ci process.wait(timeout) 1098c2ecf20Sopenharmony_ci 1108c2ecf20Sopenharmony_cidef get_kconfig_path(build_dir): 1118c2ecf20Sopenharmony_ci kconfig_path = KCONFIG_PATH 1128c2ecf20Sopenharmony_ci if build_dir: 1138c2ecf20Sopenharmony_ci kconfig_path = os.path.join(build_dir, KCONFIG_PATH) 1148c2ecf20Sopenharmony_ci return kconfig_path 1158c2ecf20Sopenharmony_ci 1168c2ecf20Sopenharmony_cidef get_kunitconfig_path(build_dir): 1178c2ecf20Sopenharmony_ci kunitconfig_path = KUNITCONFIG_PATH 1188c2ecf20Sopenharmony_ci if build_dir: 1198c2ecf20Sopenharmony_ci kunitconfig_path = os.path.join(build_dir, KUNITCONFIG_PATH) 1208c2ecf20Sopenharmony_ci return kunitconfig_path 1218c2ecf20Sopenharmony_ci 1228c2ecf20Sopenharmony_cidef get_outfile_path(build_dir): 1238c2ecf20Sopenharmony_ci outfile_path = OUTFILE_PATH 1248c2ecf20Sopenharmony_ci if build_dir: 1258c2ecf20Sopenharmony_ci outfile_path = os.path.join(build_dir, OUTFILE_PATH) 1268c2ecf20Sopenharmony_ci return outfile_path 1278c2ecf20Sopenharmony_ci 1288c2ecf20Sopenharmony_ciclass LinuxSourceTree(object): 1298c2ecf20Sopenharmony_ci """Represents a Linux kernel source tree with KUnit tests.""" 1308c2ecf20Sopenharmony_ci 1318c2ecf20Sopenharmony_ci def __init__(self): 1328c2ecf20Sopenharmony_ci self._ops = LinuxSourceTreeOperations() 1338c2ecf20Sopenharmony_ci signal.signal(signal.SIGINT, self.signal_handler) 1348c2ecf20Sopenharmony_ci 1358c2ecf20Sopenharmony_ci def clean(self): 1368c2ecf20Sopenharmony_ci try: 1378c2ecf20Sopenharmony_ci self._ops.make_mrproper() 1388c2ecf20Sopenharmony_ci except ConfigError as e: 1398c2ecf20Sopenharmony_ci logging.error(e) 1408c2ecf20Sopenharmony_ci return False 1418c2ecf20Sopenharmony_ci return True 1428c2ecf20Sopenharmony_ci 1438c2ecf20Sopenharmony_ci def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH): 1448c2ecf20Sopenharmony_ci kunitconfig_path = get_kunitconfig_path(build_dir) 1458c2ecf20Sopenharmony_ci if not os.path.exists(kunitconfig_path): 1468c2ecf20Sopenharmony_ci shutil.copyfile(defconfig, kunitconfig_path) 1478c2ecf20Sopenharmony_ci 1488c2ecf20Sopenharmony_ci def read_kunitconfig(self, build_dir): 1498c2ecf20Sopenharmony_ci kunitconfig_path = get_kunitconfig_path(build_dir) 1508c2ecf20Sopenharmony_ci self._kconfig = kunit_config.Kconfig() 1518c2ecf20Sopenharmony_ci self._kconfig.read_from_file(kunitconfig_path) 1528c2ecf20Sopenharmony_ci 1538c2ecf20Sopenharmony_ci def validate_config(self, build_dir): 1548c2ecf20Sopenharmony_ci kconfig_path = get_kconfig_path(build_dir) 1558c2ecf20Sopenharmony_ci validated_kconfig = kunit_config.Kconfig() 1568c2ecf20Sopenharmony_ci validated_kconfig.read_from_file(kconfig_path) 1578c2ecf20Sopenharmony_ci if not self._kconfig.is_subset_of(validated_kconfig): 1588c2ecf20Sopenharmony_ci invalid = self._kconfig.entries() - validated_kconfig.entries() 1598c2ecf20Sopenharmony_ci message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 1608c2ecf20Sopenharmony_ci 'but not in .config: %s' % ( 1618c2ecf20Sopenharmony_ci ', '.join([str(e) for e in invalid]) 1628c2ecf20Sopenharmony_ci ) 1638c2ecf20Sopenharmony_ci logging.error(message) 1648c2ecf20Sopenharmony_ci return False 1658c2ecf20Sopenharmony_ci return True 1668c2ecf20Sopenharmony_ci 1678c2ecf20Sopenharmony_ci def build_config(self, build_dir, make_options): 1688c2ecf20Sopenharmony_ci kconfig_path = get_kconfig_path(build_dir) 1698c2ecf20Sopenharmony_ci if build_dir and not os.path.exists(build_dir): 1708c2ecf20Sopenharmony_ci os.mkdir(build_dir) 1718c2ecf20Sopenharmony_ci self._kconfig.write_to_file(kconfig_path) 1728c2ecf20Sopenharmony_ci try: 1738c2ecf20Sopenharmony_ci self._ops.make_olddefconfig(build_dir, make_options) 1748c2ecf20Sopenharmony_ci except ConfigError as e: 1758c2ecf20Sopenharmony_ci logging.error(e) 1768c2ecf20Sopenharmony_ci return False 1778c2ecf20Sopenharmony_ci return self.validate_config(build_dir) 1788c2ecf20Sopenharmony_ci 1798c2ecf20Sopenharmony_ci def build_reconfig(self, build_dir, make_options): 1808c2ecf20Sopenharmony_ci """Creates a new .config if it is not a subset of the .kunitconfig.""" 1818c2ecf20Sopenharmony_ci kconfig_path = get_kconfig_path(build_dir) 1828c2ecf20Sopenharmony_ci if os.path.exists(kconfig_path): 1838c2ecf20Sopenharmony_ci existing_kconfig = kunit_config.Kconfig() 1848c2ecf20Sopenharmony_ci existing_kconfig.read_from_file(kconfig_path) 1858c2ecf20Sopenharmony_ci if not self._kconfig.is_subset_of(existing_kconfig): 1868c2ecf20Sopenharmony_ci print('Regenerating .config ...') 1878c2ecf20Sopenharmony_ci os.remove(kconfig_path) 1888c2ecf20Sopenharmony_ci return self.build_config(build_dir, make_options) 1898c2ecf20Sopenharmony_ci else: 1908c2ecf20Sopenharmony_ci return True 1918c2ecf20Sopenharmony_ci else: 1928c2ecf20Sopenharmony_ci print('Generating .config ...') 1938c2ecf20Sopenharmony_ci return self.build_config(build_dir, make_options) 1948c2ecf20Sopenharmony_ci 1958c2ecf20Sopenharmony_ci def build_um_kernel(self, alltests, jobs, build_dir, make_options): 1968c2ecf20Sopenharmony_ci try: 1978c2ecf20Sopenharmony_ci if alltests: 1988c2ecf20Sopenharmony_ci self._ops.make_allyesconfig(build_dir, make_options) 1998c2ecf20Sopenharmony_ci self._ops.make_olddefconfig(build_dir, make_options) 2008c2ecf20Sopenharmony_ci self._ops.make(jobs, build_dir, make_options) 2018c2ecf20Sopenharmony_ci except (ConfigError, BuildError) as e: 2028c2ecf20Sopenharmony_ci logging.error(e) 2038c2ecf20Sopenharmony_ci return False 2048c2ecf20Sopenharmony_ci return self.validate_config(build_dir) 2058c2ecf20Sopenharmony_ci 2068c2ecf20Sopenharmony_ci def run_kernel(self, args=[], build_dir='', timeout=None): 2078c2ecf20Sopenharmony_ci args.extend(['mem=1G']) 2088c2ecf20Sopenharmony_ci self._ops.linux_bin(args, timeout, build_dir) 2098c2ecf20Sopenharmony_ci outfile = get_outfile_path(build_dir) 2108c2ecf20Sopenharmony_ci subprocess.call(['stty', 'sane']) 2118c2ecf20Sopenharmony_ci with open(outfile, 'r') as file: 2128c2ecf20Sopenharmony_ci for line in file: 2138c2ecf20Sopenharmony_ci yield line 2148c2ecf20Sopenharmony_ci 2158c2ecf20Sopenharmony_ci def signal_handler(self, sig, frame): 2168c2ecf20Sopenharmony_ci logging.error('Build interruption occurred. Cleaning console.') 2178c2ecf20Sopenharmony_ci subprocess.call(['stty', 'sane']) 218