0001
0002
0003
0004
0005
0006
0007
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:
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
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
0225
0226
0227
0228
0229
0230
0231
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
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
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)
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
0365
0366
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
0380 for line in process.stdout:
0381 output.write(line)
0382 yield line
0383
0384 finally:
0385
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'])