diff options
Diffstat (limited to 'run_tests.py')
-rwxr-xr-x | run_tests.py | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/run_tests.py b/run_tests.py new file mode 100755 index 0000000..8a20937 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 + +# Copyright 2012-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Work around some pathlib bugs... +from mesonbuild import _pathlib +import sys +sys.modules['pathlib'] = _pathlib + +import collections +import os +import time +import shutil +import subprocess +import platform +import argparse +import traceback +from io import StringIO +from enum import Enum +from glob import glob +from pathlib import Path +from unittest import mock +import typing as T + +from mesonbuild.compilers.c import CCompiler +from mesonbuild.compilers.detect import detect_c_compiler +from mesonbuild import dependencies +from mesonbuild import mesonlib +from mesonbuild import mesonmain +from mesonbuild import mtest +from mesonbuild import mlog +from mesonbuild.environment import Environment, detect_ninja, detect_machine_info +from mesonbuild.coredata import backendlist, version as meson_version +from mesonbuild.mesonlib import OptionKey, setup_vsenv + +NINJA_1_9_OR_NEWER = False +NINJA_CMD = None +# If we're on CI, detecting ninja for every subprocess unit test that we run is slow +# Optimize this by respecting $NINJA and skipping detection, then exporting it on +# first run. +try: + NINJA_1_9_OR_NEWER = bool(int(os.environ['NINJA_1_9_OR_NEWER'])) + NINJA_CMD = [os.environ['NINJA']] +except (KeyError, ValueError): + # Look for 1.9 to see if https://github.com/ninja-build/ninja/issues/1219 + # is fixed + NINJA_CMD = detect_ninja('1.9') + if NINJA_CMD is not None: + NINJA_1_9_OR_NEWER = True + else: + mlog.warning('Found ninja <1.9, tests will run slower', once=True) + NINJA_CMD = detect_ninja() + +if NINJA_CMD is not None: + os.environ['NINJA_1_9_OR_NEWER'] = str(int(NINJA_1_9_OR_NEWER)) + os.environ['NINJA'] = NINJA_CMD[0] +else: + raise RuntimeError('Could not find Ninja v1.7 or newer') + +# Emulate running meson with -X utf8 by making sure all open() calls have a +# sane encoding. This should be a python default, but PEP 540 considered it not +# backwards compatible. Instead, much line noise in diffs to update this, and in +# python 3.10 we can also make it a warning when absent. +os.environ['PYTHONWARNDEFAULTENCODING'] = '1' +# work around https://bugs.python.org/issue34624 +os.environ['MESON_RUNNING_IN_PROJECT_TESTS'] = '1' +# python 3.11 adds a warning that in 3.15, UTF-8 mode will be default. +# This is fantastic news, we'd love that. Less fantastic: this warning is silly, +# we *want* these checks to be affected. Plus, the recommended alternative API +# would (in addition to warning people when UTF-8 mode removed the problem) also +# require using a minimum python version of 3.11 (in which the warning was added) +# or add verbose if/else soup. +if sys.version_info >= (3, 10): + import warnings + warnings.filterwarnings('ignore', message="UTF-8 Mode affects .*getpreferredencoding", category=EncodingWarning) + +def guess_backend(backend_str: str, msbuild_exe: str) -> T.Tuple['Backend', T.List[str]]: + # Auto-detect backend if unspecified + backend_flags = [] + if backend_str is None: + if msbuild_exe is not None and (mesonlib.is_windows() and not _using_intelcl()): + backend_str = 'vs' # Meson will auto-detect VS version to use + else: + backend_str = 'ninja' + + # Set backend arguments for Meson + if backend_str.startswith('vs'): + backend_flags = ['--backend=' + backend_str] + backend = Backend.vs + elif backend_str == 'xcode': + backend_flags = ['--backend=xcode'] + backend = Backend.xcode + elif backend_str == 'ninja': + backend_flags = ['--backend=ninja'] + backend = Backend.ninja + else: + raise RuntimeError(f'Unknown backend: {backend_str!r}') + return (backend, backend_flags) + + +def _using_intelcl() -> bool: + """ + detect if intending to using Intel-Cl compilers (Intel compilers on Windows) + Sufficient evidence of intent is that user is working in the Intel compiler + shell environment, otherwise this function returns False + """ + if not mesonlib.is_windows(): + return False + # handle where user tried to "blank" MKLROOT and left space(s) + if not os.environ.get('MKLROOT', '').strip(): + return False + if (os.environ.get('CC') == 'icl' or + os.environ.get('CXX') == 'icl' or + os.environ.get('FC') == 'ifort'): + return True + # Intel-Cl users might not have the CC,CXX,FC envvars set, + # but because they're in Intel shell, the exe's below are on PATH + if shutil.which('icl') or shutil.which('ifort'): + return True + mlog.warning('It appears you might be intending to use Intel compiler on Windows ' + 'since non-empty environment variable MKLROOT is set to {} ' + 'However, Meson cannot find the Intel WIndows compiler executables (icl,ifort).' + 'Please try using the Intel shell.'.format(os.environ.get('MKLROOT'))) + return False + + +# Fake classes and objects for mocking +class FakeBuild: + def __init__(self, env): + self.environment = env + +class FakeCompilerOptions: + def __init__(self): + self.value = [] + +# TODO: use a typing.Protocol here +def get_fake_options(prefix: str = '') -> argparse.Namespace: + opts = argparse.Namespace() + opts.native_file = [] + opts.cross_file = None + opts.wrap_mode = None + opts.prefix = prefix + opts.cmd_line_options = {} + return opts + +def get_fake_env(sdir='', bdir=None, prefix='', opts=None): + if opts is None: + opts = get_fake_options(prefix) + env = Environment(sdir, bdir, opts) + env.coredata.options[OptionKey('args', lang='c')] = FakeCompilerOptions() + env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library + return env + +def get_convincing_fake_env_and_cc(bdir, prefix): + ''' + Return a fake env and C compiler with the fake env + machine info properly detected using that compiler. + Useful for running compiler checks in the unit tests. + ''' + env = get_fake_env('', bdir, prefix) + cc = detect_c_compiler(env, mesonlib.MachineChoice.HOST) + # Detect machine info + env.machines.host = detect_machine_info({'c':cc}) + return (env, cc) + +Backend = Enum('Backend', 'ninja vs xcode') + +if 'MESON_EXE' in os.environ: + meson_exe = mesonlib.split_args(os.environ['MESON_EXE']) +else: + meson_exe = None + +if mesonlib.is_windows() or mesonlib.is_cygwin(): + exe_suffix = '.exe' +else: + exe_suffix = '' + +def get_meson_script() -> str: + ''' + Guess the meson that corresponds to the `mesonbuild` that has been imported + so we can run configure and other commands in-process, since mesonmain.run + needs to know the meson_command to use. + + Also used by run_unittests.py to determine what meson to run when not + running in-process (which is the default). + ''' + # Is there a meson.py next to the mesonbuild currently in use? + mesonbuild_dir = Path(mesonmain.__file__).resolve().parent.parent + meson_script = mesonbuild_dir / 'meson.py' + if meson_script.is_file(): + return str(meson_script) + # Then if mesonbuild is in PYTHONPATH, meson must be in PATH + mlog.warning('Could not find meson.py next to the mesonbuild module. ' + 'Trying system meson...') + meson_cmd = shutil.which('meson') + if meson_cmd: + return meson_cmd + raise RuntimeError(f'Could not find {meson_script!r} or a meson in PATH') + +def get_backend_args_for_dir(backend: Backend, builddir: str) -> T.List[str]: + ''' + Visual Studio backend needs to be given the solution to build + ''' + if backend is Backend.vs: + sln_name = glob(os.path.join(builddir, '*.sln'))[0] + return [os.path.split(sln_name)[-1]] + return [] + +def find_vcxproj_with_target(builddir, target): + import re, fnmatch + t, ext = os.path.splitext(target) + if ext: + p = fr'<TargetName>{t}</TargetName>\s*<TargetExt>\{ext}</TargetExt>' + else: + p = fr'<TargetName>{t}</TargetName>' + for _, _, files in os.walk(builddir): + for f in fnmatch.filter(files, '*.vcxproj'): + f = os.path.join(builddir, f) + with open(f, encoding='utf-8') as o: + if re.search(p, o.read(), flags=re.MULTILINE): + return f + raise RuntimeError(f'No vcxproj matching {p!r} in {builddir!r}') + +def get_builddir_target_args(backend: Backend, builddir, target): + dir_args = [] + if not target: + dir_args = get_backend_args_for_dir(backend, builddir) + if target is None: + return dir_args + if backend is Backend.vs: + vcxproj = find_vcxproj_with_target(builddir, target) + target_args = [vcxproj] + elif backend is Backend.xcode: + target_args = ['-target', target] + elif backend is Backend.ninja: + target_args = [target] + else: + raise AssertionError(f'Unknown backend: {backend!r}') + return target_args + dir_args + +def get_backend_commands(backend: Backend, debug: bool = False) -> \ + T.Tuple[T.List[str], T.List[str], T.List[str], T.List[str], T.List[str]]: + install_cmd: T.List[str] = [] + uninstall_cmd: T.List[str] = [] + clean_cmd: T.List[str] + cmd: T.List[str] + test_cmd: T.List[str] + if backend is Backend.vs: + cmd = ['msbuild'] + clean_cmd = cmd + ['/target:Clean'] + test_cmd = cmd + ['RUN_TESTS.vcxproj'] + elif backend is Backend.xcode: + cmd = ['xcodebuild'] + clean_cmd = cmd + ['-alltargets', 'clean'] + test_cmd = cmd + ['-target', 'RUN_TESTS'] + elif backend is Backend.ninja: + global NINJA_CMD + cmd = NINJA_CMD + ['-w', 'dupbuild=err', '-d', 'explain'] + if debug: + cmd += ['-v'] + clean_cmd = cmd + ['clean'] + test_cmd = cmd + ['test', 'benchmark'] + install_cmd = cmd + ['install'] + uninstall_cmd = cmd + ['uninstall'] + else: + raise AssertionError(f'Unknown backend: {backend!r}') + return cmd, clean_cmd, test_cmd, install_cmd, uninstall_cmd + +def ensure_backend_detects_changes(backend: Backend) -> None: + global NINJA_1_9_OR_NEWER + if backend is not Backend.ninja: + return + need_workaround = False + # We're using ninja >= 1.9 which has QuLogic's patch for sub-1s resolution + # timestamps + if not NINJA_1_9_OR_NEWER: + mlog.warning('Don\'t have ninja >= 1.9, enabling timestamp resolution workaround', once=True) + need_workaround = True + # Increase the difference between build.ninja's timestamp and the timestamp + # of whatever you changed: https://github.com/ninja-build/ninja/issues/371 + if need_workaround: + time.sleep(1) + +def run_mtest_inprocess(commandlist: T.List[str]) -> T.Tuple[int, str, str]: + out = StringIO() + with mock.patch.object(sys, 'stdout', out), mock.patch.object(sys, 'stderr', out): + returncode = mtest.run_with_args(commandlist) + return returncode, stdout.getvalue() + +def clear_meson_configure_class_caches() -> None: + CCompiler.find_library_cache = {} + CCompiler.find_framework_cache = {} + dependencies.PkgConfigDependency.pkgbin_cache = {} + dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None) + mesonlib.project_meson_versions = collections.defaultdict(str) + +def run_configure_inprocess(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: + stderr = StringIO() + stdout = StringIO() + returncode = 0 + with mock.patch.dict(os.environ, env or {}), mock.patch.object(sys, 'stdout', stdout), mock.patch.object(sys, 'stderr', stderr): + try: + returncode = mesonmain.run(commandlist, get_meson_script()) + except Exception: + if catch_exception: + returncode = 1 + traceback.print_exc() + else: + raise + finally: + clear_meson_configure_class_caches() + return returncode, stdout.getvalue(), stderr.getvalue() + +def run_configure_external(full_command: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]: + pc, o, e = mesonlib.Popen_safe(full_command, env=env) + return pc.returncode, o, e + +def run_configure(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: + global meson_exe + if meson_exe: + return run_configure_external(meson_exe + commandlist, env=env) + return run_configure_inprocess(commandlist, env=env, catch_exception=catch_exception) + +def print_system_info(): + print(mlog.bold('System information.')) + print('Architecture:', platform.architecture()) + print('Machine:', platform.machine()) + print('Platform:', platform.system()) + print('Processor:', platform.processor()) + print('System:', platform.system()) + print('') + print(flush=True) + +def subprocess_call(cmd, **kwargs): + print(f'$ {mesonlib.join_args(cmd)}') + return subprocess.call(cmd, **kwargs) + +def main(): + print_system_info() + parser = argparse.ArgumentParser() + parser.add_argument('--backend', default=None, dest='backend', + choices=backendlist) + parser.add_argument('--cross', default=[], dest='cross', action='append') + parser.add_argument('--cross-only', action='store_true') + parser.add_argument('--failfast', action='store_true') + parser.add_argument('--no-unittests', action='store_true', default=False) + (options, _) = parser.parse_known_args() + returncode = 0 + backend, _ = guess_backend(options.backend, shutil.which('msbuild')) + no_unittests = options.no_unittests + # Running on a developer machine? Be nice! + if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'CI' not in os.environ: + os.nice(20) + # Appveyor sets the `platform` environment variable which completely messes + # up building with the vs2010 and vs2015 backends. + # + # Specifically, MSBuild reads the `platform` environment variable to set + # the configured value for the platform (Win32/x64/arm), which breaks x86 + # builds. + # + # Appveyor setting this also breaks our 'native build arch' detection for + # Windows in environment.py:detect_windows_arch() by overwriting the value + # of `platform` set by vcvarsall.bat. + # + # While building for x86, `platform` should be unset. + if 'APPVEYOR' in os.environ and os.environ['arch'] == 'x86': + os.environ.pop('platform') + # Run tests + # Can't pass arguments to unit tests, so set the backend to use in the environment + env = os.environ.copy() + if not options.cross: + cmd = mesonlib.python_command + ['run_meson_command_tests.py', '-v'] + if options.failfast: + cmd += ['--failfast'] + returncode += subprocess_call(cmd, env=env) + if options.failfast and returncode != 0: + return returncode + if no_unittests: + print('Skipping all unit tests.') + print(flush=True) + returncode = 0 + else: + print(mlog.bold('Running unittests.')) + print(flush=True) + cmd = mesonlib.python_command + ['run_unittests.py', '--backend=' + backend.name, '-v'] + if options.failfast: + cmd += ['--failfast'] + returncode += subprocess_call(cmd, env=env) + if options.failfast and returncode != 0: + return returncode + cmd = mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:] + returncode += subprocess_call(cmd, env=env) + else: + cross_test_args = mesonlib.python_command + ['run_cross_test.py'] + for cf in options.cross: + print(mlog.bold(f'Running {cf} cross tests.')) + print(flush=True) + cmd = cross_test_args + ['cross/' + cf] + if options.failfast: + cmd += ['--failfast'] + if options.cross_only: + cmd += ['--cross-only'] + returncode += subprocess_call(cmd, env=env) + if options.failfast and returncode != 0: + return returncode + return returncode + +if __name__ == '__main__': + setup_vsenv() + print('Meson build system', meson_version, 'Project and Unit Tests') + raise SystemExit(main()) |