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.
232 lines
7.8 KiB
232 lines
7.8 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 unittest import mock |
|
|
|
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: |
|
if key in os.environ and not is_ci(): |
|
raise unittest.SkipTest(f'Env var {key!r} set, skipping') |
|
with mock.patch.dict(os.environ): |
|
os.environ.pop(key, None) |
|
return func(*args, **kwargs) |
|
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], |
|
encoding='utf-8', 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
|
|
|