The Meson Build System http://mesonbuild.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
7.9 KiB

# SPDX-License-Identifier: Apache-2.0
# Copyright © 2024 Intel Corporation
from __future__ import annotations
import subprocess
import os
import shutil
import unittest
import functools
import re
import typing as T
import zipfile
from pathlib import Path
from contextlib import contextmanager
from mesonbuild.compilers import detect_c_compiler, compiler_from_language
from mesonbuild.mesonlib import (
MachineChoice, is_osx, is_cygwin, EnvironmentException, MachineChoice,
OrderedSet
)
from mesonbuild.options import OptionKey
from run_tests import get_fake_env
if T.TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec('P')
R = T.TypeVar('R')
def is_ci() -> bool:
return os.environ.get('MESON_CI_JOBNAME', 'thirdparty') != 'thirdparty'
def skip_if_not_base_option(feature: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
"""Skip tests if The compiler does not support a given base option.
for example, ICC doesn't currently support b_sanitize.
"""
def actual(f: T.Callable[P, R]) -> T.Callable[P, R]:
@functools.wraps(f)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
env = get_fake_env()
cc = detect_c_compiler(env, MachineChoice.HOST)
key = OptionKey(feature)
if key not in cc.base_options:
raise unittest.SkipTest(
f'{feature} not available with {cc.id}')
return f(*args, **kwargs)
return wrapped
return actual
def skipIfNoPkgconfig(f: T.Callable[P, R]) -> T.Callable[P, R]:
'''
Skip this test if no pkg-config is found, unless we're on CI.
This allows users to run our test suite without having
pkg-config installed on, f.ex., macOS, while ensuring that our CI does not
silently skip the test because of misconfiguration.
Note: Yes, we provide pkg-config even while running Windows CI
'''
@functools.wraps(f)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
if not is_ci() and shutil.which('pkg-config') is None:
raise unittest.SkipTest('pkg-config not found')
return f(*args, **kwargs)
return wrapped
def skipIfNoPkgconfigDep(depname: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
'''
Skip this test if the given pkg-config dep is not found, unless we're on CI.
'''
def wrapper(func: T.Callable[P, R]) -> T.Callable[P, R]:
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
if not is_ci() and shutil.which('pkg-config') is None:
raise unittest.SkipTest('pkg-config not found')
if not is_ci() and subprocess.call(['pkg-config', '--exists', depname]) != 0:
raise unittest.SkipTest(f'pkg-config dependency {depname} not found.')
return func(*args, **kwargs)
return wrapped
return wrapper
def skip_if_no_cmake(f: T.Callable[P, R]) -> T.Callable[P, R]:
'''
Skip this test if no cmake is found, unless we're on CI.
This allows users to run our test suite without having
cmake installed on, f.ex., macOS, while ensuring that our CI does not
silently skip the test because of misconfiguration.
'''
@functools.wraps(f)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
if not is_ci() and shutil.which('cmake') is None:
raise unittest.SkipTest('cmake not found')
return f(*args, **kwargs)
return wrapped
def skip_if_not_language(lang: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
def wrapper(func: T.Callable[P, R]) -> T.Callable[P, R]:
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
try:
compiler_from_language(get_fake_env(), lang, MachineChoice.HOST)
except EnvironmentException:
raise unittest.SkipTest(f'No {lang} compiler found.')
return func(*args, **kwargs)
return wrapped
return wrapper
def skip_if_env_set(key: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
'''
Skip a test if a particular env is set, except when running under CI
'''
def wrapper(func: T.Callable[P, R]) -> T.Callable[P, R]:
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
old = None
if key in os.environ:
if not is_ci():
raise unittest.SkipTest(f'Env var {key!r} set, skipping')
old = os.environ.pop(key)
try:
return func(*args, **kwargs)
finally:
if old is not None:
os.environ[key] = old
return wrapped
return wrapper
def skipIfNoExecutable(exename: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
'''
Skip this test if the given executable is not found.
'''
def wrapper(func: T.Callable[P, R]) -> T.Callable[P, R]:
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
if shutil.which(exename) is None:
raise unittest.SkipTest(exename + ' not found')
return func(*args, **kwargs)
return wrapped
return wrapper
def is_tarball() -> bool:
return not os.path.isdir('docs')
@contextmanager
def chdir(path: str) -> T.Iterator[None]:
curdir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(curdir)
def get_dynamic_section_entry(fname: str, entry: str) -> T.Optional[str]:
if is_cygwin() or is_osx():
raise unittest.SkipTest('Test only applicable to ELF platforms')
try:
raw_out = subprocess.check_output(['readelf', '-d', fname],
universal_newlines=True)
except FileNotFoundError:
# FIXME: Try using depfixer.py:Elf() as a fallback
raise unittest.SkipTest('readelf not found')
pattern = re.compile(entry + r': \[(.*?)\]')
for line in raw_out.split('\n'):
m = pattern.search(line)
if m is not None:
return str(m.group(1))
return None # The file did not contain the specified entry.
def get_soname(fname: str) -> T.Optional[str]:
return get_dynamic_section_entry(fname, 'soname')
def get_rpath(fname: str) -> T.Optional[str]:
raw = get_dynamic_section_entry(fname, r'(?:rpath|runpath)')
# Get both '' and None here
if not raw:
return None
# nix/nixos adds a bunch of stuff to the rpath out of necessity that we
# don't check for, so clear those
final = ':'.join([e for e in raw.split(':') if not e.startswith('/nix')])
# If we didn't end up anything but nix paths, return None here
if not final:
return None
return final
def get_classpath(fname: str) -> T.Optional[str]:
with zipfile.ZipFile(fname) as zip:
with zip.open('META-INF/MANIFEST.MF') as member:
contents = member.read().decode().strip()
lines: T.List[str] = []
for line in contents.splitlines():
if line.startswith(' '):
# continuation line
lines[-1] += line[1:]
else:
lines.append(line)
manifest = {
k.lower(): v.strip() for k, v in [l.split(':', 1) for l in lines]
}
return manifest.get('class-path')
def get_path_without_cmd(cmd: str, path: str) -> str:
pathsep = os.pathsep
paths = OrderedSet([Path(p).resolve() for p in path.split(pathsep)])
while True:
full_path = shutil.which(cmd, path=path)
if full_path is None:
break
dirname = Path(full_path).resolve().parent
paths.discard(dirname)
path = pathsep.join([str(p) for p in paths])
return path
def xfail_if_jobname(name: str) -> T.Callable[[T.Callable[P, R]], T.Callable[P, R]]:
if os.environ.get('MESON_CI_JOBNAME') == name:
return unittest.expectedFailure
def wrapper(func: T.Callable[P, R]) -> T.Callable[P, R]:
return func
return wrapper