meson/run_tests.py

402 lines
15 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2012-12-26 23:05:04 +08:00
# Copyright 2012-2021 The Meson development team
2012-12-26 23:05:04 +08:00
# 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.
import os
import sys
import time
import shutil
import subprocess
2017-05-07 15:17:42 +08:00
import tempfile
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
2021-02-20 04:11:36 +08:00
import typing as T
from mesonbuild import compilers
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
from mesonbuild.coredata import backendlist, version as meson_version
from mesonbuild.mesonlib import OptionKey
NINJA_1_9_OR_NEWER = False
NINJA_CMD = None
# If we're on CI, just assume we have ninja in PATH and it's new enough because
# we provide that. This avoids having to detect ninja for every subprocess unit
# test that we run.
if 'CI' in os.environ:
NINJA_1_9_OR_NEWER = True
NINJA_CMD = ['ninja']
else:
# 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 None:
raise RuntimeError('Could not find Ninja v1.7 or newer')
2021-02-20 04:11:36 +08:00
def guess_backend(backend_str: str, msbuild_exe: str) -> T.Tuple['Backend', T.List[str]]:
# Auto-detect backend if unspecified
backend_flags = []
2021-02-20 04:11:36 +08:00
if backend_str is None:
if msbuild_exe is not None and (mesonlib.is_windows() and not _using_intelcl()):
2021-02-20 04:11:36 +08:00
backend_str = 'vs' # Meson will auto-detect VS version to use
else:
2021-02-20 04:11:36 +08:00
backend_str = 'ninja'
# Set backend arguments for Meson
2021-02-20 04:11:36 +08:00
if backend_str.startswith('vs'):
backend_flags = ['--backend=' + backend_str]
backend = Backend.vs
2021-02-20 04:11:36 +08:00
elif backend_str == 'xcode':
backend_flags = ['--backend=xcode']
backend = Backend.xcode
2021-02-20 04:11:36 +08:00
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
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>'
2019-04-23 21:00:39 +08:00
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
2021-02-20 04:11:36 +08:00
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]:
stderr = StringIO()
stdout = StringIO()
with mock.patch.object(sys, 'stdout', stdout), mock.patch.object(sys, 'stderr', stderr):
returncode = mtest.run_with_args(commandlist)
return returncode, stdout.getvalue(), stderr.getvalue()
def clear_meson_configure_class_caches() -> None:
compilers.CCompiler.find_library_cache = {}
compilers.CCompiler.find_framework_cache = {}
dependencies.PkgConfigDependency.pkgbin_cache = {}
dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None)
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 main():
print_system_info()
parser = argparse.ArgumentParser()
parser.add_argument('--cov', action='store_true')
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')
2018-05-25 05:37:57 +08:00
parser.add_argument('--failfast', action='store_true')
parser.add_argument('--no-unittests', action='store_true', default=False)
(options, _) = parser.parse_known_args()
2017-05-07 15:17:42 +08:00
# Enable coverage early...
enable_coverage = options.cov
2017-05-07 15:17:42 +08:00
if enable_coverage:
os.makedirs('.coverage', exist_ok=True)
sys.argv.remove('--cov')
import coverage
coverage.process_startup()
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()
with tempfile.TemporaryDirectory() as temp_dir:
2017-05-07 15:17:42 +08:00
# Enable coverage on all subsequent processes.
if enable_coverage:
Path(temp_dir, 'usercustomize.py').open('w').write(
'import coverage\n'
'coverage.process_startup()\n')
2017-05-07 15:17:42 +08:00
env['COVERAGE_PROCESS_START'] = '.coveragerc'
2018-06-29 05:17:27 +08:00
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = os.pathsep.join([temp_dir, env.get('PYTHONPATH')])
else:
env['PYTHONPATH'] = temp_dir
if not options.cross:
cmd = mesonlib.python_command + ['run_meson_command_tests.py', '-v']
2018-05-25 05:37:57 +08:00
if options.failfast:
cmd += ['--failfast']
returncode += subprocess.call(cmd, env=env)
2018-05-25 05:37:57 +08:00
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__':
mesonmain.setup_vsenv()
print('Meson build system', meson_version, 'Project and Unit Tests')
raise SystemExit(main())