16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0 26ebf5866SFelix Guo# 36ebf5866SFelix Guo# Runs UML kernel, collects output, and handles errors. 46ebf5866SFelix Guo# 56ebf5866SFelix Guo# Copyright (C) 2019, Google LLC. 66ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com> 76ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com> 86ebf5866SFelix Guo 96ebf5866SFelix Guoimport logging 106ebf5866SFelix Guoimport subprocess 116ebf5866SFelix Guoimport os 12fcdb0bc0SAndy Shevchenkoimport shutil 13021ed9f5SHeidi Fahimimport signal 1409641f7cSDaniel Latypovfrom typing import Iterator 15021ed9f5SHeidi Fahim 16021ed9f5SHeidi Fahimfrom contextlib import ExitStack 176ebf5866SFelix Guo 186ebf5866SFelix Guoimport kunit_config 19021ed9f5SHeidi Fahimimport kunit_parser 206ebf5866SFelix Guo 216ebf5866SFelix GuoKCONFIG_PATH = '.config' 22fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig' 23fcdb0bc0SAndy ShevchenkoDEFAULT_KUNITCONFIG_PATH = 'arch/um/configs/kunit_defconfig' 24021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 25128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log' 266ebf5866SFelix Guo 27f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default): 28f3ed003eSAndy Shevchenko if build_dir: 29f3ed003eSAndy Shevchenko default = os.path.join(build_dir, default) 30f3ed003eSAndy Shevchenko return default 31f3ed003eSAndy Shevchenko 326ebf5866SFelix Guoclass ConfigError(Exception): 336ebf5866SFelix Guo """Represents an error trying to configure the Linux kernel.""" 346ebf5866SFelix Guo 356ebf5866SFelix Guo 366ebf5866SFelix Guoclass BuildError(Exception): 376ebf5866SFelix Guo """Represents an error trying to build the Linux kernel.""" 386ebf5866SFelix Guo 396ebf5866SFelix Guo 406ebf5866SFelix Guoclass LinuxSourceTreeOperations(object): 416ebf5866SFelix Guo """An abstraction over command line operations performed on a source tree.""" 426ebf5866SFelix Guo 4309641f7cSDaniel Latypov def make_mrproper(self) -> None: 446ebf5866SFelix Guo try: 455a9fcad7SWill Chen subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 466ebf5866SFelix Guo except OSError as e: 471abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 486ebf5866SFelix Guo except subprocess.CalledProcessError as e: 491abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 506ebf5866SFelix Guo 5109641f7cSDaniel Latypov def make_olddefconfig(self, build_dir, make_options) -> None: 526ebf5866SFelix Guo command = ['make', 'ARCH=um', 'olddefconfig'] 530476e69fSGreg Thelen if make_options: 540476e69fSGreg Thelen command.extend(make_options) 556ebf5866SFelix Guo if build_dir: 566ebf5866SFelix Guo command += ['O=' + build_dir] 576ebf5866SFelix Guo try: 585a9fcad7SWill Chen subprocess.check_output(command, stderr=subprocess.STDOUT) 596ebf5866SFelix Guo except OSError as e: 601abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 616ebf5866SFelix Guo except subprocess.CalledProcessError as e: 621abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 636ebf5866SFelix Guo 6409641f7cSDaniel Latypov def make_allyesconfig(self, build_dir, make_options) -> None: 65021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 66021ed9f5SHeidi Fahim 'Enabling all CONFIGs for UML...') 6767e2fae3SBrendan Higgins command = ['make', 'ARCH=um', 'allyesconfig'] 6867e2fae3SBrendan Higgins if make_options: 6967e2fae3SBrendan Higgins command.extend(make_options) 7067e2fae3SBrendan Higgins if build_dir: 7167e2fae3SBrendan Higgins command += ['O=' + build_dir] 72021ed9f5SHeidi Fahim process = subprocess.Popen( 7367e2fae3SBrendan Higgins command, 74021ed9f5SHeidi Fahim stdout=subprocess.DEVNULL, 75021ed9f5SHeidi Fahim stderr=subprocess.STDOUT) 76021ed9f5SHeidi Fahim process.wait() 77021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 78021ed9f5SHeidi Fahim 'Disabling broken configs to run KUnit tests...') 79021ed9f5SHeidi Fahim with ExitStack() as es: 8067e2fae3SBrendan Higgins config = open(get_kconfig_path(build_dir), 'a') 81021ed9f5SHeidi Fahim disable = open(BROKEN_ALLCONFIG_PATH, 'r').read() 82021ed9f5SHeidi Fahim config.write(disable) 83021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 84021ed9f5SHeidi Fahim 'Starting Kernel with all configs takes a few minutes...') 85021ed9f5SHeidi Fahim 8609641f7cSDaniel Latypov def make(self, jobs, build_dir, make_options) -> None: 876ebf5866SFelix Guo command = ['make', 'ARCH=um', '--jobs=' + str(jobs)] 880476e69fSGreg Thelen if make_options: 890476e69fSGreg Thelen command.extend(make_options) 906ebf5866SFelix Guo if build_dir: 916ebf5866SFelix Guo command += ['O=' + build_dir] 926ebf5866SFelix Guo try: 9339088144SDaniel Latypov proc = subprocess.Popen(command, 9439088144SDaniel Latypov stderr=subprocess.PIPE, 9539088144SDaniel Latypov stdout=subprocess.DEVNULL) 966ebf5866SFelix Guo except OSError as e: 9739088144SDaniel Latypov raise BuildError('Could not call make command: ' + str(e)) 9839088144SDaniel Latypov _, stderr = proc.communicate() 9939088144SDaniel Latypov if proc.returncode != 0: 10039088144SDaniel Latypov raise BuildError(stderr.decode()) 10139088144SDaniel Latypov if stderr: # likely only due to build warnings 10239088144SDaniel Latypov print(stderr.decode()) 1036ebf5866SFelix Guo 10409641f7cSDaniel Latypov def linux_bin(self, params, timeout, build_dir) -> None: 1056ebf5866SFelix Guo """Runs the Linux UML binary. Must be named 'linux'.""" 106f3ed003eSAndy Shevchenko linux_bin = get_file_path(build_dir, 'linux') 107128dc4bcSAndy Shevchenko outfile = get_outfile_path(build_dir) 108021ed9f5SHeidi Fahim with open(outfile, 'w') as output: 109021ed9f5SHeidi Fahim process = subprocess.Popen([linux_bin] + params, 110021ed9f5SHeidi Fahim stdout=output, 111021ed9f5SHeidi Fahim stderr=subprocess.STDOUT) 112021ed9f5SHeidi Fahim process.wait(timeout) 1136ebf5866SFelix Guo 11409641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str: 115f3ed003eSAndy Shevchenko return get_file_path(build_dir, KCONFIG_PATH) 1166ebf5866SFelix Guo 11709641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str: 118f3ed003eSAndy Shevchenko return get_file_path(build_dir, KUNITCONFIG_PATH) 119fcdb0bc0SAndy Shevchenko 12009641f7cSDaniel Latypovdef get_outfile_path(build_dir) -> str: 121f3ed003eSAndy Shevchenko return get_file_path(build_dir, OUTFILE_PATH) 122128dc4bcSAndy Shevchenko 1236ebf5866SFelix Guoclass LinuxSourceTree(object): 1246ebf5866SFelix Guo """Represents a Linux kernel source tree with KUnit tests.""" 1256ebf5866SFelix Guo 126*243180f5SDaniel Latypov def __init__(self, build_dir: str, load_config=True, kunitconfig_path='') -> None: 127021ed9f5SHeidi Fahim signal.signal(signal.SIGINT, self.signal_handler) 1286ebf5866SFelix Guo 1292b8fdbbfSDaniel Latypov self._ops = LinuxSourceTreeOperations() 1302b8fdbbfSDaniel Latypov 1312b8fdbbfSDaniel Latypov if not load_config: 1322b8fdbbfSDaniel Latypov return 1332b8fdbbfSDaniel Latypov 134*243180f5SDaniel Latypov if kunitconfig_path: 135*243180f5SDaniel Latypov if not os.path.exists(kunitconfig_path): 136*243180f5SDaniel Latypov raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 137*243180f5SDaniel Latypov else: 1382b8fdbbfSDaniel Latypov kunitconfig_path = get_kunitconfig_path(build_dir) 1392b8fdbbfSDaniel Latypov if not os.path.exists(kunitconfig_path): 140*243180f5SDaniel Latypov shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 1412b8fdbbfSDaniel Latypov 1422b8fdbbfSDaniel Latypov self._kconfig = kunit_config.Kconfig() 1432b8fdbbfSDaniel Latypov self._kconfig.read_from_file(kunitconfig_path) 1442b8fdbbfSDaniel Latypov 14509641f7cSDaniel Latypov def clean(self) -> bool: 1466ebf5866SFelix Guo try: 1476ebf5866SFelix Guo self._ops.make_mrproper() 1486ebf5866SFelix Guo except ConfigError as e: 1496ebf5866SFelix Guo logging.error(e) 1506ebf5866SFelix Guo return False 1516ebf5866SFelix Guo return True 1526ebf5866SFelix Guo 15309641f7cSDaniel Latypov def validate_config(self, build_dir) -> bool: 154dde54b94SHeidi Fahim kconfig_path = get_kconfig_path(build_dir) 155dde54b94SHeidi Fahim validated_kconfig = kunit_config.Kconfig() 156dde54b94SHeidi Fahim validated_kconfig.read_from_file(kconfig_path) 157dde54b94SHeidi Fahim if not self._kconfig.is_subset_of(validated_kconfig): 158dde54b94SHeidi Fahim invalid = self._kconfig.entries() - validated_kconfig.entries() 159dde54b94SHeidi Fahim message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 160dde54b94SHeidi Fahim 'but not in .config: %s' % ( 161dde54b94SHeidi Fahim ', '.join([str(e) for e in invalid]) 162dde54b94SHeidi Fahim ) 163dde54b94SHeidi Fahim logging.error(message) 164dde54b94SHeidi Fahim return False 165dde54b94SHeidi Fahim return True 166dde54b94SHeidi Fahim 16709641f7cSDaniel Latypov def build_config(self, build_dir, make_options) -> bool: 1686ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 1696ebf5866SFelix Guo if build_dir and not os.path.exists(build_dir): 1706ebf5866SFelix Guo os.mkdir(build_dir) 1716ebf5866SFelix Guo self._kconfig.write_to_file(kconfig_path) 1726ebf5866SFelix Guo try: 1730476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 1746ebf5866SFelix Guo except ConfigError as e: 1756ebf5866SFelix Guo logging.error(e) 1766ebf5866SFelix Guo return False 177dde54b94SHeidi Fahim return self.validate_config(build_dir) 1786ebf5866SFelix Guo 17909641f7cSDaniel Latypov def build_reconfig(self, build_dir, make_options) -> bool: 18014ee5cfdSSeongJae Park """Creates a new .config if it is not a subset of the .kunitconfig.""" 1816ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 1826ebf5866SFelix Guo if os.path.exists(kconfig_path): 1836ebf5866SFelix Guo existing_kconfig = kunit_config.Kconfig() 1846ebf5866SFelix Guo existing_kconfig.read_from_file(kconfig_path) 1856ebf5866SFelix Guo if not self._kconfig.is_subset_of(existing_kconfig): 1866ebf5866SFelix Guo print('Regenerating .config ...') 1876ebf5866SFelix Guo os.remove(kconfig_path) 1880476e69fSGreg Thelen return self.build_config(build_dir, make_options) 1896ebf5866SFelix Guo else: 1906ebf5866SFelix Guo return True 1916ebf5866SFelix Guo else: 1926ebf5866SFelix Guo print('Generating .config ...') 1930476e69fSGreg Thelen return self.build_config(build_dir, make_options) 1946ebf5866SFelix Guo 19509641f7cSDaniel Latypov def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 1966ebf5866SFelix Guo try: 19767e2fae3SBrendan Higgins if alltests: 19867e2fae3SBrendan Higgins self._ops.make_allyesconfig(build_dir, make_options) 1990476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 2000476e69fSGreg Thelen self._ops.make(jobs, build_dir, make_options) 2016ebf5866SFelix Guo except (ConfigError, BuildError) as e: 2026ebf5866SFelix Guo logging.error(e) 2036ebf5866SFelix Guo return False 204dde54b94SHeidi Fahim return self.validate_config(build_dir) 2056ebf5866SFelix Guo 20609641f7cSDaniel Latypov def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]: 20765a4e529SDavid Gow args.extend(['mem=1G', 'console=tty']) 208128dc4bcSAndy Shevchenko self._ops.linux_bin(args, timeout, build_dir) 209128dc4bcSAndy Shevchenko outfile = get_outfile_path(build_dir) 210021ed9f5SHeidi Fahim subprocess.call(['stty', 'sane']) 211021ed9f5SHeidi Fahim with open(outfile, 'r') as file: 212021ed9f5SHeidi Fahim for line in file: 213021ed9f5SHeidi Fahim yield line 214021ed9f5SHeidi Fahim 21509641f7cSDaniel Latypov def signal_handler(self, sig, frame) -> None: 216021ed9f5SHeidi Fahim logging.error('Build interruption occurred. Cleaning console.') 217021ed9f5SHeidi Fahim subprocess.call(['stty', 'sane']) 218