Back to home page

OSCL-LXR

 
 

    


0001 # SPDX-License-Identifier: GPL-2.0
0002 #
0003 # Runs UML kernel, collects output, and handles errors.
0004 #
0005 # Copyright (C) 2019, Google LLC.
0006 # Author: Felix Guo <felixguoxiuping@gmail.com>
0007 # Author: Brendan Higgins <brendanhiggins@google.com>
0008 
0009 import importlib.abc
0010 import importlib.util
0011 import logging
0012 import subprocess
0013 import os
0014 import shlex
0015 import shutil
0016 import signal
0017 import threading
0018 from typing import Iterator, List, Optional, Tuple
0019 
0020 import kunit_config
0021 from kunit_printer import stdout
0022 import qemu_config
0023 
0024 KCONFIG_PATH = '.config'
0025 KUNITCONFIG_PATH = '.kunitconfig'
0026 OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
0027 DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
0028 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
0029 UML_KCONFIG_PATH = 'tools/testing/kunit/configs/arch_uml.config'
0030 OUTFILE_PATH = 'test.log'
0031 ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
0032 QEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
0033 
0034 class ConfigError(Exception):
0035     """Represents an error trying to configure the Linux kernel."""
0036 
0037 
0038 class BuildError(Exception):
0039     """Represents an error trying to build the Linux kernel."""
0040 
0041 
0042 class LinuxSourceTreeOperations:
0043     """An abstraction over command line operations performed on a source tree."""
0044 
0045     def __init__(self, linux_arch: str, cross_compile: Optional[str]):
0046         self._linux_arch = linux_arch
0047         self._cross_compile = cross_compile
0048 
0049     def make_mrproper(self) -> None:
0050         try:
0051             subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
0052         except OSError as e:
0053             raise ConfigError('Could not call make command: ' + str(e))
0054         except subprocess.CalledProcessError as e:
0055             raise ConfigError(e.output.decode())
0056 
0057     def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
0058         return base_kunitconfig
0059 
0060     def make_allyesconfig(self, build_dir: str, make_options) -> None:
0061         raise ConfigError('Only the "um" arch is supported for alltests')
0062 
0063     def make_olddefconfig(self, build_dir: str, make_options) -> None:
0064         command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
0065         if self._cross_compile:
0066             command += ['CROSS_COMPILE=' + self._cross_compile]
0067         if make_options:
0068             command.extend(make_options)
0069         print('Populating config with:\n$', ' '.join(command))
0070         try:
0071             subprocess.check_output(command, stderr=subprocess.STDOUT)
0072         except OSError as e:
0073             raise ConfigError('Could not call make command: ' + str(e))
0074         except subprocess.CalledProcessError as e:
0075             raise ConfigError(e.output.decode())
0076 
0077     def make(self, jobs, build_dir: str, make_options) -> None:
0078         command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
0079         if make_options:
0080             command.extend(make_options)
0081         if self._cross_compile:
0082             command += ['CROSS_COMPILE=' + self._cross_compile]
0083         print('Building with:\n$', ' '.join(command))
0084         try:
0085             proc = subprocess.Popen(command,
0086                         stderr=subprocess.PIPE,
0087                         stdout=subprocess.DEVNULL)
0088         except OSError as e:
0089             raise BuildError('Could not call execute make: ' + str(e))
0090         except subprocess.CalledProcessError as e:
0091             raise BuildError(e.output)
0092         _, stderr = proc.communicate()
0093         if proc.returncode != 0:
0094             raise BuildError(stderr.decode())
0095         if stderr:  # likely only due to build warnings
0096             print(stderr.decode())
0097 
0098     def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
0099         raise RuntimeError('not implemented!')
0100 
0101 
0102 class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
0103 
0104     def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
0105         super().__init__(linux_arch=qemu_arch_params.linux_arch,
0106                  cross_compile=cross_compile)
0107         self._kconfig = qemu_arch_params.kconfig
0108         self._qemu_arch = qemu_arch_params.qemu_arch
0109         self._kernel_path = qemu_arch_params.kernel_path
0110         self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
0111         self._extra_qemu_params = qemu_arch_params.extra_qemu_params
0112 
0113     def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
0114         kconfig = kunit_config.parse_from_string(self._kconfig)
0115         kconfig.merge_in_entries(base_kunitconfig)
0116         return kconfig
0117 
0118     def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
0119         kernel_path = os.path.join(build_dir, self._kernel_path)
0120         qemu_command = ['qemu-system-' + self._qemu_arch,
0121                 '-nodefaults',
0122                 '-m', '1024',
0123                 '-kernel', kernel_path,
0124                 '-append', ' '.join(params + [self._kernel_command_line]),
0125                 '-no-reboot',
0126                 '-nographic',
0127                 '-serial', 'stdio'] + self._extra_qemu_params
0128         # Note: shlex.join() does what we want, but requires python 3.8+.
0129         print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
0130         return subprocess.Popen(qemu_command,
0131                     stdin=subprocess.PIPE,
0132                     stdout=subprocess.PIPE,
0133                     stderr=subprocess.STDOUT,
0134                     text=True, errors='backslashreplace')
0135 
0136 class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
0137     """An abstraction over command line operations performed on a source tree."""
0138 
0139     def __init__(self, cross_compile=None):
0140         super().__init__(linux_arch='um', cross_compile=cross_compile)
0141 
0142     def make_arch_config(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
0143         kconfig = kunit_config.parse_file(UML_KCONFIG_PATH)
0144         kconfig.merge_in_entries(base_kunitconfig)
0145         return kconfig
0146 
0147     def make_allyesconfig(self, build_dir: str, make_options) -> None:
0148         stdout.print_with_timestamp(
0149             'Enabling all CONFIGs for UML...')
0150         command = ['make', 'ARCH=um', 'O=' + build_dir, 'allyesconfig']
0151         if make_options:
0152             command.extend(make_options)
0153         process = subprocess.Popen(
0154             command,
0155             stdout=subprocess.DEVNULL,
0156             stderr=subprocess.STDOUT)
0157         process.wait()
0158         stdout.print_with_timestamp(
0159             'Disabling broken configs to run KUnit tests...')
0160 
0161         with open(get_kconfig_path(build_dir), 'a') as config:
0162             with open(BROKEN_ALLCONFIG_PATH, 'r') as disable:
0163                 config.write(disable.read())
0164         stdout.print_with_timestamp(
0165             'Starting Kernel with all configs takes a few minutes...')
0166 
0167     def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
0168         """Runs the Linux UML binary. Must be named 'linux'."""
0169         linux_bin = os.path.join(build_dir, 'linux')
0170         params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
0171         return subprocess.Popen([linux_bin] + params,
0172                        stdin=subprocess.PIPE,
0173                        stdout=subprocess.PIPE,
0174                        stderr=subprocess.STDOUT,
0175                        text=True, errors='backslashreplace')
0176 
0177 def get_kconfig_path(build_dir: str) -> str:
0178     return os.path.join(build_dir, KCONFIG_PATH)
0179 
0180 def get_kunitconfig_path(build_dir: str) -> str:
0181     return os.path.join(build_dir, KUNITCONFIG_PATH)
0182 
0183 def get_old_kunitconfig_path(build_dir: str) -> str:
0184     return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
0185 
0186 def get_parsed_kunitconfig(build_dir: str,
0187                kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig:
0188     if not kunitconfig_paths:
0189         path = get_kunitconfig_path(build_dir)
0190         if not os.path.exists(path):
0191             shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path)
0192         return kunit_config.parse_file(path)
0193 
0194     merged = kunit_config.Kconfig()
0195 
0196     for path in kunitconfig_paths:
0197         if os.path.isdir(path):
0198             path = os.path.join(path, KUNITCONFIG_PATH)
0199         if not os.path.exists(path):
0200             raise ConfigError(f'Specified kunitconfig ({path}) does not exist')
0201 
0202         partial = kunit_config.parse_file(path)
0203         diff = merged.conflicting_options(partial)
0204         if diff:
0205             diff_str = '\n\n'.join(f'{a}\n  vs from {path}\n{b}' for a, b in diff)
0206             raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}')
0207         merged.merge_in_entries(partial)
0208     return merged
0209 
0210 def get_outfile_path(build_dir: str) -> str:
0211     return os.path.join(build_dir, OUTFILE_PATH)
0212 
0213 def _default_qemu_config_path(arch: str) -> str:
0214     config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
0215     if os.path.isfile(config_path):
0216         return config_path
0217 
0218     options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
0219     raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
0220 
0221 def _get_qemu_ops(config_path: str,
0222           extra_qemu_args: Optional[List[str]],
0223           cross_compile: Optional[str]) -> Tuple[str, LinuxSourceTreeOperations]:
0224     # The module name/path has very little to do with where the actual file
0225     # exists (I learned this through experimentation and could not find it
0226     # anywhere in the Python documentation).
0227     #
0228     # Bascially, we completely ignore the actual file location of the config
0229     # we are loading and just tell Python that the module lives in the
0230     # QEMU_CONFIGS_DIR for import purposes regardless of where it actually
0231     # exists as a file.
0232     module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
0233     spec = importlib.util.spec_from_file_location(module_path, config_path)
0234     assert spec is not None
0235     config = importlib.util.module_from_spec(spec)
0236     # See https://github.com/python/typeshed/pull/2626 for context.
0237     assert isinstance(spec.loader, importlib.abc.Loader)
0238     spec.loader.exec_module(config)
0239 
0240     if not hasattr(config, 'QEMU_ARCH'):
0241         raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
0242     params: qemu_config.QemuArchParams = config.QEMU_ARCH  # type: ignore
0243     if extra_qemu_args:
0244         params.extra_qemu_params.extend(extra_qemu_args)
0245     return params.linux_arch, LinuxSourceTreeOperationsQemu(
0246             params, cross_compile=cross_compile)
0247 
0248 class LinuxSourceTree:
0249     """Represents a Linux kernel source tree with KUnit tests."""
0250 
0251     def __init__(
0252           self,
0253           build_dir: str,
0254           kunitconfig_paths: Optional[List[str]]=None,
0255           kconfig_add: Optional[List[str]]=None,
0256           arch=None,
0257           cross_compile=None,
0258           qemu_config_path=None,
0259           extra_qemu_args=None) -> None:
0260         signal.signal(signal.SIGINT, self.signal_handler)
0261         if qemu_config_path:
0262             self._arch, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
0263         else:
0264             self._arch = 'um' if arch is None else arch
0265             if self._arch == 'um':
0266                 self._ops = LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
0267             else:
0268                 qemu_config_path = _default_qemu_config_path(self._arch)
0269                 _, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
0270 
0271         self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths)
0272         if kconfig_add:
0273             kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
0274             self._kconfig.merge_in_entries(kconfig)
0275 
0276     def arch(self) -> str:
0277         return self._arch
0278 
0279     def clean(self) -> bool:
0280         try:
0281             self._ops.make_mrproper()
0282         except ConfigError as e:
0283             logging.error(e)
0284             return False
0285         return True
0286 
0287     def validate_config(self, build_dir: str) -> bool:
0288         kconfig_path = get_kconfig_path(build_dir)
0289         validated_kconfig = kunit_config.parse_file(kconfig_path)
0290         if self._kconfig.is_subset_of(validated_kconfig):
0291             return True
0292         missing = set(self._kconfig.as_entries()) - set(validated_kconfig.as_entries())
0293         message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
0294               'This is probably due to unsatisfied dependencies.\n' \
0295               'Missing: ' + ', '.join(str(e) for e in missing)
0296         if self._arch == 'um':
0297             message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
0298                    'on a different architecture with something like "--arch=x86_64".'
0299         logging.error(message)
0300         return False
0301 
0302     def build_config(self, build_dir: str, make_options) -> bool:
0303         kconfig_path = get_kconfig_path(build_dir)
0304         if build_dir and not os.path.exists(build_dir):
0305             os.mkdir(build_dir)
0306         try:
0307             self._kconfig = self._ops.make_arch_config(self._kconfig)
0308             self._kconfig.write_to_file(kconfig_path)
0309             self._ops.make_olddefconfig(build_dir, make_options)
0310         except ConfigError as e:
0311             logging.error(e)
0312             return False
0313         if not self.validate_config(build_dir):
0314             return False
0315 
0316         old_path = get_old_kunitconfig_path(build_dir)
0317         if os.path.exists(old_path):
0318             os.remove(old_path)  # write_to_file appends to the file
0319         self._kconfig.write_to_file(old_path)
0320         return True
0321 
0322     def _kunitconfig_changed(self, build_dir: str) -> bool:
0323         old_path = get_old_kunitconfig_path(build_dir)
0324         if not os.path.exists(old_path):
0325             return True
0326 
0327         old_kconfig = kunit_config.parse_file(old_path)
0328         return old_kconfig != self._kconfig
0329 
0330     def build_reconfig(self, build_dir: str, make_options) -> bool:
0331         """Creates a new .config if it is not a subset of the .kunitconfig."""
0332         kconfig_path = get_kconfig_path(build_dir)
0333         if not os.path.exists(kconfig_path):
0334             print('Generating .config ...')
0335             return self.build_config(build_dir, make_options)
0336 
0337         existing_kconfig = kunit_config.parse_file(kconfig_path)
0338         self._kconfig = self._ops.make_arch_config(self._kconfig)
0339 
0340         if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
0341             return True
0342         print('Regenerating .config ...')
0343         os.remove(kconfig_path)
0344         return self.build_config(build_dir, make_options)
0345 
0346     def build_kernel(self, alltests, jobs, build_dir: str, make_options) -> bool:
0347         try:
0348             if alltests:
0349                 self._ops.make_allyesconfig(build_dir, make_options)
0350             self._ops.make_olddefconfig(build_dir, make_options)
0351             self._ops.make(jobs, build_dir, make_options)
0352         except (ConfigError, BuildError) as e:
0353             logging.error(e)
0354             return False
0355         return self.validate_config(build_dir)
0356 
0357     def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
0358         if not args:
0359             args = []
0360         if filter_glob:
0361             args.append('kunit.filter_glob='+filter_glob)
0362 
0363         process = self._ops.start(args, build_dir)
0364         assert process.stdout is not None  # tell mypy it's set
0365 
0366         # Enforce the timeout in a background thread.
0367         def _wait_proc():
0368             try:
0369                 process.wait(timeout=timeout)
0370             except Exception as e:
0371                 print(e)
0372                 process.terminate()
0373                 process.wait()
0374         waiter = threading.Thread(target=_wait_proc)
0375         waiter.start()
0376 
0377         output = open(get_outfile_path(build_dir), 'w')
0378         try:
0379             # Tee the output to the file and to our caller in real time.
0380             for line in process.stdout:
0381                 output.write(line)
0382                 yield line
0383         # This runs even if our caller doesn't consume every line.
0384         finally:
0385             # Flush any leftover output to the file
0386             output.write(process.stdout.read())
0387             output.close()
0388             process.stdout.close()
0389 
0390             waiter.join()
0391             subprocess.call(['stty', 'sane'])
0392 
0393     def signal_handler(self, unused_sig, unused_frame) -> None:
0394         logging.error('Build interruption occurred. Cleaning console.')
0395         subprocess.call(['stty', 'sane'])