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.
10341 lines
467 KiB
10341 lines
467 KiB
#!/usr/bin/env python3 |
|
# Copyright 2016-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 time |
|
import stat |
|
import subprocess |
|
import re |
|
import json |
|
import tempfile |
|
import textwrap |
|
import os |
|
import shutil |
|
import unittest |
|
import platform |
|
import pickle |
|
import functools |
|
import io |
|
import operator |
|
import threading |
|
import zipfile, tarfile |
|
import hashlib |
|
from itertools import chain |
|
from unittest import mock |
|
from configparser import ConfigParser |
|
from contextlib import contextmanager |
|
from glob import glob |
|
from pathlib import (PurePath, Path) |
|
from distutils.dir_util import copy_tree |
|
import typing as T |
|
|
|
import mesonbuild.mlog |
|
import mesonbuild.depfile |
|
import mesonbuild.dependencies.base |
|
import mesonbuild.dependencies.factory |
|
import mesonbuild.compilers |
|
import mesonbuild.envconfig |
|
import mesonbuild.environment |
|
import mesonbuild.mesonlib |
|
import mesonbuild.coredata |
|
import mesonbuild.modules.gnome |
|
from mesonbuild.interpreter import Interpreter |
|
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder |
|
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo |
|
from mesonbuild.ast import AstInterpreter |
|
from mesonbuild.mesonlib import ( |
|
BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows, |
|
is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, is_sunos, |
|
windows_proof_rmtree, windows_proof_rm, python_command, |
|
version_compare, split_args, quote_arg, relpath, is_linux, git, |
|
search_version |
|
) |
|
from mesonbuild.compilers import ( |
|
detect_static_linker, detect_c_compiler, detect_cpp_compiler, |
|
detect_objc_compiler, detect_objcpp_compiler, detect_d_compiler, |
|
detect_swift_compiler, compiler_from_language |
|
) |
|
from mesonbuild.environment import detect_ninja |
|
from mesonbuild.mesonlib import MesonException, EnvironmentException, OptionKey |
|
from mesonbuild.dependencies import PkgConfigDependency |
|
from mesonbuild.programs import ExternalProgram |
|
import mesonbuild.dependencies.base |
|
from mesonbuild.build import Target, ConfigurationData |
|
import mesonbuild.modules.pkgconfig |
|
from mesonbuild.scripts import destdir_join |
|
|
|
from mesonbuild.mtest import TAPParser, TestResult |
|
from mesonbuild.mesonmain import setup_vsenv |
|
from mesonbuild.wrap.wrap import PackageDefinition, WrapException |
|
|
|
from run_tests import ( |
|
Backend, FakeBuild, FakeCompilerOptions, |
|
ensure_backend_detects_changes, exe_suffix, get_backend_commands, |
|
get_builddir_target_args, get_fake_env, get_fake_options, get_meson_script, |
|
run_configure_inprocess, run_mtest_inprocess |
|
) |
|
|
|
if T.TYPE_CHECKING: |
|
from mesonbuild.compilers import Compiler |
|
|
|
URLOPEN_TIMEOUT = 5 |
|
|
|
@contextmanager |
|
def chdir(path: str): |
|
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')]) |
|
return final |
|
|
|
def is_tarball(): |
|
if not os.path.isdir('docs'): |
|
return True |
|
return False |
|
|
|
def is_ci(): |
|
if 'CI' in os.environ: |
|
return True |
|
return False |
|
|
|
def _git_init(project_dir): |
|
# If a user has git configuration init.defaultBranch set we want to override that |
|
with tempfile.TemporaryDirectory() as d: |
|
out = git(['--version'], str(d))[1] |
|
if version_compare(search_version(out), '>= 2.28'): |
|
extra_cmd = ['--initial-branch', 'master'] |
|
else: |
|
extra_cmd = [] |
|
|
|
subprocess.check_call(['git', 'init'] + extra_cmd, cwd=project_dir, stdout=subprocess.DEVNULL) |
|
subprocess.check_call(['git', 'config', |
|
'user.name', 'Author Person'], cwd=project_dir) |
|
subprocess.check_call(['git', 'config', |
|
'user.email', 'teh_coderz@example.com'], cwd=project_dir) |
|
_git_add_all(project_dir) |
|
|
|
def _git_add_all(project_dir): |
|
subprocess.check_call('git add *', cwd=project_dir, shell=True, |
|
stdout=subprocess.DEVNULL) |
|
subprocess.check_call(['git', 'commit', '-a', '-m', 'I am a project'], cwd=project_dir, |
|
stdout=subprocess.DEVNULL) |
|
|
|
@functools.lru_cache() |
|
def is_real_gnu_compiler(path): |
|
''' |
|
Check if the gcc we have is a real gcc and not a macOS wrapper around clang |
|
''' |
|
if not path: |
|
return False |
|
out = subprocess.check_output([path, '--version'], universal_newlines=True, stderr=subprocess.STDOUT) |
|
return 'Free Software Foundation' in out |
|
|
|
def skipIfNoExecutable(exename): |
|
''' |
|
Skip this test if the given executable is not found. |
|
''' |
|
def wrapper(func): |
|
@functools.wraps(func) |
|
def wrapped(*args, **kwargs): |
|
if shutil.which(exename) is None: |
|
raise unittest.SkipTest(exename + ' not found') |
|
return func(*args, **kwargs) |
|
return wrapped |
|
return wrapper |
|
|
|
def skipIfNoPkgconfig(f): |
|
''' |
|
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, **kwargs): |
|
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): |
|
''' |
|
Skip this test if the given pkg-config dep is not found, unless we're on CI. |
|
''' |
|
def wrapper(func): |
|
@functools.wraps(func) |
|
def wrapped(*args, **kwargs): |
|
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): |
|
''' |
|
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, **kwargs): |
|
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): |
|
def wrapper(func): |
|
@functools.wraps(func) |
|
def wrapped(*args, **kwargs): |
|
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): |
|
''' |
|
Skip a test if a particular env is set, except when running under CI |
|
''' |
|
def wrapper(func): |
|
@functools.wraps(func) |
|
def wrapped(*args, **kwargs): |
|
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 skip_if_not_base_option(feature): |
|
"""Skip tests if The compiler does not support a given base option. |
|
|
|
for example, ICC doesn't currently support b_sanitize. |
|
""" |
|
def actual(f): |
|
@functools.wraps(f) |
|
def wrapped(*args, **kwargs): |
|
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 |
|
|
|
|
|
@contextmanager |
|
def temp_filename(): |
|
'''A context manager which provides a filename to an empty temporary file. |
|
|
|
On exit the file will be deleted. |
|
''' |
|
|
|
fd, filename = tempfile.mkstemp() |
|
os.close(fd) |
|
try: |
|
yield filename |
|
finally: |
|
try: |
|
os.remove(filename) |
|
except OSError: |
|
pass |
|
|
|
@contextmanager |
|
def no_pkgconfig(): |
|
''' |
|
A context manager that overrides shutil.which and ExternalProgram to force |
|
them to return None for pkg-config to simulate it not existing. |
|
''' |
|
old_which = shutil.which |
|
old_search = ExternalProgram._search |
|
|
|
def new_search(self, name, search_dir): |
|
if name == 'pkg-config': |
|
return [None] |
|
return old_search(self, name, search_dir) |
|
|
|
def new_which(cmd, *kwargs): |
|
if cmd == 'pkg-config': |
|
return None |
|
return old_which(cmd, *kwargs) |
|
|
|
shutil.which = new_which |
|
ExternalProgram._search = new_search |
|
try: |
|
yield |
|
finally: |
|
shutil.which = old_which |
|
ExternalProgram._search = old_search |
|
|
|
|
|
class InternalTests(unittest.TestCase): |
|
|
|
def test_version_number(self): |
|
self.assertEqual(search_version('foobar 1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('foobar 2016.10.28 1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('2016.10.28 1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('foobar 2016.10.128'), '2016.10.128') |
|
self.assertEqual(search_version('2016.10.128'), '2016.10.128') |
|
self.assertEqual(search_version('2016.10'), '2016.10') |
|
self.assertEqual(search_version('2016.10 1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('oops v1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('2016.oops 1.2.3'), '1.2.3') |
|
self.assertEqual(search_version('2016.x'), 'unknown version') |
|
|
|
def test_mode_symbolic_to_bits(self): |
|
modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits |
|
self.assertEqual(modefunc('---------'), 0) |
|
self.assertEqual(modefunc('r--------'), stat.S_IRUSR) |
|
self.assertEqual(modefunc('---r-----'), stat.S_IRGRP) |
|
self.assertEqual(modefunc('------r--'), stat.S_IROTH) |
|
self.assertEqual(modefunc('-w-------'), stat.S_IWUSR) |
|
self.assertEqual(modefunc('----w----'), stat.S_IWGRP) |
|
self.assertEqual(modefunc('-------w-'), stat.S_IWOTH) |
|
self.assertEqual(modefunc('--x------'), stat.S_IXUSR) |
|
self.assertEqual(modefunc('-----x---'), stat.S_IXGRP) |
|
self.assertEqual(modefunc('--------x'), stat.S_IXOTH) |
|
self.assertEqual(modefunc('--S------'), stat.S_ISUID) |
|
self.assertEqual(modefunc('-----S---'), stat.S_ISGID) |
|
self.assertEqual(modefunc('--------T'), stat.S_ISVTX) |
|
self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR) |
|
self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP) |
|
self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH) |
|
self.assertEqual(modefunc('rwx------'), stat.S_IRWXU) |
|
self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG) |
|
self.assertEqual(modefunc('------rwx'), stat.S_IRWXO) |
|
# We could keep listing combinations exhaustively but that seems |
|
# tedious and pointless. Just test a few more. |
|
self.assertEqual(modefunc('rwxr-xr-x'), |
|
stat.S_IRWXU | |
|
stat.S_IRGRP | stat.S_IXGRP | |
|
stat.S_IROTH | stat.S_IXOTH) |
|
self.assertEqual(modefunc('rw-r--r--'), |
|
stat.S_IRUSR | stat.S_IWUSR | |
|
stat.S_IRGRP | |
|
stat.S_IROTH) |
|
self.assertEqual(modefunc('rwsr-x---'), |
|
stat.S_IRWXU | stat.S_ISUID | |
|
stat.S_IRGRP | stat.S_IXGRP) |
|
|
|
def test_compiler_args_class_none_flush(self): |
|
cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock()) |
|
a = cc.compiler_args(['-I.']) |
|
#first we are checking if the tree construction deduplicates the correct -I argument |
|
a += ['-I..'] |
|
a += ['-I./tests/'] |
|
a += ['-I./tests2/'] |
|
#think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes: |
|
# assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.']) |
|
a += ['-I.'] |
|
a += ['-I.', '-I./tests/'] |
|
self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..']) |
|
|
|
#then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one |
|
a += ['-I.', '-I./tests2/'] |
|
self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..']) |
|
|
|
def test_compiler_args_class_d(self): |
|
d = mesonbuild.compilers.DmdDCompiler([], 'fake', MachineChoice.HOST, 'info', 'arch') |
|
# check include order is kept when deduplicating |
|
a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird']) |
|
a += ['-Ifirst'] |
|
self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird']) |
|
|
|
def test_compiler_args_class_clike(self): |
|
cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock()) |
|
# Test that empty initialization works |
|
a = cc.compiler_args() |
|
self.assertEqual(a, []) |
|
# Test that list initialization works |
|
a = cc.compiler_args(['-I.', '-I..']) |
|
self.assertEqual(a, ['-I.', '-I..']) |
|
# Test that there is no de-dup on initialization |
|
self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.']) |
|
|
|
## Test that appending works |
|
a.append('-I..') |
|
self.assertEqual(a, ['-I..', '-I.']) |
|
a.append('-O3') |
|
self.assertEqual(a, ['-I..', '-I.', '-O3']) |
|
|
|
## Test that in-place addition works |
|
a += ['-O2', '-O2'] |
|
self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2']) |
|
# Test that removal works |
|
a.remove('-O2') |
|
self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2']) |
|
# Test that de-dup happens on addition |
|
a += ['-Ifoo', '-Ifoo'] |
|
self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2']) |
|
|
|
# .extend() is just +=, so we don't test it |
|
|
|
## Test that addition works |
|
# Test that adding a list with just one old arg works and yields the same array |
|
a = a + ['-Ifoo'] |
|
self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2']) |
|
# Test that adding a list with one arg new and one old works |
|
a = a + ['-Ifoo', '-Ibaz'] |
|
self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2']) |
|
# Test that adding args that must be prepended and appended works |
|
a = a + ['-Ibar', '-Wall'] |
|
self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall']) |
|
|
|
## Test that reflected addition works |
|
# Test that adding to a list with just one old arg works and yields the same array |
|
a = ['-Ifoo'] + a |
|
self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall']) |
|
# Test that adding to a list with just one new arg that is not pre-pended works |
|
a = ['-Werror'] + a |
|
self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall']) |
|
# Test that adding to a list with two new args preserves the order |
|
a = ['-Ldir', '-Lbah'] + a |
|
self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall']) |
|
# Test that adding to a list with old args does nothing |
|
a = ['-Ibar', '-Ibaz', '-Ifoo'] + a |
|
self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall']) |
|
|
|
## Test that adding libraries works |
|
l = cc.compiler_args(['-Lfoodir', '-lfoo']) |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo']) |
|
# Adding a library and a libpath appends both correctly |
|
l += ['-Lbardir', '-lbar'] |
|
self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar']) |
|
# Adding the same library again does nothing |
|
l += ['-lbar'] |
|
self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar']) |
|
|
|
## Test that 'direct' append and extend works |
|
l = cc.compiler_args(['-Lfoodir', '-lfoo']) |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo']) |
|
# Direct-adding a library and a libpath appends both correctly |
|
l.extend_direct(['-Lbardir', '-lbar']) |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar']) |
|
# Direct-adding the same library again still adds it |
|
l.append_direct('-lbar') |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar']) |
|
# Direct-adding with absolute path deduplicates |
|
l.append_direct('/libbaz.a') |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a']) |
|
# Adding libbaz again does nothing |
|
l.append_direct('/libbaz.a') |
|
self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a']) |
|
|
|
def test_compiler_args_class_gnuld(self): |
|
## Test --start/end-group |
|
linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) |
|
gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker) |
|
## Ensure that the fake compiler is never called by overriding the relevant function |
|
gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include'] |
|
## Test that 'direct' append and extend works |
|
l = gcc.compiler_args(['-Lfoodir', '-lfoo']) |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group']) |
|
# Direct-adding a library and a libpath appends both correctly |
|
l.extend_direct(['-Lbardir', '-lbar']) |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group']) |
|
# Direct-adding the same library again still adds it |
|
l.append_direct('-lbar') |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group']) |
|
# Direct-adding with absolute path deduplicates |
|
l.append_direct('/libbaz.a') |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group']) |
|
# Adding libbaz again does nothing |
|
l.append_direct('/libbaz.a') |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group']) |
|
# Adding a non-library argument doesn't include it in the group |
|
l += ['-Lfoo', '-Wl,--export-dynamic'] |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group', '-Wl,--export-dynamic']) |
|
# -Wl,-lfoo is detected as a library and gets added to the group |
|
l.append('-Wl,-ldl') |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group']) |
|
|
|
def test_compiler_args_remove_system(self): |
|
## Test --start/end-group |
|
linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) |
|
gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker) |
|
## Ensure that the fake compiler is never called by overriding the relevant function |
|
gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include'] |
|
## Test that 'direct' append and extend works |
|
l = gcc.compiler_args(['-Lfoodir', '-lfoo']) |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group']) |
|
## Test that to_native removes all system includes |
|
l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include'] |
|
self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group', '-DSOMETHING_IMPORTANT=1']) |
|
|
|
def test_string_templates_substitution(self): |
|
dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict |
|
substfunc = mesonbuild.mesonlib.substitute_values |
|
ME = mesonbuild.mesonlib.MesonException |
|
|
|
# Identity |
|
self.assertEqual(dictfunc([], []), {}) |
|
|
|
# One input, no outputs |
|
inputs = ['bar/foo.c.in'] |
|
outputs = [] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], |
|
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
# Check substitutions |
|
cmd = ['some', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), cmd) |
|
cmd = ['@INPUT@.out', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:]) |
|
cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), |
|
[inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:]) |
|
cmd = ['@INPUT@', '@BASENAME@.hah', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), |
|
inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:]) |
|
cmd = ['@OUTPUT@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
|
|
# One input, one output |
|
inputs = ['bar/foo.c.in'] |
|
outputs = ['out.c'] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], |
|
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c', |
|
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
# Check substitutions |
|
cmd = ['some', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), cmd) |
|
cmd = ['@INPUT@.out', '@OUTPUT@', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), |
|
[inputs[0] + '.out'] + outputs + cmd[2:]) |
|
cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@'] |
|
self.assertEqual(substfunc(cmd, d), |
|
[inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs) |
|
cmd = ['@INPUT@', '@BASENAME@.hah', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), |
|
inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:]) |
|
|
|
# One input, one output with a subdir |
|
outputs = ['dir/out.c'] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], |
|
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c', |
|
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
|
|
# Two inputs, no outputs |
|
inputs = ['bar/foo.c.in', 'baz/foo.c.in'] |
|
outputs = [] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
# Check substitutions |
|
cmd = ['some', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), cmd) |
|
cmd = ['@INPUT@', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), inputs + cmd[1:]) |
|
cmd = ['@INPUT0@.out', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:]) |
|
cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:]) |
|
cmd = ['@INPUT0@', '@INPUT1@', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), inputs + cmd[2:]) |
|
# Many inputs, can't use @INPUT@ like this |
|
cmd = ['@INPUT@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Not enough inputs |
|
cmd = ['@INPUT2@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Too many inputs |
|
cmd = ['@PLAINNAME@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
cmd = ['@BASENAME@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# No outputs |
|
cmd = ['@OUTPUT@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
cmd = ['@OUTPUT0@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
cmd = ['@OUTDIR@'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
|
|
# Two inputs, one output |
|
outputs = ['dir/out.c'] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1], |
|
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
# Check substitutions |
|
cmd = ['some', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), cmd) |
|
cmd = ['@OUTPUT@', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), outputs + cmd[1:]) |
|
cmd = ['@OUTPUT@.out', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:]) |
|
cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:]) |
|
# Many inputs, can't use @INPUT@ like this |
|
cmd = ['@INPUT@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Not enough inputs |
|
cmd = ['@INPUT2@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Not enough outputs |
|
cmd = ['@OUTPUT2@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
|
|
# Two inputs, two outputs |
|
outputs = ['dir/out.c', 'dir/out2.c'] |
|
ret = dictfunc(inputs, outputs) |
|
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1], |
|
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1], |
|
'@OUTDIR@': 'dir'} |
|
# Check dictionary |
|
self.assertEqual(ret, d) |
|
# Check substitutions |
|
cmd = ['some', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), cmd) |
|
cmd = ['@OUTPUT@', 'ordinary', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), outputs + cmd[1:]) |
|
cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings'] |
|
self.assertEqual(substfunc(cmd, d), outputs + cmd[2:]) |
|
cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@'] |
|
self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir']) |
|
# Many inputs, can't use @INPUT@ like this |
|
cmd = ['@INPUT@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Not enough inputs |
|
cmd = ['@INPUT2@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Not enough outputs |
|
cmd = ['@OUTPUT2@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
# Many outputs, can't use @OUTPUT@ like this |
|
cmd = ['@OUTPUT@.out', 'ordinary', 'strings'] |
|
self.assertRaises(ME, substfunc, cmd, d) |
|
|
|
def test_needs_exe_wrapper_override(self): |
|
config = ConfigParser() |
|
config['binaries'] = { |
|
'c': '\'/usr/bin/gcc\'', |
|
} |
|
config['host_machine'] = { |
|
'system': '\'linux\'', |
|
'cpu_family': '\'arm\'', |
|
'cpu': '\'armv7\'', |
|
'endian': '\'little\'', |
|
} |
|
# Can not be used as context manager because we need to |
|
# open it a second time and this is not possible on |
|
# Windows. |
|
configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False) |
|
configfilename = configfile.name |
|
config.write(configfile) |
|
configfile.flush() |
|
configfile.close() |
|
opts = get_fake_options() |
|
opts.cross_file = (configfilename,) |
|
env = get_fake_env(opts=opts) |
|
detected_value = env.need_exe_wrapper() |
|
os.unlink(configfilename) |
|
|
|
desired_value = not detected_value |
|
config['properties'] = { |
|
'needs_exe_wrapper': 'true' if desired_value else 'false' |
|
} |
|
|
|
configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False) |
|
configfilename = configfile.name |
|
config.write(configfile) |
|
configfile.close() |
|
opts = get_fake_options() |
|
opts.cross_file = (configfilename,) |
|
env = get_fake_env(opts=opts) |
|
forced_value = env.need_exe_wrapper() |
|
os.unlink(configfilename) |
|
|
|
self.assertEqual(forced_value, desired_value) |
|
|
|
def test_listify(self): |
|
listify = mesonbuild.mesonlib.listify |
|
# Test sanity |
|
self.assertEqual([1], listify(1)) |
|
self.assertEqual([], listify([])) |
|
self.assertEqual([1], listify([1])) |
|
# Test flattening |
|
self.assertEqual([1, 2, 3], listify([1, [2, 3]])) |
|
self.assertEqual([1, 2, 3], listify([1, [2, [3]]])) |
|
self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False)) |
|
# Test flattening and unholdering |
|
class TestHeldObj(mesonbuild.mesonlib.HoldableObject): |
|
def __init__(self, val: int) -> None: |
|
self._val = val |
|
class MockInterpreter: |
|
def __init__(self) -> None: |
|
self.subproject = '' |
|
self.environment = None |
|
heldObj1 = TestHeldObj(1) |
|
holder1 = ObjectHolder(heldObj1, MockInterpreter()) |
|
self.assertEqual([holder1], listify(holder1)) |
|
self.assertEqual([holder1], listify([holder1])) |
|
self.assertEqual([holder1, 2], listify([holder1, 2])) |
|
self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]])) |
|
|
|
def test_extract_as_list(self): |
|
extract = mesonbuild.mesonlib.extract_as_list |
|
# Test sanity |
|
kwargs = {'sources': [1, 2, 3]} |
|
self.assertEqual([1, 2, 3], extract(kwargs, 'sources')) |
|
self.assertEqual(kwargs, {'sources': [1, 2, 3]}) |
|
self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True)) |
|
self.assertEqual(kwargs, {}) |
|
|
|
class TestHeldObj(mesonbuild.mesonlib.HoldableObject): |
|
pass |
|
class MockInterpreter: |
|
def __init__(self) -> None: |
|
self.subproject = '' |
|
self.environment = None |
|
heldObj = TestHeldObj() |
|
|
|
# Test unholding |
|
holder3 = ObjectHolder(heldObj, MockInterpreter()) |
|
kwargs = {'sources': [1, 2, holder3]} |
|
self.assertEqual(kwargs, {'sources': [1, 2, holder3]}) |
|
|
|
# flatten nested lists |
|
kwargs = {'sources': [1, [2, [3]]]} |
|
self.assertEqual([1, 2, 3], extract(kwargs, 'sources')) |
|
|
|
def test_pkgconfig_module(self): |
|
dummystate = mock.Mock() |
|
dummystate.subproject = 'dummy' |
|
_mock = mock.Mock(spec=mesonbuild.dependencies.ExternalDependency) |
|
_mock.pcdep = mock.Mock() |
|
_mock.pcdep.name = "some_name" |
|
_mock.version_reqs = [] |
|
|
|
# pkgconfig dependency as lib |
|
deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib") |
|
deps.add_pub_libs([_mock]) |
|
self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name") |
|
|
|
# pkgconfig dependency as requires |
|
deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib") |
|
deps.add_pub_reqs([_mock]) |
|
self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name") |
|
|
|
def _test_all_naming(self, cc, env, patterns, platform): |
|
shr = patterns[platform]['shared'] |
|
stc = patterns[platform]['static'] |
|
shrstc = shr + tuple([x for x in stc if x not in shr]) |
|
stcshr = stc + tuple([x for x in shr if x not in stc]) |
|
p = cc.get_library_naming(env, LibType.SHARED) |
|
self.assertEqual(p, shr) |
|
p = cc.get_library_naming(env, LibType.STATIC) |
|
self.assertEqual(p, stc) |
|
p = cc.get_library_naming(env, LibType.PREFER_STATIC) |
|
self.assertEqual(p, stcshr) |
|
p = cc.get_library_naming(env, LibType.PREFER_SHARED) |
|
self.assertEqual(p, shrstc) |
|
# Test find library by mocking up openbsd |
|
if platform != 'openbsd': |
|
return |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
with open(os.path.join(tmpdir, 'libfoo.so.6.0'), 'w') as f: |
|
f.write('') |
|
with open(os.path.join(tmpdir, 'libfoo.so.5.0'), 'w') as f: |
|
f.write('') |
|
with open(os.path.join(tmpdir, 'libfoo.so.54.0'), 'w') as f: |
|
f.write('') |
|
with open(os.path.join(tmpdir, 'libfoo.so.66a.0b'), 'w') as f: |
|
f.write('') |
|
with open(os.path.join(tmpdir, 'libfoo.so.70.0.so.1'), 'w') as f: |
|
f.write('') |
|
found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED) |
|
self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0') |
|
|
|
def test_find_library_patterns(self): |
|
''' |
|
Unit test for the library search patterns used by find_library() |
|
''' |
|
unix_static = ('lib{}.a', '{}.a') |
|
msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib') |
|
# This is the priority list of pattern matching for library searching |
|
patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'), |
|
'static': unix_static}, |
|
'linux': {'shared': ('lib{}.so', '{}.so'), |
|
'static': unix_static}, |
|
'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'), |
|
'static': unix_static}, |
|
'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll', |
|
'lib{}.dll.a', '{}.dll', '{}.dll.a'), |
|
'static': ('cyg{}.a',) + unix_static}, |
|
'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'), |
|
'static': msvc_static}, |
|
'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll', |
|
'{}.dll.a', '{}.lib', '{}.dll'), |
|
'static': msvc_static}} |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if is_osx(): |
|
self._test_all_naming(cc, env, patterns, 'darwin') |
|
elif is_cygwin(): |
|
self._test_all_naming(cc, env, patterns, 'cygwin') |
|
elif is_windows(): |
|
if cc.get_argument_syntax() == 'msvc': |
|
self._test_all_naming(cc, env, patterns, 'windows-msvc') |
|
else: |
|
self._test_all_naming(cc, env, patterns, 'windows-mingw') |
|
elif is_openbsd(): |
|
self._test_all_naming(cc, env, patterns, 'openbsd') |
|
else: |
|
self._test_all_naming(cc, env, patterns, 'linux') |
|
env.machines.host.system = 'openbsd' |
|
self._test_all_naming(cc, env, patterns, 'openbsd') |
|
env.machines.host.system = 'darwin' |
|
self._test_all_naming(cc, env, patterns, 'darwin') |
|
env.machines.host.system = 'cygwin' |
|
self._test_all_naming(cc, env, patterns, 'cygwin') |
|
env.machines.host.system = 'windows' |
|
self._test_all_naming(cc, env, patterns, 'windows-mingw') |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_parse_libs(self): |
|
''' |
|
Unit test for parsing of pkg-config output to search for libraries |
|
|
|
https://github.com/mesonbuild/meson/issues/3951 |
|
''' |
|
def create_static_lib(name): |
|
if not is_osx(): |
|
name.open('w').close() |
|
return |
|
src = name.with_suffix('.c') |
|
out = name.with_suffix('.o') |
|
with src.open('w') as f: |
|
f.write('int meson_foobar (void) { return 0; }') |
|
subprocess.check_call(['clang', '-c', str(src), '-o', str(out)]) |
|
subprocess.check_call(['ar', 'csr', str(name), str(out)]) |
|
|
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True) |
|
env = get_fake_env() |
|
compiler = detect_c_compiler(env, MachineChoice.HOST) |
|
env.coredata.compilers.host = {'c': compiler} |
|
env.coredata.options[OptionKey('link_args', lang='c')] = FakeCompilerOptions() |
|
p1 = Path(tmpdir) / '1' |
|
p2 = Path(tmpdir) / '2' |
|
p1.mkdir() |
|
p2.mkdir() |
|
# libfoo.a is in one prefix |
|
create_static_lib(p1 / 'libfoo.a') |
|
# libbar.a is in both prefixes |
|
create_static_lib(p1 / 'libbar.a') |
|
create_static_lib(p2 / 'libbar.a') |
|
# Ensure that we never statically link to these |
|
create_static_lib(p1 / 'libpthread.a') |
|
create_static_lib(p1 / 'libm.a') |
|
create_static_lib(p1 / 'libc.a') |
|
create_static_lib(p1 / 'libdl.a') |
|
create_static_lib(p1 / 'librt.a') |
|
|
|
def fake_call_pkgbin(self, args, env=None): |
|
if '--libs' not in args: |
|
return 0, '', '' |
|
if args[-1] == 'foo': |
|
return 0, f'-L{p2.as_posix()} -lfoo -L{p1.as_posix()} -lbar', '' |
|
if args[-1] == 'bar': |
|
return 0, f'-L{p2.as_posix()} -lbar', '' |
|
if args[-1] == 'internal': |
|
return 0, f'-L{p1.as_posix()} -lpthread -lm -lc -lrt -ldl', '' |
|
|
|
old_call = PkgConfigDependency._call_pkgbin |
|
old_check = PkgConfigDependency.check_pkgconfig |
|
PkgConfigDependency._call_pkgbin = fake_call_pkgbin |
|
PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin |
|
# Test begins |
|
try: |
|
kwargs = {'required': True, 'silent': True} |
|
foo_dep = PkgConfigDependency('foo', env, kwargs) |
|
self.assertEqual(foo_dep.get_link_args(), |
|
[(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()]) |
|
bar_dep = PkgConfigDependency('bar', env, kwargs) |
|
self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()]) |
|
internal_dep = PkgConfigDependency('internal', env, kwargs) |
|
if compiler.get_argument_syntax() == 'msvc': |
|
self.assertEqual(internal_dep.get_link_args(), []) |
|
else: |
|
link_args = internal_dep.get_link_args() |
|
for link_arg in link_args: |
|
for lib in ('pthread', 'm', 'c', 'dl', 'rt'): |
|
self.assertNotIn(f'lib{lib}.a', link_arg, msg=link_args) |
|
finally: |
|
# Test ends |
|
PkgConfigDependency._call_pkgbin = old_call |
|
PkgConfigDependency.check_pkgconfig = old_check |
|
# Reset dependency class to ensure that in-process configure doesn't mess up |
|
PkgConfigDependency.pkgbin_cache = {} |
|
PkgConfigDependency.class_pkgbin = PerMachine(None, None) |
|
|
|
def test_version_compare(self): |
|
comparefunc = mesonbuild.mesonlib.version_compare_many |
|
for (a, b, result) in [ |
|
('0.99.beta19', '>= 0.99.beta14', True), |
|
]: |
|
self.assertEqual(comparefunc(a, b)[0], result) |
|
|
|
for (a, b, op) in [ |
|
# examples from https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison |
|
("1.0010", "1.9", operator.gt), |
|
("1.05", "1.5", operator.eq), |
|
("1.0", "1", operator.gt), |
|
("2.50", "2.5", operator.gt), |
|
("fc4", "fc.4", operator.eq), |
|
("FC5", "fc4", operator.lt), |
|
("2a", "2.0", operator.lt), |
|
("1.0", "1.fc4", operator.gt), |
|
("3.0.0_fc", "3.0.0.fc", operator.eq), |
|
# from RPM tests |
|
("1.0", "1.0", operator.eq), |
|
("1.0", "2.0", operator.lt), |
|
("2.0", "1.0", operator.gt), |
|
("2.0.1", "2.0.1", operator.eq), |
|
("2.0", "2.0.1", operator.lt), |
|
("2.0.1", "2.0", operator.gt), |
|
("2.0.1a", "2.0.1a", operator.eq), |
|
("2.0.1a", "2.0.1", operator.gt), |
|
("2.0.1", "2.0.1a", operator.lt), |
|
("5.5p1", "5.5p1", operator.eq), |
|
("5.5p1", "5.5p2", operator.lt), |
|
("5.5p2", "5.5p1", operator.gt), |
|
("5.5p10", "5.5p10", operator.eq), |
|
("5.5p1", "5.5p10", operator.lt), |
|
("5.5p10", "5.5p1", operator.gt), |
|
("10xyz", "10.1xyz", operator.lt), |
|
("10.1xyz", "10xyz", operator.gt), |
|
("xyz10", "xyz10", operator.eq), |
|
("xyz10", "xyz10.1", operator.lt), |
|
("xyz10.1", "xyz10", operator.gt), |
|
("xyz.4", "xyz.4", operator.eq), |
|
("xyz.4", "8", operator.lt), |
|
("8", "xyz.4", operator.gt), |
|
("xyz.4", "2", operator.lt), |
|
("2", "xyz.4", operator.gt), |
|
("5.5p2", "5.6p1", operator.lt), |
|
("5.6p1", "5.5p2", operator.gt), |
|
("5.6p1", "6.5p1", operator.lt), |
|
("6.5p1", "5.6p1", operator.gt), |
|
("6.0.rc1", "6.0", operator.gt), |
|
("6.0", "6.0.rc1", operator.lt), |
|
("10b2", "10a1", operator.gt), |
|
("10a2", "10b2", operator.lt), |
|
("1.0aa", "1.0aa", operator.eq), |
|
("1.0a", "1.0aa", operator.lt), |
|
("1.0aa", "1.0a", operator.gt), |
|
("10.0001", "10.0001", operator.eq), |
|
("10.0001", "10.1", operator.eq), |
|
("10.1", "10.0001", operator.eq), |
|
("10.0001", "10.0039", operator.lt), |
|
("10.0039", "10.0001", operator.gt), |
|
("4.999.9", "5.0", operator.lt), |
|
("5.0", "4.999.9", operator.gt), |
|
("20101121", "20101121", operator.eq), |
|
("20101121", "20101122", operator.lt), |
|
("20101122", "20101121", operator.gt), |
|
("2_0", "2_0", operator.eq), |
|
("2.0", "2_0", operator.eq), |
|
("2_0", "2.0", operator.eq), |
|
("a", "a", operator.eq), |
|
("a+", "a+", operator.eq), |
|
("a+", "a_", operator.eq), |
|
("a_", "a+", operator.eq), |
|
("+a", "+a", operator.eq), |
|
("+a", "_a", operator.eq), |
|
("_a", "+a", operator.eq), |
|
("+_", "+_", operator.eq), |
|
("_+", "+_", operator.eq), |
|
("_+", "_+", operator.eq), |
|
("+", "_", operator.eq), |
|
("_", "+", operator.eq), |
|
# other tests |
|
('0.99.beta19', '0.99.beta14', operator.gt), |
|
("1.0.0", "2.0.0", operator.lt), |
|
(".0.0", "2.0.0", operator.lt), |
|
("alpha", "beta", operator.lt), |
|
("1.0", "1.0.0", operator.lt), |
|
("2.456", "2.1000", operator.lt), |
|
("2.1000", "3.111", operator.lt), |
|
("2.001", "2.1", operator.eq), |
|
("2.34", "2.34", operator.eq), |
|
("6.1.2", "6.3.8", operator.lt), |
|
("1.7.3.0", "2.0.0", operator.lt), |
|
("2.24.51", "2.25", operator.lt), |
|
("2.1.5+20120813+gitdcbe778", "2.1.5", operator.gt), |
|
("3.4.1", "3.4b1", operator.gt), |
|
("041206", "200090325", operator.lt), |
|
("0.6.2+git20130413", "0.6.2", operator.gt), |
|
("2.6.0+bzr6602", "2.6.0", operator.gt), |
|
("2.6.0", "2.6b2", operator.gt), |
|
("2.6.0+bzr6602", "2.6b2x", operator.gt), |
|
("0.6.7+20150214+git3a710f9", "0.6.7", operator.gt), |
|
("15.8b", "15.8.0.1", operator.lt), |
|
("1.2rc1", "1.2.0", operator.lt), |
|
]: |
|
ver_a = Version(a) |
|
ver_b = Version(b) |
|
if op is operator.eq: |
|
for o, name in [(op, 'eq'), (operator.ge, 'ge'), (operator.le, 'le')]: |
|
self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}') |
|
if op is operator.lt: |
|
for o, name in [(op, 'lt'), (operator.le, 'le'), (operator.ne, 'ne')]: |
|
self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}') |
|
for o, name in [(operator.gt, 'gt'), (operator.ge, 'ge'), (operator.eq, 'eq')]: |
|
self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}') |
|
if op is operator.gt: |
|
for o, name in [(op, 'gt'), (operator.ge, 'ge'), (operator.ne, 'ne')]: |
|
self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}') |
|
for o, name in [(operator.lt, 'lt'), (operator.le, 'le'), (operator.eq, 'eq')]: |
|
self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}') |
|
|
|
def test_msvc_toolset_version(self): |
|
''' |
|
Ensure that the toolset version returns the correct value for this MSVC |
|
''' |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_argument_syntax() != 'msvc': |
|
raise unittest.SkipTest('Test only applies to MSVC-like compilers') |
|
toolset_ver = cc.get_toolset_version() |
|
self.assertIsNotNone(toolset_ver) |
|
# Visual Studio 2015 and older versions do not define VCToolsVersion |
|
# TODO: ICL doesn't set this in the VSC2015 profile either |
|
if cc.id == 'msvc' and int(''.join(cc.version.split('.')[0:2])) < 1910: |
|
return |
|
if 'VCToolsVersion' in os.environ: |
|
vctools_ver = os.environ['VCToolsVersion'] |
|
else: |
|
self.assertIn('VCINSTALLDIR', os.environ) |
|
# See https://devblogs.microsoft.com/cppblog/finding-the-visual-c-compiler-tools-in-visual-studio-2017/ |
|
vctools_ver = (Path(os.environ['VCINSTALLDIR']) / 'Auxiliary' / 'Build' / 'Microsoft.VCToolsVersion.default.txt').read_text() |
|
self.assertTrue(vctools_ver.startswith(toolset_ver), |
|
msg=f'{vctools_ver!r} does not start with {toolset_ver!r}') |
|
|
|
def test_split_args(self): |
|
split_args = mesonbuild.mesonlib.split_args |
|
join_args = mesonbuild.mesonlib.join_args |
|
if is_windows(): |
|
test_data = [ |
|
# examples from https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments |
|
(r'"a b c" d e', ['a b c', 'd', 'e'], True), |
|
(r'"ab\"c" "\\" d', ['ab"c', '\\', 'd'], False), |
|
(r'a\\\b d"e f"g h', [r'a\\\b', 'de fg', 'h'], False), |
|
(r'a\\\"b c d', [r'a\"b', 'c', 'd'], False), |
|
(r'a\\\\"b c" d e', [r'a\\b c', 'd', 'e'], False), |
|
# other basics |
|
(r'""', [''], True), |
|
(r'a b c d "" e', ['a', 'b', 'c', 'd', '', 'e'], True), |
|
(r"'a b c' d e", ["'a", 'b', "c'", 'd', 'e'], True), |
|
(r"'a&b&c' d e", ["'a&b&c'", 'd', 'e'], True), |
|
(r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], True), |
|
(r"'a & b & c d e'", ["'a", '&', 'b', '&', 'c', 'd', "e'"], True), |
|
('a b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False), |
|
# more illustrative tests |
|
(r'cl test.cpp /O1 /Fe:test.exe', ['cl', 'test.cpp', '/O1', '/Fe:test.exe'], True), |
|
(r'cl "test.cpp /O1 /Fe:test.exe"', ['cl', 'test.cpp /O1 /Fe:test.exe'], True), |
|
(r'cl /DNAME=\"Bob\" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], False), |
|
(r'cl "/DNAME=\"Bob\"" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], True), |
|
(r'cl /DNAME=\"Bob, Alice\" test.cpp', ['cl', '/DNAME="Bob,', 'Alice"', 'test.cpp'], False), |
|
(r'cl "/DNAME=\"Bob, Alice\"" test.cpp', ['cl', '/DNAME="Bob, Alice"', 'test.cpp'], True), |
|
(r'cl C:\path\with\backslashes.cpp', ['cl', r'C:\path\with\backslashes.cpp'], True), |
|
(r'cl C:\\path\\with\\double\\backslashes.cpp', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], True), |
|
(r'cl "C:\\path\\with\\double\\backslashes.cpp"', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], False), |
|
(r'cl C:\path with spaces\test.cpp', ['cl', r'C:\path', 'with', r'spaces\test.cpp'], False), |
|
(r'cl "C:\path with spaces\test.cpp"', ['cl', r'C:\path with spaces\test.cpp'], True), |
|
(r'cl /DPATH="C:\path\with\backslashes test.cpp', ['cl', r'/DPATH=C:\path\with\backslashes test.cpp'], False), |
|
(r'cl /DPATH=\"C:\\ends\\with\\backslashes\\\" test.cpp', ['cl', r'/DPATH="C:\\ends\\with\\backslashes\"', 'test.cpp'], False), |
|
(r'cl /DPATH="C:\\ends\\with\\backslashes\\" test.cpp', ['cl', '/DPATH=C:\\\\ends\\\\with\\\\backslashes\\', 'test.cpp'], False), |
|
(r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\"', 'test.cpp'], True), |
|
(r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\ test.cpp'], False), |
|
(r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\"', 'test.cpp'], True), |
|
] |
|
else: |
|
test_data = [ |
|
(r"'a b c' d e", ['a b c', 'd', 'e'], True), |
|
(r"a/b/c d e", ['a/b/c', 'd', 'e'], True), |
|
(r"a\b\c d e", [r'abc', 'd', 'e'], False), |
|
(r"a\\b\\c d e", [r'a\b\c', 'd', 'e'], False), |
|
(r'"a b c" d e', ['a b c', 'd', 'e'], False), |
|
(r'"a\\b\\c\\" d e', ['a\\b\\c\\', 'd', 'e'], False), |
|
(r"'a\b\c\' d e", ['a\\b\\c\\', 'd', 'e'], True), |
|
(r"'a&b&c' d e", ['a&b&c', 'd', 'e'], True), |
|
(r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], False), |
|
(r"'a & b & c d e'", ['a & b & c d e'], True), |
|
(r"abd'e f'g h", [r'abde fg', 'h'], False), |
|
('a b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False), |
|
|
|
('g++ -DNAME="Bob" test.cpp', ['g++', '-DNAME=Bob', 'test.cpp'], False), |
|
("g++ '-DNAME=\"Bob\"' test.cpp", ['g++', '-DNAME="Bob"', 'test.cpp'], True), |
|
('g++ -DNAME="Bob, Alice" test.cpp', ['g++', '-DNAME=Bob, Alice', 'test.cpp'], False), |
|
("g++ '-DNAME=\"Bob, Alice\"' test.cpp", ['g++', '-DNAME="Bob, Alice"', 'test.cpp'], True), |
|
] |
|
|
|
for (cmd, expected, roundtrip) in test_data: |
|
self.assertEqual(split_args(cmd), expected) |
|
if roundtrip: |
|
self.assertEqual(join_args(expected), cmd) |
|
|
|
def test_quote_arg(self): |
|
split_args = mesonbuild.mesonlib.split_args |
|
quote_arg = mesonbuild.mesonlib.quote_arg |
|
if is_windows(): |
|
test_data = [ |
|
('', '""'), |
|
('arg1', 'arg1'), |
|
('/option1', '/option1'), |
|
('/Ovalue', '/Ovalue'), |
|
('/OBob&Alice', '/OBob&Alice'), |
|
('/Ovalue with spaces', r'"/Ovalue with spaces"'), |
|
(r'/O"value with spaces"', r'"/O\"value with spaces\""'), |
|
(r'/OC:\path with spaces\test.exe', r'"/OC:\path with spaces\test.exe"'), |
|
('/LIBPATH:C:\\path with spaces\\ends\\with\\backslashes\\', r'"/LIBPATH:C:\path with spaces\ends\with\backslashes\\"'), |
|
('/LIBPATH:"C:\\path with spaces\\ends\\with\\backslashes\\\\"', r'"/LIBPATH:\"C:\path with spaces\ends\with\backslashes\\\\\""'), |
|
(r'/DMSG="Alice said: \"Let\'s go\""', r'"/DMSG=\"Alice said: \\\"Let\'s go\\\"\""'), |
|
] |
|
else: |
|
test_data = [ |
|
('arg1', 'arg1'), |
|
('--option1', '--option1'), |
|
('-O=value', '-O=value'), |
|
('-O=Bob&Alice', "'-O=Bob&Alice'"), |
|
('-O=value with spaces', "'-O=value with spaces'"), |
|
('-O="value with spaces"', '\'-O=\"value with spaces\"\''), |
|
('-O=/path with spaces/test', '\'-O=/path with spaces/test\''), |
|
('-DMSG="Alice said: \\"Let\'s go\\""', "'-DMSG=\"Alice said: \\\"Let'\"'\"'s go\\\"\"'"), |
|
] |
|
|
|
for (arg, expected) in test_data: |
|
self.assertEqual(quote_arg(arg), expected) |
|
self.assertEqual(split_args(expected)[0], arg) |
|
|
|
def test_depfile(self): |
|
for (f, target, expdeps) in [ |
|
# empty, unknown target |
|
([''], 'unknown', set()), |
|
# simple target & deps |
|
(['meson/foo.o : foo.c foo.h'], 'meson/foo.o', set({'foo.c', 'foo.h'})), |
|
(['meson/foo.o: foo.c foo.h'], 'foo.c', set()), |
|
# get all deps |
|
(['meson/foo.o: foo.c foo.h', |
|
'foo.c: gen.py'], 'meson/foo.o', set({'foo.c', 'foo.h', 'gen.py'})), |
|
(['meson/foo.o: foo.c foo.h', |
|
'foo.c: gen.py'], 'foo.c', set({'gen.py'})), |
|
# linue continuation, multiple targets |
|
(['foo.o \\', 'foo.h: bar'], 'foo.h', set({'bar'})), |
|
(['foo.o \\', 'foo.h: bar'], 'foo.o', set({'bar'})), |
|
# \\ handling |
|
(['foo: Program\\ F\\iles\\\\X'], 'foo', set({'Program Files\\X'})), |
|
# $ handling |
|
(['f$o.o: c/b'], 'f$o.o', set({'c/b'})), |
|
(['f$$o.o: c/b'], 'f$o.o', set({'c/b'})), |
|
# cycles |
|
(['a: b', 'b: a'], 'a', set({'a', 'b'})), |
|
(['a: b', 'b: a'], 'b', set({'a', 'b'})), |
|
]: |
|
d = mesonbuild.depfile.DepFile(f) |
|
deps = d.get_all_dependencies(target) |
|
self.assertEqual(sorted(deps), sorted(expdeps)) |
|
|
|
def test_log_once(self): |
|
f = io.StringIO() |
|
with mock.patch('mesonbuild.mlog.log_file', f), \ |
|
mock.patch('mesonbuild.mlog._logged_once', set()): |
|
mesonbuild.mlog.log_once('foo') |
|
mesonbuild.mlog.log_once('foo') |
|
actual = f.getvalue().strip() |
|
self.assertEqual(actual, 'foo', actual) |
|
|
|
def test_log_once_ansi(self): |
|
f = io.StringIO() |
|
with mock.patch('mesonbuild.mlog.log_file', f), \ |
|
mock.patch('mesonbuild.mlog._logged_once', set()): |
|
mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo')) |
|
mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo')) |
|
actual = f.getvalue().strip() |
|
self.assertEqual(actual.count('foo'), 1, actual) |
|
|
|
mesonbuild.mlog.log_once('foo') |
|
actual = f.getvalue().strip() |
|
self.assertEqual(actual.count('foo'), 1, actual) |
|
|
|
f.truncate() |
|
|
|
mesonbuild.mlog.warning('bar', once=True) |
|
mesonbuild.mlog.warning('bar', once=True) |
|
actual = f.getvalue().strip() |
|
self.assertEqual(actual.count('bar'), 1, actual) |
|
|
|
def test_sort_libpaths(self): |
|
sort_libpaths = mesonbuild.dependencies.base.sort_libpaths |
|
self.assertEqual(sort_libpaths( |
|
['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'], |
|
['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']), |
|
['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib']) |
|
self.assertEqual(sort_libpaths( |
|
['/usr/local/lib', '/home/mesonuser/.local/lib', '/usr/lib'], |
|
['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']), |
|
['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib']) |
|
self.assertEqual(sort_libpaths( |
|
['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'], |
|
['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']), |
|
['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib']) |
|
self.assertEqual(sort_libpaths( |
|
['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'], |
|
['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/libdata/pkgconfig']), |
|
['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib']) |
|
|
|
def test_dependency_factory_order(self): |
|
b = mesonbuild.dependencies.base |
|
F = mesonbuild.dependencies.factory |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
with chdir(tmpdir): |
|
env = get_fake_env() |
|
env.scratch_dir = tmpdir |
|
|
|
f = F.DependencyFactory( |
|
'test_dep', |
|
methods=[b.DependencyMethods.PKGCONFIG, b.DependencyMethods.CMAKE] |
|
) |
|
actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})] |
|
self.assertListEqual([m.type_name for m in actual], ['pkgconfig', 'cmake']) |
|
|
|
f = F.DependencyFactory( |
|
'test_dep', |
|
methods=[b.DependencyMethods.CMAKE, b.DependencyMethods.PKGCONFIG] |
|
) |
|
actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})] |
|
self.assertListEqual([m.type_name for m in actual], ['cmake', 'pkgconfig']) |
|
|
|
def test_validate_json(self) -> None: |
|
"""Validate the json schema for the test cases.""" |
|
try: |
|
from jsonschema import validate, ValidationError |
|
except ImportError: |
|
if is_ci(): |
|
raise |
|
raise unittest.SkipTest('Python jsonschema module not found.') |
|
|
|
with Path('data/test.schema.json').open() as f: |
|
schema = json.load(f) |
|
|
|
errors = [] # type: T.Tuple[str, Exception] |
|
for p in Path('test cases').glob('**/test.json'): |
|
with p.open() as f: |
|
try: |
|
validate(json.load(f), schema=schema) |
|
except ValidationError as e: |
|
errors.append((p.resolve(), e)) |
|
|
|
for f, e in errors: |
|
print(f'Failed to validate: "{f}"') |
|
print(str(e)) |
|
|
|
self.assertFalse(errors) |
|
|
|
def test_typed_pos_args_types(self) -> None: |
|
@typed_pos_args('foo', str, int, bool) |
|
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: |
|
self.assertIsInstance(args, tuple) |
|
self.assertIsInstance(args[0], str) |
|
self.assertIsInstance(args[1], int) |
|
self.assertIsInstance(args[2], bool) |
|
|
|
_(None, mock.Mock(), ['string', 1, False], None) |
|
|
|
def test_typed_pos_args_types_invalid(self) -> None: |
|
@typed_pos_args('foo', str, int, bool) |
|
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 1.0, False], None) |
|
self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been "int"') |
|
|
|
def test_typed_pos_args_types_wrong_number(self) -> None: |
|
@typed_pos_args('foo', str, int, bool) |
|
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 1], None) |
|
self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 2.') |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 1, True, True], None) |
|
self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 4.') |
|
|
|
def test_typed_pos_args_varargs(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertIsInstance(args, tuple) |
|
self.assertIsInstance(args[0], str) |
|
self.assertIsInstance(args[1], list) |
|
self.assertIsInstance(args[1][0], str) |
|
self.assertIsInstance(args[1][1], str) |
|
|
|
_(None, mock.Mock(), ['string', 'var', 'args'], None) |
|
|
|
def test_typed_pos_args_varargs_not_given(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertIsInstance(args, tuple) |
|
self.assertIsInstance(args[0], str) |
|
self.assertIsInstance(args[1], list) |
|
self.assertEqual(args[1], []) |
|
|
|
_(None, mock.Mock(), ['string'], None) |
|
|
|
def test_typed_pos_args_varargs_invalid(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 'var', 'args', 0], None) |
|
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"') |
|
|
|
def test_typed_pos_args_varargs_invalid_mulitple_types(self) -> None: |
|
@typed_pos_args('foo', str, varargs=(str, list)) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 'var', 'args', 0], None) |
|
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: "str", "list"') |
|
|
|
def test_typed_pos_args_max_varargs(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str, max_varargs=5) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertIsInstance(args, tuple) |
|
self.assertIsInstance(args[0], str) |
|
self.assertIsInstance(args[1], list) |
|
self.assertIsInstance(args[1][0], str) |
|
self.assertIsInstance(args[1][1], str) |
|
|
|
_(None, mock.Mock(), ['string', 'var', 'args'], None) |
|
|
|
def test_typed_pos_args_max_varargs_exceeded(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str, max_varargs=1) |
|
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 'var', 'args'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes between 1 and 2 arguments, but got 3.') |
|
|
|
def test_typed_pos_args_min_varargs(self) -> None: |
|
@typed_pos_args('foo', varargs=str, max_varargs=2, min_varargs=1) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertIsInstance(args, tuple) |
|
self.assertIsInstance(args[0], list) |
|
self.assertIsInstance(args[0][0], str) |
|
self.assertIsInstance(args[0][1], str) |
|
|
|
_(None, mock.Mock(), ['string', 'var'], None) |
|
|
|
def test_typed_pos_args_min_varargs_not_met(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str, min_varargs=1) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.') |
|
|
|
def test_typed_pos_args_min_and_max_varargs_exceeded(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2) |
|
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', 'var', 'args', 'bar'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 4.') |
|
|
|
def test_typed_pos_args_min_and_max_varargs_not_met(self) -> None: |
|
@typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2) |
|
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 1.') |
|
|
|
def test_typed_pos_args_variadic_and_optional(self) -> None: |
|
@typed_pos_args('foo', str, optargs=[str], varargs=str, min_varargs=0) |
|
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(AssertionError) as cm: |
|
_(None, mock.Mock(), ['string'], None) |
|
self.assertEqual( |
|
str(cm.exception), |
|
'varargs and optargs not supported together as this would be ambiguous') |
|
|
|
def test_typed_pos_args_min_optargs_not_met(self) -> None: |
|
@typed_pos_args('foo', str, str, optargs=[str]) |
|
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.') |
|
|
|
def test_typed_pos_args_min_optargs_max_exceeded(self) -> None: |
|
@typed_pos_args('foo', str, optargs=[str]) |
|
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: |
|
self.assertTrue(False) # should not be reachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), ['string', '1', '2'], None) |
|
self.assertEqual(str(cm.exception), 'foo takes at most 2 arguments, but got 3.') |
|
|
|
def test_typed_pos_args_optargs_not_given(self) -> None: |
|
@typed_pos_args('foo', str, optargs=[str]) |
|
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: |
|
self.assertEqual(len(args), 2) |
|
self.assertIsInstance(args[0], str) |
|
self.assertEqual(args[0], 'string') |
|
self.assertIsNone(args[1]) |
|
|
|
_(None, mock.Mock(), ['string'], None) |
|
|
|
def test_typed_pos_args_optargs_some_given(self) -> None: |
|
@typed_pos_args('foo', str, optargs=[str, int]) |
|
def _(obj, node, args: T.Tuple[str, T.Optional[str], T.Optional[int]], kwargs) -> None: |
|
self.assertEqual(len(args), 3) |
|
self.assertIsInstance(args[0], str) |
|
self.assertEqual(args[0], 'string') |
|
self.assertIsInstance(args[1], str) |
|
self.assertEqual(args[1], '1') |
|
self.assertIsNone(args[2]) |
|
|
|
_(None, mock.Mock(), ['string', '1'], None) |
|
|
|
def test_typed_pos_args_optargs_all_given(self) -> None: |
|
@typed_pos_args('foo', str, optargs=[str]) |
|
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: |
|
self.assertEqual(len(args), 2) |
|
self.assertIsInstance(args[0], str) |
|
self.assertEqual(args[0], 'string') |
|
self.assertIsInstance(args[1], str) |
|
|
|
_(None, mock.Mock(), ['string', '1'], None) |
|
|
|
def test_typed_kwarg_basic(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str) |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
self.assertIsInstance(kwargs['input'], str) |
|
self.assertEqual(kwargs['input'], 'foo') |
|
|
|
_(None, mock.Mock(), [], {'input': 'foo'}) |
|
|
|
def test_typed_kwarg_missing_required(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str, required=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
self.assertTrue(False) # should be unreachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), [], {}) |
|
self.assertEqual(str(cm.exception), 'testfunc is missing required keyword argument "input"') |
|
|
|
def test_typed_kwarg_missing_optional(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Optional[str]]) -> None: |
|
self.assertIsNone(kwargs['input']) |
|
|
|
_(None, mock.Mock(), [], {}) |
|
|
|
def test_typed_kwarg_default(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str, default='default'), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
self.assertEqual(kwargs['input'], 'default') |
|
|
|
_(None, mock.Mock(), [], {}) |
|
|
|
def test_typed_kwarg_container_valid(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str), required=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: |
|
self.assertEqual(kwargs['input'], ['str']) |
|
|
|
_(None, mock.Mock(), [], {'input': ['str']}) |
|
|
|
def test_typed_kwarg_container_invalid(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str), required=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: |
|
self.assertTrue(False) # should be unreachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), [], {'input': {}}) |
|
self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" container type was "dict", but should have been "list"') |
|
|
|
def test_typed_kwarg_contained_invalid(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(dict, str), required=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Dict[str, str]]) -> None: |
|
self.assertTrue(False) # should be unreachable |
|
|
|
with self.assertRaises(InvalidArguments) as cm: |
|
_(None, mock.Mock(), [], {'input': {'key': 1}}) |
|
self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" contained a value of type "int" but should have been "str"') |
|
|
|
def test_typed_kwarg_container_listify(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str), listify=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: |
|
self.assertEqual(kwargs['input'], ['str']) |
|
|
|
_(None, mock.Mock(), [], {'input': 'str'}) |
|
|
|
def test_typed_kwarg_container_default_copy(self) -> None: |
|
default: T.List[str] = [] |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=default), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: |
|
self.assertIsNot(kwargs['input'], default) |
|
|
|
_(None, mock.Mock(), [], {}) |
|
|
|
def test_typed_kwarg_container_pairs(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str, pairs=True), listify=True), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: |
|
self.assertEqual(kwargs['input'], ['a', 'b']) |
|
|
|
_(None, mock.Mock(), [], {'input': ['a', 'b']}) |
|
|
|
with self.assertRaises(MesonException) as cm: |
|
_(None, mock.Mock(), [], {'input': ['a']}) |
|
self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" container should be of even length, but is not") |
|
|
|
@mock.patch.dict(mesonbuild.mesonlib.project_meson_versions, {}) |
|
def test_typed_kwarg_since(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str, since='1.0', deprecated='2.0') |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
self.assertIsInstance(kwargs['input'], str) |
|
self.assertEqual(kwargs['input'], 'foo') |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
# With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning |
|
mesonbuild.mesonlib.project_meson_versions[''] = '0.1' |
|
_(None, mock.Mock(subproject=''), [], {'input': 'foo'}) |
|
self.assertRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc') |
|
self.assertNotRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc') |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
# With Meson 1.5 it shouldn't trigger any warning |
|
mesonbuild.mesonlib.project_meson_versions[''] = '1.5' |
|
_(None, mock.Mock(subproject=''), [], {'input': 'foo'}) |
|
self.assertNotRegex(out.getvalue(), r'WARNING:.*') |
|
self.assertNotRegex(out.getvalue(), r'WARNING:.*') |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
# With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning |
|
mesonbuild.mesonlib.project_meson_versions[''] = '2.0' |
|
_(None, mock.Mock(subproject=''), [], {'input': 'foo'}) |
|
self.assertRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc') |
|
self.assertNotRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc') |
|
|
|
def test_typed_kwarg_validator(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', str, validator=lambda x: 'invalid!' if x != 'foo' else None) |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
pass |
|
|
|
# Should be valid |
|
_(None, mock.Mock(), tuple(), dict(input='foo')) |
|
|
|
with self.assertRaises(MesonException) as cm: |
|
_(None, mock.Mock(), tuple(), dict(input='bar')) |
|
self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" invalid!") |
|
|
|
def test_typed_kwarg_convertor(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('native', bool, convertor=lambda n: MachineChoice.BUILD if n else MachineChoice.HOST) |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, MachineChoice]) -> None: |
|
assert isinstance(kwargs['native'], MachineChoice) |
|
|
|
_(None, mock.Mock(), tuple(), dict(native=True)) |
|
|
|
@mock.patch.dict(mesonbuild.mesonlib.project_meson_versions, {'': '1.0'}) |
|
def test_typed_kwarg_since_values(self) -> None: |
|
@typed_kwargs( |
|
'testfunc', |
|
KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=[], deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}), |
|
KwargInfo('output', ContainerTypeInfo(dict, str), default={}, deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}), |
|
KwargInfo( |
|
'mode', str, |
|
validator=lambda x: 'Should be one of "clean", "build", "rebuild"' if x not in {'clean', 'build', 'rebuild', 'deprecated', 'since'} else None, |
|
deprecated_values={'deprecated': '1.0'}, |
|
since_values={'since': '1.1'}), |
|
) |
|
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: |
|
pass |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'input': ['foo']}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""") |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'input': ['bar']}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""") |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'output': {'foo': 'a'}}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo".*""") |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'output': {'bar': 'b'}}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar".*""") |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'mode': 'deprecated'}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '1.0': "testfunc" keyword argument "mode" value "deprecated".*""") |
|
|
|
with mock.patch('sys.stdout', io.StringIO()) as out: |
|
_(None, mock.Mock(subproject=''), [], {'mode': 'since'}) |
|
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "mode" value "since".*""") |
|
|
|
def test_typed_kwarg_evolve(self) -> None: |
|
k = KwargInfo('foo', str, required=True, default='foo') |
|
v = k.evolve(default='bar') |
|
self.assertEqual(k.name, 'foo') |
|
self.assertEqual(k.name, v.name) |
|
self.assertEqual(k.types, str) |
|
self.assertEqual(k.types, v.types) |
|
self.assertEqual(k.required, True) |
|
self.assertEqual(k.required, v.required) |
|
self.assertEqual(k.default, 'foo') |
|
self.assertEqual(v.default, 'bar') |
|
|
|
|
|
@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release') |
|
class DataTests(unittest.TestCase): |
|
|
|
def test_snippets(self): |
|
hashcounter = re.compile('^ *(#)+') |
|
snippet_dir = Path('docs/markdown/snippets') |
|
self.assertTrue(snippet_dir.is_dir()) |
|
for f in snippet_dir.glob('*'): |
|
self.assertTrue(f.is_file()) |
|
if f.parts[-1].endswith('~'): |
|
continue |
|
if f.suffix == '.md': |
|
in_code_block = False |
|
with f.open() as snippet: |
|
for line in snippet: |
|
if line.startswith(' '): |
|
continue |
|
if line.startswith('```'): |
|
in_code_block = not in_code_block |
|
if in_code_block: |
|
continue |
|
m = re.match(hashcounter, line) |
|
if m: |
|
self.assertEqual(len(m.group(0)), 2, 'All headings in snippets must have two hash symbols: ' + f.name) |
|
self.assertFalse(in_code_block, 'Unclosed code block.') |
|
else: |
|
if f.name != 'add_release_note_snippets_here': |
|
self.assertTrue(False, 'A file without .md suffix in snippets dir: ' + f.name) |
|
|
|
def test_compiler_options_documented(self): |
|
''' |
|
Test that C and C++ compiler options and base options are documented in |
|
Builtin-Options.md. Only tests the default compiler for the current |
|
platform on the CI. |
|
''' |
|
md = None |
|
with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f: |
|
md = f.read() |
|
self.assertIsNotNone(md) |
|
env = get_fake_env() |
|
# FIXME: Support other compilers |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
cpp = detect_cpp_compiler(env, MachineChoice.HOST) |
|
for comp in (cc, cpp): |
|
for opt in comp.get_options(): |
|
self.assertIn(str(opt), md) |
|
for opt in comp.base_options: |
|
self.assertIn(str(opt), md) |
|
self.assertNotIn('b_unknown', md) |
|
|
|
@staticmethod |
|
def _get_section_content(name, sections, md): |
|
for section in sections: |
|
if section and section.group(1) == name: |
|
try: |
|
next_section = next(sections) |
|
end = next_section.start() |
|
except StopIteration: |
|
end = len(md) |
|
# Extract the content for this section |
|
return md[section.end():end] |
|
raise RuntimeError(f'Could not find "{name}" heading') |
|
|
|
def test_builtin_options_documented(self): |
|
''' |
|
Test that universal options and base options are documented in |
|
Builtin-Options.md. |
|
''' |
|
from itertools import tee |
|
md = None |
|
with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f: |
|
md = f.read() |
|
self.assertIsNotNone(md) |
|
|
|
found_entries = set() |
|
sections = re.finditer(r"^## (.+)$", md, re.MULTILINE) |
|
# Extract the content for this section |
|
content = self._get_section_content("Universal options", sections, md) |
|
subsections = tee(re.finditer(r"^### (.+)$", content, re.MULTILINE)) |
|
subcontent1 = self._get_section_content("Directories", subsections[0], content) |
|
subcontent2 = self._get_section_content("Core options", subsections[1], content) |
|
for subcontent in (subcontent1, subcontent2): |
|
# Find the option names |
|
options = set() |
|
# Match either a table row or a table heading separator: | ------ | |
|
rows = re.finditer(r"^\|(?: (\w+) .* | *-+ *)\|", subcontent, re.MULTILINE) |
|
# Skip the header of the first table |
|
next(rows) |
|
# Skip the heading separator of the first table |
|
next(rows) |
|
for m in rows: |
|
value = m.group(1) |
|
# End when the `buildtype` table starts |
|
if value is None: |
|
break |
|
options.add(value) |
|
self.assertEqual(len(found_entries & options), 0) |
|
found_entries |= options |
|
|
|
self.assertEqual(found_entries, { |
|
*[str(k) for k in mesonbuild.coredata.BUILTIN_OPTIONS], |
|
*[str(k) for k in mesonbuild.coredata.BUILTIN_OPTIONS_PER_MACHINE], |
|
}) |
|
|
|
# Check that `buildtype` table inside `Core options` matches how |
|
# setting of builtin options behaves |
|
# |
|
# Find all tables inside this subsection |
|
tables = re.finditer(r"^\| (\w+) .* \|\n\| *[-|\s]+ *\|$", subcontent2, re.MULTILINE) |
|
# Get the table we want using the header of the first column |
|
table = self._get_section_content('buildtype', tables, subcontent2) |
|
# Get table row data |
|
rows = re.finditer(r"^\|(?: (\w+)\s+\| (\w+)\s+\| (\w+) .* | *-+ *)\|", table, re.MULTILINE) |
|
env = get_fake_env() |
|
for m in rows: |
|
buildtype, debug, opt = m.groups() |
|
if debug == 'true': |
|
debug = True |
|
elif debug == 'false': |
|
debug = False |
|
else: |
|
raise RuntimeError(f'Invalid debug value {debug!r} in row:\n{m.group()}') |
|
env.coredata.set_option(OptionKey('buildtype'), buildtype) |
|
self.assertEqual(env.coredata.options[OptionKey('buildtype')].value, buildtype) |
|
self.assertEqual(env.coredata.options[OptionKey('optimization')].value, opt) |
|
self.assertEqual(env.coredata.options[OptionKey('debug')].value, debug) |
|
|
|
def test_cpu_families_documented(self): |
|
with open("docs/markdown/Reference-tables.md", encoding='utf-8') as f: |
|
md = f.read() |
|
self.assertIsNotNone(md) |
|
|
|
sections = re.finditer(r"^## (.+)$", md, re.MULTILINE) |
|
content = self._get_section_content("CPU families", sections, md) |
|
# Find the list entries |
|
arches = [m.group(1) for m in re.finditer(r"^\| (\w+) +\|", content, re.MULTILINE)] |
|
# Drop the header |
|
arches = set(arches[1:]) |
|
self.assertEqual(arches, set(mesonbuild.environment.known_cpu_families)) |
|
|
|
def test_markdown_files_in_sitemap(self): |
|
''' |
|
Test that each markdown files in docs/markdown is referenced in sitemap.txt |
|
''' |
|
with open("docs/sitemap.txt", encoding='utf-8') as f: |
|
md = f.read() |
|
self.assertIsNotNone(md) |
|
toc = list(m.group(1) for m in re.finditer(r"^\s*(\w.*)$", md, re.MULTILINE)) |
|
markdownfiles = [f.name for f in Path("docs/markdown").iterdir() if f.is_file() and f.suffix == '.md'] |
|
exceptions = ['_Sidebar.md'] |
|
for f in markdownfiles: |
|
if f not in exceptions and not f.startswith('_include'): |
|
self.assertIn(f, toc) |
|
|
|
def test_modules_in_navbar(self): |
|
''' |
|
Test that each module is referenced in navbar_links.html |
|
''' |
|
with open("docs/theme/extra/templates/navbar_links.html", encoding='utf-8') as f: |
|
html = f.read().lower() |
|
self.assertIsNotNone(html) |
|
for f in Path('mesonbuild/modules').glob('*.py'): |
|
if f.name in {'modtest.py', 'qt.py', '__init__.py'}: |
|
continue |
|
name = f'{f.stem}-module.html' |
|
name = name.replace('unstable_', '') |
|
name = name.replace('python3', 'python-3') |
|
name = name.replace('_', '-') |
|
self.assertIn(name, html) |
|
|
|
def test_vim_syntax_highlighting(self): |
|
''' |
|
Ensure that vim syntax highlighting files were updated for new |
|
functions in the global namespace in build files. |
|
''' |
|
env = get_fake_env() |
|
interp = Interpreter(FakeBuild(env), mock=True) |
|
with open('data/syntax-highlighting/vim/syntax/meson.vim') as f: |
|
res = re.search(r'syn keyword mesonBuiltin(\s+\\\s\w+)+', f.read(), re.MULTILINE) |
|
defined = set([a.strip() for a in res.group().split('\\')][1:]) |
|
self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys()))) |
|
|
|
def test_all_functions_defined_in_ast_interpreter(self): |
|
''' |
|
Ensure that the all functions defined in the Interpreter are also defined |
|
in the AstInterpreter (and vice versa). |
|
''' |
|
env = get_fake_env() |
|
interp = Interpreter(FakeBuild(env), mock=True) |
|
astint = AstInterpreter('.', '', '') |
|
self.assertEqual(set(interp.funcs.keys()), set(astint.funcs.keys())) |
|
|
|
def test_mesondata_is_up_to_date(self): |
|
from mesonbuild.mesondata import mesondata |
|
err_msg = textwrap.dedent(''' |
|
|
|
########################################################### |
|
### mesonbuild.mesondata is not up-to-date ### |
|
### Please regenerate it by running tools/gen_data.py ### |
|
########################################################### |
|
|
|
''') |
|
|
|
root_dir = Path(__file__).resolve().parent |
|
mesonbuild_dir = root_dir / 'mesonbuild' |
|
|
|
data_dirs = mesonbuild_dir.glob('**/data') |
|
data_files = [] # type: T.List[T.Tuple(str, str)] |
|
|
|
for i in data_dirs: |
|
for p in i.iterdir(): |
|
data_files += [(p.relative_to(mesonbuild_dir).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest())] |
|
|
|
current_files = set(mesondata.keys()) |
|
scanned_files = {x[0] for x in data_files} |
|
|
|
self.assertSetEqual(current_files, scanned_files, err_msg + 'Data files were added or removed\n') |
|
errors = [] |
|
for i in data_files: |
|
if mesondata[i[0]].sha256sum != i[1]: |
|
errors += [i[0]] |
|
|
|
self.assertListEqual(errors, [], err_msg + 'Files were changed') |
|
|
|
class BasePlatformTests(unittest.TestCase): |
|
prefix = '/usr' |
|
libdir = 'lib' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.maxDiff = None |
|
src_root = os.path.dirname(__file__) |
|
src_root = os.path.join(os.getcwd(), src_root) |
|
self.src_root = src_root |
|
# Get the backend |
|
self.backend = getattr(Backend, os.environ['MESON_UNIT_TEST_BACKEND']) |
|
self.meson_args = ['--backend=' + self.backend.name] |
|
self.meson_native_file = None |
|
self.meson_cross_file = None |
|
self.meson_command = python_command + [get_meson_script()] |
|
self.setup_command = self.meson_command + self.meson_args |
|
self.mconf_command = self.meson_command + ['configure'] |
|
self.mintro_command = self.meson_command + ['introspect'] |
|
self.wrap_command = self.meson_command + ['wrap'] |
|
self.rewrite_command = self.meson_command + ['rewrite'] |
|
# Backend-specific build commands |
|
self.build_command, self.clean_command, self.test_command, self.install_command, \ |
|
self.uninstall_command = get_backend_commands(self.backend) |
|
# Test directories |
|
self.common_test_dir = os.path.join(src_root, 'test cases/common') |
|
self.vala_test_dir = os.path.join(src_root, 'test cases/vala') |
|
self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') |
|
self.unit_test_dir = os.path.join(src_root, 'test cases/unit') |
|
self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite') |
|
self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike') |
|
self.objc_test_dir = os.path.join(src_root, 'test cases/objc') |
|
self.objcpp_test_dir = os.path.join(src_root, 'test cases/objcpp') |
|
|
|
# Misc stuff |
|
self.orig_env = os.environ.copy() |
|
if self.backend is Backend.ninja: |
|
self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do'] |
|
else: |
|
# VS doesn't have a stable output when no changes are done |
|
# XCode backend is untested with unit tests, help welcome! |
|
self.no_rebuild_stdout = [f'UNKNOWN BACKEND {self.backend.name!r}'] |
|
|
|
self.builddirs = [] |
|
self.new_builddir() |
|
|
|
def change_builddir(self, newdir): |
|
self.builddir = newdir |
|
self.privatedir = os.path.join(self.builddir, 'meson-private') |
|
self.logdir = os.path.join(self.builddir, 'meson-logs') |
|
self.installdir = os.path.join(self.builddir, 'install') |
|
self.distdir = os.path.join(self.builddir, 'meson-dist') |
|
self.mtest_command = self.meson_command + ['test', '-C', self.builddir] |
|
self.builddirs.append(self.builddir) |
|
|
|
def new_builddir(self): |
|
if not is_cygwin(): |
|
# Keep builddirs inside the source tree so that virus scanners |
|
# don't complain |
|
newdir = tempfile.mkdtemp(dir=os.getcwd()) |
|
else: |
|
# But not on Cygwin because that breaks the umask tests. See: |
|
# https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523 |
|
newdir = tempfile.mkdtemp() |
|
# In case the directory is inside a symlinked directory, find the real |
|
# path otherwise we might not find the srcdir from inside the builddir. |
|
newdir = os.path.realpath(newdir) |
|
self.change_builddir(newdir) |
|
|
|
def _print_meson_log(self): |
|
log = os.path.join(self.logdir, 'meson-log.txt') |
|
if not os.path.isfile(log): |
|
print(f"{log!r} doesn't exist") |
|
return |
|
with open(log, encoding='utf-8') as f: |
|
print(f.read()) |
|
|
|
def tearDown(self): |
|
for path in self.builddirs: |
|
try: |
|
windows_proof_rmtree(path) |
|
except FileNotFoundError: |
|
pass |
|
os.environ.clear() |
|
os.environ.update(self.orig_env) |
|
super().tearDown() |
|
|
|
def _run(self, command, *, workdir=None, override_envvars=None): |
|
''' |
|
Run a command while printing the stdout and stderr to stdout, |
|
and also return a copy of it |
|
''' |
|
# If this call hangs CI will just abort. It is very hard to distinguish |
|
# between CI issue and test bug in that case. Set timeout and fail loud |
|
# instead. |
|
if override_envvars is None: |
|
env = None |
|
else: |
|
env = os.environ.copy() |
|
env.update(override_envvars) |
|
|
|
p = subprocess.run(command, stdout=subprocess.PIPE, |
|
stderr=subprocess.STDOUT, env=env, |
|
encoding='utf-8', |
|
universal_newlines=True, cwd=workdir, timeout=60 * 5) |
|
print(p.stdout) |
|
if p.returncode != 0: |
|
if 'MESON_SKIP_TEST' in p.stdout: |
|
raise unittest.SkipTest('Project requested skipping.') |
|
raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout) |
|
return p.stdout |
|
|
|
def init(self, srcdir, *, |
|
extra_args=None, |
|
default_args=True, |
|
inprocess=False, |
|
override_envvars=None, |
|
workdir=None): |
|
self.assertPathExists(srcdir) |
|
if extra_args is None: |
|
extra_args = [] |
|
if not isinstance(extra_args, list): |
|
extra_args = [extra_args] |
|
args = [srcdir, self.builddir] |
|
if default_args: |
|
args += ['--prefix', self.prefix] |
|
if self.libdir: |
|
args += ['--libdir', self.libdir] |
|
if self.meson_native_file: |
|
args += ['--native-file', self.meson_native_file] |
|
if self.meson_cross_file: |
|
args += ['--cross-file', self.meson_cross_file] |
|
self.privatedir = os.path.join(self.builddir, 'meson-private') |
|
if inprocess: |
|
try: |
|
(returncode, out, err) = run_configure_inprocess(self.meson_args + args + extra_args, override_envvars) |
|
if 'MESON_SKIP_TEST' in out: |
|
raise unittest.SkipTest('Project requested skipping.') |
|
if returncode != 0: |
|
self._print_meson_log() |
|
print('Stdout:\n') |
|
print(out) |
|
print('Stderr:\n') |
|
print(err) |
|
raise RuntimeError('Configure failed') |
|
except Exception: |
|
self._print_meson_log() |
|
raise |
|
finally: |
|
# Close log file to satisfy Windows file locking |
|
mesonbuild.mlog.shutdown() |
|
mesonbuild.mlog.log_dir = None |
|
mesonbuild.mlog.log_file = None |
|
else: |
|
try: |
|
out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir) |
|
except unittest.SkipTest: |
|
raise unittest.SkipTest('Project requested skipping: ' + srcdir) |
|
except Exception: |
|
self._print_meson_log() |
|
raise |
|
return out |
|
|
|
def build(self, target=None, *, extra_args=None, override_envvars=None): |
|
if extra_args is None: |
|
extra_args = [] |
|
# Add arguments for building the target (if specified), |
|
# and using the build dir (if required, with VS) |
|
args = get_builddir_target_args(self.backend, self.builddir, target) |
|
return self._run(self.build_command + args + extra_args, workdir=self.builddir, override_envvars=override_envvars) |
|
|
|
def clean(self, *, override_envvars=None): |
|
dir_args = get_builddir_target_args(self.backend, self.builddir, None) |
|
self._run(self.clean_command + dir_args, workdir=self.builddir, override_envvars=override_envvars) |
|
|
|
def run_tests(self, *, inprocess=False, override_envvars=None): |
|
if not inprocess: |
|
self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars) |
|
else: |
|
with mock.patch.dict(os.environ, override_envvars): |
|
run_mtest_inprocess(['-C', self.builddir]) |
|
|
|
def install(self, *, use_destdir=True, override_envvars=None): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'{self.backend.name!r} backend can\'t install files') |
|
if use_destdir: |
|
destdir = {'DESTDIR': self.installdir} |
|
if override_envvars is None: |
|
override_envvars = destdir |
|
else: |
|
override_envvars.update(destdir) |
|
self._run(self.install_command, workdir=self.builddir, override_envvars=override_envvars) |
|
|
|
def uninstall(self, *, override_envvars=None): |
|
self._run(self.uninstall_command, workdir=self.builddir, override_envvars=override_envvars) |
|
|
|
def run_target(self, target, *, override_envvars=None): |
|
''' |
|
Run a Ninja target while printing the stdout and stderr to stdout, |
|
and also return a copy of it |
|
''' |
|
return self.build(target=target, override_envvars=override_envvars) |
|
|
|
def setconf(self, arg, will_build=True): |
|
if not isinstance(arg, list): |
|
arg = [arg] |
|
if will_build: |
|
ensure_backend_detects_changes(self.backend) |
|
self._run(self.mconf_command + arg + [self.builddir]) |
|
|
|
def wipe(self): |
|
windows_proof_rmtree(self.builddir) |
|
|
|
def utime(self, f): |
|
ensure_backend_detects_changes(self.backend) |
|
os.utime(f) |
|
|
|
def get_compdb(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'Compiler db not available with {self.backend.name} backend') |
|
try: |
|
with open(os.path.join(self.builddir, 'compile_commands.json')) as ifile: |
|
contents = json.load(ifile) |
|
except FileNotFoundError: |
|
raise unittest.SkipTest('Compiler db not found') |
|
# If Ninja is using .rsp files, generate them, read their contents, and |
|
# replace it as the command for all compile commands in the parsed json. |
|
if len(contents) > 0 and contents[0]['command'].endswith('.rsp'): |
|
# Pretend to build so that the rsp files are generated |
|
self.build(extra_args=['-d', 'keeprsp', '-n']) |
|
for each in contents: |
|
# Extract the actual command from the rsp file |
|
compiler, rsp = each['command'].split(' @') |
|
rsp = os.path.join(self.builddir, rsp) |
|
# Replace the command with its contents |
|
with open(rsp, encoding='utf-8') as f: |
|
each['command'] = compiler + ' ' + f.read() |
|
return contents |
|
|
|
def get_meson_log(self): |
|
with open(os.path.join(self.builddir, 'meson-logs', 'meson-log.txt')) as f: |
|
return f.readlines() |
|
|
|
def get_meson_log_compiler_checks(self): |
|
''' |
|
Fetch a list command-lines run by meson for compiler checks. |
|
Each command-line is returned as a list of arguments. |
|
''' |
|
log = self.get_meson_log() |
|
prefix = 'Command line:' |
|
cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] |
|
return cmds |
|
|
|
def get_meson_log_sanitychecks(self): |
|
''' |
|
Same as above, but for the sanity checks that were run |
|
''' |
|
log = self.get_meson_log() |
|
prefix = 'Sanity check compiler command line:' |
|
cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] |
|
return cmds |
|
|
|
def introspect(self, args): |
|
if isinstance(args, str): |
|
args = [args] |
|
out = subprocess.check_output(self.mintro_command + args + [self.builddir], |
|
universal_newlines=True) |
|
return json.loads(out) |
|
|
|
def introspect_directory(self, directory, args): |
|
if isinstance(args, str): |
|
args = [args] |
|
out = subprocess.check_output(self.mintro_command + args + [directory], |
|
universal_newlines=True) |
|
try: |
|
obj = json.loads(out) |
|
except Exception as e: |
|
print(out) |
|
raise e |
|
return obj |
|
|
|
def assertPathEqual(self, path1, path2): |
|
''' |
|
Handles a lot of platform-specific quirks related to paths such as |
|
separator, case-sensitivity, etc. |
|
''' |
|
self.assertEqual(PurePath(path1), PurePath(path2)) |
|
|
|
def assertPathListEqual(self, pathlist1, pathlist2): |
|
self.assertEqual(len(pathlist1), len(pathlist2)) |
|
worklist = list(zip(pathlist1, pathlist2)) |
|
for i in worklist: |
|
if i[0] is None: |
|
self.assertEqual(i[0], i[1]) |
|
else: |
|
self.assertPathEqual(i[0], i[1]) |
|
|
|
def assertPathBasenameEqual(self, path, basename): |
|
msg = f'{path!r} does not end with {basename!r}' |
|
# We cannot use os.path.basename because it returns '' when the path |
|
# ends with '/' for some silly reason. This is not how the UNIX utility |
|
# `basename` works. |
|
path_basename = PurePath(path).parts[-1] |
|
self.assertEqual(PurePath(path_basename), PurePath(basename), msg) |
|
|
|
def assertReconfiguredBuildIsNoop(self): |
|
'Assert that we reconfigured and then there was nothing to do' |
|
ret = self.build() |
|
self.assertIn('The Meson build system', ret) |
|
if self.backend is Backend.ninja: |
|
for line in ret.split('\n'): |
|
if line in self.no_rebuild_stdout: |
|
break |
|
else: |
|
raise AssertionError('build was reconfigured, but was not no-op') |
|
elif self.backend is Backend.vs: |
|
# Ensure that some target said that no rebuild was done |
|
# XXX: Note CustomBuild did indeed rebuild, because of the regen checker! |
|
self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) |
|
self.assertIn('Link:\n All outputs are up-to-date.', ret) |
|
# Ensure that no targets were built |
|
self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) |
|
self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) |
|
elif self.backend is Backend.xcode: |
|
raise unittest.SkipTest('Please help us fix this test on the xcode backend') |
|
else: |
|
raise RuntimeError(f'Invalid backend: {self.backend.name!r}') |
|
|
|
def assertBuildIsNoop(self): |
|
ret = self.build() |
|
if self.backend is Backend.ninja: |
|
self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout) |
|
elif self.backend is Backend.vs: |
|
# Ensure that some target of each type said that no rebuild was done |
|
# We always have at least one CustomBuild target for the regen checker |
|
self.assertIn('CustomBuild:\n All outputs are up-to-date.', ret) |
|
self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) |
|
self.assertIn('Link:\n All outputs are up-to-date.', ret) |
|
# Ensure that no targets were built |
|
self.assertNotRegex(ret, re.compile('CustomBuild:\n [^\n]*cl', flags=re.IGNORECASE)) |
|
self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) |
|
self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) |
|
elif self.backend is Backend.xcode: |
|
raise unittest.SkipTest('Please help us fix this test on the xcode backend') |
|
else: |
|
raise RuntimeError(f'Invalid backend: {self.backend.name!r}') |
|
|
|
def assertRebuiltTarget(self, target): |
|
ret = self.build() |
|
if self.backend is Backend.ninja: |
|
self.assertIn(f'Linking target {target}', ret) |
|
elif self.backend is Backend.vs: |
|
# Ensure that this target was rebuilt |
|
linkre = re.compile('Link:\n [^\n]*link[^\n]*' + target, flags=re.IGNORECASE) |
|
self.assertRegex(ret, linkre) |
|
elif self.backend is Backend.xcode: |
|
raise unittest.SkipTest('Please help us fix this test on the xcode backend') |
|
else: |
|
raise RuntimeError(f'Invalid backend: {self.backend.name!r}') |
|
|
|
@staticmethod |
|
def get_target_from_filename(filename): |
|
base = os.path.splitext(filename)[0] |
|
if base.startswith(('lib', 'cyg')): |
|
return base[3:] |
|
return base |
|
|
|
def assertBuildRelinkedOnlyTarget(self, target): |
|
ret = self.build() |
|
if self.backend is Backend.ninja: |
|
linked_targets = [] |
|
for line in ret.split('\n'): |
|
if 'Linking target' in line: |
|
fname = line.rsplit('target ')[-1] |
|
linked_targets.append(self.get_target_from_filename(fname)) |
|
self.assertEqual(linked_targets, [target]) |
|
elif self.backend is Backend.vs: |
|
# Ensure that this target was rebuilt |
|
linkre = re.compile(r'Link:\n [^\n]*link.exe[^\n]*/OUT:".\\([^"]*)"', flags=re.IGNORECASE) |
|
matches = linkre.findall(ret) |
|
self.assertEqual(len(matches), 1, msg=matches) |
|
self.assertEqual(self.get_target_from_filename(matches[0]), target) |
|
elif self.backend is Backend.xcode: |
|
raise unittest.SkipTest('Please help us fix this test on the xcode backend') |
|
else: |
|
raise RuntimeError(f'Invalid backend: {self.backend.name!r}') |
|
|
|
def assertPathExists(self, path): |
|
m = f'Path {path!r} should exist' |
|
self.assertTrue(os.path.exists(path), msg=m) |
|
|
|
def assertPathDoesNotExist(self, path): |
|
m = f'Path {path!r} should not exist' |
|
self.assertFalse(os.path.exists(path), msg=m) |
|
|
|
|
|
class AllPlatformTests(BasePlatformTests): |
|
''' |
|
Tests that should run on all platforms |
|
''' |
|
|
|
def test_default_options_prefix(self): |
|
''' |
|
Tests that setting a prefix in default_options in project() works. |
|
Can't be an ordinary test because we pass --prefix to meson there. |
|
https://github.com/mesonbuild/meson/issues/1349 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '87 default options') |
|
self.init(testdir, default_args=False, inprocess=True) |
|
opts = self.introspect('--buildoptions') |
|
for opt in opts: |
|
if opt['name'] == 'prefix': |
|
prefix = opt['value'] |
|
break |
|
else: |
|
raise self.fail('Did not find option "prefix"') |
|
self.assertEqual(prefix, '/absoluteprefix') |
|
|
|
def test_do_conf_file_preserve_newlines(self): |
|
|
|
def conf_file(in_data, confdata): |
|
with temp_filename() as fin: |
|
with open(fin, 'wb') as fobj: |
|
fobj.write(in_data.encode('utf-8')) |
|
with temp_filename() as fout: |
|
mesonbuild.mesonlib.do_conf_file(fin, fout, confdata, 'meson') |
|
with open(fout, 'rb') as fobj: |
|
return fobj.read().decode('utf-8') |
|
|
|
confdata = {'VAR': ('foo', 'bar')} |
|
self.assertEqual(conf_file('@VAR@\n@VAR@\n', confdata), 'foo\nfoo\n') |
|
self.assertEqual(conf_file('@VAR@\r\n@VAR@\r\n', confdata), 'foo\r\nfoo\r\n') |
|
|
|
def test_do_conf_file_by_format(self): |
|
def conf_str(in_data, confdata, vformat): |
|
(result, missing_variables, confdata_useless) = mesonbuild.mesonlib.do_conf_str('configuration_file', in_data, confdata, variable_format = vformat) |
|
return '\n'.join(result) |
|
|
|
def check_formats(confdata, result): |
|
self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result) |
|
self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result) |
|
self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result) |
|
|
|
confdata = ConfigurationData() |
|
# Key error as they do not exists |
|
check_formats(confdata, '/* #undef VAR */\n') |
|
|
|
# Check boolean |
|
confdata.values = {'VAR': (False, 'description')} |
|
check_formats(confdata, '#undef VAR\n') |
|
confdata.values = {'VAR': (True, 'description')} |
|
check_formats(confdata, '#define VAR\n') |
|
|
|
# Check string |
|
confdata.values = {'VAR': ('value', 'description')} |
|
check_formats(confdata, '#define VAR value\n') |
|
|
|
# Check integer |
|
confdata.values = {'VAR': (10, 'description')} |
|
check_formats(confdata, '#define VAR 10\n') |
|
|
|
# Check multiple string with cmake formats |
|
confdata.values = {'VAR': ('value', 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n') |
|
self.assertEqual(conf_str(['#define VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value') |
|
self.assertEqual(conf_str(['#cmakedefine VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value\n') |
|
self.assertEqual(conf_str(['#define VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value') |
|
|
|
# Handles meson format exceptions |
|
# Unknown format |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'unknown_format') |
|
# More than 2 params in mesondefine |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'meson') |
|
# Mismatched line with format |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#cmakedefine VAR'], confdata, 'meson') |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake') |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake@') |
|
# Dict value in confdata |
|
confdata.values = {'VAR': (['value'], 'description')} |
|
self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'meson') |
|
|
|
def test_absolute_prefix_libdir(self): |
|
''' |
|
Tests that setting absolute paths for --prefix and --libdir work. Can't |
|
be an ordinary test because these are set via the command-line. |
|
https://github.com/mesonbuild/meson/issues/1341 |
|
https://github.com/mesonbuild/meson/issues/1345 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '87 default options') |
|
# on Windows, /someabs is *not* an absolute path |
|
prefix = 'x:/someabs' if is_windows() else '/someabs' |
|
libdir = 'libdir' |
|
extra_args = ['--prefix=' + prefix, |
|
# This can just be a relative path, but we want to test |
|
# that passing this as an absolute path also works |
|
'--libdir=' + prefix + '/' + libdir] |
|
self.init(testdir, extra_args=extra_args, default_args=False) |
|
opts = self.introspect('--buildoptions') |
|
for opt in opts: |
|
if opt['name'] == 'prefix': |
|
self.assertEqual(prefix, opt['value']) |
|
elif opt['name'] == 'libdir': |
|
self.assertEqual(libdir, opt['value']) |
|
|
|
def test_libdir_must_be_inside_prefix(self): |
|
''' |
|
Tests that libdir is forced to be inside prefix no matter how it is set. |
|
Must be a unit test for obvious reasons. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
# libdir being inside prefix is ok |
|
if is_windows(): |
|
args = ['--prefix', 'x:/opt', '--libdir', 'x:/opt/lib32'] |
|
else: |
|
args = ['--prefix', '/opt', '--libdir', '/opt/lib32'] |
|
self.init(testdir, extra_args=args) |
|
self.wipe() |
|
# libdir not being inside prefix is not ok |
|
if is_windows(): |
|
args = ['--prefix', 'x:/usr', '--libdir', 'x:/opt/lib32'] |
|
else: |
|
args = ['--prefix', '/usr', '--libdir', '/opt/lib32'] |
|
self.assertRaises(subprocess.CalledProcessError, self.init, testdir, extra_args=args) |
|
self.wipe() |
|
# libdir must be inside prefix even when set via mesonconf |
|
self.init(testdir) |
|
if is_windows(): |
|
self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=x:/opt', False) |
|
else: |
|
self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False) |
|
|
|
def test_prefix_dependent_defaults(self): |
|
''' |
|
Tests that configured directory paths are set to prefix dependent |
|
defaults. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
expected = { |
|
'/opt': {'prefix': '/opt', |
|
'bindir': 'bin', 'datadir': 'share', 'includedir': 'include', |
|
'infodir': 'share/info', |
|
'libexecdir': 'libexec', 'localedir': 'share/locale', |
|
'localstatedir': 'var', 'mandir': 'share/man', |
|
'sbindir': 'sbin', 'sharedstatedir': 'com', |
|
'sysconfdir': 'etc'}, |
|
'/usr': {'prefix': '/usr', |
|
'bindir': 'bin', 'datadir': 'share', 'includedir': 'include', |
|
'infodir': 'share/info', |
|
'libexecdir': 'libexec', 'localedir': 'share/locale', |
|
'localstatedir': '/var', 'mandir': 'share/man', |
|
'sbindir': 'sbin', 'sharedstatedir': '/var/lib', |
|
'sysconfdir': '/etc'}, |
|
'/usr/local': {'prefix': '/usr/local', |
|
'bindir': 'bin', 'datadir': 'share', |
|
'includedir': 'include', 'infodir': 'share/info', |
|
'libexecdir': 'libexec', |
|
'localedir': 'share/locale', |
|
'localstatedir': '/var/local', 'mandir': 'share/man', |
|
'sbindir': 'sbin', 'sharedstatedir': '/var/local/lib', |
|
'sysconfdir': 'etc'}, |
|
# N.B. We don't check 'libdir' as it's platform dependent, see |
|
# default_libdir(): |
|
} |
|
|
|
if mesonbuild.mesonlib.default_prefix() == '/usr/local': |
|
expected[None] = expected['/usr/local'] |
|
|
|
for prefix in expected: |
|
args = [] |
|
if prefix: |
|
args += ['--prefix', prefix] |
|
self.init(testdir, extra_args=args, default_args=False) |
|
opts = self.introspect('--buildoptions') |
|
for opt in opts: |
|
name = opt['name'] |
|
value = opt['value'] |
|
if name in expected[prefix]: |
|
self.assertEqual(value, expected[prefix][name]) |
|
self.wipe() |
|
|
|
def test_default_options_prefix_dependent_defaults(self): |
|
''' |
|
Tests that setting a prefix in default_options in project() sets prefix |
|
dependent defaults for other options, and that those defaults can |
|
be overridden in default_options or by the command line. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '163 default options prefix dependent defaults') |
|
expected = { |
|
'': |
|
{'prefix': '/usr', |
|
'sysconfdir': '/etc', |
|
'localstatedir': '/var', |
|
'sharedstatedir': '/sharedstate'}, |
|
'--prefix=/usr': |
|
{'prefix': '/usr', |
|
'sysconfdir': '/etc', |
|
'localstatedir': '/var', |
|
'sharedstatedir': '/sharedstate'}, |
|
'--sharedstatedir=/var/state': |
|
{'prefix': '/usr', |
|
'sysconfdir': '/etc', |
|
'localstatedir': '/var', |
|
'sharedstatedir': '/var/state'}, |
|
'--sharedstatedir=/var/state --prefix=/usr --sysconfdir=sysconf': |
|
{'prefix': '/usr', |
|
'sysconfdir': 'sysconf', |
|
'localstatedir': '/var', |
|
'sharedstatedir': '/var/state'}, |
|
} |
|
for args in expected: |
|
self.init(testdir, extra_args=args.split(), default_args=False) |
|
opts = self.introspect('--buildoptions') |
|
for opt in opts: |
|
name = opt['name'] |
|
value = opt['value'] |
|
if name in expected[args]: |
|
self.assertEqual(value, expected[args][name]) |
|
self.wipe() |
|
|
|
def test_clike_get_library_dirs(self): |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
for d in cc.get_library_dirs(env): |
|
self.assertTrue(os.path.exists(d)) |
|
self.assertTrue(os.path.isdir(d)) |
|
self.assertTrue(os.path.isabs(d)) |
|
|
|
def test_static_library_overwrite(self): |
|
''' |
|
Tests that static libraries are never appended to, always overwritten. |
|
Has to be a unit test because this involves building a project, |
|
reconfiguring, and building it again so that `ar` is run twice on the |
|
same static library. |
|
https://github.com/mesonbuild/meson/issues/1355 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '3 static') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
static_linker = detect_static_linker(env, cc) |
|
if is_windows(): |
|
raise unittest.SkipTest('https://github.com/mesonbuild/meson/issues/1526') |
|
if not isinstance(static_linker, mesonbuild.linkers.ArLinker): |
|
raise unittest.SkipTest('static linker is not `ar`') |
|
# Configure |
|
self.init(testdir) |
|
# Get name of static library |
|
targets = self.introspect('--targets') |
|
self.assertEqual(len(targets), 1) |
|
libname = targets[0]['filename'][0] |
|
# Build and get contents of static library |
|
self.build() |
|
before = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split() |
|
# Filter out non-object-file contents |
|
before = [f for f in before if f.endswith(('.o', '.obj'))] |
|
# Static library should contain only one object |
|
self.assertEqual(len(before), 1, msg=before) |
|
# Change the source to be built into the static library |
|
self.setconf('-Dsource=libfile2.c') |
|
self.build() |
|
after = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split() |
|
# Filter out non-object-file contents |
|
after = [f for f in after if f.endswith(('.o', '.obj'))] |
|
# Static library should contain only one object |
|
self.assertEqual(len(after), 1, msg=after) |
|
# and the object must have changed |
|
self.assertNotEqual(before, after) |
|
|
|
def test_static_compile_order(self): |
|
''' |
|
Test that the order of files in a compiler command-line while compiling |
|
and linking statically is deterministic. This can't be an ordinary test |
|
case because we need to inspect the compiler database. |
|
https://github.com/mesonbuild/meson/pull/951 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '5 linkstatic') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
# Rules will get written out in this order |
|
self.assertTrue(compdb[0]['file'].endswith("libfile.c")) |
|
self.assertTrue(compdb[1]['file'].endswith("libfile2.c")) |
|
self.assertTrue(compdb[2]['file'].endswith("libfile3.c")) |
|
self.assertTrue(compdb[3]['file'].endswith("libfile4.c")) |
|
# FIXME: We don't have access to the linker command |
|
|
|
def test_run_target_files_path(self): |
|
''' |
|
Test that run_targets are run from the correct directory |
|
https://github.com/mesonbuild/meson/issues/957 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '51 run target') |
|
self.init(testdir) |
|
self.run_target('check_exists') |
|
self.run_target('check-env') |
|
self.run_target('check-env-ct') |
|
|
|
def test_run_target_subdir(self): |
|
''' |
|
Test that run_targets are run from the correct directory |
|
https://github.com/mesonbuild/meson/issues/957 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '51 run target') |
|
self.init(testdir) |
|
self.run_target('textprinter') |
|
|
|
def test_install_introspection(self): |
|
''' |
|
Tests that the Meson introspection API exposes install filenames correctly |
|
https://github.com/mesonbuild/meson/issues/829 |
|
''' |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'{self.backend.name!r} backend can\'t install files') |
|
testdir = os.path.join(self.common_test_dir, '8 install') |
|
self.init(testdir) |
|
intro = self.introspect('--targets') |
|
if intro[0]['type'] == 'executable': |
|
intro = intro[::-1] |
|
self.assertPathListEqual(intro[0]['install_filename'], ['/usr/lib/libstat.a']) |
|
self.assertPathListEqual(intro[1]['install_filename'], ['/usr/bin/prog' + exe_suffix]) |
|
|
|
def test_install_subdir_introspection(self): |
|
''' |
|
Test that the Meson introspection API also contains subdir install information |
|
https://github.com/mesonbuild/meson/issues/5556 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '59 install subdir') |
|
self.init(testdir) |
|
intro = self.introspect('--installed') |
|
expected = { |
|
'sub2': 'share/sub2', |
|
'subdir/sub1': 'share/sub1', |
|
'subdir/sub_elided': 'share', |
|
'sub1': 'share/sub1', |
|
'sub/sub1': 'share/sub1', |
|
'sub_elided': 'share', |
|
'nested_elided/sub': 'share', |
|
'new_directory': 'share/new_directory', |
|
} |
|
|
|
self.assertEqual(len(intro), len(expected)) |
|
|
|
# Convert expected to PurePath |
|
expected_converted = {PurePath(os.path.join(testdir, key)): PurePath(os.path.join(self.prefix, val)) for key, val in expected.items()} |
|
intro_converted = {PurePath(key): PurePath(val) for key, val in intro.items()} |
|
|
|
for src, dst in expected_converted.items(): |
|
self.assertIn(src, intro_converted) |
|
self.assertEqual(dst, intro_converted[src]) |
|
|
|
def test_install_introspection_multiple_outputs(self): |
|
''' |
|
Tests that the Meson introspection API exposes multiple install filenames correctly without crashing |
|
https://github.com/mesonbuild/meson/pull/4555 |
|
|
|
Reverted to the first file only because of https://github.com/mesonbuild/meson/pull/4547#discussion_r244173438 |
|
TODO Change the format to a list officially in a followup PR |
|
''' |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'{self.backend.name!r} backend can\'t install files') |
|
testdir = os.path.join(self.common_test_dir, '140 custom target multiple outputs') |
|
self.init(testdir) |
|
intro = self.introspect('--targets') |
|
if intro[0]['type'] == 'executable': |
|
intro = intro[::-1] |
|
self.assertPathListEqual(intro[0]['install_filename'], ['/usr/include/diff.h', '/usr/bin/diff.sh']) |
|
self.assertPathListEqual(intro[1]['install_filename'], ['/opt/same.h', '/opt/same.sh']) |
|
self.assertPathListEqual(intro[2]['install_filename'], ['/usr/include/first.h', None]) |
|
self.assertPathListEqual(intro[3]['install_filename'], [None, '/usr/bin/second.sh']) |
|
|
|
def test_install_log_content(self): |
|
''' |
|
Tests that the install-log.txt is consistent with the installed files and directories. |
|
Specifically checks that the log file only contains one entry per file/directory. |
|
https://github.com/mesonbuild/meson/issues/4499 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '59 install subdir') |
|
self.init(testdir) |
|
self.install() |
|
installpath = Path(self.installdir) |
|
# Find installed files and directories |
|
expected = {installpath: 0} |
|
for name in installpath.rglob('*'): |
|
expected[name] = 0 |
|
def read_logs(): |
|
# Find logged files and directories |
|
with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f: |
|
return list(map(lambda l: Path(l.strip()), |
|
filter(lambda l: not l.startswith('#'), |
|
f.readlines()))) |
|
logged = read_logs() |
|
for name in logged: |
|
self.assertTrue(name in expected, f'Log contains extra entry {name}') |
|
expected[name] += 1 |
|
|
|
for name, count in expected.items(): |
|
self.assertGreater(count, 0, f'Log is missing entry for {name}') |
|
self.assertLess(count, 2, f'Log has multiple entries for {name}') |
|
|
|
# Verify that with --dry-run we obtain the same logs but with nothing |
|
# actually installed |
|
windows_proof_rmtree(self.installdir) |
|
self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir], workdir=self.builddir) |
|
self.assertEqual(logged, read_logs()) |
|
self.assertFalse(os.path.exists(self.installdir)) |
|
|
|
def test_uninstall(self): |
|
exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix) |
|
dirname = os.path.join(self.installdir, 'usr/share/dir') |
|
testdir = os.path.join(self.common_test_dir, '8 install') |
|
self.init(testdir) |
|
self.assertPathDoesNotExist(exename) |
|
self.install() |
|
self.assertPathExists(exename) |
|
self.uninstall() |
|
self.assertPathDoesNotExist(exename) |
|
self.assertPathDoesNotExist(dirname) |
|
|
|
def test_forcefallback(self): |
|
testdir = os.path.join(self.unit_test_dir, '31 forcefallback') |
|
self.init(testdir, extra_args=['--wrap-mode=forcefallback']) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_implicit_forcefallback(self): |
|
testdir = os.path.join(self.unit_test_dir, '95 implicit force fallback') |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(testdir) |
|
self.init(testdir, extra_args=['--wrap-mode=forcefallback']) |
|
self.new_builddir() |
|
self.init(testdir, extra_args=['--force-fallback-for=something']) |
|
|
|
def test_nopromote(self): |
|
testdir = os.path.join(self.common_test_dir, '98 subproject subdir') |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(testdir, extra_args=['--wrap-mode=nopromote']) |
|
self.assertIn('dependency subsub found: NO', cm.exception.stdout) |
|
|
|
def test_force_fallback_for(self): |
|
testdir = os.path.join(self.unit_test_dir, '31 forcefallback') |
|
self.init(testdir, extra_args=['--force-fallback-for=zlib,foo']) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_testrepeat(self): |
|
testdir = os.path.join(self.common_test_dir, '206 tap tests') |
|
self.init(testdir) |
|
self.build() |
|
self._run(self.mtest_command + ['--repeat=2']) |
|
|
|
def test_testsetups(self): |
|
if not shutil.which('valgrind'): |
|
raise unittest.SkipTest('Valgrind not installed.') |
|
testdir = os.path.join(self.unit_test_dir, '2 testsetups') |
|
self.init(testdir) |
|
self.build() |
|
# Run tests without setup |
|
self.run_tests() |
|
with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f: |
|
basic_log = f.read() |
|
# Run buggy test with setup that has env that will make it fail |
|
self.assertRaises(subprocess.CalledProcessError, |
|
self._run, self.mtest_command + ['--setup=valgrind']) |
|
with open(os.path.join(self.logdir, 'testlog-valgrind.txt'), encoding='utf-8') as f: |
|
vg_log = f.read() |
|
self.assertFalse('TEST_ENV is set' in basic_log) |
|
self.assertFalse('Memcheck' in basic_log) |
|
self.assertTrue('TEST_ENV is set' in vg_log) |
|
self.assertTrue('Memcheck' in vg_log) |
|
# Run buggy test with setup without env that will pass |
|
self._run(self.mtest_command + ['--setup=wrapper']) |
|
# Setup with no properties works |
|
self._run(self.mtest_command + ['--setup=empty']) |
|
# Setup with only env works |
|
self._run(self.mtest_command + ['--setup=onlyenv']) |
|
self._run(self.mtest_command + ['--setup=onlyenv2']) |
|
self._run(self.mtest_command + ['--setup=onlyenv3']) |
|
# Setup with only a timeout works |
|
self._run(self.mtest_command + ['--setup=timeout']) |
|
# Setup that does not define a wrapper works with --wrapper |
|
self._run(self.mtest_command + ['--setup=timeout', '--wrapper', shutil.which('valgrind')]) |
|
# Setup that skips test works |
|
self._run(self.mtest_command + ['--setup=good']) |
|
with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f: |
|
exclude_suites_log = f.read() |
|
self.assertFalse('buggy' in exclude_suites_log) |
|
# --suite overrides add_test_setup(xclude_suites) |
|
self._run(self.mtest_command + ['--setup=good', '--suite', 'buggy']) |
|
with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f: |
|
include_suites_log = f.read() |
|
self.assertTrue('buggy' in include_suites_log) |
|
|
|
def test_testsetup_selection(self): |
|
testdir = os.path.join(self.unit_test_dir, '14 testsetup selection') |
|
self.init(testdir) |
|
self.build() |
|
|
|
# Run tests without setup |
|
self.run_tests() |
|
|
|
self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['--setup=missingfromfoo']) |
|
self._run(self.mtest_command + ['--setup=missingfromfoo', '--no-suite=foo:']) |
|
|
|
self._run(self.mtest_command + ['--setup=worksforall']) |
|
self._run(self.mtest_command + ['--setup=main:worksforall']) |
|
|
|
self.assertRaises(subprocess.CalledProcessError, self._run, |
|
self.mtest_command + ['--setup=onlyinbar']) |
|
self.assertRaises(subprocess.CalledProcessError, self._run, |
|
self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:']) |
|
self._run(self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:', '--no-suite=foo:']) |
|
self._run(self.mtest_command + ['--setup=bar:onlyinbar']) |
|
self.assertRaises(subprocess.CalledProcessError, self._run, |
|
self.mtest_command + ['--setup=foo:onlyinbar']) |
|
self.assertRaises(subprocess.CalledProcessError, self._run, |
|
self.mtest_command + ['--setup=main:onlyinbar']) |
|
|
|
def test_testsetup_default(self): |
|
testdir = os.path.join(self.unit_test_dir, '49 testsetup default') |
|
self.init(testdir) |
|
self.build() |
|
|
|
# Run tests without --setup will cause the default setup to be used |
|
self.run_tests() |
|
with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f: |
|
default_log = f.read() |
|
|
|
# Run tests with explicitly using the same setup that is set as default |
|
self._run(self.mtest_command + ['--setup=mydefault']) |
|
with open(os.path.join(self.logdir, 'testlog-mydefault.txt'), encoding='utf-8') as f: |
|
mydefault_log = f.read() |
|
|
|
# Run tests with another setup |
|
self._run(self.mtest_command + ['--setup=other']) |
|
with open(os.path.join(self.logdir, 'testlog-other.txt'), encoding='utf-8') as f: |
|
other_log = f.read() |
|
|
|
self.assertTrue('ENV_A is 1' in default_log) |
|
self.assertTrue('ENV_B is 2' in default_log) |
|
self.assertTrue('ENV_C is 2' in default_log) |
|
|
|
self.assertTrue('ENV_A is 1' in mydefault_log) |
|
self.assertTrue('ENV_B is 2' in mydefault_log) |
|
self.assertTrue('ENV_C is 2' in mydefault_log) |
|
|
|
self.assertTrue('ENV_A is 1' in other_log) |
|
self.assertTrue('ENV_B is 3' in other_log) |
|
self.assertTrue('ENV_C is 2' in other_log) |
|
|
|
def assertFailedTestCount(self, failure_count, command): |
|
try: |
|
self._run(command) |
|
self.assertEqual(0, failure_count, 'Expected %d tests to fail.' % failure_count) |
|
except subprocess.CalledProcessError as e: |
|
self.assertEqual(e.returncode, failure_count) |
|
|
|
def test_suite_selection(self): |
|
testdir = os.path.join(self.unit_test_dir, '4 suite selection') |
|
self.init(testdir) |
|
self.build() |
|
|
|
self.assertFailedTestCount(4, self.mtest_command) |
|
|
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', ':success']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--suite', ':fail']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', ':success']) |
|
self.assertFailedTestCount(1, self.mtest_command + ['--no-suite', ':fail']) |
|
|
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj']) |
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc']) |
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail']) |
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix']) |
|
|
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj:fail']) |
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'mainprj:success']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj:fail']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'mainprj:success']) |
|
|
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail:fail']) |
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjfail:success']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail:fail']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjfail:success']) |
|
|
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:fail']) |
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:success']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:fail']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:success']) |
|
|
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix:fail']) |
|
self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjmix:success']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix:fail']) |
|
self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjmix:success']) |
|
|
|
self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix:fail']) |
|
self.assertFailedTestCount(3, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj']) |
|
self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail']) |
|
self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail', 'mainprj-failing_test']) |
|
|
|
self.assertFailedTestCount(2, self.mtest_command + ['--no-suite', 'subprjfail:fail', '--no-suite', 'subprjmix:fail']) |
|
|
|
def test_build_by_default(self): |
|
testdir = os.path.join(self.common_test_dir, '129 build by default') |
|
self.init(testdir) |
|
self.build() |
|
genfile1 = os.path.join(self.builddir, 'generated1.dat') |
|
genfile2 = os.path.join(self.builddir, 'generated2.dat') |
|
exe1 = os.path.join(self.builddir, 'fooprog' + exe_suffix) |
|
exe2 = os.path.join(self.builddir, 'barprog' + exe_suffix) |
|
self.assertPathExists(genfile1) |
|
self.assertPathExists(genfile2) |
|
self.assertPathDoesNotExist(exe1) |
|
self.assertPathDoesNotExist(exe2) |
|
self.build(target=('fooprog' + exe_suffix)) |
|
self.assertPathExists(exe1) |
|
self.build(target=('barprog' + exe_suffix)) |
|
self.assertPathExists(exe2) |
|
|
|
def test_internal_include_order(self): |
|
if mesonbuild.environment.detect_msys2_arch() and ('MESON_RSP_THRESHOLD' in os.environ): |
|
raise unittest.SkipTest('Test does not yet support gcc rsp files on msys2') |
|
|
|
testdir = os.path.join(self.common_test_dir, '130 include order') |
|
self.init(testdir) |
|
execmd = fxecmd = None |
|
for cmd in self.get_compdb(): |
|
if 'someexe' in cmd['command']: |
|
execmd = cmd['command'] |
|
continue |
|
if 'somefxe' in cmd['command']: |
|
fxecmd = cmd['command'] |
|
continue |
|
if not execmd or not fxecmd: |
|
raise Exception('Could not find someexe and somfxe commands') |
|
# Check include order for 'someexe' |
|
incs = [a for a in split_args(execmd) if a.startswith("-I")] |
|
self.assertEqual(len(incs), 9) |
|
# Need to run the build so the private dir is created. |
|
self.build() |
|
pdirs = glob(os.path.join(self.builddir, 'sub4/someexe*.p')) |
|
self.assertEqual(len(pdirs), 1) |
|
privdir = pdirs[0][len(self.builddir)+1:] |
|
self.assertPathEqual(incs[0], "-I" + privdir) |
|
# target build subdir |
|
self.assertPathEqual(incs[1], "-Isub4") |
|
# target source subdir |
|
self.assertPathBasenameEqual(incs[2], 'sub4') |
|
# include paths added via per-target c_args: ['-I'...] |
|
self.assertPathBasenameEqual(incs[3], 'sub3') |
|
# target include_directories: build dir |
|
self.assertPathEqual(incs[4], "-Isub2") |
|
# target include_directories: source dir |
|
self.assertPathBasenameEqual(incs[5], 'sub2') |
|
# target internal dependency include_directories: build dir |
|
self.assertPathEqual(incs[6], "-Isub1") |
|
# target internal dependency include_directories: source dir |
|
self.assertPathBasenameEqual(incs[7], 'sub1') |
|
# custom target include dir |
|
self.assertPathEqual(incs[8], '-Ictsub') |
|
# Check include order for 'somefxe' |
|
incs = [a for a in split_args(fxecmd) if a.startswith('-I')] |
|
self.assertEqual(len(incs), 9) |
|
# target private dir |
|
pdirs = glob(os.path.join(self.builddir, 'somefxe*.p')) |
|
self.assertEqual(len(pdirs), 1) |
|
privdir = pdirs[0][len(self.builddir)+1:] |
|
self.assertPathEqual(incs[0], '-I' + privdir) |
|
# target build dir |
|
self.assertPathEqual(incs[1], '-I.') |
|
# target source dir |
|
self.assertPathBasenameEqual(incs[2], os.path.basename(testdir)) |
|
# target internal dependency correct include_directories: build dir |
|
self.assertPathEqual(incs[3], "-Isub4") |
|
# target internal dependency correct include_directories: source dir |
|
self.assertPathBasenameEqual(incs[4], 'sub4') |
|
# target internal dependency dep include_directories: build dir |
|
self.assertPathEqual(incs[5], "-Isub1") |
|
# target internal dependency dep include_directories: source dir |
|
self.assertPathBasenameEqual(incs[6], 'sub1') |
|
# target internal dependency wrong include_directories: build dir |
|
self.assertPathEqual(incs[7], "-Isub2") |
|
# target internal dependency wrong include_directories: source dir |
|
self.assertPathBasenameEqual(incs[8], 'sub2') |
|
|
|
def test_compiler_detection(self): |
|
''' |
|
Test that automatic compiler detection and setting from the environment |
|
both work just fine. This is needed because while running project tests |
|
and other unit tests, we always read CC/CXX/etc from the environment. |
|
''' |
|
gnu = mesonbuild.compilers.GnuCompiler |
|
clang = mesonbuild.compilers.ClangCompiler |
|
intel = mesonbuild.compilers.IntelGnuLikeCompiler |
|
msvc = (mesonbuild.compilers.VisualStudioCCompiler, mesonbuild.compilers.VisualStudioCPPCompiler) |
|
clangcl = (mesonbuild.compilers.ClangClCCompiler, mesonbuild.compilers.ClangClCPPCompiler) |
|
ar = mesonbuild.linkers.ArLinker |
|
lib = mesonbuild.linkers.VisualStudioLinker |
|
langs = [('c', 'CC'), ('cpp', 'CXX')] |
|
if not is_windows() and platform.machine().lower() != 'e2k': |
|
langs += [('objc', 'OBJC'), ('objcpp', 'OBJCXX')] |
|
testdir = os.path.join(self.unit_test_dir, '5 compiler detection') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
for lang, evar in langs: |
|
# Detect with evar and do sanity checks on that |
|
if evar in os.environ: |
|
ecc = compiler_from_language(env, lang, MachineChoice.HOST) |
|
self.assertTrue(ecc.version) |
|
elinker = detect_static_linker(env, ecc) |
|
# Pop it so we don't use it for the next detection |
|
evalue = os.environ.pop(evar) |
|
# Very rough/strict heuristics. Would never work for actual |
|
# compiler detection, but should be ok for the tests. |
|
ebase = os.path.basename(evalue) |
|
if ebase.startswith('g') or ebase.endswith(('-gcc', '-g++')): |
|
self.assertIsInstance(ecc, gnu) |
|
self.assertIsInstance(elinker, ar) |
|
elif 'clang-cl' in ebase: |
|
self.assertIsInstance(ecc, clangcl) |
|
self.assertIsInstance(elinker, lib) |
|
elif 'clang' in ebase: |
|
self.assertIsInstance(ecc, clang) |
|
self.assertIsInstance(elinker, ar) |
|
elif ebase.startswith('ic'): |
|
self.assertIsInstance(ecc, intel) |
|
self.assertIsInstance(elinker, ar) |
|
elif ebase.startswith('cl'): |
|
self.assertIsInstance(ecc, msvc) |
|
self.assertIsInstance(elinker, lib) |
|
else: |
|
raise AssertionError(f'Unknown compiler {evalue!r}') |
|
# Check that we actually used the evalue correctly as the compiler |
|
self.assertEqual(ecc.get_exelist(), split_args(evalue)) |
|
# Do auto-detection of compiler based on platform, PATH, etc. |
|
cc = compiler_from_language(env, lang, MachineChoice.HOST) |
|
self.assertTrue(cc.version) |
|
linker = detect_static_linker(env, cc) |
|
# Check compiler type |
|
if isinstance(cc, gnu): |
|
self.assertIsInstance(linker, ar) |
|
if is_osx(): |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) |
|
elif is_sunos(): |
|
self.assertIsInstance(cc.linker, (mesonbuild.linkers.SolarisDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)) |
|
else: |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin) |
|
if isinstance(cc, clangcl): |
|
self.assertIsInstance(linker, lib) |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.ClangClDynamicLinker) |
|
if isinstance(cc, clang): |
|
self.assertIsInstance(linker, ar) |
|
if is_osx(): |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) |
|
elif is_windows(): |
|
# This is clang, not clang-cl. This can be either an |
|
# ld-like linker of link.exe-like linker (usually the |
|
# former for msys2, the latter otherwise) |
|
self.assertIsInstance(cc.linker, (mesonbuild.linkers.MSVCDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)) |
|
else: |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin) |
|
if isinstance(cc, intel): |
|
self.assertIsInstance(linker, ar) |
|
if is_osx(): |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) |
|
elif is_windows(): |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.XilinkDynamicLinker) |
|
else: |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuDynamicLinker) |
|
if isinstance(cc, msvc): |
|
self.assertTrue(is_windows()) |
|
self.assertIsInstance(linker, lib) |
|
self.assertEqual(cc.id, 'msvc') |
|
self.assertTrue(hasattr(cc, 'is_64')) |
|
self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker) |
|
# If we're on Windows CI, we know what the compiler will be |
|
if 'arch' in os.environ: |
|
if os.environ['arch'] == 'x64': |
|
self.assertTrue(cc.is_64) |
|
else: |
|
self.assertFalse(cc.is_64) |
|
# Set evar ourselves to a wrapper script that just calls the same |
|
# exelist + some argument. This is meant to test that setting |
|
# something like `ccache gcc -pipe` or `distcc ccache gcc` works. |
|
wrapper = os.path.join(testdir, 'compiler wrapper.py') |
|
wrappercc = python_command + [wrapper] + cc.get_exelist() + ['-DSOME_ARG'] |
|
os.environ[evar] = ' '.join(quote_arg(w) for w in wrappercc) |
|
|
|
# Check static linker too |
|
wrapperlinker = python_command + [wrapper] + linker.get_exelist() + linker.get_always_args() |
|
os.environ['AR'] = ' '.join(quote_arg(w) for w in wrapperlinker) |
|
|
|
# Need a new env to re-run environment loading |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
|
|
wcc = compiler_from_language(env, lang, MachineChoice.HOST) |
|
wlinker = detect_static_linker(env, wcc) |
|
# Pop it so we don't use it for the next detection |
|
evalue = os.environ.pop('AR') |
|
# Must be the same type since it's a wrapper around the same exelist |
|
self.assertIs(type(cc), type(wcc)) |
|
self.assertIs(type(linker), type(wlinker)) |
|
# Ensure that the exelist is correct |
|
self.assertEqual(wcc.get_exelist(), wrappercc) |
|
self.assertEqual(wlinker.get_exelist(), wrapperlinker) |
|
# Ensure that the version detection worked correctly |
|
self.assertEqual(cc.version, wcc.version) |
|
if hasattr(cc, 'is_64'): |
|
self.assertEqual(cc.is_64, wcc.is_64) |
|
|
|
def test_always_prefer_c_compiler_for_asm(self): |
|
testdir = os.path.join(self.common_test_dir, '133 c cpp and asm') |
|
# Skip if building with MSVC |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
if detect_c_compiler(env, MachineChoice.HOST).get_id() == 'msvc': |
|
raise unittest.SkipTest('MSVC can\'t compile assembly') |
|
self.init(testdir) |
|
commands = {'c-asm': {}, 'cpp-asm': {}, 'cpp-c-asm': {}, 'c-cpp-asm': {}} |
|
for cmd in self.get_compdb(): |
|
# Get compiler |
|
split = split_args(cmd['command']) |
|
if split[0] == 'ccache': |
|
compiler = split[1] |
|
else: |
|
compiler = split[0] |
|
# Classify commands |
|
if 'Ic-asm' in cmd['command']: |
|
if cmd['file'].endswith('.S'): |
|
commands['c-asm']['asm'] = compiler |
|
elif cmd['file'].endswith('.c'): |
|
commands['c-asm']['c'] = compiler |
|
else: |
|
raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command'])) |
|
elif 'Icpp-asm' in cmd['command']: |
|
if cmd['file'].endswith('.S'): |
|
commands['cpp-asm']['asm'] = compiler |
|
elif cmd['file'].endswith('.cpp'): |
|
commands['cpp-asm']['cpp'] = compiler |
|
else: |
|
raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command'])) |
|
elif 'Ic-cpp-asm' in cmd['command']: |
|
if cmd['file'].endswith('.S'): |
|
commands['c-cpp-asm']['asm'] = compiler |
|
elif cmd['file'].endswith('.c'): |
|
commands['c-cpp-asm']['c'] = compiler |
|
elif cmd['file'].endswith('.cpp'): |
|
commands['c-cpp-asm']['cpp'] = compiler |
|
else: |
|
raise AssertionError('{!r} found in c-cpp-asm?'.format(cmd['command'])) |
|
elif 'Icpp-c-asm' in cmd['command']: |
|
if cmd['file'].endswith('.S'): |
|
commands['cpp-c-asm']['asm'] = compiler |
|
elif cmd['file'].endswith('.c'): |
|
commands['cpp-c-asm']['c'] = compiler |
|
elif cmd['file'].endswith('.cpp'): |
|
commands['cpp-c-asm']['cpp'] = compiler |
|
else: |
|
raise AssertionError('{!r} found in cpp-c-asm?'.format(cmd['command'])) |
|
else: |
|
raise AssertionError('Unknown command {!r} found'.format(cmd['command'])) |
|
# Check that .S files are always built with the C compiler |
|
self.assertEqual(commands['c-asm']['asm'], commands['c-asm']['c']) |
|
self.assertEqual(commands['c-asm']['asm'], commands['cpp-asm']['asm']) |
|
self.assertEqual(commands['cpp-asm']['asm'], commands['c-cpp-asm']['c']) |
|
self.assertEqual(commands['c-cpp-asm']['asm'], commands['c-cpp-asm']['c']) |
|
self.assertEqual(commands['cpp-c-asm']['asm'], commands['cpp-c-asm']['c']) |
|
self.assertNotEqual(commands['cpp-asm']['asm'], commands['cpp-asm']['cpp']) |
|
self.assertNotEqual(commands['c-cpp-asm']['c'], commands['c-cpp-asm']['cpp']) |
|
self.assertNotEqual(commands['cpp-c-asm']['c'], commands['cpp-c-asm']['cpp']) |
|
# Check that the c-asm target is always linked with the C linker |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
with open(build_ninja, encoding='utf-8') as f: |
|
contents = f.read() |
|
m = re.search('build c-asm.*: c_LINKER', contents) |
|
self.assertIsNotNone(m, msg=contents) |
|
|
|
def test_preprocessor_checks_CPPFLAGS(self): |
|
''' |
|
Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but |
|
not LDFLAGS. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '132 get define') |
|
define = 'MESON_TEST_DEFINE_VALUE' |
|
# NOTE: this list can't have \n, ' or " |
|
# \n is never substituted by the GNU pre-processor via a -D define |
|
# ' and " confuse split_args() even when they are escaped |
|
# % and # confuse the MSVC preprocessor |
|
# !, ^, *, and < confuse lcc preprocessor |
|
value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`' |
|
for env_var in ['CPPFLAGS', 'CFLAGS']: |
|
env = {} |
|
env[env_var] = f'-D{define}="{value}"' |
|
env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read' |
|
self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env) |
|
|
|
def test_custom_target_exe_data_deterministic(self): |
|
testdir = os.path.join(self.common_test_dir, '109 custom target capture') |
|
self.init(testdir) |
|
meson_exe_dat1 = glob(os.path.join(self.privatedir, 'meson_exe*.dat')) |
|
self.wipe() |
|
self.init(testdir) |
|
meson_exe_dat2 = glob(os.path.join(self.privatedir, 'meson_exe*.dat')) |
|
self.assertListEqual(meson_exe_dat1, meson_exe_dat2) |
|
|
|
def test_noop_changes_cause_no_rebuilds(self): |
|
''' |
|
Test that no-op changes to the build files such as mtime do not cause |
|
a rebuild of anything. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '6 linkshared') |
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Changing mtime of meson.build should not rebuild anything |
|
self.utime(os.path.join(testdir, 'meson.build')) |
|
self.assertReconfiguredBuildIsNoop() |
|
# Changing mtime of libefile.c should rebuild the library, but not relink the executable |
|
self.utime(os.path.join(testdir, 'libfile.c')) |
|
self.assertBuildRelinkedOnlyTarget('mylib') |
|
|
|
def test_source_changes_cause_rebuild(self): |
|
''' |
|
Test that changes to sources and headers cause rebuilds, but not |
|
changes to unused files (as determined by the dependency file) in the |
|
input files list. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '19 header in file list') |
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Changing mtime of header.h should rebuild everything |
|
self.utime(os.path.join(testdir, 'header.h')) |
|
self.assertBuildRelinkedOnlyTarget('prog') |
|
|
|
def test_custom_target_changes_cause_rebuild(self): |
|
''' |
|
Test that in a custom target, changes to the input files, the |
|
ExternalProgram, and any File objects on the command-line cause |
|
a rebuild. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '57 custom header generator') |
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Changing mtime of these should rebuild everything |
|
for f in ('input.def', 'makeheader.py', 'somefile.txt'): |
|
self.utime(os.path.join(testdir, f)) |
|
self.assertBuildRelinkedOnlyTarget('prog') |
|
|
|
def test_source_generator_program_cause_rebuild(self): |
|
''' |
|
Test that changes to generator programs in the source tree cause |
|
a rebuild. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '90 gen extra') |
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Changing mtime of generator should rebuild the executable |
|
self.utime(os.path.join(testdir, 'srcgen.py')) |
|
self.assertRebuiltTarget('basic') |
|
|
|
def test_static_library_lto(self): |
|
''' |
|
Test that static libraries can be built with LTO and linked to |
|
executables. On Linux, this requires the use of gcc-ar. |
|
https://github.com/mesonbuild/meson/issues/1646 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '5 linkstatic') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
if detect_c_compiler(env, MachineChoice.HOST).get_id() == 'clang' and is_windows(): |
|
raise unittest.SkipTest('LTO not (yet) supported by windows clang') |
|
|
|
self.init(testdir, extra_args='-Db_lto=true') |
|
self.build() |
|
self.run_tests() |
|
|
|
@skip_if_not_base_option('b_lto_threads') |
|
def test_lto_threads(self): |
|
testdir = os.path.join(self.common_test_dir, '6 linkshared') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
extra_args: T.List[str] = [] |
|
if cc.get_id() == 'clang': |
|
if is_windows(): |
|
raise unittest.SkipTest('LTO not (yet) supported by windows clang') |
|
else: |
|
extra_args.append('-D_cargs=-Werror=unused-command-line-argument') |
|
|
|
self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_threads=8'] + extra_args) |
|
self.build() |
|
self.run_tests() |
|
|
|
expected = set(cc.get_lto_compile_args(threads=8)) |
|
targets = self.introspect('--targets') |
|
# This assumes all of the targets support lto |
|
for t in targets: |
|
for s in t['target_sources']: |
|
for e in expected: |
|
self.assertIn(e, s['parameters']) |
|
|
|
@skip_if_not_base_option('b_lto_mode') |
|
@skip_if_not_base_option('b_lto_threads') |
|
def test_lto_mode(self): |
|
testdir = os.path.join(self.common_test_dir, '6 linkshared') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() != 'clang': |
|
raise unittest.SkipTest('Only clang currently supports thinLTO') |
|
if cc.linker.id not in {'ld.lld', 'ld.gold', 'ld64', 'lld-link'}: |
|
raise unittest.SkipTest('thinLTO requires ld.lld, ld.gold, ld64, or lld-link') |
|
elif is_windows(): |
|
raise unittest.SkipTest('LTO not (yet) supported by windows clang') |
|
|
|
self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_mode=thin', '-Db_lto_threads=8', '-Dc_args=-Werror=unused-command-line-argument']) |
|
self.build() |
|
self.run_tests() |
|
|
|
expected = set(cc.get_lto_compile_args(threads=8, mode='thin')) |
|
targets = self.introspect('--targets') |
|
# This assumes all of the targets support lto |
|
for t in targets: |
|
for s in t['target_sources']: |
|
self.assertTrue(expected.issubset(set(s['parameters'])), f'Incorrect values for {t["name"]}') |
|
|
|
def test_dist_git(self): |
|
if not shutil.which('git'): |
|
raise unittest.SkipTest('Git not found') |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Dist is only supported with Ninja') |
|
|
|
try: |
|
self.dist_impl(_git_init, _git_add_all) |
|
except PermissionError: |
|
# When run under Windows CI, something (virus scanner?) |
|
# holds on to the git files so cleaning up the dir |
|
# fails sometimes. |
|
pass |
|
|
|
def has_working_hg(self): |
|
if not shutil.which('hg'): |
|
return False |
|
try: |
|
# This check should not be necessary, but |
|
# CI under macOS passes the above test even |
|
# though Mercurial is not installed. |
|
if subprocess.call(['hg', '--version'], |
|
stdout=subprocess.DEVNULL, |
|
stderr=subprocess.DEVNULL) != 0: |
|
return False |
|
return True |
|
except FileNotFoundError: |
|
return False |
|
|
|
def test_dist_hg(self): |
|
if not self.has_working_hg(): |
|
raise unittest.SkipTest('Mercurial not found or broken.') |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Dist is only supported with Ninja') |
|
|
|
def hg_init(project_dir): |
|
subprocess.check_call(['hg', 'init'], cwd=project_dir) |
|
with open(os.path.join(project_dir, '.hg', 'hgrc'), 'w') as f: |
|
print('[ui]', file=f) |
|
print('username=Author Person <teh_coderz@example.com>', file=f) |
|
subprocess.check_call(['hg', 'add', 'meson.build', 'distexe.c'], cwd=project_dir) |
|
subprocess.check_call(['hg', 'commit', '-m', 'I am a project'], cwd=project_dir) |
|
|
|
try: |
|
self.dist_impl(hg_init, include_subprojects=False) |
|
except PermissionError: |
|
# When run under Windows CI, something (virus scanner?) |
|
# holds on to the hg files so cleaning up the dir |
|
# fails sometimes. |
|
pass |
|
|
|
def test_dist_git_script(self): |
|
if not shutil.which('git'): |
|
raise unittest.SkipTest('Git not found') |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Dist is only supported with Ninja') |
|
|
|
try: |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
project_dir = os.path.join(tmpdir, 'a') |
|
shutil.copytree(os.path.join(self.unit_test_dir, '35 dist script'), |
|
project_dir) |
|
_git_init(project_dir) |
|
self.init(project_dir) |
|
self.build('dist') |
|
|
|
self.new_builddir() |
|
self.init(project_dir, extra_args=['-Dsub:broken_dist_script=false']) |
|
self._run(self.meson_command + ['dist', '--include-subprojects'], workdir=self.builddir) |
|
except PermissionError: |
|
# When run under Windows CI, something (virus scanner?) |
|
# holds on to the git files so cleaning up the dir |
|
# fails sometimes. |
|
pass |
|
|
|
def create_dummy_subproject(self, project_dir, name): |
|
path = os.path.join(project_dir, 'subprojects', name) |
|
os.makedirs(path) |
|
with open(os.path.join(path, 'meson.build'), 'w') as ofile: |
|
ofile.write(f"project('{name}', version: '1.0')") |
|
return path |
|
|
|
def dist_impl(self, vcs_init, vcs_add_all=None, include_subprojects=True): |
|
# Create this on the fly because having rogue .git directories inside |
|
# the source tree leads to all kinds of trouble. |
|
with tempfile.TemporaryDirectory() as project_dir: |
|
with open(os.path.join(project_dir, 'meson.build'), 'w') as ofile: |
|
ofile.write(textwrap.dedent('''\ |
|
project('disttest', 'c', version : '1.4.3') |
|
e = executable('distexe', 'distexe.c') |
|
test('dist test', e) |
|
subproject('vcssub', required : false) |
|
subproject('tarballsub', required : false) |
|
subproject('samerepo', required : false) |
|
''')) |
|
with open(os.path.join(project_dir, 'distexe.c'), 'w') as ofile: |
|
ofile.write(textwrap.dedent('''\ |
|
#include<stdio.h> |
|
|
|
int main(int argc, char **argv) { |
|
printf("I am a distribution test.\\n"); |
|
return 0; |
|
} |
|
''')) |
|
xz_distfile = os.path.join(self.distdir, 'disttest-1.4.3.tar.xz') |
|
xz_checksumfile = xz_distfile + '.sha256sum' |
|
gz_distfile = os.path.join(self.distdir, 'disttest-1.4.3.tar.gz') |
|
gz_checksumfile = gz_distfile + '.sha256sum' |
|
zip_distfile = os.path.join(self.distdir, 'disttest-1.4.3.zip') |
|
zip_checksumfile = zip_distfile + '.sha256sum' |
|
vcs_init(project_dir) |
|
if include_subprojects: |
|
vcs_init(self.create_dummy_subproject(project_dir, 'vcssub')) |
|
self.create_dummy_subproject(project_dir, 'tarballsub') |
|
self.create_dummy_subproject(project_dir, 'unusedsub') |
|
if vcs_add_all: |
|
vcs_add_all(self.create_dummy_subproject(project_dir, 'samerepo')) |
|
self.init(project_dir) |
|
self.build('dist') |
|
self.assertPathExists(xz_distfile) |
|
self.assertPathExists(xz_checksumfile) |
|
self.assertPathDoesNotExist(gz_distfile) |
|
self.assertPathDoesNotExist(gz_checksumfile) |
|
self.assertPathDoesNotExist(zip_distfile) |
|
self.assertPathDoesNotExist(zip_checksumfile) |
|
self._run(self.meson_command + ['dist', '--formats', 'gztar'], |
|
workdir=self.builddir) |
|
self.assertPathExists(gz_distfile) |
|
self.assertPathExists(gz_checksumfile) |
|
self._run(self.meson_command + ['dist', '--formats', 'zip'], |
|
workdir=self.builddir) |
|
self.assertPathExists(zip_distfile) |
|
self.assertPathExists(zip_checksumfile) |
|
os.remove(xz_distfile) |
|
os.remove(xz_checksumfile) |
|
os.remove(gz_distfile) |
|
os.remove(gz_checksumfile) |
|
os.remove(zip_distfile) |
|
os.remove(zip_checksumfile) |
|
self._run(self.meson_command + ['dist', '--formats', 'xztar,gztar,zip'], |
|
workdir=self.builddir) |
|
self.assertPathExists(xz_distfile) |
|
self.assertPathExists(xz_checksumfile) |
|
self.assertPathExists(gz_distfile) |
|
self.assertPathExists(gz_checksumfile) |
|
self.assertPathExists(zip_distfile) |
|
self.assertPathExists(zip_checksumfile) |
|
|
|
if include_subprojects: |
|
# Verify that without --include-subprojects we have files from |
|
# the main project and also files from subprojects part of the |
|
# main vcs repository. |
|
z = zipfile.ZipFile(zip_distfile) |
|
expected = ['disttest-1.4.3/', |
|
'disttest-1.4.3/meson.build', |
|
'disttest-1.4.3/distexe.c'] |
|
if vcs_add_all: |
|
expected += ['disttest-1.4.3/subprojects/', |
|
'disttest-1.4.3/subprojects/samerepo/', |
|
'disttest-1.4.3/subprojects/samerepo/meson.build'] |
|
self.assertEqual(sorted(expected), |
|
sorted(z.namelist())) |
|
# Verify that with --include-subprojects we now also have files |
|
# from tarball and separate vcs subprojects. But not files from |
|
# unused subprojects. |
|
self._run(self.meson_command + ['dist', '--formats', 'zip', '--include-subprojects'], |
|
workdir=self.builddir) |
|
z = zipfile.ZipFile(zip_distfile) |
|
expected += ['disttest-1.4.3/subprojects/tarballsub/', |
|
'disttest-1.4.3/subprojects/tarballsub/meson.build', |
|
'disttest-1.4.3/subprojects/vcssub/', |
|
'disttest-1.4.3/subprojects/vcssub/meson.build'] |
|
self.assertEqual(sorted(expected), |
|
sorted(z.namelist())) |
|
if vcs_add_all: |
|
# Verify we can distribute separately subprojects in the same vcs |
|
# repository as the main project. |
|
subproject_dir = os.path.join(project_dir, 'subprojects', 'samerepo') |
|
self.new_builddir() |
|
self.init(subproject_dir) |
|
self.build('dist') |
|
xz_distfile = os.path.join(self.distdir, 'samerepo-1.0.tar.xz') |
|
xz_checksumfile = xz_distfile + '.sha256sum' |
|
self.assertPathExists(xz_distfile) |
|
self.assertPathExists(xz_checksumfile) |
|
tar = tarfile.open(xz_distfile, "r:xz") |
|
self.assertEqual(sorted(['samerepo-1.0', |
|
'samerepo-1.0/meson.build']), |
|
sorted([i.name for i in tar])) |
|
|
|
def test_rpath_uses_ORIGIN(self): |
|
''' |
|
Test that built targets use $ORIGIN in rpath, which ensures that they |
|
are relocatable and ensures that builds are reproducible since the |
|
build directory won't get embedded into the built binaries. |
|
''' |
|
if is_windows() or is_cygwin(): |
|
raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH') |
|
testdir = os.path.join(self.common_test_dir, '39 library chain') |
|
self.init(testdir) |
|
self.build() |
|
for each in ('prog', 'subdir/liblib1.so', ): |
|
rpath = get_rpath(os.path.join(self.builddir, each)) |
|
self.assertTrue(rpath, f'Rpath could not be determined for {each}.') |
|
if is_dragonflybsd(): |
|
# DragonflyBSD will prepend /usr/lib/gccVERSION to the rpath, |
|
# so ignore that. |
|
self.assertTrue(rpath.startswith('/usr/lib/gcc')) |
|
rpaths = rpath.split(':')[1:] |
|
else: |
|
rpaths = rpath.split(':') |
|
for path in rpaths: |
|
self.assertTrue(path.startswith('$ORIGIN'), msg=(each, path)) |
|
# These two don't link to anything else, so they do not need an rpath entry. |
|
for each in ('subdir/subdir2/liblib2.so', 'subdir/subdir3/liblib3.so'): |
|
rpath = get_rpath(os.path.join(self.builddir, each)) |
|
if is_dragonflybsd(): |
|
# The rpath should be equal to /usr/lib/gccVERSION |
|
self.assertTrue(rpath.startswith('/usr/lib/gcc')) |
|
self.assertEqual(len(rpath.split(':')), 1) |
|
else: |
|
self.assertTrue(rpath is None) |
|
|
|
def test_dash_d_dedup(self): |
|
testdir = os.path.join(self.unit_test_dir, '9 d dedup') |
|
self.init(testdir) |
|
cmd = self.get_compdb()[0]['command'] |
|
self.assertTrue('-D FOO -D BAR' in cmd or |
|
'"-D" "FOO" "-D" "BAR"' in cmd or |
|
'/D FOO /D BAR' in cmd or |
|
'"/D" "FOO" "/D" "BAR"' in cmd) |
|
|
|
def test_all_forbidden_targets_tested(self): |
|
''' |
|
Test that all forbidden targets are tested in the '150 reserved targets' |
|
test. Needs to be a unit test because it accesses Meson internals. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '150 reserved targets') |
|
targets = mesonbuild.coredata.FORBIDDEN_TARGET_NAMES |
|
# We don't actually define a target with this name |
|
targets.pop('build.ninja') |
|
# Remove this to avoid multiple entries with the same name |
|
# but different case. |
|
targets.pop('PHONY') |
|
for i in targets: |
|
self.assertPathExists(os.path.join(testdir, i)) |
|
|
|
def detect_prebuild_env(self): |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
stlinker = detect_static_linker(env, cc) |
|
if mesonbuild.mesonlib.is_windows(): |
|
object_suffix = 'obj' |
|
shared_suffix = 'dll' |
|
elif mesonbuild.mesonlib.is_cygwin(): |
|
object_suffix = 'o' |
|
shared_suffix = 'dll' |
|
elif mesonbuild.mesonlib.is_osx(): |
|
object_suffix = 'o' |
|
shared_suffix = 'dylib' |
|
else: |
|
object_suffix = 'o' |
|
shared_suffix = 'so' |
|
return (cc, stlinker, object_suffix, shared_suffix) |
|
|
|
def pbcompile(self, compiler, source, objectfile, extra_args=None): |
|
cmd = compiler.get_exelist() |
|
extra_args = extra_args or [] |
|
if compiler.get_argument_syntax() == 'msvc': |
|
cmd += ['/nologo', '/Fo' + objectfile, '/c', source] + extra_args |
|
else: |
|
cmd += ['-c', source, '-o', objectfile] + extra_args |
|
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
|
|
|
def test_prebuilt_object(self): |
|
(compiler, _, object_suffix, _) = self.detect_prebuild_env() |
|
tdir = os.path.join(self.unit_test_dir, '15 prebuilt object') |
|
source = os.path.join(tdir, 'source.c') |
|
objectfile = os.path.join(tdir, 'prebuilt.' + object_suffix) |
|
self.pbcompile(compiler, source, objectfile) |
|
try: |
|
self.init(tdir) |
|
self.build() |
|
self.run_tests() |
|
finally: |
|
os.unlink(objectfile) |
|
|
|
def build_static_lib(self, compiler, linker, source, objectfile, outfile, extra_args=None): |
|
if extra_args is None: |
|
extra_args = [] |
|
if compiler.get_argument_syntax() == 'msvc': |
|
link_cmd = ['lib', '/NOLOGO', '/OUT:' + outfile, objectfile] |
|
else: |
|
link_cmd = ['ar', 'csr', outfile, objectfile] |
|
link_cmd = linker.get_exelist() |
|
link_cmd += linker.get_always_args() |
|
link_cmd += linker.get_std_link_args() |
|
link_cmd += linker.get_output_args(outfile) |
|
link_cmd += [objectfile] |
|
self.pbcompile(compiler, source, objectfile, extra_args=extra_args) |
|
try: |
|
subprocess.check_call(link_cmd) |
|
finally: |
|
os.unlink(objectfile) |
|
|
|
def test_prebuilt_static_lib(self): |
|
(cc, stlinker, object_suffix, _) = self.detect_prebuild_env() |
|
tdir = os.path.join(self.unit_test_dir, '16 prebuilt static') |
|
source = os.path.join(tdir, 'libdir/best.c') |
|
objectfile = os.path.join(tdir, 'libdir/best.' + object_suffix) |
|
stlibfile = os.path.join(tdir, 'libdir/libbest.a') |
|
self.build_static_lib(cc, stlinker, source, objectfile, stlibfile) |
|
# Run the test |
|
try: |
|
self.init(tdir) |
|
self.build() |
|
self.run_tests() |
|
finally: |
|
os.unlink(stlibfile) |
|
|
|
def build_shared_lib(self, compiler, source, objectfile, outfile, impfile, extra_args=None): |
|
if extra_args is None: |
|
extra_args = [] |
|
if compiler.get_argument_syntax() == 'msvc': |
|
link_cmd = compiler.get_linker_exelist() + [ |
|
'/NOLOGO', '/DLL', '/DEBUG', '/IMPLIB:' + impfile, |
|
'/OUT:' + outfile, objectfile] |
|
else: |
|
if not (compiler.info.is_windows() or compiler.info.is_cygwin() or compiler.info.is_darwin()): |
|
extra_args += ['-fPIC'] |
|
link_cmd = compiler.get_exelist() + ['-shared', '-o', outfile, objectfile] |
|
if not mesonbuild.mesonlib.is_osx(): |
|
link_cmd += ['-Wl,-soname=' + os.path.basename(outfile)] |
|
self.pbcompile(compiler, source, objectfile, extra_args=extra_args) |
|
try: |
|
subprocess.check_call(link_cmd) |
|
finally: |
|
os.unlink(objectfile) |
|
|
|
def test_prebuilt_shared_lib(self): |
|
(cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env() |
|
tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') |
|
source = os.path.join(tdir, 'alexandria.c') |
|
objectfile = os.path.join(tdir, 'alexandria.' + object_suffix) |
|
impfile = os.path.join(tdir, 'alexandria.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(tdir, 'alexandria.' + shared_suffix) |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(tdir, 'cygalexandria.' + shared_suffix) |
|
else: |
|
shlibfile = os.path.join(tdir, 'libalexandria.' + shared_suffix) |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
# Run the test |
|
try: |
|
self.init(tdir) |
|
self.build() |
|
self.run_tests() |
|
finally: |
|
os.unlink(shlibfile) |
|
if mesonbuild.mesonlib.is_windows(): |
|
# Clean up all the garbage MSVC writes in the |
|
# source tree. |
|
for fname in glob(os.path.join(tdir, 'alexandria.*')): |
|
if os.path.splitext(fname)[1] not in ['.c', '.h']: |
|
os.unlink(fname) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_static(self): |
|
''' |
|
Test that the we prefer static libraries when `static: true` is |
|
passed to dependency() with pkg-config. Can't be an ordinary test |
|
because we need to build libs and try to find them from meson.build |
|
|
|
Also test that it's not a hard error to have unsatisfiable library deps |
|
since system libraries -lm will never be found statically. |
|
https://github.com/mesonbuild/meson/issues/2785 |
|
''' |
|
(cc, stlinker, objext, shext) = self.detect_prebuild_env() |
|
testdir = os.path.join(self.unit_test_dir, '18 pkgconfig static') |
|
source = os.path.join(testdir, 'foo.c') |
|
objectfile = os.path.join(testdir, 'foo.' + objext) |
|
stlibfile = os.path.join(testdir, 'libfoo.a') |
|
impfile = os.path.join(testdir, 'foo.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(testdir, 'foo.' + shext) |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(testdir, 'cygfoo.' + shext) |
|
else: |
|
shlibfile = os.path.join(testdir, 'libfoo.' + shext) |
|
# Build libs |
|
self.build_static_lib(cc, stlinker, source, objectfile, stlibfile, extra_args=['-DFOO_STATIC']) |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
# Run test |
|
try: |
|
self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': self.builddir}) |
|
self.build() |
|
self.run_tests() |
|
finally: |
|
os.unlink(stlibfile) |
|
os.unlink(shlibfile) |
|
if mesonbuild.mesonlib.is_windows(): |
|
# Clean up all the garbage MSVC writes in the |
|
# source tree. |
|
for fname in glob(os.path.join(testdir, 'foo.*')): |
|
if os.path.splitext(fname)[1] not in ['.c', '.h', '.in']: |
|
os.unlink(fname) |
|
|
|
@skipIfNoPkgconfig |
|
@mock.patch.dict(os.environ) |
|
def test_pkgconfig_gen_escaping(self): |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
prefix = '/usr/with spaces' |
|
libdir = 'lib' |
|
self.init(testdir, extra_args=['--prefix=' + prefix, |
|
'--libdir=' + libdir]) |
|
# Find foo dependency |
|
os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
kwargs = {'required': True, 'silent': True} |
|
foo_dep = PkgConfigDependency('libfoo', env, kwargs) |
|
# Ensure link_args are properly quoted |
|
libdir = PurePath(prefix) / PurePath(libdir) |
|
link_args = ['-L' + libdir.as_posix(), '-lfoo'] |
|
self.assertEqual(foo_dep.get_link_args(), link_args) |
|
# Ensure include args are properly quoted |
|
incdir = PurePath(prefix) / PurePath('include') |
|
cargs = ['-I' + incdir.as_posix(), '-DLIBFOO'] |
|
# pkg-config and pkgconf does not respect the same order |
|
self.assertEqual(sorted(foo_dep.get_compile_args()), sorted(cargs)) |
|
|
|
def test_array_option_change(self): |
|
def get_opt(): |
|
opts = self.introspect('--buildoptions') |
|
for x in opts: |
|
if x.get('name') == 'list': |
|
return x |
|
raise Exception(opts) |
|
|
|
expected = { |
|
'name': 'list', |
|
'description': 'list', |
|
'section': 'user', |
|
'type': 'array', |
|
'value': ['foo', 'bar'], |
|
'machine': 'any', |
|
} |
|
tdir = os.path.join(self.unit_test_dir, '19 array option') |
|
self.init(tdir) |
|
original = get_opt() |
|
self.assertDictEqual(original, expected) |
|
|
|
expected['value'] = ['oink', 'boink'] |
|
self.setconf('-Dlist=oink,boink') |
|
changed = get_opt() |
|
self.assertEqual(changed, expected) |
|
|
|
def test_array_option_bad_change(self): |
|
def get_opt(): |
|
opts = self.introspect('--buildoptions') |
|
for x in opts: |
|
if x.get('name') == 'list': |
|
return x |
|
raise Exception(opts) |
|
|
|
expected = { |
|
'name': 'list', |
|
'description': 'list', |
|
'section': 'user', |
|
'type': 'array', |
|
'value': ['foo', 'bar'], |
|
'machine': 'any', |
|
} |
|
tdir = os.path.join(self.unit_test_dir, '19 array option') |
|
self.init(tdir) |
|
original = get_opt() |
|
self.assertDictEqual(original, expected) |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.setconf('-Dlist=bad') |
|
changed = get_opt() |
|
self.assertDictEqual(changed, expected) |
|
|
|
def test_array_option_empty_equivalents(self): |
|
"""Array options treat -Dopt=[] and -Dopt= as equivalent.""" |
|
def get_opt(): |
|
opts = self.introspect('--buildoptions') |
|
for x in opts: |
|
if x.get('name') == 'list': |
|
return x |
|
raise Exception(opts) |
|
|
|
expected = { |
|
'name': 'list', |
|
'description': 'list', |
|
'section': 'user', |
|
'type': 'array', |
|
'value': [], |
|
'machine': 'any', |
|
} |
|
tdir = os.path.join(self.unit_test_dir, '19 array option') |
|
self.init(tdir, extra_args='-Dlist=') |
|
original = get_opt() |
|
self.assertDictEqual(original, expected) |
|
|
|
def opt_has(self, name, value): |
|
res = self.introspect('--buildoptions') |
|
found = False |
|
for i in res: |
|
if i['name'] == name: |
|
self.assertEqual(i['value'], value) |
|
found = True |
|
break |
|
self.assertTrue(found, "Array option not found in introspect data.") |
|
|
|
def test_free_stringarray_setting(self): |
|
testdir = os.path.join(self.common_test_dir, '40 options') |
|
self.init(testdir) |
|
self.opt_has('free_array_opt', []) |
|
self.setconf('-Dfree_array_opt=foo,bar', will_build=False) |
|
self.opt_has('free_array_opt', ['foo', 'bar']) |
|
self.setconf("-Dfree_array_opt=['a,b', 'c,d']", will_build=False) |
|
self.opt_has('free_array_opt', ['a,b', 'c,d']) |
|
|
|
# When running under Travis Mac CI, the file updates seem to happen |
|
# too fast so the timestamps do not get properly updated. |
|
# Call this method before file operations in appropriate places |
|
# to make things work. |
|
def mac_ci_delay(self): |
|
if is_osx() and is_ci(): |
|
import time |
|
time.sleep(1) |
|
|
|
def test_options_with_choices_changing(self) -> None: |
|
"""Detect when options like arrays or combos have their choices change.""" |
|
testdir = Path(os.path.join(self.unit_test_dir, '84 change option choices')) |
|
options1 = str(testdir / 'meson_options.1.txt') |
|
options2 = str(testdir / 'meson_options.2.txt') |
|
|
|
# Test that old options are changed to the new defaults if they are not valid |
|
real_options = str(testdir / 'meson_options.txt') |
|
self.addCleanup(os.unlink, real_options) |
|
|
|
shutil.copy(options1, real_options) |
|
self.init(str(testdir)) |
|
self.mac_ci_delay() |
|
shutil.copy(options2, real_options) |
|
|
|
self.build() |
|
opts = self.introspect('--buildoptions') |
|
for item in opts: |
|
if item['name'] == 'combo': |
|
self.assertEqual(item['value'], 'b') |
|
self.assertEqual(item['choices'], ['b', 'c', 'd']) |
|
elif item['name'] == 'arr': |
|
self.assertEqual(item['value'], ['b']) |
|
self.assertEqual(item['choices'], ['b', 'c', 'd']) |
|
|
|
self.wipe() |
|
self.mac_ci_delay() |
|
|
|
# When the old options are valid they should remain |
|
shutil.copy(options1, real_options) |
|
self.init(str(testdir), extra_args=['-Dcombo=c', '-Darray=b,c']) |
|
self.mac_ci_delay() |
|
shutil.copy(options2, real_options) |
|
self.build() |
|
opts = self.introspect('--buildoptions') |
|
for item in opts: |
|
if item['name'] == 'combo': |
|
self.assertEqual(item['value'], 'c') |
|
self.assertEqual(item['choices'], ['b', 'c', 'd']) |
|
elif item['name'] == 'arr': |
|
self.assertEqual(item['value'], ['b', 'c']) |
|
self.assertEqual(item['choices'], ['b', 'c', 'd']) |
|
|
|
def test_subproject_promotion(self): |
|
testdir = os.path.join(self.unit_test_dir, '12 promote') |
|
workdir = os.path.join(self.builddir, 'work') |
|
shutil.copytree(testdir, workdir) |
|
spdir = os.path.join(workdir, 'subprojects') |
|
s3dir = os.path.join(spdir, 's3') |
|
scommondir = os.path.join(spdir, 'scommon') |
|
self.assertFalse(os.path.isdir(s3dir)) |
|
subprocess.check_call(self.wrap_command + ['promote', 's3'], |
|
cwd=workdir, |
|
stdout=subprocess.DEVNULL) |
|
self.assertTrue(os.path.isdir(s3dir)) |
|
self.assertFalse(os.path.isdir(scommondir)) |
|
self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'scommon'], |
|
cwd=workdir, |
|
stderr=subprocess.DEVNULL), 0) |
|
self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'invalid/path/to/scommon'], |
|
cwd=workdir, |
|
stderr=subprocess.DEVNULL), 0) |
|
self.assertFalse(os.path.isdir(scommondir)) |
|
subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/scommon'], cwd=workdir) |
|
self.assertTrue(os.path.isdir(scommondir)) |
|
promoted_wrap = os.path.join(spdir, 'athing.wrap') |
|
self.assertFalse(os.path.isfile(promoted_wrap)) |
|
subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir) |
|
self.assertTrue(os.path.isfile(promoted_wrap)) |
|
self.init(workdir) |
|
self.build() |
|
|
|
def test_subproject_promotion_wrap(self): |
|
testdir = os.path.join(self.unit_test_dir, '44 promote wrap') |
|
workdir = os.path.join(self.builddir, 'work') |
|
shutil.copytree(testdir, workdir) |
|
spdir = os.path.join(workdir, 'subprojects') |
|
|
|
ambiguous_wrap = os.path.join(spdir, 'ambiguous.wrap') |
|
self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'ambiguous'], |
|
cwd=workdir, |
|
stderr=subprocess.DEVNULL), 0) |
|
self.assertFalse(os.path.isfile(ambiguous_wrap)) |
|
subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/ambiguous.wrap'], cwd=workdir) |
|
self.assertTrue(os.path.isfile(ambiguous_wrap)) |
|
|
|
def test_warning_location(self): |
|
tdir = os.path.join(self.unit_test_dir, '22 warning location') |
|
out = self.init(tdir) |
|
for expected in [ |
|
r'meson.build:4: WARNING: Keyword argument "link_with" defined multiple times.', |
|
r'sub' + os.path.sep + r'meson.build:3: WARNING: Keyword argument "link_with" defined multiple times.', |
|
r'meson.build:6: WARNING: a warning of some sort', |
|
r'sub' + os.path.sep + r'meson.build:4: WARNING: subdir warning', |
|
r'meson.build:7: WARNING: Module unstable-simd has no backwards or forwards compatibility and might not exist in future releases.', |
|
r"meson.build:11: WARNING: The variable(s) 'MISSING' in the input file 'conf.in' are not present in the given configuration data.", |
|
r'meson.build:1: WARNING: Passed invalid keyword argument "invalid".', |
|
]: |
|
self.assertRegex(out, re.escape(expected)) |
|
|
|
for wd in [ |
|
self.src_root, |
|
self.builddir, |
|
os.getcwd(), |
|
]: |
|
self.new_builddir() |
|
out = self.init(tdir, workdir=wd) |
|
expected = os.path.join(relpath(tdir, self.src_root), 'meson.build') |
|
relwd = relpath(self.src_root, wd) |
|
if relwd != '.': |
|
expected = os.path.join(relwd, expected) |
|
expected = '\n' + expected + ':' |
|
self.assertIn(expected, out) |
|
|
|
def test_error_location_path(self): |
|
'''Test locations in meson errors contain correct paths''' |
|
# this list contains errors from all the different steps in the |
|
# lexer/parser/interpreter we have tests for. |
|
for (t, f) in [ |
|
('10 out of bounds', 'meson.build'), |
|
('18 wrong plusassign', 'meson.build'), |
|
('61 bad option argument', 'meson_options.txt'), |
|
('102 subdir parse error', os.path.join('subdir', 'meson.build')), |
|
('103 invalid option file', 'meson_options.txt'), |
|
]: |
|
tdir = os.path.join(self.src_root, 'test cases', 'failing', t) |
|
|
|
for wd in [ |
|
self.src_root, |
|
self.builddir, |
|
os.getcwd(), |
|
]: |
|
try: |
|
self.init(tdir, workdir=wd) |
|
except subprocess.CalledProcessError as e: |
|
expected = os.path.join('test cases', 'failing', t, f) |
|
relwd = relpath(self.src_root, wd) |
|
if relwd != '.': |
|
expected = os.path.join(relwd, expected) |
|
expected = '\n' + expected + ':' |
|
self.assertIn(expected, e.output) |
|
else: |
|
self.fail('configure unexpectedly succeeded') |
|
|
|
def test_permitted_method_kwargs(self): |
|
tdir = os.path.join(self.unit_test_dir, '25 non-permitted kwargs') |
|
out = self.init(tdir) |
|
for expected in [ |
|
r'WARNING: Passed invalid keyword argument "prefixxx".', |
|
r'WARNING: Passed invalid keyword argument "argsxx".', |
|
r'WARNING: Passed invalid keyword argument "invalidxx".', |
|
]: |
|
self.assertRegex(out, re.escape(expected)) |
|
|
|
def test_templates(self): |
|
ninja = detect_ninja() |
|
if ninja is None: |
|
raise unittest.SkipTest('This test currently requires ninja. Fix this once "meson build" works.') |
|
|
|
langs = ['c'] |
|
env = get_fake_env() |
|
for l in ['cpp', 'cs', 'd', 'java', 'cuda', 'fortran', 'objc', 'objcpp', 'rust']: |
|
try: |
|
comp = mesonbuild.compilers.detect_compiler_for(env, l, MachineChoice.HOST) |
|
with tempfile.TemporaryDirectory() as d: |
|
comp.sanity_check(d, env) |
|
langs.append(l) |
|
except EnvironmentException: |
|
pass |
|
|
|
# The D template fails under mac CI and we don't know why. |
|
# Patches welcome |
|
if is_osx(): |
|
langs = [l for l in langs if l != 'd'] |
|
|
|
for lang in langs: |
|
for target_type in ('executable', 'library'): |
|
# test empty directory |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
self._run(self.meson_command + ['init', '--language', lang, '--type', target_type], |
|
workdir=tmpdir) |
|
self._run(self.setup_command + ['--backend=ninja', 'builddir'], |
|
workdir=tmpdir) |
|
self._run(ninja, |
|
workdir=os.path.join(tmpdir, 'builddir')) |
|
# test directory with existing code file |
|
if lang in {'c', 'cpp', 'd'}: |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
with open(os.path.join(tmpdir, 'foo.' + lang), 'w') as f: |
|
f.write('int main(void) {}') |
|
self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) |
|
elif lang in {'java'}: |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
with open(os.path.join(tmpdir, 'Foo.' + lang), 'w') as f: |
|
f.write('public class Foo { public static void main() {} }') |
|
self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) |
|
|
|
def test_compiler_run_command(self): |
|
''' |
|
The test checks that the compiler object can be passed to |
|
run_command(). |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '24 compiler run_command') |
|
self.init(testdir) |
|
|
|
def test_identical_target_name_in_subproject_flat_layout(self): |
|
''' |
|
Test that identical targets in different subprojects do not collide |
|
if layout is flat. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '172 identical target name in subproject flat layout') |
|
self.init(testdir, extra_args=['--layout=flat']) |
|
self.build() |
|
|
|
def test_identical_target_name_in_subdir_flat_layout(self): |
|
''' |
|
Test that identical targets in different subdirs do not collide |
|
if layout is flat. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '181 same target name flat layout') |
|
self.init(testdir, extra_args=['--layout=flat']) |
|
self.build() |
|
|
|
def test_flock(self): |
|
exception_raised = False |
|
with tempfile.TemporaryDirectory() as tdir: |
|
os.mkdir(os.path.join(tdir, 'meson-private')) |
|
with BuildDirLock(tdir): |
|
try: |
|
with BuildDirLock(tdir): |
|
pass |
|
except MesonException: |
|
exception_raised = True |
|
self.assertTrue(exception_raised, 'Double locking did not raise exception.') |
|
|
|
@unittest.skipIf(is_osx(), 'Test not applicable to OSX') |
|
def test_check_module_linking(self): |
|
""" |
|
Test that link_with: a shared module issues a warning |
|
https://github.com/mesonbuild/meson/issues/2865 |
|
(That an error is raised on OSX is exercised by test failing/78) |
|
""" |
|
tdir = os.path.join(self.unit_test_dir, '30 shared_mod linking') |
|
out = self.init(tdir) |
|
msg = ('WARNING: target links against shared modules. This is not ' |
|
'recommended as it is not supported on some platforms') |
|
self.assertIn(msg, out) |
|
|
|
def test_ndebug_if_release_disabled(self): |
|
testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release') |
|
self.init(testdir, extra_args=['--buildtype=release', '-Db_ndebug=if-release']) |
|
self.build() |
|
exe = os.path.join(self.builddir, 'main') |
|
self.assertEqual(b'NDEBUG=1', subprocess.check_output(exe).strip()) |
|
|
|
def test_ndebug_if_release_enabled(self): |
|
testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release') |
|
self.init(testdir, extra_args=['--buildtype=debugoptimized', '-Db_ndebug=if-release']) |
|
self.build() |
|
exe = os.path.join(self.builddir, 'main') |
|
self.assertEqual(b'NDEBUG=0', subprocess.check_output(exe).strip()) |
|
|
|
def test_guessed_linker_dependencies(self): |
|
''' |
|
Test that meson adds dependencies for libraries based on the final |
|
linker command line. |
|
''' |
|
testdirbase = os.path.join(self.unit_test_dir, '29 guessed linker dependencies') |
|
testdirlib = os.path.join(testdirbase, 'lib') |
|
|
|
extra_args = None |
|
libdir_flags = ['-L'] |
|
env = get_fake_env(testdirlib, self.builddir, self.prefix) |
|
if detect_c_compiler(env, MachineChoice.HOST).get_id() in {'msvc', 'clang-cl', 'intel-cl'}: |
|
# msvc-like compiler, also test it with msvc-specific flags |
|
libdir_flags += ['/LIBPATH:', '-LIBPATH:'] |
|
else: |
|
# static libraries are not linkable with -l with msvc because meson installs them |
|
# as .a files which unix_args_to_native will not know as it expects libraries to use |
|
# .lib as extension. For a DLL the import library is installed as .lib. Thus for msvc |
|
# this tests needs to use shared libraries to test the path resolving logic in the |
|
# dependency generation code path. |
|
extra_args = ['--default-library', 'static'] |
|
|
|
initial_builddir = self.builddir |
|
initial_installdir = self.installdir |
|
|
|
for libdir_flag in libdir_flags: |
|
# build library |
|
self.new_builddir() |
|
self.init(testdirlib, extra_args=extra_args) |
|
self.build() |
|
self.install() |
|
libbuilddir = self.builddir |
|
installdir = self.installdir |
|
libdir = os.path.join(self.installdir, self.prefix.lstrip('/').lstrip('\\'), 'lib') |
|
|
|
# build user of library |
|
self.new_builddir() |
|
# replace is needed because meson mangles platform paths passed via LDFLAGS |
|
self.init(os.path.join(testdirbase, 'exe'), |
|
override_envvars={"LDFLAGS": '{}{}'.format(libdir_flag, libdir.replace('\\', '/'))}) |
|
self.build() |
|
self.assertBuildIsNoop() |
|
|
|
# rebuild library |
|
exebuilddir = self.builddir |
|
self.installdir = installdir |
|
self.builddir = libbuilddir |
|
# Microsoft's compiler is quite smart about touching import libs on changes, |
|
# so ensure that there is actually a change in symbols. |
|
self.setconf('-Dmore_exports=true') |
|
self.build() |
|
self.install() |
|
# no ensure_backend_detects_changes needed because self.setconf did that already |
|
|
|
# assert user of library will be rebuild |
|
self.builddir = exebuilddir |
|
self.assertRebuiltTarget('app') |
|
|
|
# restore dirs for the next test case |
|
self.installdir = initial_builddir |
|
self.builddir = initial_installdir |
|
|
|
def test_conflicting_d_dash_option(self): |
|
testdir = os.path.join(self.unit_test_dir, '37 mixed command line args') |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as e: |
|
self.init(testdir, extra_args=['-Dbindir=foo', '--bindir=bar']) |
|
# Just to ensure that we caught the correct error |
|
self.assertIn('as both', e.stderr) |
|
|
|
def _test_same_option_twice(self, arg, args): |
|
testdir = os.path.join(self.unit_test_dir, '37 mixed command line args') |
|
self.init(testdir, extra_args=args) |
|
opts = self.introspect('--buildoptions') |
|
for item in opts: |
|
if item['name'] == arg: |
|
self.assertEqual(item['value'], 'bar') |
|
return |
|
raise Exception(f'Missing {arg} value?') |
|
|
|
def test_same_dash_option_twice(self): |
|
self._test_same_option_twice('bindir', ['--bindir=foo', '--bindir=bar']) |
|
|
|
def test_same_d_option_twice(self): |
|
self._test_same_option_twice('bindir', ['-Dbindir=foo', '-Dbindir=bar']) |
|
|
|
def test_same_project_d_option_twice(self): |
|
self._test_same_option_twice('one', ['-Done=foo', '-Done=bar']) |
|
|
|
def _test_same_option_twice_configure(self, arg, args): |
|
testdir = os.path.join(self.unit_test_dir, '37 mixed command line args') |
|
self.init(testdir) |
|
self.setconf(args) |
|
opts = self.introspect('--buildoptions') |
|
for item in opts: |
|
if item['name'] == arg: |
|
self.assertEqual(item['value'], 'bar') |
|
return |
|
raise Exception(f'Missing {arg} value?') |
|
|
|
def test_same_dash_option_twice_configure(self): |
|
self._test_same_option_twice_configure( |
|
'bindir', ['--bindir=foo', '--bindir=bar']) |
|
|
|
def test_same_d_option_twice_configure(self): |
|
self._test_same_option_twice_configure( |
|
'bindir', ['-Dbindir=foo', '-Dbindir=bar']) |
|
|
|
def test_same_project_d_option_twice_configure(self): |
|
self._test_same_option_twice_configure( |
|
'one', ['-Done=foo', '-Done=bar']) |
|
|
|
def test_command_line(self): |
|
testdir = os.path.join(self.unit_test_dir, '34 command line') |
|
|
|
# Verify default values when passing no args that affect the |
|
# configuration, and as a bonus, test that --profile-self works. |
|
out = self.init(testdir, extra_args=['--profile-self', '--fatal-meson-warnings']) |
|
self.assertNotIn('[default: true]', out) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('default_library')].value, 'static') |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '1') |
|
self.assertEqual(obj.options[OptionKey('set_sub_opt')].value, True) |
|
self.assertEqual(obj.options[OptionKey('subp_opt', 'subp')].value, 'default3') |
|
self.wipe() |
|
|
|
# warning_level is special, it's --warnlevel instead of --warning-level |
|
# for historical reasons |
|
self.init(testdir, extra_args=['--warnlevel=2', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '2') |
|
self.setconf('--warnlevel=3') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '3') |
|
self.wipe() |
|
|
|
# But when using -D syntax, it should be 'warning_level' |
|
self.init(testdir, extra_args=['-Dwarning_level=2', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '2') |
|
self.setconf('-Dwarning_level=3') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '3') |
|
self.wipe() |
|
|
|
# Mixing --option and -Doption is forbidden |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm: |
|
self.init(testdir, extra_args=['--warnlevel=1', '-Dwarning_level=3']) |
|
if isinstance(cm.exception, subprocess.CalledProcessError): |
|
self.assertNotEqual(0, cm.exception.returncode) |
|
self.assertIn('as both', cm.exception.output) |
|
else: |
|
self.assertIn('as both', str(cm.exception)) |
|
self.init(testdir) |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm: |
|
self.setconf(['--warnlevel=1', '-Dwarning_level=3']) |
|
if isinstance(cm.exception, subprocess.CalledProcessError): |
|
self.assertNotEqual(0, cm.exception.returncode) |
|
self.assertIn('as both', cm.exception.output) |
|
else: |
|
self.assertIn('as both', str(cm.exception)) |
|
self.wipe() |
|
|
|
# --default-library should override default value from project() |
|
self.init(testdir, extra_args=['--default-library=both', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('default_library')].value, 'both') |
|
self.setconf('--default-library=shared') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('default_library')].value, 'shared') |
|
if self.backend is Backend.ninja: |
|
# reconfigure target works only with ninja backend |
|
self.build('reconfigure') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('default_library')].value, 'shared') |
|
self.wipe() |
|
|
|
# Should warn on unknown options |
|
out = self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo']) |
|
self.assertIn('Unknown options: "bad, foo, wrong_link_args"', out) |
|
self.wipe() |
|
|
|
# Should fail on malformed option |
|
msg = "Option 'foo' must have a value separated by equals sign." |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm: |
|
self.init(testdir, extra_args=['-Dfoo']) |
|
if isinstance(cm.exception, subprocess.CalledProcessError): |
|
self.assertNotEqual(0, cm.exception.returncode) |
|
self.assertIn(msg, cm.exception.output) |
|
else: |
|
self.assertIn(msg, str(cm.exception)) |
|
self.init(testdir) |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm: |
|
self.setconf('-Dfoo') |
|
if isinstance(cm.exception, subprocess.CalledProcessError): |
|
self.assertNotEqual(0, cm.exception.returncode) |
|
self.assertIn(msg, cm.exception.output) |
|
else: |
|
self.assertIn(msg, str(cm.exception)) |
|
self.wipe() |
|
|
|
# It is not an error to set wrong option for unknown subprojects or |
|
# language because we don't have control on which one will be selected. |
|
self.init(testdir, extra_args=['-Dc_wrong=1', '-Dwrong:bad=1', '-Db_wrong=1']) |
|
self.wipe() |
|
|
|
# Test we can set subproject option |
|
self.init(testdir, extra_args=['-Dsubp:subp_opt=foo', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('subp_opt', 'subp')].value, 'foo') |
|
self.wipe() |
|
|
|
# c_args value should be parsed with split_args |
|
self.init(testdir, extra_args=['-Dc_args=-Dfoo -Dbar "-Dthird=one two"', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dfoo', '-Dbar', '-Dthird=one two']) |
|
|
|
self.setconf('-Dc_args="foo bar" one two') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['foo bar', 'one', 'two']) |
|
self.wipe() |
|
|
|
self.init(testdir, extra_args=['-Dset_percent_opt=myoption%', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('set_percent_opt')].value, 'myoption%') |
|
self.wipe() |
|
|
|
# Setting a 2nd time the same option should override the first value |
|
try: |
|
self.init(testdir, extra_args=['--bindir=foo', '--bindir=bar', |
|
'-Dbuildtype=plain', '-Dbuildtype=release', |
|
'-Db_sanitize=address', '-Db_sanitize=thread', |
|
'-Dc_args=-Dfoo', '-Dc_args=-Dbar', |
|
'-Db_lundef=false', '--fatal-meson-warnings']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('bindir')].value, 'bar') |
|
self.assertEqual(obj.options[OptionKey('buildtype')].value, 'release') |
|
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'thread') |
|
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dbar']) |
|
self.setconf(['--bindir=bar', '--bindir=foo', |
|
'-Dbuildtype=release', '-Dbuildtype=plain', |
|
'-Db_sanitize=thread', '-Db_sanitize=address', |
|
'-Dc_args=-Dbar', '-Dc_args=-Dfoo']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('bindir')].value, 'foo') |
|
self.assertEqual(obj.options[OptionKey('buildtype')].value, 'plain') |
|
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'address') |
|
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dfoo']) |
|
self.wipe() |
|
except KeyError: |
|
# Ignore KeyError, it happens on CI for compilers that does not |
|
# support b_sanitize. We have to test with a base option because |
|
# they used to fail this test with Meson 0.46 an earlier versions. |
|
pass |
|
|
|
def test_warning_level_0(self): |
|
testdir = os.path.join(self.common_test_dir, '207 warning level 0') |
|
|
|
# Verify default values when passing no args |
|
self.init(testdir) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '0') |
|
self.wipe() |
|
|
|
# verify we can override w/ --warnlevel |
|
self.init(testdir, extra_args=['--warnlevel=1']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '1') |
|
self.setconf('--warnlevel=0') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '0') |
|
self.wipe() |
|
|
|
# verify we can override w/ -Dwarning_level |
|
self.init(testdir, extra_args=['-Dwarning_level=1']) |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '1') |
|
self.setconf('-Dwarning_level=0') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, '0') |
|
self.wipe() |
|
|
|
def test_feature_check_usage_subprojects(self): |
|
testdir = os.path.join(self.unit_test_dir, '41 featurenew subprojects') |
|
out = self.init(testdir) |
|
# Parent project warns correctly |
|
self.assertRegex(out, "WARNING: Project targeting '>=0.45'.*'0.47.0': dict") |
|
# Subprojects warn correctly |
|
self.assertRegex(out, r"\| WARNING: Project targeting '>=0.40'.*'0.44.0': disabler") |
|
self.assertRegex(out, r"\| WARNING: Project targeting '!=0.40'.*'0.44.0': disabler") |
|
# Subproject has a new-enough meson_version, no warning |
|
self.assertNotRegex(out, "WARNING: Project targeting.*Python") |
|
# Ensure a summary is printed in the subproject and the outer project |
|
self.assertRegex(out, r"\| WARNING: Project specifies a minimum meson_version '>=0.40'") |
|
self.assertRegex(out, r"\| \* 0.44.0: {'disabler'}") |
|
self.assertRegex(out, "WARNING: Project specifies a minimum meson_version '>=0.45'") |
|
self.assertRegex(out, " * 0.47.0: {'dict'}") |
|
|
|
def test_configure_file_warnings(self): |
|
testdir = os.path.join(self.common_test_dir, "14 configure file") |
|
out = self.init(testdir) |
|
self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*") |
|
self.assertRegex(out, "WARNING:.*'FOO_BAR'.*nosubst-nocopy2.txt.in.*not present.*") |
|
self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*") |
|
self.assertRegex(out, "WARNING:.*empty configuration_data.*test.py.in") |
|
# Warnings for configuration files that are overwritten. |
|
self.assertRegex(out, "WARNING:.*\"double_output.txt\".*overwrites") |
|
self.assertRegex(out, "WARNING:.*\"subdir.double_output2.txt\".*overwrites") |
|
self.assertNotRegex(out, "WARNING:.*no_write_conflict.txt.*overwrites") |
|
self.assertNotRegex(out, "WARNING:.*@BASENAME@.*overwrites") |
|
self.assertRegex(out, "WARNING:.*\"sameafterbasename\".*overwrites") |
|
# No warnings about empty configuration data objects passed to files with substitutions |
|
self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy1.txt.in") |
|
self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy2.txt.in") |
|
with open(os.path.join(self.builddir, 'nosubst-nocopy1.txt'), 'rb') as f: |
|
self.assertEqual(f.read().strip(), b'/* #undef FOO_BAR */') |
|
with open(os.path.join(self.builddir, 'nosubst-nocopy2.txt'), 'rb') as f: |
|
self.assertEqual(f.read().strip(), b'') |
|
self.assertRegex(out, r"DEPRECATION:.*\['array'\] is invalid.*dict") |
|
|
|
def test_dirs(self): |
|
with tempfile.TemporaryDirectory() as containing: |
|
with tempfile.TemporaryDirectory(dir=containing) as srcdir: |
|
mfile = os.path.join(srcdir, 'meson.build') |
|
of = open(mfile, 'w') |
|
of.write("project('foobar', 'c')\n") |
|
of.close() |
|
pc = subprocess.run(self.setup_command, |
|
cwd=srcdir, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.DEVNULL) |
|
self.assertIn(b'Must specify at least one directory name', pc.stdout) |
|
with tempfile.TemporaryDirectory(dir=srcdir) as builddir: |
|
subprocess.run(self.setup_command, |
|
check=True, |
|
cwd=builddir, |
|
stdout=subprocess.DEVNULL, |
|
stderr=subprocess.DEVNULL) |
|
|
|
def get_opts_as_dict(self): |
|
result = {} |
|
for i in self.introspect('--buildoptions'): |
|
result[i['name']] = i['value'] |
|
return result |
|
|
|
def test_buildtype_setting(self): |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
self.init(testdir) |
|
opts = self.get_opts_as_dict() |
|
self.assertEqual(opts['buildtype'], 'debug') |
|
self.assertEqual(opts['debug'], True) |
|
self.setconf('-Ddebug=false') |
|
opts = self.get_opts_as_dict() |
|
self.assertEqual(opts['debug'], False) |
|
self.assertEqual(opts['buildtype'], 'debug') |
|
self.assertEqual(opts['optimization'], '0') |
|
self.setconf('-Doptimization=g') |
|
opts = self.get_opts_as_dict() |
|
self.assertEqual(opts['debug'], False) |
|
self.assertEqual(opts['buildtype'], 'debug') |
|
self.assertEqual(opts['optimization'], 'g') |
|
|
|
@skipIfNoPkgconfig |
|
@unittest.skipIf(is_windows(), 'Help needed with fixing this test on windows') |
|
def test_native_dep_pkgconfig(self): |
|
testdir = os.path.join(self.unit_test_dir, |
|
'46 native dep pkgconfig var') |
|
with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile: |
|
crossfile.write(textwrap.dedent( |
|
'''[binaries] |
|
pkgconfig = '{}' |
|
|
|
[properties] |
|
|
|
[host_machine] |
|
system = 'linux' |
|
cpu_family = 'arm' |
|
cpu = 'armv7' |
|
endian = 'little' |
|
'''.format(os.path.join(testdir, 'cross_pkgconfig.py')))) |
|
crossfile.flush() |
|
self.meson_cross_file = crossfile.name |
|
|
|
env = {'PKG_CONFIG_LIBDIR': os.path.join(testdir, |
|
'native_pkgconfig')} |
|
self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env) |
|
self.wipe() |
|
self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env) |
|
|
|
@skipIfNoPkgconfig |
|
@unittest.skipIf(is_windows(), 'Help needed with fixing this test on windows') |
|
def test_pkg_config_libdir(self): |
|
testdir = os.path.join(self.unit_test_dir, |
|
'46 native dep pkgconfig var') |
|
with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile: |
|
crossfile.write(textwrap.dedent( |
|
'''[binaries] |
|
pkgconfig = 'pkg-config' |
|
|
|
[properties] |
|
pkg_config_libdir = ['{}'] |
|
|
|
[host_machine] |
|
system = 'linux' |
|
cpu_family = 'arm' |
|
cpu = 'armv7' |
|
endian = 'little' |
|
'''.format(os.path.join(testdir, 'cross_pkgconfig')))) |
|
crossfile.flush() |
|
self.meson_cross_file = crossfile.name |
|
|
|
env = {'PKG_CONFIG_LIBDIR': os.path.join(testdir, |
|
'native_pkgconfig')} |
|
self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env) |
|
self.wipe() |
|
self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env) |
|
|
|
def __reconfigure(self, change_minor=False): |
|
# Set an older version to force a reconfigure from scratch |
|
filename = os.path.join(self.privatedir, 'coredata.dat') |
|
with open(filename, 'rb') as f: |
|
obj = pickle.load(f) |
|
if change_minor: |
|
v = mesonbuild.coredata.version.split('.') |
|
obj.version = '.'.join(v[0:2] + [str(int(v[2]) + 1)]) |
|
else: |
|
obj.version = '0.47.0' |
|
with open(filename, 'wb') as f: |
|
pickle.dump(obj, f) |
|
|
|
def test_reconfigure(self): |
|
testdir = os.path.join(self.unit_test_dir, '48 reconfigure') |
|
self.init(testdir, extra_args=['-Dopt1=val1', '-Dsub1:werror=true']) |
|
self.setconf('-Dopt2=val2') |
|
|
|
self.__reconfigure() |
|
|
|
out = self.init(testdir, extra_args=['--reconfigure', '-Dopt3=val3']) |
|
self.assertRegex(out, 'Regenerating configuration from scratch') |
|
self.assertRegex(out, 'opt1 val1') |
|
self.assertRegex(out, 'opt2 val2') |
|
self.assertRegex(out, 'opt3 val3') |
|
self.assertRegex(out, 'opt4 default4') |
|
self.assertRegex(out, 'sub1:werror True') |
|
self.build() |
|
self.run_tests() |
|
|
|
# Create a file in builddir and verify wipe command removes it |
|
filename = os.path.join(self.builddir, 'something') |
|
open(filename, 'w').close() |
|
self.assertTrue(os.path.exists(filename)) |
|
out = self.init(testdir, extra_args=['--wipe', '-Dopt4=val4']) |
|
self.assertFalse(os.path.exists(filename)) |
|
self.assertRegex(out, 'opt1 val1') |
|
self.assertRegex(out, 'opt2 val2') |
|
self.assertRegex(out, 'opt3 val3') |
|
self.assertRegex(out, 'opt4 val4') |
|
self.assertRegex(out, 'sub1:werror True') |
|
self.assertTrue(Path(self.builddir, '.gitignore').exists()) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_wipe_from_builddir(self): |
|
testdir = os.path.join(self.common_test_dir, '157 custom target subdir depend files') |
|
self.init(testdir) |
|
self.__reconfigure() |
|
|
|
with Path(self.builddir): |
|
self.init(testdir, extra_args=['--wipe']) |
|
|
|
def test_minor_version_does_not_reconfigure_wipe(self): |
|
testdir = os.path.join(self.unit_test_dir, '48 reconfigure') |
|
self.init(testdir, extra_args=['-Dopt1=val1']) |
|
self.setconf('-Dopt2=val2') |
|
|
|
self.__reconfigure(change_minor=True) |
|
|
|
out = self.init(testdir, extra_args=['--reconfigure', '-Dopt3=val3']) |
|
self.assertNotRegex(out, 'Regenerating configuration from scratch') |
|
self.assertRegex(out, 'opt1 val1') |
|
self.assertRegex(out, 'opt2 val2') |
|
self.assertRegex(out, 'opt3 val3') |
|
self.assertRegex(out, 'opt4 default4') |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_target_construct_id_from_path(self): |
|
# This id is stable but not guessable. |
|
# The test is supposed to prevent unintentional |
|
# changes of target ID generation. |
|
target_id = Target.construct_id_from_path('some/obscure/subdir', |
|
'target-id', '@suffix') |
|
self.assertEqual('5e002d3@@target-id@suffix', target_id) |
|
target_id = Target.construct_id_from_path('subproject/foo/subdir/bar', |
|
'target2-id', '@other') |
|
self.assertEqual('81d46d1@@target2-id@other', target_id) |
|
|
|
def test_introspect_projectinfo_without_configured_build(self): |
|
testfile = os.path.join(self.common_test_dir, '33 run program', 'meson.build') |
|
res = self.introspect_directory(testfile, '--projectinfo') |
|
self.assertEqual(set(res['buildsystem_files']), {'meson.build'}) |
|
self.assertEqual(res['version'], 'undefined') |
|
self.assertEqual(res['descriptive_name'], 'run command') |
|
self.assertEqual(res['subprojects'], []) |
|
|
|
testfile = os.path.join(self.common_test_dir, '40 options', 'meson.build') |
|
res = self.introspect_directory(testfile, '--projectinfo') |
|
self.assertEqual(set(res['buildsystem_files']), {'meson_options.txt', 'meson.build'}) |
|
self.assertEqual(res['version'], 'undefined') |
|
self.assertEqual(res['descriptive_name'], 'options') |
|
self.assertEqual(res['subprojects'], []) |
|
|
|
testfile = os.path.join(self.common_test_dir, '43 subproject options', 'meson.build') |
|
res = self.introspect_directory(testfile, '--projectinfo') |
|
self.assertEqual(set(res['buildsystem_files']), {'meson_options.txt', 'meson.build'}) |
|
self.assertEqual(res['version'], 'undefined') |
|
self.assertEqual(res['descriptive_name'], 'suboptions') |
|
self.assertEqual(len(res['subprojects']), 1) |
|
subproject_files = {f.replace('\\', '/') for f in res['subprojects'][0]['buildsystem_files']} |
|
self.assertEqual(subproject_files, {'subprojects/subproject/meson_options.txt', 'subprojects/subproject/meson.build'}) |
|
self.assertEqual(res['subprojects'][0]['name'], 'subproject') |
|
self.assertEqual(res['subprojects'][0]['version'], 'undefined') |
|
self.assertEqual(res['subprojects'][0]['descriptive_name'], 'subproject') |
|
|
|
def test_introspect_projectinfo_subprojects(self): |
|
testdir = os.path.join(self.common_test_dir, '98 subproject subdir') |
|
self.init(testdir) |
|
res = self.introspect('--projectinfo') |
|
expected = { |
|
'descriptive_name': 'proj', |
|
'version': 'undefined', |
|
'subproject_dir': 'subprojects', |
|
'subprojects': [ |
|
{ |
|
'descriptive_name': 'sub', |
|
'name': 'sub', |
|
'version': '1.0' |
|
}, |
|
{ |
|
'descriptive_name': 'sub_implicit', |
|
'name': 'sub_implicit', |
|
'version': '1.0', |
|
}, |
|
{ |
|
'descriptive_name': 'sub-novar', |
|
'name': 'sub_novar', |
|
'version': '1.0', |
|
}, |
|
{ |
|
'descriptive_name': 'subsub', |
|
'name': 'subsub', |
|
'version': 'undefined' |
|
}, |
|
{ |
|
'descriptive_name': 'subsubsub', |
|
'name': 'subsubsub', |
|
'version': 'undefined' |
|
}, |
|
] |
|
} |
|
res['subprojects'] = sorted(res['subprojects'], key=lambda i: i['name']) |
|
self.assertDictEqual(expected, res) |
|
|
|
def test_introspection_target_subproject(self): |
|
testdir = os.path.join(self.common_test_dir, '42 subproject') |
|
self.init(testdir) |
|
res = self.introspect('--targets') |
|
|
|
expected = { |
|
'sublib': 'sublib', |
|
'simpletest': 'sublib', |
|
'user': None |
|
} |
|
|
|
for entry in res: |
|
name = entry['name'] |
|
self.assertEqual(entry['subproject'], expected[name]) |
|
|
|
def test_introspect_projectinfo_subproject_dir(self): |
|
testdir = os.path.join(self.common_test_dir, '75 custom subproject dir') |
|
self.init(testdir) |
|
res = self.introspect('--projectinfo') |
|
|
|
self.assertEqual(res['subproject_dir'], 'custom_subproject_dir') |
|
|
|
def test_introspect_projectinfo_subproject_dir_from_source(self): |
|
testfile = os.path.join(self.common_test_dir, '75 custom subproject dir', 'meson.build') |
|
res = self.introspect_directory(testfile, '--projectinfo') |
|
|
|
self.assertEqual(res['subproject_dir'], 'custom_subproject_dir') |
|
|
|
@skipIfNoExecutable('clang-format') |
|
def test_clang_format(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'Clang-format is for now only supported on Ninja, not {self.backend.name}') |
|
testdir = os.path.join(self.unit_test_dir, '54 clang-format') |
|
testfile = os.path.join(testdir, 'prog.c') |
|
badfile = os.path.join(testdir, 'prog_orig_c') |
|
goodfile = os.path.join(testdir, 'prog_expected_c') |
|
testheader = os.path.join(testdir, 'header.h') |
|
badheader = os.path.join(testdir, 'header_orig_h') |
|
goodheader = os.path.join(testdir, 'header_expected_h') |
|
try: |
|
shutil.copyfile(badfile, testfile) |
|
shutil.copyfile(badheader, testheader) |
|
self.init(testdir) |
|
self.assertNotEqual(Path(testfile).read_text(), |
|
Path(goodfile).read_text()) |
|
self.assertNotEqual(Path(testheader).read_text(), |
|
Path(goodheader).read_text()) |
|
self.run_target('clang-format') |
|
self.assertEqual(Path(testheader).read_text(), |
|
Path(goodheader).read_text()) |
|
finally: |
|
if os.path.exists(testfile): |
|
os.unlink(testfile) |
|
if os.path.exists(testheader): |
|
os.unlink(testheader) |
|
|
|
@skipIfNoExecutable('clang-tidy') |
|
def test_clang_tidy(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'Clang-tidy is for now only supported on Ninja, not {self.backend.name}') |
|
if shutil.which('c++') is None: |
|
raise unittest.SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.') |
|
if is_osx(): |
|
raise unittest.SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.') |
|
testdir = os.path.join(self.unit_test_dir, '69 clang-tidy') |
|
dummydir = os.path.join(testdir, 'dummydir.h') |
|
self.init(testdir, override_envvars={'CXX': 'c++'}) |
|
out = self.run_target('clang-tidy') |
|
self.assertIn('cttest.cpp:4:20', out) |
|
self.assertNotIn(dummydir, out) |
|
|
|
def test_identity_cross(self): |
|
testdir = os.path.join(self.unit_test_dir, '70 cross') |
|
# Do a build to generate a cross file where the host is this target |
|
self.init(testdir, extra_args=['-Dgenerate=true']) |
|
self.meson_cross_file = os.path.join(self.builddir, "crossfile") |
|
self.assertTrue(os.path.exists(self.meson_cross_file)) |
|
# Now verify that this is detected as cross |
|
self.new_builddir() |
|
self.init(testdir) |
|
|
|
def test_introspect_buildoptions_without_configured_build(self): |
|
testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions') |
|
testfile = os.path.join(testdir, 'meson.build') |
|
res_nb = self.introspect_directory(testfile, ['--buildoptions'] + self.meson_args) |
|
self.init(testdir, default_args=False) |
|
res_wb = self.introspect('--buildoptions') |
|
self.maxDiff = None |
|
# XXX: These now generate in a different order, is that okay? |
|
self.assertListEqual(sorted(res_nb, key=lambda x: x['name']), sorted(res_wb, key=lambda x: x['name'])) |
|
|
|
def test_meson_configure_from_source_does_not_crash(self): |
|
testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions') |
|
self._run(self.mconf_command + [testdir]) |
|
|
|
def test_introspect_buildoptions_cross_only(self): |
|
testdir = os.path.join(self.unit_test_dir, '83 cross only introspect') |
|
testfile = os.path.join(testdir, 'meson.build') |
|
res = self.introspect_directory(testfile, ['--buildoptions'] + self.meson_args) |
|
optnames = [o['name'] for o in res] |
|
self.assertIn('c_args', optnames) |
|
self.assertNotIn('build.c_args', optnames) |
|
|
|
def test_introspect_json_flat(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
out = self.init(testdir, extra_args=['-Dlayout=flat']) |
|
infodir = os.path.join(self.builddir, 'meson-info') |
|
self.assertPathExists(infodir) |
|
|
|
with open(os.path.join(infodir, 'intro-targets.json')) as fp: |
|
targets = json.load(fp) |
|
|
|
for i in targets: |
|
for out in i['filename']: |
|
assert(os.path.relpath(out, self.builddir).startswith('meson-out')) |
|
|
|
def test_introspect_json_dump(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
self.init(testdir) |
|
infodir = os.path.join(self.builddir, 'meson-info') |
|
self.assertPathExists(infodir) |
|
|
|
def assertKeyTypes(key_type_list, obj, strict: bool = True): |
|
for i in key_type_list: |
|
if isinstance(i[1], (list, tuple)) and None in i[1]: |
|
i = (i[0], tuple([x for x in i[1] if x is not None])) |
|
if i[0] not in obj or obj[i[0]] is None: |
|
continue |
|
self.assertIn(i[0], obj) |
|
self.assertIsInstance(obj[i[0]], i[1]) |
|
if strict: |
|
for k in obj.keys(): |
|
found = False |
|
for i in key_type_list: |
|
if k == i[0]: |
|
found = True |
|
break |
|
self.assertTrue(found, f'Key "{k}" not in expected list') |
|
|
|
root_keylist = [ |
|
('benchmarks', list), |
|
('buildoptions', list), |
|
('buildsystem_files', list), |
|
('dependencies', list), |
|
('installed', dict), |
|
('projectinfo', dict), |
|
('targets', list), |
|
('tests', list), |
|
] |
|
|
|
test_keylist = [ |
|
('cmd', list), |
|
('env', dict), |
|
('name', str), |
|
('timeout', int), |
|
('suite', list), |
|
('is_parallel', bool), |
|
('protocol', str), |
|
('depends', list), |
|
('workdir', (str, None)), |
|
('priority', int), |
|
] |
|
|
|
buildoptions_keylist = [ |
|
('name', str), |
|
('section', str), |
|
('type', str), |
|
('description', str), |
|
('machine', str), |
|
('choices', (list, None)), |
|
('value', (str, int, bool, list)), |
|
] |
|
|
|
buildoptions_typelist = [ |
|
('combo', str, [('choices', list)]), |
|
('string', str, []), |
|
('boolean', bool, []), |
|
('integer', int, []), |
|
('array', list, []), |
|
] |
|
|
|
buildoptions_sections = ['core', 'backend', 'base', 'compiler', 'directory', 'user', 'test'] |
|
buildoptions_machines = ['any', 'build', 'host'] |
|
|
|
dependencies_typelist = [ |
|
('name', str), |
|
('version', str), |
|
('compile_args', list), |
|
('link_args', list), |
|
] |
|
|
|
targets_typelist = [ |
|
('name', str), |
|
('id', str), |
|
('type', str), |
|
('defined_in', str), |
|
('filename', list), |
|
('build_by_default', bool), |
|
('target_sources', list), |
|
('extra_files', list), |
|
('subproject', (str, None)), |
|
('install_filename', (list, None)), |
|
('installed', bool), |
|
] |
|
|
|
targets_sources_typelist = [ |
|
('language', str), |
|
('compiler', list), |
|
('parameters', list), |
|
('sources', list), |
|
('generated_sources', list), |
|
] |
|
|
|
# First load all files |
|
res = {} |
|
for i in root_keylist: |
|
curr = os.path.join(infodir, 'intro-{}.json'.format(i[0])) |
|
self.assertPathExists(curr) |
|
with open(curr) as fp: |
|
res[i[0]] = json.load(fp) |
|
|
|
assertKeyTypes(root_keylist, res) |
|
|
|
# Match target ids to input and output files for ease of reference |
|
src_to_id = {} |
|
out_to_id = {} |
|
name_to_out = {} |
|
for i in res['targets']: |
|
print(json.dump(i, sys.stdout)) |
|
out_to_id.update({os.path.relpath(out, self.builddir): i['id'] |
|
for out in i['filename']}) |
|
name_to_out.update({i['name']: i['filename']}) |
|
for group in i['target_sources']: |
|
src_to_id.update({os.path.relpath(src, testdir): i['id'] |
|
for src in group['sources']}) |
|
|
|
# Check Tests and benchmarks |
|
tests_to_find = ['test case 1', 'test case 2', 'benchmark 1'] |
|
deps_to_find = {'test case 1': [src_to_id['t1.cpp']], |
|
'test case 2': [src_to_id['t2.cpp'], src_to_id['t3.cpp']], |
|
'benchmark 1': [out_to_id['file2'], out_to_id['file3'], out_to_id['file4'], src_to_id['t3.cpp']]} |
|
for i in res['benchmarks'] + res['tests']: |
|
assertKeyTypes(test_keylist, i) |
|
if i['name'] in tests_to_find: |
|
tests_to_find.remove(i['name']) |
|
self.assertEqual(sorted(i['depends']), |
|
sorted(deps_to_find[i['name']])) |
|
self.assertListEqual(tests_to_find, []) |
|
|
|
# Check buildoptions |
|
buildopts_to_find = {'cpp_std': 'c++11'} |
|
for i in res['buildoptions']: |
|
assertKeyTypes(buildoptions_keylist, i) |
|
valid_type = False |
|
for j in buildoptions_typelist: |
|
if i['type'] == j[0]: |
|
self.assertIsInstance(i['value'], j[1]) |
|
assertKeyTypes(j[2], i, strict=False) |
|
valid_type = True |
|
break |
|
|
|
self.assertIn(i['section'], buildoptions_sections) |
|
self.assertIn(i['machine'], buildoptions_machines) |
|
self.assertTrue(valid_type) |
|
if i['name'] in buildopts_to_find: |
|
self.assertEqual(i['value'], buildopts_to_find[i['name']]) |
|
buildopts_to_find.pop(i['name'], None) |
|
self.assertDictEqual(buildopts_to_find, {}) |
|
|
|
# Check buildsystem_files |
|
bs_files = ['meson.build', 'meson_options.txt', 'sharedlib/meson.build', 'staticlib/meson.build'] |
|
bs_files = [os.path.join(testdir, x) for x in bs_files] |
|
self.assertPathListEqual(list(sorted(res['buildsystem_files'])), list(sorted(bs_files))) |
|
|
|
# Check dependencies |
|
dependencies_to_find = ['threads'] |
|
for i in res['dependencies']: |
|
assertKeyTypes(dependencies_typelist, i) |
|
if i['name'] in dependencies_to_find: |
|
dependencies_to_find.remove(i['name']) |
|
self.assertListEqual(dependencies_to_find, []) |
|
|
|
# Check projectinfo |
|
self.assertDictEqual(res['projectinfo'], {'version': '1.2.3', 'descriptive_name': 'introspection', 'subproject_dir': 'subprojects', 'subprojects': []}) |
|
|
|
# Check targets |
|
targets_to_find = { |
|
'sharedTestLib': ('shared library', True, False, 'sharedlib/meson.build', |
|
[os.path.join(testdir, 'sharedlib', 'shared.cpp')]), |
|
'staticTestLib': ('static library', True, False, 'staticlib/meson.build', |
|
[os.path.join(testdir, 'staticlib', 'static.c')]), |
|
'custom target test 1': ('custom', False, False, 'meson.build', |
|
[os.path.join(testdir, 'cp.py')]), |
|
'custom target test 2': ('custom', False, False, 'meson.build', |
|
name_to_out['custom target test 1']), |
|
'test1': ('executable', True, True, 'meson.build', |
|
[os.path.join(testdir, 't1.cpp')]), |
|
'test2': ('executable', True, False, 'meson.build', |
|
[os.path.join(testdir, 't2.cpp')]), |
|
'test3': ('executable', True, False, 'meson.build', |
|
[os.path.join(testdir, 't3.cpp')]), |
|
'custom target test 3': ('custom', False, False, 'meson.build', |
|
name_to_out['test3']), |
|
} |
|
for i in res['targets']: |
|
assertKeyTypes(targets_typelist, i) |
|
if i['name'] in targets_to_find: |
|
tgt = targets_to_find[i['name']] |
|
self.assertEqual(i['type'], tgt[0]) |
|
self.assertEqual(i['build_by_default'], tgt[1]) |
|
self.assertEqual(i['installed'], tgt[2]) |
|
self.assertPathEqual(i['defined_in'], os.path.join(testdir, tgt[3])) |
|
targets_to_find.pop(i['name'], None) |
|
for j in i['target_sources']: |
|
assertKeyTypes(targets_sources_typelist, j) |
|
self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]]) |
|
self.assertDictEqual(targets_to_find, {}) |
|
|
|
def test_introspect_file_dump_equals_all(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
self.init(testdir) |
|
res_all = self.introspect('--all') |
|
res_file = {} |
|
|
|
root_keylist = [ |
|
'benchmarks', |
|
'buildoptions', |
|
'buildsystem_files', |
|
'dependencies', |
|
'installed', |
|
'projectinfo', |
|
'targets', |
|
'tests', |
|
] |
|
|
|
infodir = os.path.join(self.builddir, 'meson-info') |
|
self.assertPathExists(infodir) |
|
for i in root_keylist: |
|
curr = os.path.join(infodir, f'intro-{i}.json') |
|
self.assertPathExists(curr) |
|
with open(curr) as fp: |
|
res_file[i] = json.load(fp) |
|
|
|
self.assertEqual(res_all, res_file) |
|
|
|
def test_introspect_meson_info(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'meson-info.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile) as fp: |
|
res1 = json.load(fp) |
|
|
|
for i in ['meson_version', 'directories', 'introspection', 'build_files_updated', 'error']: |
|
self.assertIn(i, res1) |
|
|
|
self.assertEqual(res1['error'], False) |
|
self.assertEqual(res1['build_files_updated'], True) |
|
|
|
def test_introspect_config_update(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'intro-buildoptions.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile) as fp: |
|
res1 = json.load(fp) |
|
|
|
for i in res1: |
|
if i['name'] == 'cpp_std': |
|
i['value'] = 'c++14' |
|
if i['name'] == 'build.cpp_std': |
|
i['value'] = 'c++14' |
|
if i['name'] == 'buildtype': |
|
i['value'] = 'release' |
|
if i['name'] == 'optimization': |
|
i['value'] = '3' |
|
if i['name'] == 'debug': |
|
i['value'] = False |
|
|
|
self.setconf('-Dcpp_std=c++14') |
|
self.setconf('-Dbuildtype=release') |
|
|
|
with open(introfile) as fp: |
|
res2 = json.load(fp) |
|
|
|
self.assertListEqual(res1, res2) |
|
|
|
def test_introspect_targets_from_source(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
testfile = os.path.join(testdir, 'meson.build') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'intro-targets.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile) as fp: |
|
res_wb = json.load(fp) |
|
|
|
res_nb = self.introspect_directory(testfile, ['--targets'] + self.meson_args) |
|
|
|
# Account for differences in output |
|
res_wb = [i for i in res_wb if i['type'] != 'custom'] |
|
for i in res_wb: |
|
i['filename'] = [os.path.relpath(x, self.builddir) for x in i['filename']] |
|
if 'install_filename' in i: |
|
del i['install_filename'] |
|
|
|
sources = [] |
|
for j in i['target_sources']: |
|
sources += j['sources'] |
|
i['target_sources'] = [{ |
|
'language': 'unknown', |
|
'compiler': [], |
|
'parameters': [], |
|
'sources': sources, |
|
'generated_sources': [] |
|
}] |
|
|
|
self.maxDiff = None |
|
self.assertListEqual(res_nb, res_wb) |
|
|
|
def test_introspect_ast_source(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
testfile = os.path.join(testdir, 'meson.build') |
|
res_nb = self.introspect_directory(testfile, ['--ast'] + self.meson_args) |
|
|
|
node_counter = {} |
|
|
|
def accept_node(json_node): |
|
self.assertIsInstance(json_node, dict) |
|
for i in ['lineno', 'colno', 'end_lineno', 'end_colno']: |
|
self.assertIn(i, json_node) |
|
self.assertIsInstance(json_node[i], int) |
|
self.assertIn('node', json_node) |
|
n = json_node['node'] |
|
self.assertIsInstance(n, str) |
|
self.assertIn(n, nodes) |
|
if n not in node_counter: |
|
node_counter[n] = 0 |
|
node_counter[n] = node_counter[n] + 1 |
|
for nodeDesc in nodes[n]: |
|
key = nodeDesc[0] |
|
func = nodeDesc[1] |
|
self.assertIn(key, json_node) |
|
if func is None: |
|
tp = nodeDesc[2] |
|
self.assertIsInstance(json_node[key], tp) |
|
continue |
|
func(json_node[key]) |
|
|
|
def accept_node_list(node_list): |
|
self.assertIsInstance(node_list, list) |
|
for i in node_list: |
|
accept_node(i) |
|
|
|
def accept_kwargs(kwargs): |
|
self.assertIsInstance(kwargs, list) |
|
for i in kwargs: |
|
self.assertIn('key', i) |
|
self.assertIn('val', i) |
|
accept_node(i['key']) |
|
accept_node(i['val']) |
|
|
|
nodes = { |
|
'BooleanNode': [('value', None, bool)], |
|
'IdNode': [('value', None, str)], |
|
'NumberNode': [('value', None, int)], |
|
'StringNode': [('value', None, str)], |
|
'ContinueNode': [], |
|
'BreakNode': [], |
|
'ArgumentNode': [('positional', accept_node_list), ('kwargs', accept_kwargs)], |
|
'ArrayNode': [('args', accept_node)], |
|
'DictNode': [('args', accept_node)], |
|
'EmptyNode': [], |
|
'OrNode': [('left', accept_node), ('right', accept_node)], |
|
'AndNode': [('left', accept_node), ('right', accept_node)], |
|
'ComparisonNode': [('left', accept_node), ('right', accept_node), ('ctype', None, str)], |
|
'ArithmeticNode': [('left', accept_node), ('right', accept_node), ('op', None, str)], |
|
'NotNode': [('right', accept_node)], |
|
'CodeBlockNode': [('lines', accept_node_list)], |
|
'IndexNode': [('object', accept_node), ('index', accept_node)], |
|
'MethodNode': [('object', accept_node), ('args', accept_node), ('name', None, str)], |
|
'FunctionNode': [('args', accept_node), ('name', None, str)], |
|
'AssignmentNode': [('value', accept_node), ('var_name', None, str)], |
|
'PlusAssignmentNode': [('value', accept_node), ('var_name', None, str)], |
|
'ForeachClauseNode': [('items', accept_node), ('block', accept_node), ('varnames', None, list)], |
|
'IfClauseNode': [('ifs', accept_node_list), ('else', accept_node)], |
|
'IfNode': [('condition', accept_node), ('block', accept_node)], |
|
'UMinusNode': [('right', accept_node)], |
|
'TernaryNode': [('condition', accept_node), ('true', accept_node), ('false', accept_node)], |
|
} |
|
|
|
accept_node(res_nb) |
|
|
|
for n, c in [('ContinueNode', 2), ('BreakNode', 1), ('NotNode', 3)]: |
|
self.assertIn(n, node_counter) |
|
self.assertEqual(node_counter[n], c) |
|
|
|
def test_introspect_dependencies_from_source(self): |
|
testdir = os.path.join(self.unit_test_dir, '57 introspection') |
|
testfile = os.path.join(testdir, 'meson.build') |
|
res_nb = self.introspect_directory(testfile, ['--scan-dependencies'] + self.meson_args) |
|
expected = [ |
|
{ |
|
'name': 'threads', |
|
'required': True, |
|
'version': [], |
|
'has_fallback': False, |
|
'conditional': False |
|
}, |
|
{ |
|
'name': 'zlib', |
|
'required': False, |
|
'version': [], |
|
'has_fallback': False, |
|
'conditional': False |
|
}, |
|
{ |
|
'name': 'bugDep1', |
|
'required': True, |
|
'version': [], |
|
'has_fallback': False, |
|
'conditional': False |
|
}, |
|
{ |
|
'name': 'somethingthatdoesnotexist', |
|
'required': True, |
|
'version': ['>=1.2.3'], |
|
'has_fallback': False, |
|
'conditional': True |
|
}, |
|
{ |
|
'name': 'look_i_have_a_fallback', |
|
'required': True, |
|
'version': ['>=1.0.0', '<=99.9.9'], |
|
'has_fallback': True, |
|
'conditional': True |
|
} |
|
] |
|
self.maxDiff = None |
|
self.assertListEqual(res_nb, expected) |
|
|
|
def test_unstable_coredata(self): |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
self.init(testdir) |
|
# just test that the command does not fail (e.g. because it throws an exception) |
|
self._run([*self.meson_command, 'unstable-coredata', self.builddir]) |
|
|
|
@skip_if_no_cmake |
|
def test_cmake_prefix_path(self): |
|
testdir = os.path.join(self.unit_test_dir, '63 cmake_prefix_path') |
|
self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')]) |
|
|
|
@skip_if_no_cmake |
|
def test_cmake_parser(self): |
|
testdir = os.path.join(self.unit_test_dir, '64 cmake parser') |
|
self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')]) |
|
|
|
def test_alias_target(self): |
|
if self.backend is Backend.vs: |
|
# FIXME: This unit test is broken with vs backend, needs investigation |
|
raise unittest.SkipTest(f'Skipping alias_target test with {self.backend.name} backend') |
|
testdir = os.path.join(self.unit_test_dir, '65 alias target') |
|
self.init(testdir) |
|
self.build() |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, 'prog' + exe_suffix)) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, 'hello.txt')) |
|
self.run_target('build-all') |
|
self.assertPathExists(os.path.join(self.builddir, 'prog' + exe_suffix)) |
|
self.assertPathExists(os.path.join(self.builddir, 'hello.txt')) |
|
|
|
def test_configure(self): |
|
testdir = os.path.join(self.common_test_dir, '2 cpp') |
|
self.init(testdir) |
|
self._run(self.mconf_command + [self.builddir]) |
|
|
|
def test_summary(self): |
|
testdir = os.path.join(self.unit_test_dir, '72 summary') |
|
out = self.init(testdir) |
|
expected = textwrap.dedent(r''' |
|
Some Subproject 2.0 |
|
|
|
string : bar |
|
integer: 1 |
|
boolean: True |
|
|
|
My Project 1.0 |
|
|
|
Configuration |
|
Some boolean : False |
|
Another boolean: True |
|
Some string : Hello World |
|
A list : string |
|
1 |
|
True |
|
empty list : |
|
enabled_opt : enabled |
|
A number : 1 |
|
yes : YES |
|
no : NO |
|
coma list : a, b, c |
|
|
|
Stuff |
|
missing prog : NO |
|
existing prog : ''' + sys.executable + ''' |
|
missing dep : NO |
|
internal dep : YES |
|
|
|
Plugins |
|
long coma list : alpha, alphacolor, apetag, audiofx, audioparsers, auparse, |
|
autodetect, avi |
|
|
|
Subprojects |
|
sub : YES |
|
sub2 : NO Problem encountered: This subproject failed |
|
''') |
|
expected_lines = expected.split('\n')[1:] |
|
out_start = out.find(expected_lines[0]) |
|
out_lines = out[out_start:].split('\n')[:len(expected_lines)] |
|
if sys.version_info < (3, 7, 0): |
|
# Dictionary order is not stable in Python <3.7, so sort the lines |
|
# while comparing |
|
self.assertEqual(sorted(expected_lines), sorted(out_lines)) |
|
else: |
|
self.assertEqual(expected_lines, out_lines) |
|
|
|
def test_meson_compile(self): |
|
"""Test the meson compile command.""" |
|
|
|
def get_exe_name(basename: str) -> str: |
|
if is_windows(): |
|
return f'{basename}.exe' |
|
else: |
|
return basename |
|
|
|
def get_shared_lib_name(basename: str) -> str: |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
return f'lib{basename}.dll' |
|
elif is_windows(): |
|
return f'{basename}.dll' |
|
elif is_cygwin(): |
|
return f'cyg{basename}.dll' |
|
elif is_osx(): |
|
return f'lib{basename}.dylib' |
|
else: |
|
return f'lib{basename}.so' |
|
|
|
def get_static_lib_name(basename: str) -> str: |
|
return f'lib{basename}.a' |
|
|
|
# Base case (no targets or additional arguments) |
|
|
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
self.init(testdir) |
|
|
|
self._run([*self.meson_command, 'compile', '-C', self.builddir]) |
|
self.assertPathExists(os.path.join(self.builddir, get_exe_name('trivialprog'))) |
|
|
|
# `--clean` |
|
|
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--clean']) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog'))) |
|
|
|
# Target specified in a project with unique names |
|
|
|
testdir = os.path.join(self.common_test_dir, '6 linkshared') |
|
self.init(testdir, extra_args=['--wipe']) |
|
# Multiple targets and target type specified |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, 'mylib', 'mycpplib:shared_library']) |
|
# Check that we have a shared lib, but not an executable, i.e. check that target actually worked |
|
self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mylib'))) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('prog'))) |
|
self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mycpplib'))) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('cppprog'))) |
|
|
|
# Target specified in a project with non unique names |
|
|
|
testdir = os.path.join(self.common_test_dir, '185 same target name') |
|
self.init(testdir, extra_args=['--wipe']) |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo']) |
|
self.assertPathExists(os.path.join(self.builddir, get_static_lib_name('foo'))) |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, 'sub/foo']) |
|
self.assertPathExists(os.path.join(self.builddir, 'sub', get_static_lib_name('foo'))) |
|
|
|
# run_target |
|
|
|
testdir = os.path.join(self.common_test_dir, '51 run target') |
|
self.init(testdir, extra_args=['--wipe']) |
|
out = self._run([*self.meson_command, 'compile', '-C', self.builddir, 'py3hi']) |
|
self.assertIn('I am Python3.', out) |
|
|
|
# `--$BACKEND-args` |
|
|
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
if self.backend is Backend.ninja: |
|
self.init(testdir, extra_args=['--wipe']) |
|
# Dry run - should not create a program |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--ninja-args=-n']) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog'))) |
|
elif self.backend is Backend.vs: |
|
self.init(testdir, extra_args=['--wipe']) |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir]) |
|
# Explicitly clean the target through msbuild interface |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--vs-args=-t:{}:Clean'.format(re.sub(r'[\%\$\@\;\.\(\)\']', '_', get_exe_name('trivialprog')))]) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog'))) |
|
|
|
def test_spurious_reconfigure_built_dep_file(self): |
|
testdir = os.path.join(self.unit_test_dir, '74 dep files') |
|
|
|
# Regression test: Spurious reconfigure was happening when build |
|
# directory is inside source directory. |
|
# See https://gitlab.freedesktop.org/gstreamer/gst-build/-/issues/85. |
|
srcdir = os.path.join(self.builddir, 'srctree') |
|
shutil.copytree(testdir, srcdir) |
|
builddir = os.path.join(srcdir, '_build') |
|
self.change_builddir(builddir) |
|
|
|
self.init(srcdir) |
|
self.build() |
|
|
|
# During first configure the file did not exist so no dependency should |
|
# have been set. A rebuild should not trigger a reconfigure. |
|
self.clean() |
|
out = self.build() |
|
self.assertNotIn('Project configured', out) |
|
|
|
self.init(srcdir, extra_args=['--reconfigure']) |
|
|
|
# During the reconfigure the file did exist, but is inside build |
|
# directory, so no dependency should have been set. A rebuild should not |
|
# trigger a reconfigure. |
|
self.clean() |
|
out = self.build() |
|
self.assertNotIn('Project configured', out) |
|
|
|
def _test_junit(self, case: str) -> None: |
|
try: |
|
import lxml.etree as et |
|
except ImportError: |
|
raise unittest.SkipTest('lxml required, but not found.') |
|
|
|
schema = et.XMLSchema(et.parse(str(Path(__file__).parent / 'data' / 'schema.xsd'))) |
|
|
|
self.init(case) |
|
self.run_tests() |
|
|
|
junit = et.parse(str(Path(self.builddir) / 'meson-logs' / 'testlog.junit.xml')) |
|
try: |
|
schema.assertValid(junit) |
|
except et.DocumentInvalid as e: |
|
self.fail(e.error_log) |
|
|
|
def test_junit_valid_tap(self): |
|
self._test_junit(os.path.join(self.common_test_dir, '206 tap tests')) |
|
|
|
def test_junit_valid_exitcode(self): |
|
self._test_junit(os.path.join(self.common_test_dir, '41 test args')) |
|
|
|
def test_junit_valid_gtest(self): |
|
self._test_junit(os.path.join(self.framework_test_dir, '2 gtest')) |
|
|
|
def test_link_language_linker(self): |
|
# TODO: there should be some way to query how we're linking things |
|
# without resorting to reading the ninja.build file |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('This test reads the ninja file') |
|
|
|
testdir = os.path.join(self.common_test_dir, '225 link language') |
|
self.init(testdir) |
|
|
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
with open(build_ninja, encoding='utf-8') as f: |
|
contents = f.read() |
|
|
|
self.assertRegex(contents, r'build main(\.exe)?.*: c_LINKER') |
|
self.assertRegex(contents, r'build (lib|cyg)?mylib.*: c_LINKER') |
|
|
|
def test_commands_documented(self): |
|
''' |
|
Test that all listed meson commands are documented in Commands.md. |
|
''' |
|
|
|
# The docs directory is not in release tarballs. |
|
if not os.path.isdir('docs'): |
|
raise unittest.SkipTest('Doc directory does not exist.') |
|
doc_path = 'docs/markdown/Commands.md' |
|
|
|
md = None |
|
with open(doc_path, encoding='utf-8') as f: |
|
md = f.read() |
|
self.assertIsNotNone(md) |
|
|
|
## Get command sections |
|
|
|
section_pattern = re.compile(r'^### (.+)$', re.MULTILINE) |
|
md_command_section_matches = [i for i in section_pattern.finditer(md)] |
|
md_command_sections = dict() |
|
for i, s in enumerate(md_command_section_matches): |
|
section_end = len(md) if i == len(md_command_section_matches) - 1 else md_command_section_matches[i + 1].start() |
|
md_command_sections[s.group(1)] = (s.start(), section_end) |
|
|
|
## Validate commands |
|
|
|
md_commands = {k for k,v in md_command_sections.items()} |
|
|
|
help_output = self._run(self.meson_command + ['--help']) |
|
help_commands = {c.strip() for c in re.findall(r'usage:(?:.+)?{((?:[a-z]+,*)+?)}', help_output, re.MULTILINE|re.DOTALL)[0].split(',')} |
|
|
|
self.assertEqual(md_commands | {'help'}, help_commands, f'Doc file: `{doc_path}`') |
|
|
|
## Validate that each section has proper placeholders |
|
|
|
def get_data_pattern(command): |
|
return re.compile( |
|
r'{{ ' + command + r'_usage.inc }}[\r\n]' |
|
r'.*?' |
|
r'{{ ' + command + r'_arguments.inc }}[\r\n]', |
|
flags = re.MULTILINE|re.DOTALL) |
|
|
|
for command in md_commands: |
|
m = get_data_pattern(command).search(md, pos=md_command_sections[command][0], endpos=md_command_sections[command][1]) |
|
self.assertIsNotNone(m, f'Command `{command}` is missing placeholders for dynamic data. Doc file: `{doc_path}`') |
|
|
|
def _check_coverage_files(self, types=('text', 'xml', 'html')): |
|
covdir = Path(self.builddir) / 'meson-logs' |
|
files = [] |
|
if 'text' in types: |
|
files.append('coverage.txt') |
|
if 'xml' in types: |
|
files.append('coverage.xml') |
|
if 'html' in types: |
|
files.append('coveragereport/index.html') |
|
for f in files: |
|
self.assertTrue((covdir / f).is_file(), msg=f'{f} is not a file') |
|
|
|
def test_coverage(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise unittest.SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'clang': |
|
if not mesonbuild.environment.detect_llvm_cov(): |
|
raise unittest.SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise unittest.SkipTest('Test only applies to non-MSVC compilers') |
|
self.init(testdir, extra_args=['-Db_coverage=true']) |
|
self.build() |
|
self.run_tests() |
|
self.run_target('coverage') |
|
self._check_coverage_files() |
|
|
|
def test_coverage_complex(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise unittest.SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '105 generatorcustom') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'clang': |
|
if not mesonbuild.environment.detect_llvm_cov(): |
|
raise unittest.SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise unittest.SkipTest('Test only applies to non-MSVC compilers') |
|
self.init(testdir, extra_args=['-Db_coverage=true']) |
|
self.build() |
|
self.run_tests() |
|
self.run_target('coverage') |
|
self._check_coverage_files() |
|
|
|
def test_coverage_html(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise unittest.SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'clang': |
|
if not mesonbuild.environment.detect_llvm_cov(): |
|
raise unittest.SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise unittest.SkipTest('Test only applies to non-MSVC compilers') |
|
self.init(testdir, extra_args=['-Db_coverage=true']) |
|
self.build() |
|
self.run_tests() |
|
self.run_target('coverage-html') |
|
self._check_coverage_files(['html']) |
|
|
|
def test_coverage_text(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise unittest.SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'clang': |
|
if not mesonbuild.environment.detect_llvm_cov(): |
|
raise unittest.SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise unittest.SkipTest('Test only applies to non-MSVC compilers') |
|
self.init(testdir, extra_args=['-Db_coverage=true']) |
|
self.build() |
|
self.run_tests() |
|
self.run_target('coverage-text') |
|
self._check_coverage_files(['text']) |
|
|
|
def test_coverage_xml(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise unittest.SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'clang': |
|
if not mesonbuild.environment.detect_llvm_cov(): |
|
raise unittest.SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise unittest.SkipTest('Test only applies to non-MSVC compilers') |
|
self.init(testdir, extra_args=['-Db_coverage=true']) |
|
self.build() |
|
self.run_tests() |
|
self.run_target('coverage-xml') |
|
self._check_coverage_files(['xml']) |
|
|
|
def test_cross_file_constants(self): |
|
with temp_filename() as crossfile1, temp_filename() as crossfile2: |
|
with open(crossfile1, 'w') as f: |
|
f.write(textwrap.dedent( |
|
''' |
|
[constants] |
|
compiler = 'gcc' |
|
''')) |
|
with open(crossfile2, 'w') as f: |
|
f.write(textwrap.dedent( |
|
''' |
|
[constants] |
|
toolchain = '/toolchain/' |
|
common_flags = ['--sysroot=' + toolchain / 'sysroot'] |
|
|
|
[properties] |
|
c_args = common_flags + ['-DSOMETHING'] |
|
cpp_args = c_args + ['-DSOMETHING_ELSE'] |
|
|
|
[binaries] |
|
c = toolchain / compiler |
|
''')) |
|
|
|
values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2]) |
|
self.assertEqual(values['binaries']['c'], '/toolchain/gcc') |
|
self.assertEqual(values['properties']['c_args'], |
|
['--sysroot=/toolchain/sysroot', '-DSOMETHING']) |
|
self.assertEqual(values['properties']['cpp_args'], |
|
['--sysroot=/toolchain/sysroot', '-DSOMETHING', '-DSOMETHING_ELSE']) |
|
|
|
@unittest.skipIf(is_windows(), 'Directory cleanup fails for some reason') |
|
def test_wrap_git(self): |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
srcdir = os.path.join(tmpdir, 'src') |
|
shutil.copytree(os.path.join(self.unit_test_dir, '81 wrap-git'), srcdir) |
|
upstream = os.path.join(srcdir, 'subprojects', 'wrap_git_upstream') |
|
upstream_uri = Path(upstream).as_uri() |
|
_git_init(upstream) |
|
with open(os.path.join(srcdir, 'subprojects', 'wrap_git.wrap'), 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-git] |
|
url = {} |
|
patch_directory = wrap_git_builddef |
|
revision = master |
|
'''.format(upstream_uri))) |
|
self.init(srcdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_multi_output_custom_target_no_warning(self): |
|
testdir = os.path.join(self.common_test_dir, '228 custom_target source') |
|
|
|
out = self.init(testdir) |
|
self.assertNotRegex(out, 'WARNING:.*Using the first one.') |
|
self.build() |
|
self.run_tests() |
|
|
|
@unittest.skipUnless(is_linux() and (re.search('^i.86$|^x86$|^x64$|^x86_64$|^amd64$', platform.processor()) is not None), |
|
'Requires ASM compiler for x86 or x86_64 platform currently only available on Linux CI runners') |
|
def test_nostdlib(self): |
|
testdir = os.path.join(self.unit_test_dir, '78 nostdlib') |
|
machinefile = os.path.join(self.builddir, 'machine.txt') |
|
with open(machinefile, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[properties] |
|
c_stdlib = 'mylibc' |
|
''')) |
|
|
|
# Test native C stdlib |
|
self.meson_native_file = machinefile |
|
self.init(testdir) |
|
self.build() |
|
|
|
# Test cross C stdlib |
|
self.new_builddir() |
|
self.meson_native_file = None |
|
self.meson_cross_file = machinefile |
|
self.init(testdir) |
|
self.build() |
|
|
|
def test_meson_version_compare(self): |
|
testdir = os.path.join(self.unit_test_dir, '82 meson version compare') |
|
out = self.init(testdir) |
|
self.assertNotRegex(out, r'WARNING') |
|
|
|
def test_wrap_redirect(self): |
|
redirect_wrap = os.path.join(self.builddir, 'redirect.wrap') |
|
real_wrap = os.path.join(self.builddir, 'foo/subprojects/real.wrap') |
|
os.makedirs(os.path.dirname(real_wrap)) |
|
|
|
# Invalid redirect, filename must have .wrap extension |
|
with open(redirect_wrap, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-redirect] |
|
filename = foo/subprojects/real.wrapper |
|
''')) |
|
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be a .wrap file'): |
|
PackageDefinition(redirect_wrap) |
|
|
|
# Invalid redirect, filename cannot be in parent directory |
|
with open(redirect_wrap, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-redirect] |
|
filename = ../real.wrap |
|
''')) |
|
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename cannot contain ".."'): |
|
PackageDefinition(redirect_wrap) |
|
|
|
# Invalid redirect, filename must be in foo/subprojects/real.wrap |
|
with open(redirect_wrap, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-redirect] |
|
filename = foo/real.wrap |
|
''')) |
|
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be in the form foo/subprojects/bar.wrap'): |
|
wrap = PackageDefinition(redirect_wrap) |
|
|
|
# Correct redirect |
|
with open(redirect_wrap, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-redirect] |
|
filename = foo/subprojects/real.wrap |
|
''')) |
|
with open(real_wrap, 'w') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-git] |
|
url = http://invalid |
|
''')) |
|
wrap = PackageDefinition(redirect_wrap) |
|
self.assertEqual(wrap.get('url'), 'http://invalid') |
|
|
|
@skip_if_no_cmake |
|
def test_nested_cmake_rebuild(self) -> None: |
|
# This checks a bug where if a non-meson project is used as a third |
|
# level (or deeper) subproject it doesn't cause a rebuild if the build |
|
# files for that project are changed |
|
testdir = os.path.join(self.unit_test_dir, '85 nested subproject regenerate depends') |
|
cmakefile = Path(testdir) / 'subprojects' / 'sub2' / 'CMakeLists.txt' |
|
self.init(testdir) |
|
self.build() |
|
with cmakefile.open('a') as f: |
|
os.utime(str(cmakefile)) |
|
self.assertReconfiguredBuildIsNoop() |
|
|
|
def test_version_file(self): |
|
srcdir = os.path.join(self.common_test_dir, '2 cpp') |
|
self.init(srcdir) |
|
projinfo = self.introspect('--projectinfo') |
|
self.assertEqual(projinfo['version'], '1.0.0') |
|
|
|
def test_cflags_cppflags(self): |
|
envs = {'CPPFLAGS': '-DCPPFLAG', |
|
'CFLAGS': '-DCFLAG', |
|
'CXXFLAGS': '-DCXXFLAG'} |
|
srcdir = os.path.join(self.unit_test_dir, '89 multiple envvars') |
|
self.init(srcdir, override_envvars=envs) |
|
self.build() |
|
|
|
def test_build_b_options(self) -> None: |
|
# Currently (0.57) these do nothing, but they've always been allowed |
|
srcdir = os.path.join(self.common_test_dir, '2 cpp') |
|
self.init(srcdir, extra_args=['-Dbuild.b_lto=true']) |
|
|
|
def test_install_skip_subprojects(self): |
|
testdir = os.path.join(self.unit_test_dir, '92 install skip subprojects') |
|
self.init(testdir) |
|
self.build() |
|
|
|
main_expected = [ |
|
'', |
|
'share', |
|
'include', |
|
'foo', |
|
'bin', |
|
'share/foo', |
|
'share/foo/foo.dat', |
|
'include/foo.h', |
|
'foo/foofile', |
|
'bin/foo' + exe_suffix, |
|
] |
|
bar_expected = [ |
|
'bar', |
|
'share/foo/bar.dat', |
|
'include/bar.h', |
|
'bin/bar' + exe_suffix, |
|
'bar/barfile' |
|
] |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_argument_syntax() == 'msvc': |
|
main_expected.append('bin/foo.pdb') |
|
bar_expected.append('bin/bar.pdb') |
|
prefix = destdir_join(self.installdir, self.prefix) |
|
main_expected = [Path(prefix, p) for p in main_expected] |
|
bar_expected = [Path(prefix, p) for p in bar_expected] |
|
all_expected = main_expected + bar_expected |
|
|
|
def check_installed_files(extra_args, expected): |
|
args = ['install', '--destdir', self.installdir] + extra_args |
|
self._run(self.meson_command + args, workdir=self.builddir) |
|
all_files = [p for p in Path(self.installdir).rglob('*')] |
|
self.assertEqual(sorted(expected), sorted(all_files)) |
|
windows_proof_rmtree(self.installdir) |
|
|
|
check_installed_files([], all_expected) |
|
check_installed_files(['--skip-subprojects'], main_expected) |
|
check_installed_files(['--skip-subprojects', 'bar'], main_expected) |
|
check_installed_files(['--skip-subprojects', 'another'], all_expected) |
|
|
|
def test_adding_subproject_to_configure_project(self) -> None: |
|
srcdir = os.path.join(self.unit_test_dir, '93 new subproject in configured project') |
|
self.init(srcdir) |
|
self.build() |
|
self.setconf('-Duse-sub=true') |
|
self.build() |
|
|
|
def test_devenv(self): |
|
testdir = os.path.join(self.unit_test_dir, '91 devenv') |
|
self.init(testdir) |
|
self.build() |
|
|
|
cmd = self.meson_command + ['devenv', '-C', self.builddir] |
|
script = os.path.join(testdir, 'test-devenv.py') |
|
app = os.path.join(self.builddir, 'app') |
|
self._run(cmd + python_command + [script]) |
|
self.assertEqual('This is text.', self._run(cmd + [app]).strip()) |
|
|
|
def test_clang_format_check(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'Skipping clang-format tests with {self.backend.name} backend') |
|
if not shutil.which('clang-format'): |
|
raise unittest.SkipTest('clang-format not found') |
|
|
|
testdir = os.path.join(self.unit_test_dir, '94 clangformat') |
|
newdir = os.path.join(self.builddir, 'testdir') |
|
shutil.copytree(testdir, newdir) |
|
self.new_builddir() |
|
self.init(newdir) |
|
|
|
# Should reformat 1 file but not return error |
|
output = self.build('clang-format') |
|
self.assertEqual(1, output.count('File reformatted:')) |
|
|
|
# Reset source tree then try again with clang-format-check, it should |
|
# return an error code this time. |
|
windows_proof_rmtree(newdir) |
|
shutil.copytree(testdir, newdir) |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
output = self.build('clang-format-check') |
|
self.assertEqual(1, output.count('File reformatted:')) |
|
|
|
# The check format should not touch any files. Thus |
|
# running format again has some work to do. |
|
output = self.build('clang-format') |
|
self.assertEqual(1, output.count('File reformatted:')) |
|
self.build('clang-format-check') |
|
|
|
def test_custom_target_implicit_include(self): |
|
testdir = os.path.join(self.unit_test_dir, '95 custominc') |
|
self.init(testdir) |
|
self.build() |
|
compdb = self.get_compdb() |
|
matches = 0 |
|
for c in compdb: |
|
if 'prog.c' in c['file']: |
|
self.assertNotIn('easytogrepfor', c['command']) |
|
matches += 1 |
|
self.assertEqual(matches, 1) |
|
matches = 0 |
|
for c in compdb: |
|
if 'prog2.c' in c['file']: |
|
self.assertIn('easytogrepfor', c['command']) |
|
matches += 1 |
|
self.assertEqual(matches, 1) |
|
|
|
def test_env_flags_to_linker(self) -> None: |
|
# Compilers that act as drivers should add their compiler flags to the |
|
# linker, those that do not shouldn't |
|
with mock.patch.dict(os.environ, {'CFLAGS': '-DCFLAG', 'LDFLAGS': '-flto'}): |
|
env = get_fake_env() |
|
|
|
# Get the compiler so we know which compiler class to mock. |
|
cc = mesonbuild.compilers.detect_compiler_for(env, 'c', MachineChoice.HOST) |
|
cc_type = type(cc) |
|
|
|
# Test a compiler that acts as a linker |
|
with mock.patch.object(cc_type, 'INVOKES_LINKER', True): |
|
cc = mesonbuild.compilers.detect_compiler_for(env, 'c', MachineChoice.HOST) |
|
link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) |
|
self.assertEqual(sorted(link_args), sorted(['-DCFLAG', '-flto'])) |
|
|
|
# And one that doesn't |
|
with mock.patch.object(cc_type, 'INVOKES_LINKER', False): |
|
cc = mesonbuild.compilers.detect_compiler_for(env, 'c', MachineChoice.HOST) |
|
link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) |
|
self.assertEqual(sorted(link_args), sorted(['-flto'])) |
|
|
|
class FailureTests(BasePlatformTests): |
|
''' |
|
Tests that test failure conditions. Build files here should be dynamically |
|
generated and static tests should go into `test cases/failing*`. |
|
This is useful because there can be many ways in which a particular |
|
function can fail, and creating failing tests for all of them is tedious |
|
and slows down testing. |
|
''' |
|
dnf = "[Dd]ependency.*not found(:.*)?" |
|
nopkg = '[Pp]kg-config.*not found' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.srcdir = os.path.realpath(tempfile.mkdtemp()) |
|
self.mbuild = os.path.join(self.srcdir, 'meson.build') |
|
self.moptions = os.path.join(self.srcdir, 'meson_options.txt') |
|
|
|
def tearDown(self): |
|
super().tearDown() |
|
windows_proof_rmtree(self.srcdir) |
|
|
|
def assertMesonRaises(self, contents, match, *, |
|
extra_args=None, |
|
langs=None, |
|
meson_version=None, |
|
options=None, |
|
override_envvars=None): |
|
''' |
|
Assert that running meson configure on the specified @contents raises |
|
a error message matching regex @match. |
|
''' |
|
if langs is None: |
|
langs = [] |
|
with open(self.mbuild, 'w') as f: |
|
f.write("project('failure test', 'c', 'cpp'") |
|
if meson_version: |
|
f.write(f", meson_version: '{meson_version}'") |
|
f.write(")\n") |
|
for lang in langs: |
|
f.write(f"add_languages('{lang}', required : false)\n") |
|
f.write(contents) |
|
if options is not None: |
|
with open(self.moptions, 'w') as f: |
|
f.write(options) |
|
o = {'MESON_FORCE_BACKTRACE': '1'} |
|
if override_envvars is None: |
|
override_envvars = o |
|
else: |
|
override_envvars.update(o) |
|
# Force tracebacks so we can detect them properly |
|
with self.assertRaisesRegex(MesonException, match, msg=contents): |
|
# Must run in-process or we'll get a generic CalledProcessError |
|
self.init(self.srcdir, extra_args=extra_args, |
|
inprocess=True, |
|
override_envvars = override_envvars) |
|
|
|
def obtainMesonOutput(self, contents, match, extra_args, langs, meson_version=None): |
|
if langs is None: |
|
langs = [] |
|
with open(self.mbuild, 'w') as f: |
|
f.write("project('output test', 'c', 'cpp'") |
|
if meson_version: |
|
f.write(f", meson_version: '{meson_version}'") |
|
f.write(")\n") |
|
for lang in langs: |
|
f.write(f"add_languages('{lang}', required : false)\n") |
|
f.write(contents) |
|
# Run in-process for speed and consistency with assertMesonRaises |
|
return self.init(self.srcdir, extra_args=extra_args, inprocess=True) |
|
|
|
def assertMesonOutputs(self, contents, match, extra_args=None, langs=None, meson_version=None): |
|
''' |
|
Assert that running meson configure on the specified @contents outputs |
|
something that matches regex @match. |
|
''' |
|
out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version) |
|
self.assertRegex(out, match) |
|
|
|
def assertMesonDoesNotOutput(self, contents, match, extra_args=None, langs=None, meson_version=None): |
|
''' |
|
Assert that running meson configure on the specified @contents does not output |
|
something that matches regex @match. |
|
''' |
|
out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version) |
|
self.assertNotRegex(out, match) |
|
|
|
@skipIfNoPkgconfig |
|
def test_dependency(self): |
|
if subprocess.call(['pkg-config', '--exists', 'zlib']) != 0: |
|
raise unittest.SkipTest('zlib not found with pkg-config') |
|
a = (("dependency('zlib', method : 'fail')", "'fail' is invalid"), |
|
("dependency('zlib', static : '1')", "[Ss]tatic.*boolean"), |
|
("dependency('zlib', version : 1)", "Item must be a list or one of <class 'str'>"), |
|
("dependency('zlib', required : 1)", "[Rr]equired.*boolean"), |
|
("dependency('zlib', method : 1)", "[Mm]ethod.*string"), |
|
("dependency('zlibfail')", self.dnf),) |
|
for contents, match in a: |
|
self.assertMesonRaises(contents, match) |
|
|
|
def test_apple_frameworks_dependency(self): |
|
if not is_osx(): |
|
raise unittest.SkipTest('only run on macOS') |
|
self.assertMesonRaises("dependency('appleframeworks')", |
|
"requires at least one module") |
|
|
|
def test_extraframework_dependency_method(self): |
|
code = "dependency('python', method : 'extraframework')" |
|
if not is_osx(): |
|
self.assertMesonRaises(code, self.dnf) |
|
else: |
|
# Python2 framework is always available on macOS |
|
self.assertMesonOutputs(code, '[Dd]ependency.*python.*found.*YES') |
|
|
|
def test_sdl2_notfound_dependency(self): |
|
# Want to test failure, so skip if available |
|
if shutil.which('sdl2-config'): |
|
raise unittest.SkipTest('sdl2-config found') |
|
self.assertMesonRaises("dependency('sdl2', method : 'sdlconfig')", self.dnf) |
|
if shutil.which('pkg-config'): |
|
self.assertMesonRaises("dependency('sdl2', method : 'pkg-config')", self.dnf) |
|
with no_pkgconfig(): |
|
# Look for pkg-config, cache it, then |
|
# Use cached pkg-config without erroring out, then |
|
# Use cached pkg-config to error out |
|
code = "dependency('foobarrr', method : 'pkg-config', required : false)\n" \ |
|
"dependency('foobarrr2', method : 'pkg-config', required : false)\n" \ |
|
"dependency('sdl2', method : 'pkg-config')" |
|
self.assertMesonRaises(code, self.nopkg) |
|
|
|
def test_gnustep_notfound_dependency(self): |
|
# Want to test failure, so skip if available |
|
if shutil.which('gnustep-config'): |
|
raise unittest.SkipTest('gnustep-config found') |
|
self.assertMesonRaises("dependency('gnustep')", |
|
f"(requires a Objc compiler|{self.dnf})", |
|
langs = ['objc']) |
|
|
|
def test_wx_notfound_dependency(self): |
|
# Want to test failure, so skip if available |
|
if shutil.which('wx-config-3.0') or shutil.which('wx-config') or shutil.which('wx-config-gtk3'): |
|
raise unittest.SkipTest('wx-config, wx-config-3.0 or wx-config-gtk3 found') |
|
self.assertMesonRaises("dependency('wxwidgets')", self.dnf) |
|
self.assertMesonOutputs("dependency('wxwidgets', required : false)", |
|
"Run-time dependency .*WxWidgets.* found: .*NO.*") |
|
|
|
def test_wx_dependency(self): |
|
if not shutil.which('wx-config-3.0') and not shutil.which('wx-config') and not shutil.which('wx-config-gtk3'): |
|
raise unittest.SkipTest('Neither wx-config, wx-config-3.0 nor wx-config-gtk3 found') |
|
self.assertMesonRaises("dependency('wxwidgets', modules : 1)", |
|
"module argument is not a string") |
|
|
|
def test_llvm_dependency(self): |
|
self.assertMesonRaises("dependency('llvm', modules : 'fail')", |
|
f"(required.*fail|{self.dnf})") |
|
|
|
def test_boost_notfound_dependency(self): |
|
# Can be run even if Boost is found or not |
|
self.assertMesonRaises("dependency('boost', modules : 1)", |
|
"module.*not a string") |
|
self.assertMesonRaises("dependency('boost', modules : 'fail')", |
|
f"(fail.*not found|{self.dnf})") |
|
|
|
def test_boost_BOOST_ROOT_dependency(self): |
|
# Test BOOST_ROOT; can be run even if Boost is found or not |
|
self.assertMesonRaises("dependency('boost')", |
|
f"(boost_root.*absolute|{self.dnf})", |
|
override_envvars = {'BOOST_ROOT': 'relative/path'}) |
|
|
|
def test_dependency_invalid_method(self): |
|
code = '''zlib_dep = dependency('zlib', required : false) |
|
zlib_dep.get_configtool_variable('foo') |
|
''' |
|
self.assertMesonRaises(code, ".* is not a config-tool dependency") |
|
code = '''zlib_dep = dependency('zlib', required : false) |
|
dep = declare_dependency(dependencies : zlib_dep) |
|
dep.get_pkgconfig_variable('foo') |
|
''' |
|
self.assertMesonRaises(code, "Method.*pkgconfig.*is invalid.*internal") |
|
code = '''zlib_dep = dependency('zlib', required : false) |
|
dep = declare_dependency(dependencies : zlib_dep) |
|
dep.get_configtool_variable('foo') |
|
''' |
|
self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal") |
|
|
|
def test_objc_cpp_detection(self): |
|
''' |
|
Test that when we can't detect objc or objcpp, we fail gracefully. |
|
''' |
|
env = get_fake_env() |
|
try: |
|
detect_objc_compiler(env, MachineChoice.HOST) |
|
detect_objcpp_compiler(env, MachineChoice.HOST) |
|
except EnvironmentException: |
|
code = "add_languages('objc')\nadd_languages('objcpp')" |
|
self.assertMesonRaises(code, "Unknown compiler") |
|
return |
|
raise unittest.SkipTest("objc and objcpp found, can't test detection failure") |
|
|
|
def test_subproject_variables(self): |
|
''' |
|
Test that: |
|
1. The correct message is outputted when a not-required dep is not |
|
found and the fallback subproject is also not found. |
|
2. A not-required fallback dependency is not found because the |
|
subproject failed to parse. |
|
3. A not-found not-required dep with a fallback subproject outputs the |
|
correct message when the fallback subproject is found but the |
|
variable inside it is not. |
|
4. A fallback dependency is found from the subproject parsed in (3) |
|
5. A wrap file from a subproject is used but fails because it does not |
|
contain required keys. |
|
''' |
|
tdir = os.path.join(self.unit_test_dir, '20 subproj dep variables') |
|
stray_file = os.path.join(tdir, 'subprojects/subsubproject.wrap') |
|
if os.path.exists(stray_file): |
|
windows_proof_rm(stray_file) |
|
out = self.init(tdir, inprocess=True) |
|
self.assertRegex(out, r"Neither a subproject directory nor a .*nosubproj.wrap.* file was found") |
|
self.assertRegex(out, r'Function does not take positional arguments.') |
|
self.assertRegex(out, r'Dependency .*somenotfounddep.* from subproject .*subprojects/somesubproj.* found: .*NO.*') |
|
self.assertRegex(out, r'Dependency .*zlibproxy.* from subproject .*subprojects.*somesubproj.* found: .*YES.*') |
|
self.assertRegex(out, r'Missing key .*source_filename.* in subsubproject.wrap') |
|
windows_proof_rm(stray_file) |
|
|
|
def test_exception_exit_status(self): |
|
''' |
|
Test exit status on python exception |
|
''' |
|
tdir = os.path.join(self.unit_test_dir, '21 exit status') |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(tdir, inprocess=False, override_envvars = {'MESON_UNIT_TEST': '1', 'MESON_FORCE_BACKTRACE': ''}) |
|
self.assertEqual(cm.exception.returncode, 2) |
|
self.wipe() |
|
|
|
def test_dict_requires_key_value_pairs(self): |
|
self.assertMesonRaises("dict = {3, 'foo': 'bar'}", |
|
'Only key:value pairs are valid in dict construction.') |
|
self.assertMesonRaises("{'foo': 'bar', 3}", |
|
'Only key:value pairs are valid in dict construction.') |
|
|
|
def test_dict_forbids_duplicate_keys(self): |
|
self.assertMesonRaises("dict = {'a': 41, 'a': 42}", |
|
'Duplicate dictionary key: a.*') |
|
|
|
def test_dict_forbids_integer_key(self): |
|
self.assertMesonRaises("dict = {3: 'foo'}", |
|
'Key must be a string.*') |
|
|
|
def test_using_too_recent_feature(self): |
|
# Here we use a dict, which was introduced in 0.47.0 |
|
self.assertMesonOutputs("dict = {}", |
|
".*WARNING.*Project targeting.*but.*", |
|
meson_version='>= 0.46.0') |
|
|
|
def test_using_recent_feature(self): |
|
# Same as above, except the meson version is now appropriate |
|
self.assertMesonDoesNotOutput("dict = {}", |
|
".*WARNING.*Project targeting.*but.*", |
|
meson_version='>= 0.47') |
|
|
|
def test_using_too_recent_feature_dependency(self): |
|
self.assertMesonOutputs("dependency('pcap', required: false)", |
|
".*WARNING.*Project targeting.*but.*", |
|
meson_version='>= 0.41.0') |
|
|
|
def test_vcs_tag_featurenew_build_always_stale(self): |
|
'https://github.com/mesonbuild/meson/issues/3904' |
|
vcs_tag = '''version_data = configuration_data() |
|
version_data.set('PROJVER', '@VCS_TAG@') |
|
vf = configure_file(output : 'version.h.in', configuration: version_data) |
|
f = vcs_tag(input : vf, output : 'version.h') |
|
''' |
|
msg = '.*WARNING:.*feature.*build_always_stale.*custom_target.*' |
|
self.assertMesonDoesNotOutput(vcs_tag, msg, meson_version='>=0.43') |
|
|
|
def test_missing_subproject_not_required_and_required(self): |
|
self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" + |
|
"sub2 = subproject('not-found-subproject', required: true)", |
|
""".*Subproject "subprojects/not-found-subproject" required but not found.*""") |
|
|
|
def test_get_variable_on_not_found_project(self): |
|
self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" + |
|
"sub1.get_variable('naaa')", |
|
"""Subproject "subprojects/not-found-subproject" disabled can't get_variable on it.""") |
|
|
|
def test_version_checked_before_parsing_options(self): |
|
''' |
|
https://github.com/mesonbuild/meson/issues/5281 |
|
''' |
|
options = "option('some-option', type: 'foo', value: '')" |
|
match = 'Meson version is.*but project requires >=2000' |
|
self.assertMesonRaises("", match, meson_version='>=2000', options=options) |
|
|
|
def test_assert_default_message(self): |
|
self.assertMesonRaises("k1 = 'a'\n" + |
|
"assert({\n" + |
|
" k1: 1,\n" + |
|
"}['a'] == 2)\n", |
|
r"Assert failed: {k1 : 1}\['a'\] == 2") |
|
|
|
def test_wrap_nofallback(self): |
|
self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])", |
|
r"Dependency 'notfound' is required but not found.", |
|
extra_args=['--wrap-mode=nofallback']) |
|
|
|
def test_message(self): |
|
self.assertMesonOutputs("message('Array:', ['a', 'b'])", |
|
r"Message:.* Array: \['a', 'b'\]") |
|
|
|
def test_warning(self): |
|
self.assertMesonOutputs("warning('Array:', ['a', 'b'])", |
|
r"WARNING:.* Array: \['a', 'b'\]") |
|
|
|
def test_override_dependency_twice(self): |
|
self.assertMesonRaises("meson.override_dependency('foo', declare_dependency())\n" + |
|
"meson.override_dependency('foo', declare_dependency())", |
|
"""Tried to override dependency 'foo' which has already been resolved or overridden""") |
|
|
|
@unittest.skipIf(is_windows(), 'zlib is not available on Windows') |
|
def test_override_resolved_dependency(self): |
|
self.assertMesonRaises("dependency('zlib')\n" + |
|
"meson.override_dependency('zlib', declare_dependency())", |
|
"""Tried to override dependency 'zlib' which has already been resolved or overridden""") |
|
|
|
def test_error_func(self): |
|
self.assertMesonRaises("error('a', 'b', ['c', ['d', {'e': 'f'}]], 'g')", |
|
r"Problem encountered: a b \['c', \['d', {'e' : 'f'}\]\] g") |
|
|
|
|
|
@unittest.skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)") |
|
class WindowsTests(BasePlatformTests): |
|
''' |
|
Tests that should run on Cygwin, MinGW, and MSVC |
|
''' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.platform_test_dir = os.path.join(self.src_root, 'test cases/windows') |
|
|
|
@unittest.skipIf(is_cygwin(), 'Test only applicable to Windows') |
|
@mock.patch.dict(os.environ) |
|
def test_find_program(self): |
|
''' |
|
Test that Windows-specific edge-cases in find_program are functioning |
|
correctly. Cannot be an ordinary test because it involves manipulating |
|
PATH to point to a directory with Python scripts. |
|
''' |
|
testdir = os.path.join(self.platform_test_dir, '8 find program') |
|
# Find `cmd` and `cmd.exe` |
|
prog1 = ExternalProgram('cmd') |
|
self.assertTrue(prog1.found(), msg='cmd not found') |
|
prog2 = ExternalProgram('cmd.exe') |
|
self.assertTrue(prog2.found(), msg='cmd.exe not found') |
|
self.assertPathEqual(prog1.get_path(), prog2.get_path()) |
|
# Find cmd.exe with args without searching |
|
prog = ExternalProgram('cmd', command=['cmd', '/C']) |
|
self.assertTrue(prog.found(), msg='cmd not found with args') |
|
self.assertPathEqual(prog.get_command()[0], 'cmd') |
|
# Find cmd with an absolute path that's missing the extension |
|
cmd_path = prog2.get_path()[:-4] |
|
prog = ExternalProgram(cmd_path) |
|
self.assertTrue(prog.found(), msg=f'{cmd_path!r} not found') |
|
# Finding a script with no extension inside a directory works |
|
prog = ExternalProgram(os.path.join(testdir, 'test-script')) |
|
self.assertTrue(prog.found(), msg='test-script not found') |
|
# Finding a script with an extension inside a directory works |
|
prog = ExternalProgram(os.path.join(testdir, 'test-script-ext.py')) |
|
self.assertTrue(prog.found(), msg='test-script-ext.py not found') |
|
# Finding a script in PATH |
|
os.environ['PATH'] += os.pathsep + testdir |
|
# If `.PY` is in PATHEXT, scripts can be found as programs |
|
if '.PY' in [ext.upper() for ext in os.environ['PATHEXT'].split(';')]: |
|
# Finding a script in PATH w/o extension works and adds the interpreter |
|
prog = ExternalProgram('test-script-ext') |
|
self.assertTrue(prog.found(), msg='test-script-ext not found in PATH') |
|
self.assertPathEqual(prog.get_command()[0], python_command[0]) |
|
self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') |
|
# Finding a script in PATH with extension works and adds the interpreter |
|
prog = ExternalProgram('test-script-ext.py') |
|
self.assertTrue(prog.found(), msg='test-script-ext.py not found in PATH') |
|
self.assertPathEqual(prog.get_command()[0], python_command[0]) |
|
self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') |
|
# Using a script with an extension directly via command= works and adds the interpreter |
|
prog = ExternalProgram('test-script-ext.py', command=[os.path.join(testdir, 'test-script-ext.py'), '--help']) |
|
self.assertTrue(prog.found(), msg='test-script-ext.py with full path not picked up via command=') |
|
self.assertPathEqual(prog.get_command()[0], python_command[0]) |
|
self.assertPathEqual(prog.get_command()[2], '--help') |
|
self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') |
|
# Using a script without an extension directly via command= works and adds the interpreter |
|
prog = ExternalProgram('test-script', command=[os.path.join(testdir, 'test-script'), '--help']) |
|
self.assertTrue(prog.found(), msg='test-script with full path not picked up via command=') |
|
self.assertPathEqual(prog.get_command()[0], python_command[0]) |
|
self.assertPathEqual(prog.get_command()[2], '--help') |
|
self.assertPathBasenameEqual(prog.get_path(), 'test-script') |
|
# Ensure that WindowsApps gets removed from PATH |
|
path = os.environ['PATH'] |
|
if 'WindowsApps' not in path: |
|
username = os.environ['USERNAME'] |
|
appstore_dir = fr'C:\Users\{username}\AppData\Local\Microsoft\WindowsApps' |
|
path = os.pathsep + appstore_dir |
|
path = ExternalProgram._windows_sanitize_path(path) |
|
self.assertNotIn('WindowsApps', path) |
|
|
|
def test_ignore_libs(self): |
|
''' |
|
Test that find_library on libs that are to be ignored returns an empty |
|
array of arguments. Must be a unit test because we cannot inspect |
|
ExternalLibraryHolder from build files. |
|
''' |
|
testdir = os.path.join(self.platform_test_dir, '1 basic') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_argument_syntax() != 'msvc': |
|
raise unittest.SkipTest('Not using MSVC') |
|
# To force people to update this test, and also test |
|
self.assertEqual(set(cc.ignore_libs), {'c', 'm', 'pthread', 'dl', 'rt', 'execinfo'}) |
|
for l in cc.ignore_libs: |
|
self.assertEqual(cc.find_library(l, env, []), []) |
|
|
|
def test_rc_depends_files(self): |
|
testdir = os.path.join(self.platform_test_dir, '5 resources') |
|
|
|
# resource compiler depfile generation is not yet implemented for msvc |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
depfile_works = detect_c_compiler(env, MachineChoice.HOST).get_id() not in {'msvc', 'clang-cl', 'intel-cl'} |
|
|
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Test compile_resources(depend_file:) |
|
# Changing mtime of sample.ico should rebuild prog |
|
self.utime(os.path.join(testdir, 'res', 'sample.ico')) |
|
self.assertRebuiltTarget('prog') |
|
# Test depfile generation by compile_resources |
|
# Changing mtime of resource.h should rebuild myres.rc and then prog |
|
if depfile_works: |
|
self.utime(os.path.join(testdir, 'inc', 'resource', 'resource.h')) |
|
self.assertRebuiltTarget('prog') |
|
self.wipe() |
|
|
|
if depfile_works: |
|
testdir = os.path.join(self.platform_test_dir, '12 resources with custom targets') |
|
self.init(testdir) |
|
self.build() |
|
# Immediately rebuilding should not do anything |
|
self.assertBuildIsNoop() |
|
# Changing mtime of resource.h should rebuild myres_1.rc and then prog_1 |
|
self.utime(os.path.join(testdir, 'res', 'resource.h')) |
|
self.assertRebuiltTarget('prog_1') |
|
|
|
def test_msvc_cpp17(self): |
|
testdir = os.path.join(self.unit_test_dir, '45 vscpp17') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_argument_syntax() != 'msvc': |
|
raise unittest.SkipTest('Test only applies to MSVC-like compilers') |
|
|
|
try: |
|
self.init(testdir) |
|
except subprocess.CalledProcessError: |
|
# According to Python docs, output is only stored when |
|
# using check_output. We don't use it, so we can't check |
|
# that the output is correct (i.e. that it failed due |
|
# to the right reason). |
|
return |
|
self.build() |
|
|
|
def test_install_pdb_introspection(self): |
|
testdir = os.path.join(self.platform_test_dir, '1 basic') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_argument_syntax() != 'msvc': |
|
raise unittest.SkipTest('Test only applies to MSVC-like compilers') |
|
|
|
self.init(testdir) |
|
installed = self.introspect('--installed') |
|
files = [os.path.basename(path) for path in installed.values()] |
|
|
|
self.assertTrue('prog.pdb' in files) |
|
|
|
def _check_ld(self, name: str, lang: str, expected: str) -> None: |
|
if not shutil.which(name): |
|
raise unittest.SkipTest(f'Could not find {name}.') |
|
envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']] |
|
|
|
# Also test a deprecated variable if there is one. |
|
if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP: |
|
envvars.append( |
|
mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld']) |
|
|
|
for envvar in envvars: |
|
with mock.patch.dict(os.environ, {envvar: name}): |
|
env = get_fake_env() |
|
try: |
|
comp = compiler_from_language(env, lang, MachineChoice.HOST) |
|
except EnvironmentException: |
|
raise unittest.SkipTest(f'Could not find a compiler for {lang}') |
|
self.assertEqual(comp.linker.id, expected) |
|
|
|
def test_link_environment_variable_lld_link(self): |
|
env = get_fake_env() |
|
comp = detect_c_compiler(env, MachineChoice.HOST) |
|
if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler): |
|
raise unittest.SkipTest('GCC cannot be used with link compatible linkers.') |
|
self._check_ld('lld-link', 'c', 'lld-link') |
|
|
|
def test_link_environment_variable_link(self): |
|
env = get_fake_env() |
|
comp = detect_c_compiler(env, MachineChoice.HOST) |
|
if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler): |
|
raise unittest.SkipTest('GCC cannot be used with link compatible linkers.') |
|
self._check_ld('link', 'c', 'link') |
|
|
|
def test_link_environment_variable_optlink(self): |
|
env = get_fake_env() |
|
comp = detect_c_compiler(env, MachineChoice.HOST) |
|
if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler): |
|
raise unittest.SkipTest('GCC cannot be used with link compatible linkers.') |
|
self._check_ld('optlink', 'c', 'optlink') |
|
|
|
@skip_if_not_language('rust') |
|
def test_link_environment_variable_rust(self): |
|
self._check_ld('link', 'rust', 'link') |
|
|
|
@skip_if_not_language('d') |
|
def test_link_environment_variable_d(self): |
|
env = get_fake_env() |
|
comp = detect_d_compiler(env, MachineChoice.HOST) |
|
if comp.id == 'dmd': |
|
raise unittest.SkipTest('meson cannot reliably make DMD use a different linker.') |
|
self._check_ld('lld-link', 'd', 'lld-link') |
|
|
|
def test_pefile_checksum(self): |
|
try: |
|
import pefile |
|
except ImportError: |
|
if is_ci(): |
|
raise |
|
raise unittest.SkipTest('pefile module not found') |
|
testdir = os.path.join(self.common_test_dir, '6 linkshared') |
|
self.init(testdir, extra_args=['--buildtype=release']) |
|
self.build() |
|
# Test that binaries have a non-zero checksum |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
cc_id = cc.get_id() |
|
ld_id = cc.get_linker_id() |
|
dll = glob(os.path.join(self.builddir, '*mycpplib.dll'))[0] |
|
exe = os.path.join(self.builddir, 'cppprog.exe') |
|
for f in (dll, exe): |
|
pe = pefile.PE(f) |
|
msg = f'PE file: {f!r}, compiler: {cc_id!r}, linker: {ld_id!r}' |
|
if cc_id == 'clang-cl': |
|
# Latest clang-cl tested (7.0) does not write checksums out |
|
self.assertFalse(pe.verify_checksum(), msg=msg) |
|
else: |
|
# Verify that a valid checksum was written by all other compilers |
|
self.assertTrue(pe.verify_checksum(), msg=msg) |
|
|
|
def test_qt5dependency_vscrt(self): |
|
''' |
|
Test that qt5 dependencies use the debug module suffix when b_vscrt is |
|
set to 'mdd' |
|
''' |
|
# Verify that the `b_vscrt` option is available |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if OptionKey('b_vscrt') not in cc.base_options: |
|
raise unittest.SkipTest('Compiler does not support setting the VS CRT') |
|
# Verify that qmake is for Qt5 |
|
if not shutil.which('qmake-qt5'): |
|
if not shutil.which('qmake') and not is_ci(): |
|
raise unittest.SkipTest('QMake not found') |
|
output = subprocess.getoutput('qmake --version') |
|
if 'Qt version 5' not in output and not is_ci(): |
|
raise unittest.SkipTest('Qmake found, but it is not for Qt 5.') |
|
# Setup with /MDd |
|
testdir = os.path.join(self.framework_test_dir, '4 qt') |
|
self.init(testdir, extra_args=['-Db_vscrt=mdd']) |
|
# Verify that we're linking to the debug versions of Qt DLLs |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
with open(build_ninja, encoding='utf-8') as f: |
|
contents = f.read() |
|
m = re.search('build qt5core.exe: cpp_LINKER.*Qt5Cored.lib', contents) |
|
self.assertIsNotNone(m, msg=contents) |
|
|
|
def test_compiler_checks_vscrt(self): |
|
''' |
|
Test that the correct VS CRT is used when running compiler checks |
|
''' |
|
# Verify that the `b_vscrt` option is available |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if OptionKey('b_vscrt') not in cc.base_options: |
|
raise unittest.SkipTest('Compiler does not support setting the VS CRT') |
|
|
|
def sanitycheck_vscrt(vscrt): |
|
checks = self.get_meson_log_sanitychecks() |
|
self.assertTrue(len(checks) > 0) |
|
for check in checks: |
|
self.assertIn(vscrt, check) |
|
|
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
self.init(testdir) |
|
sanitycheck_vscrt('/MDd') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Dbuildtype=debugoptimized']) |
|
sanitycheck_vscrt('/MD') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Dbuildtype=release']) |
|
sanitycheck_vscrt('/MD') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Db_vscrt=md']) |
|
sanitycheck_vscrt('/MD') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Db_vscrt=mdd']) |
|
sanitycheck_vscrt('/MDd') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Db_vscrt=mt']) |
|
sanitycheck_vscrt('/MT') |
|
|
|
self.new_builddir() |
|
self.init(testdir, extra_args=['-Db_vscrt=mtd']) |
|
sanitycheck_vscrt('/MTd') |
|
|
|
def test_modules(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'C++ modules only work with the Ninja backend (not {self.backend.name}).') |
|
if 'VSCMD_VER' not in os.environ: |
|
raise unittest.SkipTest('C++ modules is only supported with Visual Studio.') |
|
if version_compare(os.environ['VSCMD_VER'], '<16.10.0'): |
|
raise unittest.SkipTest('C++ modules are only supported with VS 2019 Preview or newer.') |
|
self.init(os.path.join(self.unit_test_dir, '86 cpp modules')) |
|
self.build() |
|
|
|
|
|
@unittest.skipUnless(is_osx(), "requires Darwin") |
|
class DarwinTests(BasePlatformTests): |
|
''' |
|
Tests that should run on macOS |
|
''' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.platform_test_dir = os.path.join(self.src_root, 'test cases/osx') |
|
|
|
def test_apple_bitcode(self): |
|
''' |
|
Test that -fembed-bitcode is correctly added while compiling and |
|
-bitcode_bundle is added while linking when b_bitcode is true and not |
|
when it is false. This can't be an ordinary test case because we need |
|
to inspect the compiler database. |
|
''' |
|
testdir = os.path.join(self.platform_test_dir, '7 bitcode') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.id != 'clang': |
|
raise unittest.SkipTest('Not using Clang on OSX') |
|
# Try with bitcode enabled |
|
out = self.init(testdir, extra_args='-Db_bitcode=true') |
|
# Warning was printed |
|
self.assertRegex(out, 'WARNING:.*b_bitcode') |
|
# Compiler options were added |
|
for compdb in self.get_compdb(): |
|
if 'module' in compdb['file']: |
|
self.assertNotIn('-fembed-bitcode', compdb['command']) |
|
else: |
|
self.assertIn('-fembed-bitcode', compdb['command']) |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
# Linker options were added |
|
with open(build_ninja, encoding='utf-8') as f: |
|
contents = f.read() |
|
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents) |
|
self.assertIsNotNone(m, msg=contents) |
|
# Try with bitcode disabled |
|
self.setconf('-Db_bitcode=false') |
|
# Regenerate build |
|
self.build() |
|
for compdb in self.get_compdb(): |
|
self.assertNotIn('-fembed-bitcode', compdb['command']) |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
with open(build_ninja, encoding='utf-8') as f: |
|
contents = f.read() |
|
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents) |
|
self.assertIsNone(m, msg=contents) |
|
|
|
def test_apple_bitcode_modules(self): |
|
''' |
|
Same as above, just for shared_module() |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '148 shared module resolving symbol in executable') |
|
# Ensure that it builds even with bitcode enabled |
|
self.init(testdir, extra_args='-Db_bitcode=true') |
|
self.build() |
|
self.run_tests() |
|
|
|
def _get_darwin_versions(self, fname): |
|
fname = os.path.join(self.builddir, fname) |
|
out = subprocess.check_output(['otool', '-L', fname], universal_newlines=True) |
|
m = re.match(r'.*version (.*), current version (.*)\)', out.split('\n')[1]) |
|
self.assertIsNotNone(m, msg=out) |
|
return m.groups() |
|
|
|
@skipIfNoPkgconfig |
|
def test_library_versioning(self): |
|
''' |
|
Ensure that compatibility_version and current_version are set correctly |
|
''' |
|
testdir = os.path.join(self.platform_test_dir, '2 library versions') |
|
self.init(testdir) |
|
self.build() |
|
targets = {} |
|
for t in self.introspect('--targets'): |
|
targets[t['name']] = t['filename'][0] if isinstance(t['filename'], list) else t['filename'] |
|
self.assertEqual(self._get_darwin_versions(targets['some']), ('7.0.0', '7.0.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['noversion']), ('0.0.0', '0.0.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['onlyversion']), ('1.0.0', '1.0.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['onlysoversion']), ('5.0.0', '5.0.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['intver']), ('2.0.0', '2.0.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['stringver']), ('2.3.0', '2.3.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['stringlistver']), ('2.4.0', '2.4.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['intstringver']), ('1111.0.0', '2.5.0')) |
|
self.assertEqual(self._get_darwin_versions(targets['stringlistvers']), ('2.6.0', '2.6.1')) |
|
|
|
def test_duplicate_rpath(self): |
|
testdir = os.path.join(self.unit_test_dir, '10 build_rpath') |
|
# We purposely pass a duplicate rpath to Meson, in order |
|
# to ascertain that Meson does not call install_name_tool |
|
# with duplicate -delete_rpath arguments, which would |
|
# lead to erroring out on installation |
|
env = {"LDFLAGS": "-Wl,-rpath,/foo/bar"} |
|
self.init(testdir, override_envvars=env) |
|
self.build() |
|
self.install() |
|
|
|
def test_removing_unused_linker_args(self): |
|
testdir = os.path.join(self.common_test_dir, '104 has arg') |
|
env = {'CFLAGS': '-L/tmp -L /var/tmp -headerpad_max_install_names -Wl,-export_dynamic -framework Foundation'} |
|
self.init(testdir, override_envvars=env) |
|
|
|
def test_objc_versions(self): |
|
# Objective-C always uses the C standard version. |
|
# Objecttive-C++ always uses the C++ standard version. |
|
# This is what most people seem to want and in addition |
|
# it is the only setup supported by Xcode. |
|
testdir = os.path.join(self.objc_test_dir, '1 simple') |
|
self.init(testdir) |
|
self.assertIn('-std=c99', self.get_compdb()[0]['command']) |
|
self.wipe() |
|
testdir = os.path.join(self.objcpp_test_dir, '1 simple') |
|
self.init(testdir) |
|
self.assertIn('-std=c++14', self.get_compdb()[0]['command']) |
|
|
|
@unittest.skipUnless(not is_windows(), "requires something Unix-like") |
|
class LinuxlikeTests(BasePlatformTests): |
|
''' |
|
Tests that should run on Linux, macOS, and *BSD |
|
''' |
|
|
|
def test_basic_soname(self): |
|
''' |
|
Test that the soname is set correctly for shared libraries. This can't |
|
be an ordinary test case because we need to run `readelf` and actually |
|
check the soname. |
|
https://github.com/mesonbuild/meson/issues/785 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '4 shared') |
|
self.init(testdir) |
|
self.build() |
|
lib1 = os.path.join(self.builddir, 'libmylib.so') |
|
soname = get_soname(lib1) |
|
self.assertEqual(soname, 'libmylib.so') |
|
|
|
def test_custom_soname(self): |
|
''' |
|
Test that the soname is set correctly for shared libraries when |
|
a custom prefix and/or suffix is used. This can't be an ordinary test |
|
case because we need to run `readelf` and actually check the soname. |
|
https://github.com/mesonbuild/meson/issues/785 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '24 library versions') |
|
self.init(testdir) |
|
self.build() |
|
lib1 = os.path.join(self.builddir, 'prefixsomelib.suffix') |
|
soname = get_soname(lib1) |
|
self.assertEqual(soname, 'prefixsomelib.suffix') |
|
|
|
def test_pic(self): |
|
''' |
|
Test that -fPIC is correctly added to static libraries when b_staticpic |
|
is true and not when it is false. This can't be an ordinary test case |
|
because we need to inspect the compiler database. |
|
''' |
|
if is_windows() or is_cygwin() or is_osx(): |
|
raise unittest.SkipTest('PIC not relevant') |
|
|
|
testdir = os.path.join(self.common_test_dir, '3 static') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
self.assertIn('-fPIC', compdb[0]['command']) |
|
self.setconf('-Db_staticpic=false') |
|
# Regenerate build |
|
self.build() |
|
compdb = self.get_compdb() |
|
self.assertNotIn('-fPIC', compdb[0]['command']) |
|
|
|
@mock.patch.dict(os.environ) |
|
def test_pkgconfig_gen(self): |
|
''' |
|
Test that generated pkg-config files can be found and have the correct |
|
version and link args. This can't be an ordinary test case because we |
|
need to run pkg-config outside of a Meson build file. |
|
https://github.com/mesonbuild/meson/issues/889 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
self.init(testdir) |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
kwargs = {'required': True, 'silent': True} |
|
os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir |
|
foo_dep = PkgConfigDependency('libfoo', env, kwargs) |
|
self.assertTrue(foo_dep.found()) |
|
self.assertEqual(foo_dep.get_version(), '1.0') |
|
self.assertIn('-lfoo', foo_dep.get_link_args()) |
|
self.assertEqual(foo_dep.get_pkgconfig_variable('foo', {}), 'bar') |
|
self.assertPathEqual(foo_dep.get_pkgconfig_variable('datadir', {}), '/usr/data') |
|
|
|
libhello_nolib = PkgConfigDependency('libhello_nolib', env, kwargs) |
|
self.assertTrue(libhello_nolib.found()) |
|
self.assertEqual(libhello_nolib.get_link_args(), []) |
|
self.assertEqual(libhello_nolib.get_compile_args(), []) |
|
self.assertEqual(libhello_nolib.get_pkgconfig_variable('foo', {}), 'bar') |
|
self.assertEqual(libhello_nolib.get_pkgconfig_variable('prefix', {}), self.prefix) |
|
self.assertEqual(libhello_nolib.get_pkgconfig_variable('escaped_var', {}), r'hello\ world') |
|
self.assertEqual(libhello_nolib.get_pkgconfig_variable('unescaped_var', {}), 'hello world') |
|
|
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() in {'gcc', 'clang'}: |
|
for name in {'ct', 'ct0'}: |
|
ct_dep = PkgConfigDependency(name, env, kwargs) |
|
self.assertTrue(ct_dep.found()) |
|
self.assertIn('-lct', ct_dep.get_link_args()) |
|
|
|
def test_pkgconfig_gen_deps(self): |
|
''' |
|
Test that generated pkg-config files correctly handle dependencies |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
self.init(testdir) |
|
privatedir1 = self.privatedir |
|
|
|
self.new_builddir() |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies') |
|
self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': privatedir1}) |
|
privatedir2 = self.privatedir |
|
|
|
env = { |
|
'PKG_CONFIG_LIBDIR': os.pathsep.join([privatedir1, privatedir2]), |
|
'PKG_CONFIG_SYSTEM_LIBRARY_PATH': '/usr/lib', |
|
} |
|
self._run(['pkg-config', 'dependency-test', '--validate'], override_envvars=env) |
|
|
|
# pkg-config strips some duplicated flags so we have to parse the |
|
# generated file ourself. |
|
expected = { |
|
'Requires': 'libexposed', |
|
'Requires.private': 'libfoo >= 1.0', |
|
'Libs': '-L${libdir} -llibmain -pthread -lcustom', |
|
'Libs.private': '-lcustom2 -L${libdir} -llibinternal', |
|
'Cflags': '-I${includedir} -pthread -DCUSTOM', |
|
} |
|
if is_osx() or is_haiku(): |
|
expected['Cflags'] = expected['Cflags'].replace('-pthread ', '') |
|
with open(os.path.join(privatedir2, 'dependency-test.pc')) as f: |
|
matched_lines = 0 |
|
for line in f: |
|
parts = line.split(':', 1) |
|
if parts[0] in expected: |
|
key = parts[0] |
|
val = parts[1].strip() |
|
expected_val = expected[key] |
|
self.assertEqual(expected_val, val) |
|
matched_lines += 1 |
|
self.assertEqual(len(expected), matched_lines) |
|
|
|
cmd = ['pkg-config', 'requires-test'] |
|
out = self._run(cmd + ['--print-requires'], override_envvars=env).strip().split('\n') |
|
if not is_openbsd(): |
|
self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello'])) |
|
else: |
|
self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello'])) |
|
|
|
cmd = ['pkg-config', 'requires-private-test'] |
|
out = self._run(cmd + ['--print-requires-private'], override_envvars=env).strip().split('\n') |
|
if not is_openbsd(): |
|
self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello'])) |
|
else: |
|
self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello'])) |
|
|
|
cmd = ['pkg-config', 'pub-lib-order'] |
|
out = self._run(cmd + ['--libs'], override_envvars=env).strip().split() |
|
self.assertEqual(out, ['-llibmain2', '-llibinternal']) |
|
|
|
# See common/44 pkgconfig-gen/meson.build for description of the case this test |
|
with open(os.path.join(privatedir1, 'simple2.pc')) as f: |
|
content = f.read() |
|
self.assertIn('Libs: -L${libdir} -lsimple2 -lsimple1', content) |
|
self.assertIn('Libs.private: -lz', content) |
|
|
|
with open(os.path.join(privatedir1, 'simple3.pc')) as f: |
|
content = f.read() |
|
self.assertEqual(1, content.count('-lsimple3')) |
|
|
|
with open(os.path.join(privatedir1, 'simple5.pc')) as f: |
|
content = f.read() |
|
self.assertNotIn('-lstat2', content) |
|
|
|
@mock.patch.dict(os.environ) |
|
def test_pkgconfig_uninstalled(self): |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
self.init(testdir) |
|
self.build() |
|
|
|
os.environ['PKG_CONFIG_LIBDIR'] = os.path.join(self.builddir, 'meson-uninstalled') |
|
if is_cygwin(): |
|
os.environ['PATH'] += os.pathsep + self.builddir |
|
|
|
self.new_builddir() |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies') |
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_pkg_unfound(self): |
|
testdir = os.path.join(self.unit_test_dir, '23 unfound pkgconfig') |
|
self.init(testdir) |
|
with open(os.path.join(self.privatedir, 'somename.pc')) as f: |
|
pcfile = f.read() |
|
self.assertFalse('blub_blob_blib' in pcfile) |
|
|
|
def test_vala_c_warnings(self): |
|
''' |
|
Test that no warnings are emitted for C code generated by Vala. This |
|
can't be an ordinary test case because we need to inspect the compiler |
|
database. |
|
https://github.com/mesonbuild/meson/issues/864 |
|
''' |
|
if not shutil.which('valac'): |
|
raise unittest.SkipTest('valac not installed.') |
|
testdir = os.path.join(self.vala_test_dir, '5 target glib') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
vala_command = None |
|
c_command = None |
|
for each in compdb: |
|
if each['file'].endswith('GLib.Thread.c'): |
|
vala_command = each['command'] |
|
elif each['file'].endswith('GLib.Thread.vala'): |
|
continue |
|
elif each['file'].endswith('retcode.c'): |
|
c_command = each['command'] |
|
else: |
|
m = 'Unknown file {!r} in vala_c_warnings test'.format(each['file']) |
|
raise AssertionError(m) |
|
self.assertIsNotNone(vala_command) |
|
self.assertIsNotNone(c_command) |
|
# -w suppresses all warnings, should be there in Vala but not in C |
|
self.assertIn(" -w ", vala_command) |
|
self.assertNotIn(" -w ", c_command) |
|
# -Wall enables all warnings, should be there in C but not in Vala |
|
self.assertNotIn(" -Wall ", vala_command) |
|
self.assertIn(" -Wall ", c_command) |
|
# -Werror converts warnings to errors, should always be there since it's |
|
# injected by an unrelated piece of code and the project has werror=true |
|
self.assertIn(" -Werror ", vala_command) |
|
self.assertIn(" -Werror ", c_command) |
|
|
|
@skipIfNoPkgconfig |
|
def test_qtdependency_pkgconfig_detection(self): |
|
''' |
|
Test that qt4 and qt5 detection with pkgconfig works. |
|
''' |
|
# Verify Qt4 or Qt5 can be found with pkg-config |
|
qt4 = subprocess.call(['pkg-config', '--exists', 'QtCore']) |
|
qt5 = subprocess.call(['pkg-config', '--exists', 'Qt5Core']) |
|
testdir = os.path.join(self.framework_test_dir, '4 qt') |
|
self.init(testdir, extra_args=['-Dmethod=pkg-config']) |
|
# Confirm that the dependency was found with pkg-config |
|
mesonlog = self.get_meson_log() |
|
if qt4 == 0: |
|
self.assertRegex('\n'.join(mesonlog), |
|
r'Run-time dependency qt4 \(modules: Core\) found: YES 4.* \(pkg-config\)') |
|
if qt5 == 0: |
|
self.assertRegex('\n'.join(mesonlog), |
|
r'Run-time dependency qt5 \(modules: Core\) found: YES 5.* \(pkg-config\)') |
|
|
|
@skip_if_not_base_option('b_sanitize') |
|
def test_generate_gir_with_address_sanitizer(self): |
|
if is_cygwin(): |
|
raise unittest.SkipTest('asan not available on Cygwin') |
|
if is_openbsd(): |
|
raise unittest.SkipTest('-fsanitize=address is not supported on OpenBSD') |
|
|
|
testdir = os.path.join(self.framework_test_dir, '7 gnome') |
|
self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false']) |
|
self.build() |
|
|
|
def test_qt5dependency_qmake_detection(self): |
|
''' |
|
Test that qt5 detection with qmake works. This can't be an ordinary |
|
test case because it involves setting the environment. |
|
''' |
|
# Verify that qmake is for Qt5 |
|
if not shutil.which('qmake-qt5'): |
|
if not shutil.which('qmake'): |
|
raise unittest.SkipTest('QMake not found') |
|
output = subprocess.getoutput('qmake --version') |
|
if 'Qt version 5' not in output: |
|
raise unittest.SkipTest('Qmake found, but it is not for Qt 5.') |
|
# Disable pkg-config codepath and force searching with qmake/qmake-qt5 |
|
testdir = os.path.join(self.framework_test_dir, '4 qt') |
|
self.init(testdir, extra_args=['-Dmethod=qmake']) |
|
# Confirm that the dependency was found with qmake |
|
mesonlog = self.get_meson_log() |
|
self.assertRegex('\n'.join(mesonlog), |
|
r'Run-time dependency qt5 \(modules: Core\) found: YES .* \(qmake\)\n') |
|
|
|
def test_qt6dependency_qmake_detection(self): |
|
''' |
|
Test that qt6 detection with qmake works. This can't be an ordinary |
|
test case because it involves setting the environment. |
|
''' |
|
# Verify that qmake is for Qt5 |
|
if not shutil.which('qmake-qt6'): |
|
if not shutil.which('qmake'): |
|
raise unittest.SkipTest('QMake not found') |
|
output = subprocess.getoutput('qmake --version') |
|
if 'Qt version 6' not in output: |
|
raise unittest.SkipTest('Qmake found, but it is not for Qt 6.') |
|
# Disable pkg-config codepath and force searching with qmake/qmake-qt6 |
|
testdir = os.path.join(self.framework_test_dir, '4 qt') |
|
self.init(testdir, extra_args=['-Dmethod=qmake']) |
|
# Confirm that the dependency was found with qmake |
|
mesonlog = self.get_meson_log() |
|
self.assertRegex('\n'.join(mesonlog), |
|
r'Run-time dependency qt6 \(modules: Core\) found: YES .* \(qmake\)\n') |
|
|
|
def glob_sofiles_without_privdir(self, g): |
|
files = glob(g) |
|
return [f for f in files if not f.endswith('.p')] |
|
|
|
def _test_soname_impl(self, libpath, install): |
|
if is_cygwin() or is_osx(): |
|
raise unittest.SkipTest('Test only applicable to ELF and linuxlike sonames') |
|
|
|
testdir = os.path.join(self.unit_test_dir, '1 soname') |
|
self.init(testdir) |
|
self.build() |
|
if install: |
|
self.install() |
|
|
|
# File without aliases set. |
|
nover = os.path.join(libpath, 'libnover.so') |
|
self.assertPathExists(nover) |
|
self.assertFalse(os.path.islink(nover)) |
|
self.assertEqual(get_soname(nover), 'libnover.so') |
|
self.assertEqual(len(self.glob_sofiles_without_privdir(nover[:-3] + '*')), 1) |
|
|
|
# File with version set |
|
verset = os.path.join(libpath, 'libverset.so') |
|
self.assertPathExists(verset + '.4.5.6') |
|
self.assertEqual(os.readlink(verset), 'libverset.so.4') |
|
self.assertEqual(get_soname(verset), 'libverset.so.4') |
|
self.assertEqual(len(self.glob_sofiles_without_privdir(verset[:-3] + '*')), 3) |
|
|
|
# File with soversion set |
|
soverset = os.path.join(libpath, 'libsoverset.so') |
|
self.assertPathExists(soverset + '.1.2.3') |
|
self.assertEqual(os.readlink(soverset), 'libsoverset.so.1.2.3') |
|
self.assertEqual(get_soname(soverset), 'libsoverset.so.1.2.3') |
|
self.assertEqual(len(self.glob_sofiles_without_privdir(soverset[:-3] + '*')), 2) |
|
|
|
# File with version and soversion set to same values |
|
settosame = os.path.join(libpath, 'libsettosame.so') |
|
self.assertPathExists(settosame + '.7.8.9') |
|
self.assertEqual(os.readlink(settosame), 'libsettosame.so.7.8.9') |
|
self.assertEqual(get_soname(settosame), 'libsettosame.so.7.8.9') |
|
self.assertEqual(len(self.glob_sofiles_without_privdir(settosame[:-3] + '*')), 2) |
|
|
|
# File with version and soversion set to different values |
|
bothset = os.path.join(libpath, 'libbothset.so') |
|
self.assertPathExists(bothset + '.1.2.3') |
|
self.assertEqual(os.readlink(bothset), 'libbothset.so.1.2.3') |
|
self.assertEqual(os.readlink(bothset + '.1.2.3'), 'libbothset.so.4.5.6') |
|
self.assertEqual(get_soname(bothset), 'libbothset.so.1.2.3') |
|
self.assertEqual(len(self.glob_sofiles_without_privdir(bothset[:-3] + '*')), 3) |
|
|
|
def test_soname(self): |
|
self._test_soname_impl(self.builddir, False) |
|
|
|
def test_installed_soname(self): |
|
libdir = self.installdir + os.path.join(self.prefix, self.libdir) |
|
self._test_soname_impl(libdir, True) |
|
|
|
def test_compiler_check_flags_order(self): |
|
''' |
|
Test that compiler check flags override all other flags. This can't be |
|
an ordinary test case because it needs the environment to be set. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '36 has function') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cpp = detect_cpp_compiler(env, MachineChoice.HOST) |
|
Oflag = '-O3' |
|
OflagCPP = Oflag |
|
if cpp.get_id() in ('clang', 'gcc'): |
|
# prevent developers from adding "int main(int argc, char **argv)" |
|
# to small Meson checks unless these parameters are actually used |
|
OflagCPP += ' -Werror=unused-parameter' |
|
env = {'CFLAGS': Oflag, |
|
'CXXFLAGS': OflagCPP} |
|
self.init(testdir, override_envvars=env) |
|
cmds = self.get_meson_log_compiler_checks() |
|
for cmd in cmds: |
|
if cmd[0] == 'ccache': |
|
cmd = cmd[1:] |
|
# Verify that -I flags from the `args` kwarg are first |
|
# This is set in the '36 has function' test case |
|
self.assertEqual(cmd[1], '-I/tmp') |
|
# Verify that -O3 set via the environment is overridden by -O0 |
|
Oargs = [arg for arg in cmd if arg.startswith('-O')] |
|
self.assertEqual(Oargs, [Oflag, '-O0']) |
|
|
|
def _test_stds_impl(self, testdir: str, compiler: 'Compiler') -> None: |
|
has_cpp17 = (compiler.get_id() not in {'clang', 'gcc'} or |
|
compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=5.0.0', '>=9.1') or |
|
compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=5.0.0')) |
|
has_cpp2a_c17 = (compiler.get_id() not in {'clang', 'gcc'} or |
|
compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=6.0.0', '>=10.0') or |
|
compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0')) |
|
has_cpp20 = (compiler.get_id() not in {'clang', 'gcc'} or |
|
compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=10.0.0', None) or |
|
compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=10.0.0')) |
|
has_c18 = (compiler.get_id() not in {'clang', 'gcc'} or |
|
compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=8.0.0', '>=11.0') or |
|
compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0')) |
|
# Check that all the listed -std=xxx options for this compiler work just fine when used |
|
# https://en.wikipedia.org/wiki/Xcode#Latest_versions |
|
# https://www.gnu.org/software/gcc/projects/cxx-status.html |
|
key = OptionKey('std', lang=compiler.language) |
|
for v in compiler.get_options()[key].choices: |
|
# we do it like this to handle gnu++17,c++17 and gnu17,c17 cleanly |
|
# thus, C++ first |
|
if '++17' in v and not has_cpp17: |
|
continue |
|
elif '++2a' in v and not has_cpp2a_c17: # https://en.cppreference.com/w/cpp/compiler_support |
|
continue |
|
elif '++20' in v and not has_cpp20: |
|
continue |
|
# now C |
|
elif '17' in v and not has_cpp2a_c17: |
|
continue |
|
elif '18' in v and not has_c18: |
|
continue |
|
self.init(testdir, extra_args=[f'-D{key!s}={v}']) |
|
cmd = self.get_compdb()[0]['command'] |
|
# c++03 and gnu++03 are not understood by ICC, don't try to look for them |
|
skiplist = frozenset([ |
|
('intel', 'c++03'), |
|
('intel', 'gnu++03')]) |
|
if v != 'none' and not (compiler.get_id(), v) in skiplist: |
|
cmd_std = f" -std={v} " |
|
self.assertIn(cmd_std, cmd) |
|
try: |
|
self.build() |
|
except Exception: |
|
print(f'{key!s} was {v!r}') |
|
raise |
|
self.wipe() |
|
# Check that an invalid std option in CFLAGS/CPPFLAGS fails |
|
# Needed because by default ICC ignores invalid options |
|
cmd_std = '-std=FAIL' |
|
if compiler.language == 'c': |
|
env_flag_name = 'CFLAGS' |
|
elif compiler.language == 'cpp': |
|
env_flag_name = 'CXXFLAGS' |
|
else: |
|
raise NotImplementedError(f'Language {compiler.language} not defined.') |
|
env = {} |
|
env[env_flag_name] = cmd_std |
|
with self.assertRaises((subprocess.CalledProcessError, mesonbuild.mesonlib.EnvironmentException), |
|
msg='C compiler should have failed with -std=FAIL'): |
|
self.init(testdir, override_envvars = env) |
|
# ICC won't fail in the above because additional flags are needed to |
|
# make unknown -std=... options errors. |
|
self.build() |
|
|
|
def test_compiler_c_stds(self): |
|
''' |
|
Test that C stds specified for this compiler can all be used. Can't be |
|
an ordinary test because it requires passing options to meson. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
self._test_stds_impl(testdir, cc) |
|
|
|
def test_compiler_cpp_stds(self): |
|
''' |
|
Test that C++ stds specified for this compiler can all be used. Can't |
|
be an ordinary test because it requires passing options to meson. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '2 cpp') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cpp = detect_cpp_compiler(env, MachineChoice.HOST) |
|
self._test_stds_impl(testdir, cpp) |
|
|
|
def test_unity_subproj(self): |
|
testdir = os.path.join(self.common_test_dir, '42 subproject') |
|
self.init(testdir, extra_args='--unity=subprojects') |
|
pdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/simpletest*.p')) |
|
self.assertEqual(len(pdirs), 1) |
|
self.assertPathExists(os.path.join(pdirs[0], 'simpletest-unity0.c')) |
|
sdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/*sublib*.p')) |
|
self.assertEqual(len(sdirs), 1) |
|
self.assertPathExists(os.path.join(sdirs[0], 'sublib-unity0.c')) |
|
self.assertPathDoesNotExist(os.path.join(self.builddir, 'user@exe/user-unity.c')) |
|
self.build() |
|
|
|
def test_installed_modes(self): |
|
''' |
|
Test that files installed by these tests have the correct permissions. |
|
Can't be an ordinary test because our installed_files.txt is very basic. |
|
''' |
|
# Test file modes |
|
testdir = os.path.join(self.common_test_dir, '12 data') |
|
self.init(testdir) |
|
self.install() |
|
|
|
f = os.path.join(self.installdir, 'etc', 'etcfile.dat') |
|
found_mode = stat.filemode(os.stat(f).st_mode) |
|
want_mode = 'rw------T' |
|
self.assertEqual(want_mode, found_mode[1:]) |
|
|
|
f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh') |
|
statf = os.stat(f) |
|
found_mode = stat.filemode(statf.st_mode) |
|
want_mode = 'rwxr-sr-x' |
|
self.assertEqual(want_mode, found_mode[1:]) |
|
if os.getuid() == 0: |
|
# The chown failed nonfatally if we're not root |
|
self.assertEqual(0, statf.st_uid) |
|
self.assertEqual(0, statf.st_gid) |
|
|
|
f = os.path.join(self.installdir, 'usr', 'share', 'progname', |
|
'fileobject_datafile.dat') |
|
orig = os.path.join(testdir, 'fileobject_datafile.dat') |
|
statf = os.stat(f) |
|
statorig = os.stat(orig) |
|
found_mode = stat.filemode(statf.st_mode) |
|
orig_mode = stat.filemode(statorig.st_mode) |
|
self.assertEqual(orig_mode[1:], found_mode[1:]) |
|
self.assertEqual(os.getuid(), statf.st_uid) |
|
if os.getuid() == 0: |
|
# The chown failed nonfatally if we're not root |
|
self.assertEqual(0, statf.st_gid) |
|
|
|
self.wipe() |
|
# Test directory modes |
|
testdir = os.path.join(self.common_test_dir, '59 install subdir') |
|
self.init(testdir) |
|
self.install() |
|
|
|
f = os.path.join(self.installdir, 'usr', 'share', 'sub1', 'second.dat') |
|
statf = os.stat(f) |
|
found_mode = stat.filemode(statf.st_mode) |
|
want_mode = 'rwxr-x--t' |
|
self.assertEqual(want_mode, found_mode[1:]) |
|
if os.getuid() == 0: |
|
# The chown failed nonfatally if we're not root |
|
self.assertEqual(0, statf.st_uid) |
|
|
|
def test_installed_modes_extended(self): |
|
''' |
|
Test that files are installed with correct permissions using install_mode. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '190 install_mode') |
|
self.init(testdir) |
|
self.build() |
|
self.install() |
|
|
|
for fsobj, want_mode in [ |
|
('bin', 'drwxr-x---'), |
|
('bin/runscript.sh', '-rwxr-sr-x'), |
|
('bin/trivialprog', '-rwxr-sr-x'), |
|
('include', 'drwxr-x---'), |
|
('include/config.h', '-rw-rwSr--'), |
|
('include/rootdir.h', '-r--r--r-T'), |
|
('lib', 'drwxr-x---'), |
|
('lib/libstat.a', '-rw---Sr--'), |
|
('share', 'drwxr-x---'), |
|
('share/man', 'drwxr-x---'), |
|
('share/man/man1', 'drwxr-x---'), |
|
('share/man/man1/foo.1', '-r--r--r-T'), |
|
('share/sub1', 'drwxr-x---'), |
|
('share/sub1/second.dat', '-rwxr-x--t'), |
|
('subdir', 'drwxr-x---'), |
|
('subdir/data.dat', '-rw-rwSr--'), |
|
]: |
|
f = os.path.join(self.installdir, 'usr', *fsobj.split('/')) |
|
found_mode = stat.filemode(os.stat(f).st_mode) |
|
self.assertEqual(want_mode, found_mode, |
|
msg=('Expected file %s to have mode %s but found %s instead.' % |
|
(fsobj, want_mode, found_mode))) |
|
# Ensure that introspect --installed works on all types of files |
|
# FIXME: also verify the files list |
|
self.introspect('--installed') |
|
|
|
def test_install_umask(self): |
|
''' |
|
Test that files are installed with correct permissions using default |
|
install umask of 022, regardless of the umask at time the worktree |
|
was checked out or the build was executed. |
|
''' |
|
# Copy source tree to a temporary directory and change permissions |
|
# there to simulate a checkout with umask 002. |
|
orig_testdir = os.path.join(self.unit_test_dir, '26 install umask') |
|
# Create a new testdir under tmpdir. |
|
tmpdir = os.path.realpath(tempfile.mkdtemp()) |
|
self.addCleanup(windows_proof_rmtree, tmpdir) |
|
testdir = os.path.join(tmpdir, '26 install umask') |
|
# Copy the tree using shutil.copyfile, which will use the current umask |
|
# instead of preserving permissions of the old tree. |
|
save_umask = os.umask(0o002) |
|
self.addCleanup(os.umask, save_umask) |
|
shutil.copytree(orig_testdir, testdir, copy_function=shutil.copyfile) |
|
# Preserve the executable status of subdir/sayhello though. |
|
os.chmod(os.path.join(testdir, 'subdir', 'sayhello'), 0o775) |
|
self.init(testdir) |
|
# Run the build under a 027 umask now. |
|
os.umask(0o027) |
|
self.build() |
|
# And keep umask 027 for the install step too. |
|
self.install() |
|
|
|
for executable in [ |
|
'bin/prog', |
|
'share/subdir/sayhello', |
|
]: |
|
f = os.path.join(self.installdir, 'usr', *executable.split('/')) |
|
found_mode = stat.filemode(os.stat(f).st_mode) |
|
want_mode = '-rwxr-xr-x' |
|
self.assertEqual(want_mode, found_mode, |
|
msg=('Expected file %s to have mode %s but found %s instead.' % |
|
(executable, want_mode, found_mode))) |
|
|
|
for directory in [ |
|
'usr', |
|
'usr/bin', |
|
'usr/include', |
|
'usr/share', |
|
'usr/share/man', |
|
'usr/share/man/man1', |
|
'usr/share/subdir', |
|
]: |
|
f = os.path.join(self.installdir, *directory.split('/')) |
|
found_mode = stat.filemode(os.stat(f).st_mode) |
|
want_mode = 'drwxr-xr-x' |
|
self.assertEqual(want_mode, found_mode, |
|
msg=('Expected directory %s to have mode %s but found %s instead.' % |
|
(directory, want_mode, found_mode))) |
|
|
|
for datafile in [ |
|
'include/sample.h', |
|
'share/datafile.cat', |
|
'share/file.dat', |
|
'share/man/man1/prog.1', |
|
'share/subdir/datafile.dog', |
|
]: |
|
f = os.path.join(self.installdir, 'usr', *datafile.split('/')) |
|
found_mode = stat.filemode(os.stat(f).st_mode) |
|
want_mode = '-rw-r--r--' |
|
self.assertEqual(want_mode, found_mode, |
|
msg=('Expected file %s to have mode %s but found %s instead.' % |
|
(datafile, want_mode, found_mode))) |
|
|
|
def test_cpp_std_override(self): |
|
testdir = os.path.join(self.unit_test_dir, '6 std override') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
# Don't try to use -std=c++03 as a check for the |
|
# presence of a compiler flag, as ICC does not |
|
# support it. |
|
for i in compdb: |
|
if 'prog98' in i['file']: |
|
c98_comp = i['command'] |
|
if 'prog11' in i['file']: |
|
c11_comp = i['command'] |
|
if 'progp' in i['file']: |
|
plain_comp = i['command'] |
|
self.assertNotEqual(len(plain_comp), 0) |
|
self.assertIn('-std=c++98', c98_comp) |
|
self.assertNotIn('-std=c++11', c98_comp) |
|
self.assertIn('-std=c++11', c11_comp) |
|
self.assertNotIn('-std=c++98', c11_comp) |
|
self.assertNotIn('-std=c++98', plain_comp) |
|
self.assertNotIn('-std=c++11', plain_comp) |
|
# Now werror |
|
self.assertIn('-Werror', plain_comp) |
|
self.assertNotIn('-Werror', c98_comp) |
|
|
|
def test_run_installed(self): |
|
if is_cygwin() or is_osx(): |
|
raise unittest.SkipTest('LD_LIBRARY_PATH and RPATH not applicable') |
|
|
|
testdir = os.path.join(self.unit_test_dir, '7 run installed') |
|
self.init(testdir) |
|
self.build() |
|
self.install() |
|
installed_exe = os.path.join(self.installdir, 'usr/bin/prog') |
|
installed_libdir = os.path.join(self.installdir, 'usr/foo') |
|
installed_lib = os.path.join(installed_libdir, 'libfoo.so') |
|
self.assertTrue(os.path.isfile(installed_exe)) |
|
self.assertTrue(os.path.isdir(installed_libdir)) |
|
self.assertTrue(os.path.isfile(installed_lib)) |
|
# Must fail when run without LD_LIBRARY_PATH to ensure that |
|
# rpath has been properly stripped rather than pointing to the builddir. |
|
self.assertNotEqual(subprocess.call(installed_exe, stderr=subprocess.DEVNULL), 0) |
|
# When LD_LIBRARY_PATH is set it should start working. |
|
# For some reason setting LD_LIBRARY_PATH in os.environ fails |
|
# when all tests are run (but works when only this test is run), |
|
# but doing this explicitly works. |
|
env = os.environ.copy() |
|
env['LD_LIBRARY_PATH'] = ':'.join([installed_libdir, env.get('LD_LIBRARY_PATH', '')]) |
|
self.assertEqual(subprocess.call(installed_exe, env=env), 0) |
|
# Ensure that introspect --installed works |
|
installed = self.introspect('--installed') |
|
for v in installed.values(): |
|
self.assertTrue('prog' in v or 'foo' in v) |
|
|
|
@skipIfNoPkgconfig |
|
def test_order_of_l_arguments(self): |
|
testdir = os.path.join(self.unit_test_dir, '8 -L -l order') |
|
self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir}) |
|
# NOTE: .pc file has -Lfoo -lfoo -Lbar -lbar but pkg-config reorders |
|
# the flags before returning them to -Lfoo -Lbar -lfoo -lbar |
|
# but pkgconf seems to not do that. Sigh. Support both. |
|
expected_order = [('-L/me/first', '-lfoo1'), |
|
('-L/me/second', '-lfoo2'), |
|
('-L/me/first', '-L/me/second'), |
|
('-lfoo1', '-lfoo2'), |
|
('-L/me/second', '-L/me/third'), |
|
('-L/me/third', '-L/me/fourth',), |
|
('-L/me/third', '-lfoo3'), |
|
('-L/me/fourth', '-lfoo4'), |
|
('-lfoo3', '-lfoo4'), |
|
] |
|
with open(os.path.join(self.builddir, 'build.ninja')) as ifile: |
|
for line in ifile: |
|
if expected_order[0][0] in line: |
|
for first, second in expected_order: |
|
self.assertLess(line.index(first), line.index(second)) |
|
return |
|
raise RuntimeError('Linker entries not found in the Ninja file.') |
|
|
|
def test_introspect_dependencies(self): |
|
''' |
|
Tests that mesonintrospect --dependencies returns expected output. |
|
''' |
|
testdir = os.path.join(self.framework_test_dir, '7 gnome') |
|
self.init(testdir) |
|
glib_found = False |
|
gobject_found = False |
|
deps = self.introspect('--dependencies') |
|
self.assertIsInstance(deps, list) |
|
for dep in deps: |
|
self.assertIsInstance(dep, dict) |
|
self.assertIn('name', dep) |
|
self.assertIn('compile_args', dep) |
|
self.assertIn('link_args', dep) |
|
if dep['name'] == 'glib-2.0': |
|
glib_found = True |
|
elif dep['name'] == 'gobject-2.0': |
|
gobject_found = True |
|
self.assertTrue(glib_found) |
|
self.assertTrue(gobject_found) |
|
if subprocess.call(['pkg-config', '--exists', 'glib-2.0 >= 2.56.2']) != 0: |
|
raise unittest.SkipTest('glib >= 2.56.2 needed for the rest') |
|
targets = self.introspect('--targets') |
|
docbook_target = None |
|
for t in targets: |
|
if t['name'] == 'generated-gdbus-docbook': |
|
docbook_target = t |
|
break |
|
self.assertIsInstance(docbook_target, dict) |
|
self.assertEqual(os.path.basename(t['filename'][0]), 'generated-gdbus-doc-' + os.path.basename(t['target_sources'][0]['sources'][0])) |
|
|
|
def test_introspect_installed(self): |
|
testdir = os.path.join(self.linuxlike_test_dir, '7 library versions') |
|
self.init(testdir) |
|
|
|
install = self.introspect('--installed') |
|
install = {os.path.basename(k): v for k, v in install.items()} |
|
print(install) |
|
if is_osx(): |
|
the_truth = { |
|
'libmodule.dylib': '/usr/lib/libmodule.dylib', |
|
'libnoversion.dylib': '/usr/lib/libnoversion.dylib', |
|
'libonlysoversion.5.dylib': '/usr/lib/libonlysoversion.5.dylib', |
|
'libonlysoversion.dylib': '/usr/lib/libonlysoversion.dylib', |
|
'libonlyversion.1.dylib': '/usr/lib/libonlyversion.1.dylib', |
|
'libonlyversion.dylib': '/usr/lib/libonlyversion.dylib', |
|
'libsome.0.dylib': '/usr/lib/libsome.0.dylib', |
|
'libsome.dylib': '/usr/lib/libsome.dylib', |
|
} |
|
the_truth_2 = {'/usr/lib/libsome.dylib', |
|
'/usr/lib/libsome.0.dylib', |
|
} |
|
else: |
|
the_truth = { |
|
'libmodule.so': '/usr/lib/libmodule.so', |
|
'libnoversion.so': '/usr/lib/libnoversion.so', |
|
'libonlysoversion.so': '/usr/lib/libonlysoversion.so', |
|
'libonlysoversion.so.5': '/usr/lib/libonlysoversion.so.5', |
|
'libonlyversion.so': '/usr/lib/libonlyversion.so', |
|
'libonlyversion.so.1': '/usr/lib/libonlyversion.so.1', |
|
'libonlyversion.so.1.4.5': '/usr/lib/libonlyversion.so.1.4.5', |
|
'libsome.so': '/usr/lib/libsome.so', |
|
'libsome.so.0': '/usr/lib/libsome.so.0', |
|
'libsome.so.1.2.3': '/usr/lib/libsome.so.1.2.3', |
|
} |
|
the_truth_2 = {'/usr/lib/libsome.so', |
|
'/usr/lib/libsome.so.0', |
|
'/usr/lib/libsome.so.1.2.3'} |
|
self.assertDictEqual(install, the_truth) |
|
|
|
targets = self.introspect('--targets') |
|
for t in targets: |
|
if t['name'] != 'some': |
|
continue |
|
self.assertSetEqual(the_truth_2, set(t['install_filename'])) |
|
|
|
def test_build_rpath(self): |
|
if is_cygwin(): |
|
raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH') |
|
testdir = os.path.join(self.unit_test_dir, '10 build_rpath') |
|
self.init(testdir) |
|
self.build() |
|
build_rpath = get_rpath(os.path.join(self.builddir, 'prog')) |
|
self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar') |
|
build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx')) |
|
self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar') |
|
self.install() |
|
install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog')) |
|
self.assertEqual(install_rpath, '/baz') |
|
install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx')) |
|
self.assertEqual(install_rpath, 'baz') |
|
|
|
@skipIfNoPkgconfig |
|
def test_build_rpath_pkgconfig(self): |
|
''' |
|
Test that current build artefacts (libs) are found first on the rpath, |
|
manually specified rpath comes second and additional rpath elements (from |
|
pkg-config files) come last |
|
''' |
|
if is_cygwin(): |
|
raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH') |
|
testdir = os.path.join(self.unit_test_dir, '90 pkgconfig build rpath order') |
|
self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir}) |
|
self.build() |
|
build_rpath = get_rpath(os.path.join(self.builddir, 'prog')) |
|
self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy') |
|
build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx')) |
|
self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy') |
|
self.install() |
|
install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog')) |
|
self.assertEqual(install_rpath, '/baz:/foo/dummy') |
|
install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx')) |
|
self.assertEqual(install_rpath, 'baz:/foo/dummy') |
|
|
|
def test_global_rpath(self): |
|
if is_cygwin(): |
|
raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH') |
|
if is_osx(): |
|
raise unittest.SkipTest('Global RPATHs via LDFLAGS not yet supported on MacOS (does anybody need it?)') |
|
|
|
testdir = os.path.join(self.unit_test_dir, '80 global-rpath') |
|
oldinstalldir = self.installdir |
|
|
|
# Build and install an external library without DESTDIR. |
|
# The external library generates a .pc file without an rpath. |
|
yonder_dir = os.path.join(testdir, 'yonder') |
|
yonder_prefix = os.path.join(oldinstalldir, 'yonder') |
|
yonder_libdir = os.path.join(yonder_prefix, self.libdir) |
|
self.prefix = yonder_prefix |
|
self.installdir = yonder_prefix |
|
self.init(yonder_dir) |
|
self.build() |
|
self.install(use_destdir=False) |
|
|
|
# Since rpath has multiple valid formats we need to |
|
# test that they are all properly used. |
|
rpath_formats = [ |
|
('-Wl,-rpath=', False), |
|
('-Wl,-rpath,', False), |
|
('-Wl,--just-symbols=', True), |
|
('-Wl,--just-symbols,', True), |
|
('-Wl,-R', False), |
|
('-Wl,-R,', False) |
|
] |
|
for rpath_format, exception in rpath_formats: |
|
# Build an app that uses that installed library. |
|
# Supply the rpath to the installed library via LDFLAGS |
|
# (as systems like buildroot and guix are wont to do) |
|
# and verify install preserves that rpath. |
|
self.new_builddir() |
|
env = {'LDFLAGS': rpath_format + yonder_libdir, |
|
'PKG_CONFIG_PATH': os.path.join(yonder_libdir, 'pkgconfig')} |
|
if exception: |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.init(testdir, override_envvars=env) |
|
continue |
|
self.init(testdir, override_envvars=env) |
|
self.build() |
|
self.install(use_destdir=False) |
|
got_rpath = get_rpath(os.path.join(yonder_prefix, 'bin/rpathified')) |
|
self.assertEqual(got_rpath, yonder_libdir, rpath_format) |
|
|
|
@skip_if_not_base_option('b_sanitize') |
|
def test_pch_with_address_sanitizer(self): |
|
if is_cygwin(): |
|
raise unittest.SkipTest('asan not available on Cygwin') |
|
if is_openbsd(): |
|
raise unittest.SkipTest('-fsanitize=address is not supported on OpenBSD') |
|
|
|
testdir = os.path.join(self.common_test_dir, '13 pch') |
|
self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false']) |
|
self.build() |
|
compdb = self.get_compdb() |
|
for i in compdb: |
|
self.assertIn("-fsanitize=address", i["command"]) |
|
|
|
def test_cross_find_program(self): |
|
testdir = os.path.join(self.unit_test_dir, '11 cross prog') |
|
crossfile = tempfile.NamedTemporaryFile(mode='w') |
|
print(os.path.join(testdir, 'some_cross_tool.py')) |
|
|
|
tool_path = os.path.join(testdir, 'some_cross_tool.py') |
|
|
|
crossfile.write(textwrap.dedent(f'''\ |
|
[binaries] |
|
c = '{shutil.which('gcc' if is_sunos() else 'cc')}' |
|
ar = '{shutil.which('ar')}' |
|
strip = '{shutil.which('strip')}' |
|
sometool.py = ['{tool_path}'] |
|
someothertool.py = '{tool_path}' |
|
|
|
[properties] |
|
|
|
[host_machine] |
|
system = 'linux' |
|
cpu_family = 'arm' |
|
cpu = 'armv7' # Not sure if correct. |
|
endian = 'little' |
|
''')) |
|
crossfile.flush() |
|
self.meson_cross_file = crossfile.name |
|
self.init(testdir) |
|
|
|
def test_reconfigure(self): |
|
testdir = os.path.join(self.unit_test_dir, '13 reconfigure') |
|
self.init(testdir, extra_args=['-Db_coverage=true'], default_args=False) |
|
self.build('reconfigure') |
|
|
|
def test_vala_generated_source_buildir_inside_source_tree(self): |
|
''' |
|
Test that valac outputs generated C files in the expected location when |
|
the builddir is a subdir of the source tree. |
|
''' |
|
if not shutil.which('valac'): |
|
raise unittest.SkipTest('valac not installed.') |
|
|
|
testdir = os.path.join(self.vala_test_dir, '8 generated sources') |
|
newdir = os.path.join(self.builddir, 'srctree') |
|
shutil.copytree(testdir, newdir) |
|
testdir = newdir |
|
# New builddir |
|
builddir = os.path.join(testdir, 'subdir/_build') |
|
os.makedirs(builddir, exist_ok=True) |
|
self.change_builddir(builddir) |
|
self.init(testdir) |
|
self.build() |
|
|
|
def test_old_gnome_module_codepaths(self): |
|
''' |
|
A lot of code in the GNOME module is conditional on the version of the |
|
glib tools that are installed, and breakages in the old code can slip |
|
by once the CI has a newer glib version. So we force the GNOME module |
|
to pretend that it's running on an ancient glib so the fallback code is |
|
also tested. |
|
''' |
|
testdir = os.path.join(self.framework_test_dir, '7 gnome') |
|
mesonbuild.modules.gnome.native_glib_version = '2.20' |
|
env = {'MESON_UNIT_TEST_PRETEND_GLIB_OLD': "1"} |
|
try: |
|
self.init(testdir, |
|
inprocess=True, |
|
override_envvars=env) |
|
self.build(override_envvars=env) |
|
finally: |
|
mesonbuild.modules.gnome.native_glib_version = None |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_usage(self): |
|
testdir1 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency') |
|
testdir2 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependee') |
|
if subprocess.call(['pkg-config', '--cflags', 'glib-2.0'], |
|
stdout=subprocess.DEVNULL, |
|
stderr=subprocess.DEVNULL) != 0: |
|
raise unittest.SkipTest('Glib 2.0 dependency not available.') |
|
with tempfile.TemporaryDirectory() as tempdirname: |
|
self.init(testdir1, extra_args=['--prefix=' + tempdirname, '--libdir=lib'], default_args=False) |
|
self.install(use_destdir=False) |
|
shutil.rmtree(self.builddir) |
|
os.mkdir(self.builddir) |
|
pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig') |
|
self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'libpkgdep.pc'))) |
|
lib_dir = os.path.join(tempdirname, 'lib') |
|
myenv = os.environ.copy() |
|
myenv['PKG_CONFIG_PATH'] = pkg_dir |
|
# Private internal libraries must not leak out. |
|
pkg_out = subprocess.check_output(['pkg-config', '--static', '--libs', 'libpkgdep'], env=myenv) |
|
self.assertFalse(b'libpkgdep-int' in pkg_out, 'Internal library leaked out.') |
|
# Dependencies must not leak to cflags when building only a shared library. |
|
pkg_out = subprocess.check_output(['pkg-config', '--cflags', 'libpkgdep'], env=myenv) |
|
self.assertFalse(b'glib' in pkg_out, 'Internal dependency leaked to headers.') |
|
# Test that the result is usable. |
|
self.init(testdir2, override_envvars=myenv) |
|
self.build(override_envvars=myenv) |
|
myenv = os.environ.copy() |
|
myenv['LD_LIBRARY_PATH'] = ':'.join([lib_dir, myenv.get('LD_LIBRARY_PATH', '')]) |
|
if is_cygwin(): |
|
bin_dir = os.path.join(tempdirname, 'bin') |
|
myenv['PATH'] = bin_dir + os.pathsep + myenv['PATH'] |
|
self.assertTrue(os.path.isdir(lib_dir)) |
|
test_exe = os.path.join(self.builddir, 'pkguser') |
|
self.assertTrue(os.path.isfile(test_exe)) |
|
subprocess.check_call(test_exe, env=myenv) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_relative_paths(self): |
|
testdir = os.path.join(self.unit_test_dir, '62 pkgconfig relative paths') |
|
pkg_dir = os.path.join(testdir, 'pkgconfig') |
|
self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'librelativepath.pc'))) |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='') |
|
kwargs = {'required': True, 'silent': True} |
|
relative_path_dep = PkgConfigDependency('librelativepath', env, kwargs) |
|
self.assertTrue(relative_path_dep.found()) |
|
|
|
# Ensure link_args are properly quoted |
|
libpath = Path(self.builddir) / '../relativepath/lib' |
|
link_args = ['-L' + libpath.as_posix(), '-lrelativepath'] |
|
self.assertEqual(relative_path_dep.get_link_args(), link_args) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_duplicate_path_entries(self): |
|
testdir = os.path.join(self.unit_test_dir, '111 pkgconfig duplicate path entries') |
|
pkg_dir = os.path.join(testdir, 'pkgconfig') |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='') |
|
|
|
PkgConfigDependency.setup_env({}, env, MachineChoice.HOST, pkg_dir) |
|
pkg_config_path = env.coredata.options[OptionKey('pkg_config_path')].value |
|
self.assertTrue(len(pkg_config_path) == 1) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_internal_libraries(self): |
|
''' |
|
''' |
|
with tempfile.TemporaryDirectory() as tempdirname: |
|
# build library |
|
testdirbase = os.path.join(self.unit_test_dir, '32 pkgconfig use libraries') |
|
testdirlib = os.path.join(testdirbase, 'lib') |
|
self.init(testdirlib, extra_args=['--prefix=' + tempdirname, |
|
'--libdir=lib', |
|
'--default-library=static'], default_args=False) |
|
self.build() |
|
self.install(use_destdir=False) |
|
|
|
# build user of library |
|
pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig') |
|
self.new_builddir() |
|
self.init(os.path.join(testdirbase, 'app'), |
|
override_envvars={'PKG_CONFIG_PATH': pkg_dir}) |
|
self.build() |
|
|
|
@skipIfNoPkgconfig |
|
def test_static_archive_stripping(self): |
|
''' |
|
Check that Meson produces valid static archives with --strip enabled |
|
''' |
|
with tempfile.TemporaryDirectory() as tempdirname: |
|
testdirbase = os.path.join(self.unit_test_dir, '66 static archive stripping') |
|
|
|
# build lib |
|
self.new_builddir() |
|
testdirlib = os.path.join(testdirbase, 'lib') |
|
testlibprefix = os.path.join(tempdirname, 'libprefix') |
|
self.init(testdirlib, extra_args=['--prefix=' + testlibprefix, |
|
'--libdir=lib', |
|
'--default-library=static', |
|
'--buildtype=debug', |
|
'--strip'], default_args=False) |
|
self.build() |
|
self.install(use_destdir=False) |
|
|
|
# build executable (uses lib, fails if static archive has been stripped incorrectly) |
|
pkg_dir = os.path.join(testlibprefix, 'lib/pkgconfig') |
|
self.new_builddir() |
|
self.init(os.path.join(testdirbase, 'app'), |
|
override_envvars={'PKG_CONFIG_PATH': pkg_dir}) |
|
self.build() |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_formatting(self): |
|
testdir = os.path.join(self.unit_test_dir, '38 pkgconfig format') |
|
self.init(testdir) |
|
myenv = os.environ.copy() |
|
myenv['PKG_CONFIG_PATH'] = self.privatedir |
|
stdo = subprocess.check_output(['pkg-config', '--libs-only-l', 'libsomething'], env=myenv) |
|
deps = [b'-lgobject-2.0', b'-lgio-2.0', b'-lglib-2.0', b'-lsomething'] |
|
if is_windows() or is_cygwin() or is_osx() or is_openbsd(): |
|
# On Windows, libintl is a separate library |
|
deps.append(b'-lintl') |
|
self.assertEqual(set(deps), set(stdo.split())) |
|
|
|
@skipIfNoPkgconfig |
|
@skip_if_not_language('cs') |
|
def test_pkgconfig_csharp_library(self): |
|
testdir = os.path.join(self.unit_test_dir, '50 pkgconfig csharp library') |
|
self.init(testdir) |
|
myenv = os.environ.copy() |
|
myenv['PKG_CONFIG_PATH'] = self.privatedir |
|
stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv) |
|
|
|
self.assertEqual("-r/usr/lib/libsomething.dll", str(stdo.decode('ascii')).strip()) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_link_order(self): |
|
''' |
|
Test that libraries are listed before their dependencies. |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '53 pkgconfig static link order') |
|
self.init(testdir) |
|
myenv = os.environ.copy() |
|
myenv['PKG_CONFIG_PATH'] = self.privatedir |
|
stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv) |
|
deps = stdo.split() |
|
self.assertTrue(deps.index(b'-lsomething') < deps.index(b'-ldependency')) |
|
|
|
def test_deterministic_dep_order(self): |
|
''' |
|
Test that the dependencies are always listed in a deterministic order. |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '43 dep order') |
|
self.init(testdir) |
|
with open(os.path.join(self.builddir, 'build.ninja')) as bfile: |
|
for line in bfile: |
|
if 'build myexe:' in line or 'build myexe.exe:' in line: |
|
self.assertIn('liblib1.a liblib2.a', line) |
|
return |
|
raise RuntimeError('Could not find the build rule') |
|
|
|
def test_deterministic_rpath_order(self): |
|
''' |
|
Test that the rpaths are always listed in a deterministic order. |
|
''' |
|
if is_cygwin(): |
|
raise unittest.SkipTest('rpath are not used on Cygwin') |
|
testdir = os.path.join(self.unit_test_dir, '42 rpath order') |
|
self.init(testdir) |
|
if is_osx(): |
|
rpathre = re.compile(r'-rpath,.*/subprojects/sub1.*-rpath,.*/subprojects/sub2') |
|
else: |
|
rpathre = re.compile(r'-rpath,\$\$ORIGIN/subprojects/sub1:\$\$ORIGIN/subprojects/sub2') |
|
with open(os.path.join(self.builddir, 'build.ninja')) as bfile: |
|
for line in bfile: |
|
if '-rpath' in line: |
|
self.assertRegex(line, rpathre) |
|
return |
|
raise RuntimeError('Could not find the rpath') |
|
|
|
def test_override_with_exe_dep(self): |
|
''' |
|
Test that we produce the correct dependencies when a program is overridden with an executable. |
|
''' |
|
testdir = os.path.join(self.src_root, 'test cases', 'native', '9 override with exe') |
|
self.init(testdir) |
|
with open(os.path.join(self.builddir, 'build.ninja')) as bfile: |
|
for line in bfile: |
|
if 'main1.c:' in line or 'main2.c:' in line: |
|
self.assertIn('| subprojects/sub/foobar', line) |
|
|
|
@skipIfNoPkgconfig |
|
def test_usage_external_library(self): |
|
''' |
|
Test that uninstalled usage of an external library (from the system or |
|
PkgConfigDependency) works. On macOS, this workflow works out of the |
|
box. On Linux, BSDs, Windows, etc, you need to set extra arguments such |
|
as LD_LIBRARY_PATH, etc, so this test is skipped. |
|
|
|
The system library is found with cc.find_library() and pkg-config deps. |
|
''' |
|
oldprefix = self.prefix |
|
# Install external library so we can find it |
|
testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'external library') |
|
# install into installdir without using DESTDIR |
|
installdir = self.installdir |
|
self.prefix = installdir |
|
self.init(testdir) |
|
self.prefix = oldprefix |
|
self.build() |
|
self.install(use_destdir=False) |
|
## New builddir for the consumer |
|
self.new_builddir() |
|
env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir), |
|
'PKG_CONFIG_PATH': os.path.join(installdir, self.libdir, 'pkgconfig')} |
|
testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'built library') |
|
# install into installdir without using DESTDIR |
|
self.prefix = self.installdir |
|
self.init(testdir, override_envvars=env) |
|
self.prefix = oldprefix |
|
self.build(override_envvars=env) |
|
# test uninstalled |
|
self.run_tests(override_envvars=env) |
|
if not (is_osx() or is_linux()): |
|
return |
|
# test running after installation |
|
self.install(use_destdir=False) |
|
prog = os.path.join(self.installdir, 'bin', 'prog') |
|
self._run([prog]) |
|
if not is_osx(): |
|
# Rest of the workflow only works on macOS |
|
return |
|
out = self._run(['otool', '-L', prog]) |
|
self.assertNotIn('@rpath', out) |
|
## New builddir for testing that DESTDIR is not added to install_name |
|
self.new_builddir() |
|
# install into installdir with DESTDIR |
|
self.init(testdir, override_envvars=env) |
|
self.build(override_envvars=env) |
|
# test running after installation |
|
self.install(override_envvars=env) |
|
prog = self.installdir + os.path.join(self.prefix, 'bin', 'prog') |
|
lib = self.installdir + os.path.join(self.prefix, 'lib', 'libbar_built.dylib') |
|
for f in prog, lib: |
|
out = self._run(['otool', '-L', f]) |
|
# Ensure that the otool output does not contain self.installdir |
|
self.assertNotRegex(out, self.installdir + '.*dylib ') |
|
|
|
@skipIfNoPkgconfig |
|
def test_usage_pkgconfig_prefixes(self): |
|
''' |
|
Build and install two external libraries, to different prefixes, |
|
then build and install a client program that finds them via pkgconfig, |
|
and verify the installed client program runs. |
|
''' |
|
oldinstalldir = self.installdir |
|
|
|
# Build and install both external libraries without DESTDIR |
|
val1dir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'val1') |
|
val1prefix = os.path.join(oldinstalldir, 'val1') |
|
self.prefix = val1prefix |
|
self.installdir = val1prefix |
|
self.init(val1dir) |
|
self.build() |
|
self.install(use_destdir=False) |
|
self.new_builddir() |
|
|
|
env1 = {} |
|
env1['PKG_CONFIG_PATH'] = os.path.join(val1prefix, self.libdir, 'pkgconfig') |
|
val2dir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'val2') |
|
val2prefix = os.path.join(oldinstalldir, 'val2') |
|
self.prefix = val2prefix |
|
self.installdir = val2prefix |
|
self.init(val2dir, override_envvars=env1) |
|
self.build() |
|
self.install(use_destdir=False) |
|
self.new_builddir() |
|
|
|
# Build, install, and run the client program |
|
env2 = {} |
|
env2['PKG_CONFIG_PATH'] = os.path.join(val2prefix, self.libdir, 'pkgconfig') |
|
testdir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'client') |
|
testprefix = os.path.join(oldinstalldir, 'client') |
|
self.prefix = testprefix |
|
self.installdir = testprefix |
|
self.init(testdir, override_envvars=env2) |
|
self.build() |
|
self.install(use_destdir=False) |
|
prog = os.path.join(self.installdir, 'bin', 'client') |
|
env3 = {} |
|
if is_cygwin(): |
|
env3['PATH'] = os.path.join(val1prefix, 'bin') + \ |
|
os.pathsep + \ |
|
os.path.join(val2prefix, 'bin') + \ |
|
os.pathsep + os.environ['PATH'] |
|
out = self._run([prog], override_envvars=env3).strip() |
|
# Expected output is val1 + val2 = 3 |
|
self.assertEqual(out, '3') |
|
|
|
def install_subdir_invalid_symlinks(self, testdir, subdir_path): |
|
''' |
|
Test that installation of broken symlinks works fine. |
|
https://github.com/mesonbuild/meson/issues/3914 |
|
''' |
|
testdir = os.path.join(self.common_test_dir, testdir) |
|
subdir = os.path.join(testdir, subdir_path) |
|
with chdir(subdir): |
|
# Can't distribute broken symlinks in the source tree because it breaks |
|
# the creation of zipapps. Create it dynamically and run the test by |
|
# hand. |
|
src = '../../nonexistent.txt' |
|
os.symlink(src, 'invalid-symlink.txt') |
|
try: |
|
self.init(testdir) |
|
self.build() |
|
self.install() |
|
install_path = subdir_path.split(os.path.sep)[-1] |
|
link = os.path.join(self.installdir, 'usr', 'share', install_path, 'invalid-symlink.txt') |
|
self.assertTrue(os.path.islink(link), msg=link) |
|
self.assertEqual(src, os.readlink(link)) |
|
self.assertFalse(os.path.isfile(link), msg=link) |
|
finally: |
|
os.remove(os.path.join(subdir, 'invalid-symlink.txt')) |
|
|
|
def test_install_subdir_symlinks(self): |
|
self.install_subdir_invalid_symlinks('59 install subdir', os.path.join('sub', 'sub1')) |
|
|
|
def test_install_subdir_symlinks_with_default_umask(self): |
|
self.install_subdir_invalid_symlinks('190 install_mode', 'sub2') |
|
|
|
def test_install_subdir_symlinks_with_default_umask_and_mode(self): |
|
self.install_subdir_invalid_symlinks('190 install_mode', 'sub1') |
|
|
|
@skipIfNoPkgconfigDep('gmodule-2.0') |
|
def test_ldflag_dedup(self): |
|
testdir = os.path.join(self.unit_test_dir, '52 ldflagdedup') |
|
if is_cygwin() or is_osx(): |
|
raise unittest.SkipTest('Not applicable on Cygwin or OSX.') |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
linker = cc.linker |
|
if not linker.export_dynamic_args(env): |
|
raise unittest.SkipTest('Not applicable for linkers without --export-dynamic') |
|
self.init(testdir) |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
max_count = 0 |
|
search_term = '-Wl,--export-dynamic' |
|
with open(build_ninja, encoding='utf-8') as f: |
|
for line in f: |
|
max_count = max(max_count, line.count(search_term)) |
|
self.assertEqual(max_count, 1, 'Export dynamic incorrectly deduplicated.') |
|
|
|
def test_compiler_libs_static_dedup(self): |
|
testdir = os.path.join(self.unit_test_dir, '56 dedup compiler libs') |
|
self.init(testdir) |
|
build_ninja = os.path.join(self.builddir, 'build.ninja') |
|
with open(build_ninja, encoding='utf-8') as f: |
|
lines = f.readlines() |
|
for lib in ('-ldl', '-lm', '-lc', '-lrt'): |
|
for line in lines: |
|
if lib not in line: |
|
continue |
|
# Assert that |
|
self.assertEqual(len(line.split(lib)), 2, msg=(lib, line)) |
|
|
|
@skipIfNoPkgconfig |
|
def test_noncross_options(self): |
|
# C_std defined in project options must be in effect also when native compiling. |
|
testdir = os.path.join(self.unit_test_dir, '51 noncross options') |
|
self.init(testdir, extra_args=['-Dpkg_config_path=' + testdir]) |
|
compdb = self.get_compdb() |
|
self.assertEqual(len(compdb), 2) |
|
self.assertRegex(compdb[0]['command'], '-std=c99') |
|
self.assertRegex(compdb[1]['command'], '-std=c99') |
|
self.build() |
|
|
|
def test_identity_cross(self): |
|
testdir = os.path.join(self.unit_test_dir, '61 identity cross') |
|
|
|
nativefile = tempfile.NamedTemporaryFile(mode='w') |
|
nativefile.write(textwrap.dedent('''\ |
|
[binaries] |
|
c = ['{}'] |
|
'''.format(os.path.join(testdir, 'build_wrapper.py')))) |
|
nativefile.flush() |
|
self.meson_native_file = nativefile.name |
|
|
|
crossfile = tempfile.NamedTemporaryFile(mode='w') |
|
crossfile.write(textwrap.dedent('''\ |
|
[binaries] |
|
c = ['{}'] |
|
'''.format(os.path.join(testdir, 'host_wrapper.py')))) |
|
crossfile.flush() |
|
self.meson_cross_file = crossfile.name |
|
|
|
# TODO should someday be explicit about build platform only here |
|
self.init(testdir) |
|
|
|
def test_identity_cross_env(self): |
|
testdir = os.path.join(self.unit_test_dir, '61 identity cross') |
|
env = { |
|
'CC_FOR_BUILD': '"' + os.path.join(testdir, 'build_wrapper.py') + '"', |
|
} |
|
crossfile = tempfile.NamedTemporaryFile(mode='w') |
|
crossfile.write(textwrap.dedent('''\ |
|
[binaries] |
|
c = ['{}'] |
|
'''.format(os.path.join(testdir, 'host_wrapper.py')))) |
|
crossfile.flush() |
|
self.meson_cross_file = crossfile.name |
|
# TODO should someday be explicit about build platform only here |
|
self.init(testdir, override_envvars=env) |
|
|
|
@skipIfNoPkgconfig |
|
def test_static_link(self): |
|
if is_cygwin(): |
|
raise unittest.SkipTest("Cygwin doesn't support LD_LIBRARY_PATH.") |
|
|
|
# Build some libraries and install them |
|
testdir = os.path.join(self.unit_test_dir, '67 static link/lib') |
|
libdir = os.path.join(self.installdir, self.libdir) |
|
oldprefix = self.prefix |
|
self.prefix = self.installdir |
|
self.init(testdir) |
|
self.install(use_destdir=False) |
|
|
|
# Test that installed libraries works |
|
self.new_builddir() |
|
self.prefix = oldprefix |
|
meson_args = [f'-Dc_link_args=-L{libdir}', |
|
'--fatal-meson-warnings'] |
|
testdir = os.path.join(self.unit_test_dir, '67 static link') |
|
env = {'PKG_CONFIG_LIBDIR': os.path.join(libdir, 'pkgconfig')} |
|
self.init(testdir, extra_args=meson_args, override_envvars=env) |
|
self.build() |
|
self.run_tests() |
|
|
|
def _check_ld(self, check: str, name: str, lang: str, expected: str) -> None: |
|
if is_sunos(): |
|
raise unittest.SkipTest('Solaris currently cannot override the linker.') |
|
if not shutil.which(check): |
|
raise unittest.SkipTest(f'Could not find {check}.') |
|
envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']] |
|
|
|
# Also test a deprecated variable if there is one. |
|
if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP: |
|
envvars.append( |
|
mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld']) |
|
|
|
for envvar in envvars: |
|
with mock.patch.dict(os.environ, {envvar: name}): |
|
env = get_fake_env() |
|
comp = compiler_from_language(env, lang, MachineChoice.HOST) |
|
if isinstance(comp, (mesonbuild.compilers.AppleClangCCompiler, |
|
mesonbuild.compilers.AppleClangCPPCompiler, |
|
mesonbuild.compilers.AppleClangObjCCompiler, |
|
mesonbuild.compilers.AppleClangObjCPPCompiler)): |
|
raise unittest.SkipTest('AppleClang is currently only supported with ld64') |
|
if lang != 'rust' and comp.use_linker_args('bfd') == []: |
|
raise unittest.SkipTest( |
|
f'Compiler {comp.id} does not support using alternative linkers') |
|
self.assertEqual(comp.linker.id, expected) |
|
|
|
def test_ld_environment_variable_bfd(self): |
|
self._check_ld('ld.bfd', 'bfd', 'c', 'ld.bfd') |
|
|
|
def test_ld_environment_variable_gold(self): |
|
self._check_ld('ld.gold', 'gold', 'c', 'ld.gold') |
|
|
|
def test_ld_environment_variable_lld(self): |
|
self._check_ld('ld.lld', 'lld', 'c', 'ld.lld') |
|
|
|
@skip_if_not_language('rust') |
|
@skipIfNoExecutable('ld.gold') # need an additional check here because _check_ld checks for gcc |
|
def test_ld_environment_variable_rust(self): |
|
self._check_ld('gcc', 'gcc -fuse-ld=gold', 'rust', 'ld.gold') |
|
|
|
def test_ld_environment_variable_cpp(self): |
|
self._check_ld('ld.gold', 'gold', 'cpp', 'ld.gold') |
|
|
|
@skip_if_not_language('objc') |
|
def test_ld_environment_variable_objc(self): |
|
self._check_ld('ld.gold', 'gold', 'objc', 'ld.gold') |
|
|
|
@skip_if_not_language('objcpp') |
|
def test_ld_environment_variable_objcpp(self): |
|
self._check_ld('ld.gold', 'gold', 'objcpp', 'ld.gold') |
|
|
|
@skip_if_not_language('fortran') |
|
def test_ld_environment_variable_fortran(self): |
|
self._check_ld('ld.gold', 'gold', 'fortran', 'ld.gold') |
|
|
|
@skip_if_not_language('d') |
|
def test_ld_environment_variable_d(self): |
|
# At least for me, ldc defaults to gold, and gdc defaults to bfd, so |
|
# let's pick lld, which isn't the default for either (currently) |
|
if is_osx(): |
|
expected = 'ld64' |
|
else: |
|
expected = 'ld.lld' |
|
self._check_ld('ld.lld', 'lld', 'd', expected) |
|
|
|
def compute_sha256(self, filename): |
|
with open(filename, 'rb') as f: |
|
return hashlib.sha256(f.read()).hexdigest() |
|
|
|
def test_wrap_with_file_url(self): |
|
testdir = os.path.join(self.unit_test_dir, '73 wrap file url') |
|
source_filename = os.path.join(testdir, 'subprojects', 'foo.tar.xz') |
|
patch_filename = os.path.join(testdir, 'subprojects', 'foo-patch.tar.xz') |
|
wrap_filename = os.path.join(testdir, 'subprojects', 'foo.wrap') |
|
source_hash = self.compute_sha256(source_filename) |
|
patch_hash = self.compute_sha256(patch_filename) |
|
wrap = textwrap.dedent("""\ |
|
[wrap-file] |
|
directory = foo |
|
|
|
source_url = http://server.invalid/foo |
|
source_fallback_url = file://{} |
|
source_filename = foo.tar.xz |
|
source_hash = {} |
|
|
|
patch_url = http://server.invalid/foo |
|
patch_fallback_url = file://{} |
|
patch_filename = foo-patch.tar.xz |
|
patch_hash = {} |
|
""".format(source_filename, source_hash, patch_filename, patch_hash)) |
|
with open(wrap_filename, 'w') as f: |
|
f.write(wrap) |
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'packagecache')) |
|
windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'foo')) |
|
os.unlink(wrap_filename) |
|
|
|
def test_no_rpath_for_static(self): |
|
testdir = os.path.join(self.common_test_dir, '5 linkstatic') |
|
self.init(testdir) |
|
self.build() |
|
build_rpath = get_rpath(os.path.join(self.builddir, 'prog')) |
|
self.assertIsNone(build_rpath) |
|
|
|
def test_lookup_system_after_broken_fallback(self): |
|
# Just to generate libfoo.pc so we can test system dependency lookup. |
|
testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
self.init(testdir) |
|
privatedir = self.privatedir |
|
|
|
# Write test project where the first dependency() returns not-found |
|
# because 'broken' subproject does not exit, but that should not prevent |
|
# the 2nd dependency() to lookup on system. |
|
self.new_builddir() |
|
with tempfile.TemporaryDirectory() as d: |
|
with open(os.path.join(d, 'meson.build'), 'w') as f: |
|
f.write(textwrap.dedent('''\ |
|
project('test') |
|
dependency('notfound', fallback: 'broken', required: false) |
|
dependency('libfoo', fallback: 'broken', required: true) |
|
''')) |
|
self.init(d, override_envvars={'PKG_CONFIG_LIBDIR': privatedir}) |
|
|
|
def test_as_link_whole(self): |
|
testdir = os.path.join(self.unit_test_dir, '77 as link whole') |
|
self.init(testdir) |
|
with open(os.path.join(self.privatedir, 'bar1.pc')) as f: |
|
content = f.read() |
|
self.assertIn('-lfoo', content) |
|
with open(os.path.join(self.privatedir, 'bar2.pc')) as f: |
|
content = f.read() |
|
self.assertNotIn('-lfoo', content) |
|
|
|
def test_prelinking(self): |
|
# Prelinking currently only works on recently new GNU toolchains. |
|
# Skip everything else. When support for other toolchains is added, |
|
# remove limitations as necessary. |
|
if is_osx(): |
|
raise unittest.SkipTest('Prelinking not supported on Darwin.') |
|
if 'clang' in os.environ.get('CC', 'dummy'): |
|
raise unittest.SkipTest('Prelinking not supported with Clang.') |
|
gccver = subprocess.check_output(['cc', '--version']) |
|
if b'7.5.0' in gccver: |
|
raise unittest.SkipTest('GCC on Bionic is too old to be supported.') |
|
testdir = os.path.join(self.unit_test_dir, '87 prelinking') |
|
self.init(testdir) |
|
self.build() |
|
outlib = os.path.join(self.builddir, 'libprelinked.a') |
|
ar = shutil.which('ar') |
|
self.assertTrue(os.path.exists(outlib)) |
|
self.assertTrue(ar is not None) |
|
p = subprocess.run([ar, 't', outlib], |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.DEVNULL, |
|
universal_newlines=True, timeout=1) |
|
obj_files = p.stdout.strip().split('\n') |
|
self.assertEqual(len(obj_files), 1) |
|
self.assertTrue(obj_files[0].endswith('-prelink.o')) |
|
|
|
class BaseLinuxCrossTests(BasePlatformTests): |
|
# Don't pass --libdir when cross-compiling. We have tests that |
|
# check whether meson auto-detects it correctly. |
|
libdir = None |
|
|
|
|
|
def should_run_cross_arm_tests(): |
|
return shutil.which('arm-linux-gnueabihf-gcc') and not platform.machine().lower().startswith('arm') |
|
|
|
@unittest.skipUnless(not is_windows() and should_run_cross_arm_tests(), "requires ability to cross compile to ARM") |
|
class LinuxCrossArmTests(BaseLinuxCrossTests): |
|
''' |
|
Tests that cross-compilation to Linux/ARM works |
|
''' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
src_root = os.path.dirname(__file__) |
|
self.meson_cross_file = os.path.join(src_root, 'cross', 'ubuntu-armhf.txt') |
|
|
|
def test_cflags_cross_environment_pollution(self): |
|
''' |
|
Test that the CFLAGS environment variable does not pollute the cross |
|
environment. This can't be an ordinary test case because we need to |
|
inspect the compiler database. |
|
''' |
|
testdir = os.path.join(self.common_test_dir, '3 static') |
|
self.init(testdir, override_envvars={'CFLAGS': '-DBUILD_ENVIRONMENT_ONLY'}) |
|
compdb = self.get_compdb() |
|
self.assertNotIn('-DBUILD_ENVIRONMENT_ONLY', compdb[0]['command']) |
|
|
|
def test_cross_file_overrides_always_args(self): |
|
''' |
|
Test that $lang_args in cross files always override get_always_args(). |
|
Needed for overriding the default -D_FILE_OFFSET_BITS=64 on some |
|
architectures such as some Android versions and Raspbian. |
|
https://github.com/mesonbuild/meson/issues/3049 |
|
https://github.com/mesonbuild/meson/issues/3089 |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '33 cross file overrides always args') |
|
self.meson_cross_file = os.path.join(testdir, 'ubuntu-armhf-overrides.txt') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
self.assertRegex(compdb[0]['command'], '-D_FILE_OFFSET_BITS=64.*-U_FILE_OFFSET_BITS') |
|
self.build() |
|
|
|
def test_cross_libdir(self): |
|
# When cross compiling "libdir" should default to "lib" |
|
# rather than "lib/x86_64-linux-gnu" or something like that. |
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
self.init(testdir) |
|
for i in self.introspect('--buildoptions'): |
|
if i['name'] == 'libdir': |
|
self.assertEqual(i['value'], 'lib') |
|
return |
|
self.assertTrue(False, 'Option libdir not in introspect data.') |
|
|
|
def test_cross_libdir_subproject(self): |
|
# Guard against a regression where calling "subproject" |
|
# would reset the value of libdir to its default value. |
|
testdir = os.path.join(self.unit_test_dir, '76 subdir libdir') |
|
self.init(testdir, extra_args=['--libdir=fuf']) |
|
for i in self.introspect('--buildoptions'): |
|
if i['name'] == 'libdir': |
|
self.assertEqual(i['value'], 'fuf') |
|
return |
|
self.assertTrue(False, 'Libdir specified on command line gets reset.') |
|
|
|
def test_std_remains(self): |
|
# C_std defined in project options must be in effect also when cross compiling. |
|
testdir = os.path.join(self.unit_test_dir, '51 noncross options') |
|
self.init(testdir) |
|
compdb = self.get_compdb() |
|
self.assertRegex(compdb[0]['command'], '-std=c99') |
|
self.build() |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkg_config_option(self): |
|
if not shutil.which('arm-linux-gnueabihf-pkg-config'): |
|
raise unittest.SkipTest('Cross-pkgconfig not found.') |
|
testdir = os.path.join(self.unit_test_dir, '58 pkg_config_path option') |
|
self.init(testdir, extra_args=[ |
|
'-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'), |
|
'-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'), |
|
]) |
|
|
|
def test_run_native_test(self): |
|
''' |
|
https://github.com/mesonbuild/meson/issues/7997 |
|
check run native test in crossbuild without exe wrapper |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '88 run native test') |
|
stamp_file = os.path.join(self.builddir, 'native_test_has_run.stamp') |
|
self.init(testdir) |
|
self.build() |
|
self.assertPathDoesNotExist(stamp_file) |
|
self.run_tests() |
|
self.assertPathExists(stamp_file) |
|
|
|
|
|
def should_run_cross_mingw_tests(): |
|
return shutil.which('x86_64-w64-mingw32-gcc') and not (is_windows() or is_cygwin()) |
|
|
|
@unittest.skipUnless(not is_windows() and should_run_cross_mingw_tests(), "requires ability to cross compile with MinGW") |
|
class LinuxCrossMingwTests(BaseLinuxCrossTests): |
|
''' |
|
Tests that cross-compilation to Windows/MinGW works |
|
''' |
|
|
|
def setUp(self): |
|
super().setUp() |
|
src_root = os.path.dirname(__file__) |
|
self.meson_cross_file = os.path.join(src_root, 'cross', 'linux-mingw-w64-64bit.txt') |
|
|
|
def test_exe_wrapper_behaviour(self): |
|
''' |
|
Test that an exe wrapper that isn't found doesn't cause compiler sanity |
|
checks and compiler checks to fail, but causes configure to fail if it |
|
requires running a cross-built executable (custom_target or run_target) |
|
and causes the tests to be skipped if they are run. |
|
''' |
|
testdir = os.path.join(self.unit_test_dir, '36 exe_wrapper behaviour') |
|
# Configures, builds, and tests fine by default |
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
self.wipe() |
|
os.mkdir(self.builddir) |
|
# Change cross file to use a non-existing exe_wrapper and it should fail |
|
self.meson_cross_file = os.path.join(testdir, 'broken-cross.txt') |
|
# Force tracebacks so we can detect them properly |
|
env = {'MESON_FORCE_BACKTRACE': '1'} |
|
error_message = "An exe_wrapper is needed but was not found. Please define one in cross file and check the command and/or add it to PATH." |
|
|
|
with self.assertRaises(MesonException) as cm: |
|
# Must run in-process or we'll get a generic CalledProcessError |
|
self.init(testdir, extra_args='-Drun-target=false', |
|
inprocess=True, |
|
override_envvars=env) |
|
self.assertEqual(str(cm.exception), error_message) |
|
|
|
with self.assertRaises(MesonException) as cm: |
|
# Must run in-process or we'll get a generic CalledProcessError |
|
self.init(testdir, extra_args='-Dcustom-target=false', |
|
inprocess=True, |
|
override_envvars=env) |
|
self.assertEqual(str(cm.exception), error_message) |
|
|
|
self.init(testdir, extra_args=['-Dcustom-target=false', '-Drun-target=false'], |
|
override_envvars=env) |
|
self.build() |
|
|
|
with self.assertRaises(MesonException) as cm: |
|
# Must run in-process or we'll get a generic CalledProcessError |
|
self.run_tests(inprocess=True, override_envvars=env) |
|
self.assertEqual(str(cm.exception), |
|
"The exe_wrapper defined in the cross file 'broken' was not found. Please check the command and/or add it to PATH.") |
|
|
|
@skipIfNoPkgconfig |
|
def test_cross_pkg_config_option(self): |
|
testdir = os.path.join(self.unit_test_dir, '58 pkg_config_path option') |
|
self.init(testdir, extra_args=[ |
|
'-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'), |
|
'-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'), |
|
]) |
|
|
|
|
|
class PythonTests(BasePlatformTests): |
|
''' |
|
Tests that verify compilation of python extension modules |
|
''' |
|
|
|
def test_versions(self): |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest(f'Skipping python tests with {self.backend.name} backend') |
|
|
|
testdir = os.path.join(self.src_root, 'test cases', 'unit', '39 python extmodule') |
|
|
|
# No python version specified, this will use meson's python |
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
self.wipe() |
|
|
|
# When specifying a known name, (python2 / python3) the module |
|
# will also try 'python' as a fallback and use it if the major |
|
# version matches |
|
try: |
|
self.init(testdir, extra_args=['-Dpython=python2']) |
|
self.build() |
|
self.run_tests() |
|
except unittest.SkipTest: |
|
# python2 is not necessarily installed on the test machine, |
|
# if it is not, or the python headers can't be found, the test |
|
# will raise MESON_SKIP_TEST, we could check beforehand what version |
|
# of python is available, but it's a bit of a chicken and egg situation, |
|
# as that is the job of the module, so we just ask for forgiveness rather |
|
# than permission. |
|
pass |
|
|
|
self.wipe() |
|
|
|
for py in ('pypy', 'pypy3'): |
|
try: |
|
self.init(testdir, extra_args=['-Dpython=%s' % py]) |
|
except unittest.SkipTest: |
|
# Same as above, pypy2 and pypy3 are not expected to be present |
|
# on the test system, the test project only raises in these cases |
|
continue |
|
|
|
# We have a pypy, this is expected to work |
|
self.build() |
|
self.run_tests() |
|
self.wipe() |
|
|
|
# The test is configured to error out with MESON_SKIP_TEST |
|
# in case it could not find python |
|
with self.assertRaises(unittest.SkipTest): |
|
self.init(testdir, extra_args=['-Dpython=not-python']) |
|
self.wipe() |
|
|
|
# While dir is an external command on both Windows and Linux, |
|
# it certainly isn't python |
|
with self.assertRaises(unittest.SkipTest): |
|
self.init(testdir, extra_args=['-Dpython=dir']) |
|
self.wipe() |
|
|
|
|
|
class RewriterTests(BasePlatformTests): |
|
def setUp(self): |
|
super().setUp() |
|
self.maxDiff = None |
|
|
|
def prime(self, dirname): |
|
copy_tree(os.path.join(self.rewrite_test_dir, dirname), self.builddir) |
|
|
|
def rewrite_raw(self, directory, args): |
|
if isinstance(args, str): |
|
args = [args] |
|
command = self.rewrite_command + ['--verbose', '--skip', '--sourcedir', directory] + args |
|
p = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
|
universal_newlines=True, timeout=60) |
|
print('STDOUT:') |
|
print(p.stdout) |
|
print('STDERR:') |
|
print(p.stderr) |
|
if p.returncode != 0: |
|
if 'MESON_SKIP_TEST' in p.stdout: |
|
raise unittest.SkipTest('Project requested skipping.') |
|
raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout) |
|
if not p.stderr: |
|
return {} |
|
return json.loads(p.stderr) |
|
|
|
def rewrite(self, directory, args): |
|
if isinstance(args, str): |
|
args = [args] |
|
return self.rewrite_raw(directory, ['command'] + args) |
|
|
|
def test_target_source_list(self): |
|
self.prime('1 basic') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'target': { |
|
'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']}, |
|
'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_add_sources(self): |
|
self.prime('1 basic') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json')) |
|
expected = { |
|
'target': { |
|
'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}, |
|
'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['a7.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['a5.cpp', 'fileA.cpp', 'main.cpp']}, |
|
'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['a5.cpp', 'main.cpp', 'fileA.cpp']}, |
|
'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['a3.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp', 'a4.cpp']}, |
|
'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}, |
|
'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}, |
|
'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}, |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
# Check the written file |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_add_sources_abs(self): |
|
self.prime('1 basic') |
|
abs_src = [os.path.join(self.builddir, x) for x in ['a1.cpp', 'a2.cpp', 'a6.cpp']] |
|
add = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "src_add", "sources": abs_src}]) |
|
inf = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "info"}]) |
|
self.rewrite(self.builddir, add) |
|
out = self.rewrite(self.builddir, inf) |
|
expected = {'target': {'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}}} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_remove_sources(self): |
|
self.prime('1 basic') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'rmSrc.json')) |
|
expected = { |
|
'target': { |
|
'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileC.cpp']}, |
|
'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp']}, |
|
'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileC.cpp']}, |
|
'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp']}, |
|
'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp']}, |
|
'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileC.cpp']}, |
|
'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp']}, |
|
'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileC.cpp', 'main.cpp']}, |
|
'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp']}, |
|
'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp']}, |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
# Check the written file |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_subdir(self): |
|
self.prime('2 subdirs') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json')) |
|
expected = {'name': 'something', 'sources': ['first.c', 'second.c', 'third.c']} |
|
self.assertDictEqual(list(out['target'].values())[0], expected) |
|
|
|
# Check the written file |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
self.assertDictEqual(list(out['target'].values())[0], expected) |
|
|
|
def test_target_remove(self): |
|
self.prime('1 basic') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
|
|
expected = { |
|
'target': { |
|
'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']}, |
|
'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_tatrget_add(self): |
|
self.prime('1 basic') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
|
|
expected = { |
|
'target': { |
|
'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']}, |
|
'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']}, |
|
'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp']}, |
|
'trivialprog10@sha': {'name': 'trivialprog10', 'sources': ['new1.cpp', 'new2.cpp']}, |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_remove_subdir(self): |
|
self.prime('2 subdirs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
self.assertDictEqual(out, {}) |
|
|
|
def test_target_add_subdir(self): |
|
self.prime('2 subdirs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = {'name': 'something', 'sources': ['first.c', 'second.c']} |
|
self.assertDictEqual(out['target']['94b671c@@something@exe'], expected) |
|
|
|
def test_target_source_sorting(self): |
|
self.prime('5 sorting') |
|
add_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'src_add', 'sources': ['a666.c']}]) |
|
inf_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'info'}]) |
|
out = self.rewrite(self.builddir, add_json) |
|
out = self.rewrite(self.builddir, inf_json) |
|
expected = { |
|
'target': { |
|
'exe1@exe': { |
|
'name': 'exe1', |
|
'sources': [ |
|
'aaa/a/a1.c', |
|
'aaa/b/b1.c', |
|
'aaa/b/b2.c', |
|
'aaa/f1.c', |
|
'aaa/f2.c', |
|
'aaa/f3.c', |
|
'bbb/a/b1.c', |
|
'bbb/b/b2.c', |
|
'bbb/c1/b5.c', |
|
'bbb/c2/b7.c', |
|
'bbb/c10/b6.c', |
|
'bbb/a4.c', |
|
'bbb/b3.c', |
|
'bbb/b4.c', |
|
'bbb/b5.c', |
|
'a1.c', |
|
'a2.c', |
|
'a3.c', |
|
'a10.c', |
|
'a20.c', |
|
'a30.c', |
|
'a100.c', |
|
'a101.c', |
|
'a110.c', |
|
'a210.c', |
|
'a666.c', |
|
'b1.c', |
|
'c2.c' |
|
] |
|
} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_target_same_name_skip(self): |
|
self.prime('4 same name targets') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = {'name': 'myExe', 'sources': ['main.cpp']} |
|
self.assertEqual(len(out['target']), 2) |
|
for val in out['target'].values(): |
|
self.assertDictEqual(expected, val) |
|
|
|
def test_kwargs_info(self): |
|
self.prime('3 kwargs') |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1'}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_kwargs_set(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'set.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.2', 'meson_version': '0.50.0', 'license': ['GPL', 'MIT']}, |
|
'target#tgt1': {'build_by_default': False, 'build_rpath': '/usr/local', 'dependencies': 'dep1'}, |
|
'dependency#dep1': {'required': True, 'method': 'cmake'} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_kwargs_add(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'add.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1', 'license': ['GPL', 'MIT', 'BSD', 'Boost']}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_kwargs_remove(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'remove.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1', 'license': 'GPL'}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_kwargs_remove_regex(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'remove_regex.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1', 'default_options': 'debug=true'}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_kwargs_delete(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'delete.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {}, |
|
'target#tgt1': {}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_default_options_set(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_set.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1', 'default_options': ['buildtype=release', 'debug=True', 'cpp_std=c++11']}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
def test_default_options_delete(self): |
|
self.prime('3 kwargs') |
|
self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_delete.json')) |
|
out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) |
|
expected = { |
|
'kwargs': { |
|
'project#/': {'version': '0.0.1', 'default_options': ['cpp_std=c++14', 'debug=true']}, |
|
'target#tgt1': {'build_by_default': True}, |
|
'dependency#dep1': {'required': False} |
|
} |
|
} |
|
self.assertDictEqual(out, expected) |
|
|
|
class NativeFileTests(BasePlatformTests): |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.testcase = os.path.join(self.unit_test_dir, '47 native file binary') |
|
self.current_config = 0 |
|
self.current_wrapper = 0 |
|
|
|
def helper_create_native_file(self, values): |
|
"""Create a config file as a temporary file. |
|
|
|
values should be a nested dictionary structure of {section: {key: |
|
value}} |
|
""" |
|
filename = os.path.join(self.builddir, f'generated{self.current_config}.config') |
|
self.current_config += 1 |
|
with open(filename, 'wt') as f: |
|
for section, entries in values.items(): |
|
f.write(f'[{section}]\n') |
|
for k, v in entries.items(): |
|
if isinstance(v, (bool, int, float)): |
|
f.write(f"{k}={v}\n") |
|
elif isinstance(v, list): |
|
f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v]))) |
|
else: |
|
f.write(f"{k}='{v}'\n") |
|
return filename |
|
|
|
def helper_create_binary_wrapper(self, binary, dir_=None, extra_args=None, **kwargs): |
|
"""Creates a wrapper around a binary that overrides specific values.""" |
|
filename = os.path.join(dir_ or self.builddir, f'binary_wrapper{self.current_wrapper}.py') |
|
extra_args = extra_args or {} |
|
self.current_wrapper += 1 |
|
if is_haiku(): |
|
chbang = '#!/bin/env python3' |
|
else: |
|
chbang = '#!/usr/bin/env python3' |
|
|
|
with open(filename, 'wt') as f: |
|
f.write(textwrap.dedent('''\ |
|
{} |
|
import argparse |
|
import subprocess |
|
import sys |
|
|
|
def main(): |
|
parser = argparse.ArgumentParser() |
|
'''.format(chbang))) |
|
for name in chain(extra_args, kwargs): |
|
f.write(' parser.add_argument("-{0}", "--{0}", action="store_true")\n'.format(name)) |
|
f.write(' args, extra_args = parser.parse_known_args()\n') |
|
for name, value in chain(extra_args.items(), kwargs.items()): |
|
f.write(f' if args.{name}:\n') |
|
f.write(' print("{}", file=sys.{})\n'.format(value, kwargs.get('outfile', 'stdout'))) |
|
f.write(' sys.exit(0)\n') |
|
f.write(textwrap.dedent(''' |
|
ret = subprocess.run( |
|
["{}"] + extra_args, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE) |
|
print(ret.stdout.decode('utf-8')) |
|
print(ret.stderr.decode('utf-8'), file=sys.stderr) |
|
sys.exit(ret.returncode) |
|
|
|
if __name__ == '__main__': |
|
main() |
|
'''.format(binary))) |
|
|
|
if not is_windows(): |
|
os.chmod(filename, 0o755) |
|
return filename |
|
|
|
# On windows we need yet another level of indirection, as cmd cannot |
|
# invoke python files itself, so instead we generate a .bat file, which |
|
# invokes our python wrapper |
|
batfile = os.path.join(self.builddir, f'binary_wrapper{self.current_wrapper}.bat') |
|
with open(batfile, 'wt') as f: |
|
f.write(fr'@{sys.executable} {filename} %*') |
|
return batfile |
|
|
|
def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST): |
|
"""Helper for generating tests for overriding compilers for langaugages |
|
with more than one implementation, such as C, C++, ObjC, ObjC++, and D. |
|
""" |
|
env = get_fake_env() |
|
getter = lambda: compiler_from_language(env, lang, for_machine) |
|
cc = getter() |
|
binary, newid = cb(cc) |
|
env.binaries[for_machine].binaries[lang] = binary |
|
compiler = getter() |
|
self.assertEqual(compiler.id, newid) |
|
|
|
def test_multiple_native_files_override(self): |
|
wrapper = self.helper_create_binary_wrapper('bash', version='foo') |
|
config = self.helper_create_native_file({'binaries': {'bash': wrapper}}) |
|
wrapper = self.helper_create_binary_wrapper('bash', version='12345') |
|
config2 = self.helper_create_native_file({'binaries': {'bash': wrapper}}) |
|
self.init(self.testcase, extra_args=[ |
|
'--native-file', config, '--native-file', config2, |
|
'-Dcase=find_program']) |
|
|
|
# This test hangs on cygwin. |
|
@unittest.skipIf(os.name != 'posix' or is_cygwin(), 'Uses fifos, which are not available on non Unix OSes.') |
|
def test_native_file_is_pipe(self): |
|
fifo = os.path.join(self.builddir, 'native.file') |
|
os.mkfifo(fifo) |
|
with tempfile.TemporaryDirectory() as d: |
|
wrapper = self.helper_create_binary_wrapper('bash', d, version='12345') |
|
|
|
def filler(): |
|
with open(fifo, 'w') as f: |
|
f.write('[binaries]\n') |
|
f.write(f"bash = '{wrapper}'\n") |
|
|
|
thread = threading.Thread(target=filler) |
|
thread.start() |
|
|
|
self.init(self.testcase, extra_args=['--native-file', fifo, '-Dcase=find_program']) |
|
|
|
thread.join() |
|
os.unlink(fifo) |
|
|
|
self.init(self.testcase, extra_args=['--wipe']) |
|
|
|
def test_multiple_native_files(self): |
|
wrapper = self.helper_create_binary_wrapper('bash', version='12345') |
|
config = self.helper_create_native_file({'binaries': {'bash': wrapper}}) |
|
wrapper = self.helper_create_binary_wrapper('python') |
|
config2 = self.helper_create_native_file({'binaries': {'python': wrapper}}) |
|
self.init(self.testcase, extra_args=[ |
|
'--native-file', config, '--native-file', config2, |
|
'-Dcase=find_program']) |
|
|
|
def _simple_test(self, case, binary, entry=None): |
|
wrapper = self.helper_create_binary_wrapper(binary, version='12345') |
|
config = self.helper_create_native_file({'binaries': {entry or binary: wrapper}}) |
|
self.init(self.testcase, extra_args=['--native-file', config, f'-Dcase={case}']) |
|
|
|
def test_find_program(self): |
|
self._simple_test('find_program', 'bash') |
|
|
|
def test_config_tool_dep(self): |
|
# Do the skip at this level to avoid screwing up the cache |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise unittest.SkipTest('Skipped due to problems with LLVM on MSYS2') |
|
if not shutil.which('llvm-config'): |
|
raise unittest.SkipTest('No llvm-installed, cannot test') |
|
self._simple_test('config_dep', 'llvm-config') |
|
|
|
def test_python3_module(self): |
|
self._simple_test('python3', 'python3') |
|
|
|
def test_python_module(self): |
|
if is_windows(): |
|
# Bat adds extra crap to stdout, so the version check logic in the |
|
# python module breaks. This is fine on other OSes because they |
|
# don't need the extra indirection. |
|
raise unittest.SkipTest('bat indirection breaks internal sanity checks.') |
|
elif is_osx(): |
|
binary = 'python' |
|
else: |
|
binary = 'python2' |
|
|
|
# We not have python2, check for it |
|
for v in ['2', '2.7', '-2.7']: |
|
rc = subprocess.call(['pkg-config', '--cflags', f'python{v}'], |
|
stdout=subprocess.DEVNULL, |
|
stderr=subprocess.DEVNULL) |
|
if rc == 0: |
|
break |
|
else: |
|
raise unittest.SkipTest('Not running Python 2 tests because dev packages not installed.') |
|
self._simple_test('python', binary, entry='python') |
|
|
|
@unittest.skipIf(is_windows(), 'Setting up multiple compilers on windows is hard') |
|
@skip_if_env_set('CC') |
|
def test_c_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'gcc': |
|
if not shutil.which('clang'): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'clang', 'clang' |
|
if not is_real_gnu_compiler(shutil.which('gcc')): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'gcc', 'gcc' |
|
self.helper_for_compiler('c', cb) |
|
|
|
@unittest.skipIf(is_windows(), 'Setting up multiple compilers on windows is hard') |
|
@skip_if_env_set('CXX') |
|
def test_cpp_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'gcc': |
|
if not shutil.which('clang++'): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'clang++', 'clang' |
|
if not is_real_gnu_compiler(shutil.which('g++')): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'g++', 'gcc' |
|
self.helper_for_compiler('cpp', cb) |
|
|
|
@skip_if_not_language('objc') |
|
@skip_if_env_set('OBJC') |
|
def test_objc_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'gcc': |
|
if not shutil.which('clang'): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'clang', 'clang' |
|
if not is_real_gnu_compiler(shutil.which('gcc')): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'gcc', 'gcc' |
|
self.helper_for_compiler('objc', cb) |
|
|
|
@skip_if_not_language('objcpp') |
|
@skip_if_env_set('OBJCXX') |
|
def test_objcpp_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'gcc': |
|
if not shutil.which('clang++'): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'clang++', 'clang' |
|
if not is_real_gnu_compiler(shutil.which('g++')): |
|
raise unittest.SkipTest('Only one compiler found, cannot test.') |
|
return 'g++', 'gcc' |
|
self.helper_for_compiler('objcpp', cb) |
|
|
|
@skip_if_not_language('d') |
|
@skip_if_env_set('DC') |
|
def test_d_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'dmd': |
|
if shutil.which('ldc'): |
|
return 'ldc', 'ldc' |
|
elif shutil.which('gdc'): |
|
return 'gdc', 'gdc' |
|
else: |
|
raise unittest.SkipTest('No alternative dlang compiler found.') |
|
if shutil.which('dmd'): |
|
return 'dmd', 'dmd' |
|
raise unittest.SkipTest('No alternative dlang compiler found.') |
|
self.helper_for_compiler('d', cb) |
|
|
|
@skip_if_not_language('cs') |
|
@skip_if_env_set('CSC') |
|
def test_cs_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'csc': |
|
if not shutil.which('mcs'): |
|
raise unittest.SkipTest('No alternate C# implementation.') |
|
return 'mcs', 'mcs' |
|
if not shutil.which('csc'): |
|
raise unittest.SkipTest('No alternate C# implementation.') |
|
return 'csc', 'csc' |
|
self.helper_for_compiler('cs', cb) |
|
|
|
@skip_if_not_language('fortran') |
|
@skip_if_env_set('FC') |
|
def test_fortran_compiler(self): |
|
def cb(comp): |
|
if comp.id == 'lcc': |
|
if shutil.which('lfortran'): |
|
return 'lfortran', 'lcc' |
|
raise unittest.SkipTest('No alternate Fortran implementation.') |
|
elif comp.id == 'gcc': |
|
if shutil.which('ifort'): |
|
# There is an ICC for windows (windows build, linux host), |
|
# but we don't support that ATM so lets not worry about it. |
|
if is_windows(): |
|
return 'ifort', 'intel-cl' |
|
return 'ifort', 'intel' |
|
elif shutil.which('flang'): |
|
return 'flang', 'flang' |
|
elif shutil.which('pgfortran'): |
|
return 'pgfortran', 'pgi' |
|
# XXX: there are several other fortran compilers meson |
|
# supports, but I don't have any of them to test with |
|
raise unittest.SkipTest('No alternate Fortran implementation.') |
|
if not shutil.which('gfortran'): |
|
raise unittest.SkipTest('No alternate Fortran implementation.') |
|
return 'gfortran', 'gcc' |
|
self.helper_for_compiler('fortran', cb) |
|
|
|
def _single_implementation_compiler(self, lang: str, binary: str, version_str: str, version: str) -> None: |
|
"""Helper for languages with a single (supported) implementation. |
|
|
|
Builds a wrapper around the compiler to override the version. |
|
""" |
|
wrapper = self.helper_create_binary_wrapper(binary, version=version_str) |
|
env = get_fake_env() |
|
env.binaries.host.binaries[lang] = [wrapper] |
|
compiler = compiler_from_language(env, lang, MachineChoice.HOST) |
|
self.assertEqual(compiler.version, version) |
|
|
|
@skip_if_not_language('vala') |
|
@skip_if_env_set('VALAC') |
|
def test_vala_compiler(self): |
|
self._single_implementation_compiler( |
|
'vala', 'valac', 'Vala 1.2345', '1.2345') |
|
|
|
@skip_if_not_language('rust') |
|
@skip_if_env_set('RUSTC') |
|
def test_rust_compiler(self): |
|
self._single_implementation_compiler( |
|
'rust', 'rustc', 'rustc 1.2345', '1.2345') |
|
|
|
@skip_if_not_language('java') |
|
def test_java_compiler(self): |
|
self._single_implementation_compiler( |
|
'java', 'javac', 'javac 9.99.77', '9.99.77') |
|
|
|
@skip_if_not_language('swift') |
|
def test_swift_compiler(self): |
|
wrapper = self.helper_create_binary_wrapper( |
|
'swiftc', version='Swift 1.2345', outfile='stderr', |
|
extra_args={'Xlinker': 'macosx_version. PROJECT:ld - 1.2.3'}) |
|
env = get_fake_env() |
|
env.binaries.host.binaries['swift'] = [wrapper] |
|
compiler = detect_swift_compiler(env, MachineChoice.HOST) |
|
self.assertEqual(compiler.version, '1.2345') |
|
|
|
def test_native_file_dirs(self): |
|
testcase = os.path.join(self.unit_test_dir, '60 native file override') |
|
self.init(testcase, default_args=False, |
|
extra_args=['--native-file', os.path.join(testcase, 'nativefile')]) |
|
|
|
def test_native_file_dirs_overridden(self): |
|
testcase = os.path.join(self.unit_test_dir, '60 native file override') |
|
self.init(testcase, default_args=False, |
|
extra_args=['--native-file', os.path.join(testcase, 'nativefile'), |
|
'-Ddef_libdir=liblib', '-Dlibdir=liblib']) |
|
|
|
def test_compile_sys_path(self): |
|
"""Compiling with a native file stored in a system path works. |
|
|
|
There was a bug which caused the paths to be stored incorrectly and |
|
would result in ninja invoking meson in an infinite loop. This tests |
|
for that by actually invoking ninja. |
|
""" |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
|
|
# It really doesn't matter what's in the native file, just that it exists |
|
config = self.helper_create_native_file({'binaries': {'bash': 'false'}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
self.build() |
|
|
|
def test_user_options(self): |
|
testcase = os.path.join(self.common_test_dir, '40 options') |
|
for opt, value in [('testoption', 'some other val'), ('other_one', True), |
|
('combo_opt', 'one'), ('array_opt', ['two']), |
|
('integer_opt', 0), |
|
('CaseSenSiTivE', 'SOME other Value'), |
|
('CASESENSITIVE', 'some other Value')]: |
|
config = self.helper_create_native_file({'project options': {opt: value}}) |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') |
|
|
|
def test_user_options_command_line_overrides(self): |
|
testcase = os.path.join(self.common_test_dir, '40 options') |
|
config = self.helper_create_native_file({'project options': {'other_one': True}}) |
|
self.init(testcase, extra_args=['--native-file', config, '-Dother_one=false']) |
|
|
|
def test_user_options_subproject(self): |
|
testcase = os.path.join(self.unit_test_dir, '79 user options for subproject') |
|
|
|
s = os.path.join(testcase, 'subprojects') |
|
if not os.path.exists(s): |
|
os.mkdir(s) |
|
s = os.path.join(s, 'sub') |
|
if not os.path.exists(s): |
|
sub = os.path.join(self.common_test_dir, '40 options') |
|
shutil.copytree(sub, s) |
|
|
|
for opt, value in [('testoption', 'some other val'), ('other_one', True), |
|
('combo_opt', 'one'), ('array_opt', ['two']), |
|
('integer_opt', 0)]: |
|
config = self.helper_create_native_file({'sub:project options': {opt: value}}) |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') |
|
|
|
def test_option_bool(self): |
|
# Bools are allowed to be unquoted |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({'built-in options': {'werror': True}}) |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
# Test that no-per subproject options are inherited from the parent |
|
if 'werror' in each['name']: |
|
self.assertEqual(each['value'], True) |
|
break |
|
else: |
|
self.fail('Did not find werror in build options?') |
|
|
|
def test_option_integer(self): |
|
# Bools are allowed to be unquoted |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({'built-in options': {'unity_size': 100}}) |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
# Test that no-per subproject options are inherited from the parent |
|
if 'unity_size' in each['name']: |
|
self.assertEqual(each['value'], 100) |
|
break |
|
else: |
|
self.fail('Did not find unity_size in build options?') |
|
|
|
def test_builtin_options(self): |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
config = self.helper_create_native_file({'built-in options': {'cpp_std': 'c++14'}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'cpp_std': |
|
self.assertEqual(each['value'], 'c++14') |
|
break |
|
else: |
|
self.fail('Did not find werror in build options?') |
|
|
|
def test_builtin_options_conf_overrides_env(self): |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
config = self.helper_create_native_file({'built-in options': {'pkg_config_path': '/foo'}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config], override_envvars={'PKG_CONFIG_PATH': '/bar'}) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'pkg_config_path': |
|
self.assertEqual(each['value'], ['/foo']) |
|
break |
|
else: |
|
self.fail('Did not find pkg_config_path in build options?') |
|
|
|
def test_builtin_options_subprojects(self): |
|
testcase = os.path.join(self.common_test_dir, '98 subproject subdir') |
|
config = self.helper_create_native_file({'built-in options': {'default_library': 'both', 'c_args': ['-Dfoo']}, 'sub:built-in options': {'default_library': 'static'}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
found = 0 |
|
for each in configuration: |
|
# Test that no-per subproject options are inherited from the parent |
|
if 'c_args' in each['name']: |
|
# This path will be hit twice, once for build and once for host, |
|
self.assertEqual(each['value'], ['-Dfoo']) |
|
found += 1 |
|
elif each['name'] == 'default_library': |
|
self.assertEqual(each['value'], 'both') |
|
found += 1 |
|
elif each['name'] == 'sub:default_library': |
|
self.assertEqual(each['value'], 'static') |
|
found += 1 |
|
self.assertEqual(found, 4, 'Did not find all three sections') |
|
|
|
def test_builtin_options_subprojects_overrides_buildfiles(self): |
|
# If the buildfile says subproject(... default_library: shared), ensure that's overwritten |
|
testcase = os.path.join(self.common_test_dir, '223 persubproject options') |
|
config = self.helper_create_native_file({'sub2:built-in options': {'default_library': 'shared'}}) |
|
|
|
with self.assertRaises((RuntimeError, subprocess.CalledProcessError)) as cm: |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
if isinstance(cm, RuntimeError): |
|
check = str(cm.exception) |
|
else: |
|
check = cm.exception.stdout |
|
self.assertIn(check, 'Parent should override default_library') |
|
|
|
def test_builtin_options_subprojects_dont_inherits_parent_override(self): |
|
# If the buildfile says subproject(... default_library: shared), ensure that's overwritten |
|
testcase = os.path.join(self.common_test_dir, '223 persubproject options') |
|
config = self.helper_create_native_file({'built-in options': {'default_library': 'both'}}) |
|
self.init(testcase, extra_args=['--native-file', config]) |
|
|
|
def test_builtin_options_compiler_properties(self): |
|
# the properties section can have lang_args, and those need to be |
|
# overwritten by the built-in options |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({ |
|
'built-in options': {'c_args': ['-DFOO']}, |
|
'properties': {'c_args': ['-DBAR']}, |
|
}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'c_args': |
|
self.assertEqual(each['value'], ['-DFOO']) |
|
break |
|
else: |
|
self.fail('Did not find c_args in build options?') |
|
|
|
def test_builtin_options_compiler_properties_legacy(self): |
|
# The legacy placement in properties is still valid if a 'built-in |
|
# options' setting is present, but doesn't have the lang_args |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({ |
|
'built-in options': {'default_library': 'static'}, |
|
'properties': {'c_args': ['-DBAR']}, |
|
}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'c_args': |
|
self.assertEqual(each['value'], ['-DBAR']) |
|
break |
|
else: |
|
self.fail('Did not find c_args in build options?') |
|
|
|
def test_builtin_options_paths(self): |
|
# the properties section can have lang_args, and those need to be |
|
# overwritten by the built-in options |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({ |
|
'built-in options': {'bindir': 'foo'}, |
|
'paths': {'bindir': 'bar'}, |
|
}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'bindir': |
|
self.assertEqual(each['value'], 'foo') |
|
break |
|
else: |
|
self.fail('Did not find bindir in build options?') |
|
|
|
def test_builtin_options_paths_legacy(self): |
|
testcase = os.path.join(self.common_test_dir, '1 trivial') |
|
config = self.helper_create_native_file({ |
|
'built-in options': {'default_library': 'static'}, |
|
'paths': {'bindir': 'bar'}, |
|
}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'bindir': |
|
self.assertEqual(each['value'], 'bar') |
|
break |
|
else: |
|
self.fail('Did not find bindir in build options?') |
|
|
|
|
|
class CrossFileTests(BasePlatformTests): |
|
|
|
"""Tests for cross file functionality not directly related to |
|
cross compiling. |
|
|
|
This is mainly aimed to testing overrides from cross files. |
|
""" |
|
|
|
def setUp(self): |
|
super().setUp() |
|
self.current_config = 0 |
|
self.current_wrapper = 0 |
|
|
|
def _cross_file_generator(self, *, needs_exe_wrapper: bool = False, |
|
exe_wrapper: T.Optional[T.List[str]] = None) -> str: |
|
if is_windows(): |
|
raise unittest.SkipTest('Cannot run this test on non-mingw/non-cygwin windows') |
|
|
|
return textwrap.dedent(f"""\ |
|
[binaries] |
|
c = '{shutil.which('gcc' if is_sunos() else 'cc')}' |
|
ar = '{shutil.which('ar')}' |
|
strip = '{shutil.which('strip')}' |
|
exe_wrapper = {str(exe_wrapper) if exe_wrapper is not None else '[]'} |
|
|
|
[properties] |
|
needs_exe_wrapper = {needs_exe_wrapper} |
|
|
|
[host_machine] |
|
system = 'linux' |
|
cpu_family = 'x86' |
|
cpu = 'i686' |
|
endian = 'little' |
|
""") |
|
|
|
def _stub_exe_wrapper(self) -> str: |
|
return textwrap.dedent('''\ |
|
#!/usr/bin/env python3 |
|
import subprocess |
|
import sys |
|
|
|
sys.exit(subprocess.run(sys.argv[1:]).returncode) |
|
''') |
|
|
|
def test_needs_exe_wrapper_true(self): |
|
testdir = os.path.join(self.unit_test_dir, '71 cross test passed') |
|
with tempfile.TemporaryDirectory() as d: |
|
p = Path(d) / 'crossfile' |
|
with p.open('wt') as f: |
|
f.write(self._cross_file_generator(needs_exe_wrapper=True)) |
|
self.init(testdir, extra_args=['--cross-file=' + str(p)]) |
|
out = self.run_target('test') |
|
self.assertRegex(out, r'Skipped:\s*1\s*\n') |
|
|
|
def test_needs_exe_wrapper_false(self): |
|
testdir = os.path.join(self.unit_test_dir, '71 cross test passed') |
|
with tempfile.TemporaryDirectory() as d: |
|
p = Path(d) / 'crossfile' |
|
with p.open('wt') as f: |
|
f.write(self._cross_file_generator(needs_exe_wrapper=False)) |
|
self.init(testdir, extra_args=['--cross-file=' + str(p)]) |
|
out = self.run_target('test') |
|
self.assertNotRegex(out, r'Skipped:\s*1\n') |
|
|
|
def test_needs_exe_wrapper_true_wrapper(self): |
|
testdir = os.path.join(self.unit_test_dir, '71 cross test passed') |
|
with tempfile.TemporaryDirectory() as d: |
|
s = Path(d) / 'wrapper.py' |
|
with s.open('wt') as f: |
|
f.write(self._stub_exe_wrapper()) |
|
s.chmod(0o774) |
|
p = Path(d) / 'crossfile' |
|
with p.open('wt') as f: |
|
f.write(self._cross_file_generator( |
|
needs_exe_wrapper=True, |
|
exe_wrapper=[str(s)])) |
|
|
|
self.init(testdir, extra_args=['--cross-file=' + str(p), '-Dexpect=true']) |
|
out = self.run_target('test') |
|
self.assertRegex(out, r'Ok:\s*3\s*\n') |
|
|
|
def test_cross_exe_passed_no_wrapper(self): |
|
testdir = os.path.join(self.unit_test_dir, '71 cross test passed') |
|
with tempfile.TemporaryDirectory() as d: |
|
p = Path(d) / 'crossfile' |
|
with p.open('wt') as f: |
|
f.write(self._cross_file_generator(needs_exe_wrapper=True)) |
|
|
|
self.init(testdir, extra_args=['--cross-file=' + str(p)]) |
|
self.build() |
|
out = self.run_target('test') |
|
self.assertRegex(out, r'Skipped:\s*1\s*\n') |
|
|
|
# The test uses mocking and thus requires that the current process is the |
|
# one to run the Meson steps. If we are using an external test executable |
|
# (most commonly in Debian autopkgtests) then the mocking won't work. |
|
@unittest.skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.') |
|
def test_cross_file_system_paths(self): |
|
if is_windows(): |
|
raise unittest.SkipTest('system crossfile paths not defined for Windows (yet)') |
|
|
|
testdir = os.path.join(self.common_test_dir, '1 trivial') |
|
cross_content = self._cross_file_generator() |
|
with tempfile.TemporaryDirectory() as d: |
|
dir_ = os.path.join(d, 'meson', 'cross') |
|
os.makedirs(dir_) |
|
with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f: |
|
f.write(cross_content) |
|
name = os.path.basename(f.name) |
|
|
|
with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}): |
|
self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) |
|
self.wipe() |
|
|
|
with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}): |
|
os.environ.pop('XDG_DATA_HOME', None) |
|
self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) |
|
self.wipe() |
|
|
|
with tempfile.TemporaryDirectory() as d: |
|
dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross') |
|
os.makedirs(dir_) |
|
with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f: |
|
f.write(cross_content) |
|
name = os.path.basename(f.name) |
|
|
|
# If XDG_DATA_HOME is set in the environment running the |
|
# tests this test will fail, os mock the environment, pop |
|
# it, then test |
|
with mock.patch.dict(os.environ): |
|
os.environ.pop('XDG_DATA_HOME', None) |
|
with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)): |
|
self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) |
|
self.wipe() |
|
|
|
def helper_create_cross_file(self, values): |
|
"""Create a config file as a temporary file. |
|
|
|
values should be a nested dictionary structure of {section: {key: |
|
value}} |
|
""" |
|
filename = os.path.join(self.builddir, f'generated{self.current_config}.config') |
|
self.current_config += 1 |
|
with open(filename, 'wt') as f: |
|
for section, entries in values.items(): |
|
f.write(f'[{section}]\n') |
|
for k, v in entries.items(): |
|
f.write(f"{k}={v!r}\n") |
|
return filename |
|
|
|
def test_cross_file_dirs(self): |
|
testcase = os.path.join(self.unit_test_dir, '60 native file override') |
|
self.init(testcase, default_args=False, |
|
extra_args=['--native-file', os.path.join(testcase, 'nativefile'), |
|
'--cross-file', os.path.join(testcase, 'crossfile'), |
|
'-Ddef_bindir=binbar', |
|
'-Ddef_datadir=databar', |
|
'-Ddef_includedir=includebar', |
|
'-Ddef_infodir=infobar', |
|
'-Ddef_libdir=libbar', |
|
'-Ddef_libexecdir=libexecbar', |
|
'-Ddef_localedir=localebar', |
|
'-Ddef_localstatedir=localstatebar', |
|
'-Ddef_mandir=manbar', |
|
'-Ddef_sbindir=sbinbar', |
|
'-Ddef_sharedstatedir=sharedstatebar', |
|
'-Ddef_sysconfdir=sysconfbar']) |
|
|
|
def test_cross_file_dirs_overridden(self): |
|
testcase = os.path.join(self.unit_test_dir, '60 native file override') |
|
self.init(testcase, default_args=False, |
|
extra_args=['--native-file', os.path.join(testcase, 'nativefile'), |
|
'--cross-file', os.path.join(testcase, 'crossfile'), |
|
'-Ddef_libdir=liblib', '-Dlibdir=liblib', |
|
'-Ddef_bindir=binbar', |
|
'-Ddef_datadir=databar', |
|
'-Ddef_includedir=includebar', |
|
'-Ddef_infodir=infobar', |
|
'-Ddef_libexecdir=libexecbar', |
|
'-Ddef_localedir=localebar', |
|
'-Ddef_localstatedir=localstatebar', |
|
'-Ddef_mandir=manbar', |
|
'-Ddef_sbindir=sbinbar', |
|
'-Ddef_sharedstatedir=sharedstatebar', |
|
'-Ddef_sysconfdir=sysconfbar']) |
|
|
|
def test_cross_file_dirs_chain(self): |
|
# crossfile2 overrides crossfile overrides nativefile |
|
testcase = os.path.join(self.unit_test_dir, '60 native file override') |
|
self.init(testcase, default_args=False, |
|
extra_args=['--native-file', os.path.join(testcase, 'nativefile'), |
|
'--cross-file', os.path.join(testcase, 'crossfile'), |
|
'--cross-file', os.path.join(testcase, 'crossfile2'), |
|
'-Ddef_bindir=binbar2', |
|
'-Ddef_datadir=databar', |
|
'-Ddef_includedir=includebar', |
|
'-Ddef_infodir=infobar', |
|
'-Ddef_libdir=libbar', |
|
'-Ddef_libexecdir=libexecbar', |
|
'-Ddef_localedir=localebar', |
|
'-Ddef_localstatedir=localstatebar', |
|
'-Ddef_mandir=manbar', |
|
'-Ddef_sbindir=sbinbar', |
|
'-Ddef_sharedstatedir=sharedstatebar', |
|
'-Ddef_sysconfdir=sysconfbar']) |
|
|
|
def test_user_options(self): |
|
# This is just a touch test for cross file, since the implementation |
|
# shares code after loading from the files |
|
testcase = os.path.join(self.common_test_dir, '40 options') |
|
config = self.helper_create_cross_file({'project options': {'testoption': 'some other value'}}) |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(testcase, extra_args=['--cross-file', config]) |
|
self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') |
|
|
|
def test_builtin_options(self): |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
config = self.helper_create_cross_file({'built-in options': {'cpp_std': 'c++14'}}) |
|
|
|
self.init(testcase, extra_args=['--cross-file', config]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'cpp_std': |
|
self.assertEqual(each['value'], 'c++14') |
|
break |
|
else: |
|
self.fail('No c++ standard set?') |
|
|
|
def test_builtin_options_per_machine(self): |
|
"""Test options that are allowed to be set on a per-machine basis. |
|
|
|
Such options could be passed twice, once for the build machine, and |
|
once for the host machine. I've picked pkg-config path, but any would |
|
do that can be set for both. |
|
""" |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross/path', 'cpp_std': 'c++17'}}) |
|
native = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native/path', 'cpp_std': 'c++14'}}) |
|
|
|
# Ensure that PKG_CONFIG_PATH is not set in the environment |
|
with mock.patch.dict('os.environ'): |
|
for k in ['PKG_CONFIG_PATH', 'PKG_CONFIG_PATH_FOR_BUILD']: |
|
try: |
|
del os.environ[k] |
|
except KeyError: |
|
pass |
|
self.init(testcase, extra_args=['--cross-file', cross, '--native-file', native]) |
|
|
|
configuration = self.introspect('--buildoptions') |
|
found = 0 |
|
for each in configuration: |
|
if each['name'] == 'pkg_config_path': |
|
self.assertEqual(each['value'], ['/cross/path']) |
|
found += 1 |
|
elif each['name'] == 'cpp_std': |
|
self.assertEqual(each['value'], 'c++17') |
|
found += 1 |
|
elif each['name'] == 'build.pkg_config_path': |
|
self.assertEqual(each['value'], ['/native/path']) |
|
found += 1 |
|
elif each['name'] == 'build.cpp_std': |
|
self.assertEqual(each['value'], 'c++14') |
|
found += 1 |
|
|
|
if found == 4: |
|
break |
|
self.assertEqual(found, 4, 'Did not find all sections.') |
|
|
|
def test_builtin_options_conf_overrides_env(self): |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
config = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native', 'cpp_args': '-DFILE'}}) |
|
cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross', 'cpp_args': '-DFILE'}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross], |
|
override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir', |
|
'CXXFLAGS': '-DENV', 'CXXFLAGS_FOR_BUILD': '-DENV'}) |
|
configuration = self.introspect('--buildoptions') |
|
found = 0 |
|
expected = 4 |
|
for each in configuration: |
|
if each['name'] == 'pkg_config_path': |
|
self.assertEqual(each['value'], ['/cross']) |
|
found += 1 |
|
elif each['name'] == 'build.pkg_config_path': |
|
self.assertEqual(each['value'], ['/native']) |
|
found += 1 |
|
elif each['name'].endswith('cpp_args'): |
|
self.assertEqual(each['value'], ['-DFILE']) |
|
found += 1 |
|
if found == expected: |
|
break |
|
self.assertEqual(found, expected, 'Did not find all sections.') |
|
|
|
def test_for_build_env_vars(self) -> None: |
|
testcase = os.path.join(self.common_test_dir, '2 cpp') |
|
config = self.helper_create_cross_file({'built-in options': {}}) |
|
cross = self.helper_create_cross_file({'built-in options': {}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross], |
|
override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir'}) |
|
configuration = self.introspect('--buildoptions') |
|
found = 0 |
|
for each in configuration: |
|
if each['name'] == 'pkg_config_path': |
|
self.assertEqual(each['value'], ['/bar']) |
|
found += 1 |
|
elif each['name'] == 'build.pkg_config_path': |
|
self.assertEqual(each['value'], ['/dir']) |
|
found += 1 |
|
if found == 2: |
|
break |
|
self.assertEqual(found, 2, 'Did not find all sections.') |
|
|
|
def test_project_options_native_only(self) -> None: |
|
# Do not load project options from a native file when doing a cross |
|
# build |
|
testcase = os.path.join(self.unit_test_dir, '19 array option') |
|
config = self.helper_create_cross_file({'project options': {'list': ['bar', 'foo']}}) |
|
cross = self.helper_create_cross_file({'binaries': {}}) |
|
|
|
self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross]) |
|
configuration = self.introspect('--buildoptions') |
|
for each in configuration: |
|
if each['name'] == 'list': |
|
self.assertEqual(each['value'], ['foo', 'bar']) |
|
break |
|
else: |
|
self.fail('Did not find expected option.') |
|
|
|
|
|
class TAPParserTests(unittest.TestCase): |
|
def assert_test(self, events, **kwargs): |
|
if 'explanation' not in kwargs: |
|
kwargs['explanation'] = None |
|
self.assertEqual(next(events), TAPParser.Test(**kwargs)) |
|
|
|
def assert_plan(self, events, **kwargs): |
|
if 'skipped' not in kwargs: |
|
kwargs['skipped'] = False |
|
if 'explanation' not in kwargs: |
|
kwargs['explanation'] = None |
|
self.assertEqual(next(events), TAPParser.Plan(**kwargs)) |
|
|
|
def assert_version(self, events, **kwargs): |
|
self.assertEqual(next(events), TAPParser.Version(**kwargs)) |
|
|
|
def assert_error(self, events): |
|
self.assertEqual(type(next(events)), TAPParser.Error) |
|
|
|
def assert_bailout(self, events, **kwargs): |
|
self.assertEqual(next(events), TAPParser.Bailout(**kwargs)) |
|
|
|
def assert_last(self, events): |
|
with self.assertRaises(StopIteration): |
|
next(events) |
|
|
|
def parse_tap(self, s): |
|
parser = TAPParser() |
|
return iter(parser.parse(io.StringIO(s))) |
|
|
|
def parse_tap_v13(self, s): |
|
events = self.parse_tap('TAP version 13\n' + s) |
|
self.assert_version(events, version=13) |
|
return events |
|
|
|
def test_empty(self): |
|
events = self.parse_tap('') |
|
self.assert_last(events) |
|
|
|
def test_empty_plan(self): |
|
events = self.parse_tap('1..0') |
|
self.assert_plan(events, num_tests=0, late=False, skipped=True) |
|
self.assert_last(events) |
|
|
|
def test_plan_directive(self): |
|
events = self.parse_tap('1..0 # skipped for some reason') |
|
self.assert_plan(events, num_tests=0, late=False, skipped=True, |
|
explanation='for some reason') |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('1..1 # skipped for some reason\nok 1') |
|
self.assert_error(events) |
|
self.assert_plan(events, num_tests=1, late=False, skipped=True, |
|
explanation='for some reason') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('1..1 # todo not supported here\nok 1') |
|
self.assert_error(events) |
|
self.assert_plan(events, num_tests=1, late=False, skipped=False, |
|
explanation='not supported here') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_one_test_ok(self): |
|
events = self.parse_tap('ok') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_one_test_with_number(self): |
|
events = self.parse_tap('ok 1') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_one_test_with_name(self): |
|
events = self.parse_tap('ok 1 abc') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_one_test_not_ok(self): |
|
events = self.parse_tap('not ok') |
|
self.assert_test(events, number=1, name='', result=TestResult.FAIL) |
|
self.assert_last(events) |
|
|
|
def test_one_test_todo(self): |
|
events = self.parse_tap('not ok 1 abc # TODO') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.EXPECTEDFAIL) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('ok 1 abc # TODO') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS) |
|
self.assert_last(events) |
|
|
|
def test_one_test_skip(self): |
|
events = self.parse_tap('ok 1 abc # SKIP') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.SKIP) |
|
self.assert_last(events) |
|
|
|
def test_one_test_skip_failure(self): |
|
events = self.parse_tap('not ok 1 abc # SKIP') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.FAIL) |
|
self.assert_last(events) |
|
|
|
def test_many_early_plan(self): |
|
events = self.parse_tap('1..4\nok 1\nnot ok 2\nok 3\nnot ok 4') |
|
self.assert_plan(events, num_tests=4, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_test(events, number=3, name='', result=TestResult.OK) |
|
self.assert_test(events, number=4, name='', result=TestResult.FAIL) |
|
self.assert_last(events) |
|
|
|
def test_many_late_plan(self): |
|
events = self.parse_tap('ok 1\nnot ok 2\nok 3\nnot ok 4\n1..4') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_test(events, number=3, name='', result=TestResult.OK) |
|
self.assert_test(events, number=4, name='', result=TestResult.FAIL) |
|
self.assert_plan(events, num_tests=4, late=True) |
|
self.assert_last(events) |
|
|
|
def test_directive_case(self): |
|
events = self.parse_tap('ok 1 abc # skip') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.SKIP) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('ok 1 abc # ToDo') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS) |
|
self.assert_last(events) |
|
|
|
def test_directive_explanation(self): |
|
events = self.parse_tap('ok 1 abc # skip why') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.SKIP, |
|
explanation='why') |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('ok 1 abc # ToDo Because') |
|
self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS, |
|
explanation='Because') |
|
self.assert_last(events) |
|
|
|
def test_one_test_early_plan(self): |
|
events = self.parse_tap('1..1\nok') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_one_test_late_plan(self): |
|
events = self.parse_tap('ok\n1..1') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_plan(events, num_tests=1, late=True) |
|
self.assert_last(events) |
|
|
|
def test_out_of_order(self): |
|
events = self.parse_tap('ok 2') |
|
self.assert_error(events) |
|
self.assert_test(events, number=2, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_middle_plan(self): |
|
events = self.parse_tap('ok 1\n1..2\nok 2') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_plan(events, num_tests=2, late=True) |
|
self.assert_error(events) |
|
self.assert_test(events, number=2, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_too_many_plans(self): |
|
events = self.parse_tap('1..1\n1..2\nok 1') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_error(events) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_too_many(self): |
|
events = self.parse_tap('ok 1\nnot ok 2\n1..1') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_plan(events, num_tests=1, late=True) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('1..1\nok 1\nnot ok 2') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
def test_too_few(self): |
|
events = self.parse_tap('ok 1\nnot ok 2\n1..3') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_plan(events, num_tests=3, late=True) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('1..3\nok 1\nnot ok 2') |
|
self.assert_plan(events, num_tests=3, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
def test_too_few_bailout(self): |
|
events = self.parse_tap('1..3\nok 1\nnot ok 2\nBail out! no third test') |
|
self.assert_plan(events, num_tests=3, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_bailout(events, message='no third test') |
|
self.assert_last(events) |
|
|
|
def test_diagnostics(self): |
|
events = self.parse_tap('1..1\n# ignored\nok 1') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('# ignored\n1..1\nok 1\n# ignored too') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('# ignored\nok 1\n1..1\n# ignored too') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_plan(events, num_tests=1, late=True) |
|
self.assert_last(events) |
|
|
|
def test_empty_line(self): |
|
events = self.parse_tap('1..1\n\nok 1') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_unexpected(self): |
|
events = self.parse_tap('1..1\ninvalid\nok 1') |
|
self.assert_plan(events, num_tests=1, late=False) |
|
self.assert_error(events) |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
def test_version(self): |
|
events = self.parse_tap('TAP version 13\n') |
|
self.assert_version(events, version=13) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('TAP version 12\n') |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap('1..0\nTAP version 13\n') |
|
self.assert_plan(events, num_tests=0, late=False, skipped=True) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
def test_yaml(self): |
|
events = self.parse_tap_v13('ok\n ---\n foo: abc\n bar: def\n ...\nok 2') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_test(events, number=2, name='', result=TestResult.OK) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap_v13('ok\n ---\n foo: abc\n bar: def') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_error(events) |
|
self.assert_last(events) |
|
|
|
events = self.parse_tap_v13('ok 1\n ---\n foo: abc\n bar: def\nnot ok 2') |
|
self.assert_test(events, number=1, name='', result=TestResult.OK) |
|
self.assert_error(events) |
|
self.assert_test(events, number=2, name='', result=TestResult.FAIL) |
|
self.assert_last(events) |
|
|
|
class SubprojectsCommandTests(BasePlatformTests): |
|
def setUp(self): |
|
super().setUp() |
|
self.root_dir = Path(self.builddir) |
|
|
|
self.project_dir = self.root_dir / 'src' |
|
self._create_project(self.project_dir) |
|
|
|
self.subprojects_dir = self.project_dir / 'subprojects' |
|
os.makedirs(str(self.subprojects_dir)) |
|
self.packagecache_dir = self.subprojects_dir / 'packagecache' |
|
os.makedirs(str(self.packagecache_dir)) |
|
|
|
def _create_project(self, path, project_name='dummy'): |
|
os.makedirs(str(path), exist_ok=True) |
|
with open(str(path / 'meson.build'), 'w') as f: |
|
f.write(f"project('{project_name}')") |
|
|
|
def _git(self, cmd, workdir): |
|
return git(cmd, str(workdir), check=True)[1].strip() |
|
|
|
def _git_config(self, workdir): |
|
self._git(['config', 'user.name', 'Meson Test'], workdir) |
|
self._git(['config', 'user.email', 'meson.test@example.com'], workdir) |
|
|
|
def _git_remote(self, cmd, name): |
|
return self._git(cmd, self.root_dir / name) |
|
|
|
def _git_local(self, cmd, name): |
|
return self._git(cmd, self.subprojects_dir / name) |
|
|
|
def _git_local_branch(self, name): |
|
# Same as `git branch --show-current` but compatible with older git version |
|
branch = self._git_local(['rev-parse', '--abbrev-ref', 'HEAD'], name) |
|
return branch if branch != 'HEAD' else '' |
|
|
|
def _git_local_commit(self, name, ref='HEAD'): |
|
return self._git_local(['rev-parse', ref], name) |
|
|
|
def _git_remote_commit(self, name, ref='HEAD'): |
|
return self._git_remote(['rev-parse', ref], name) |
|
|
|
def _git_create_repo(self, path): |
|
# If a user has git configuration init.defaultBranch set we want to override that |
|
with tempfile.TemporaryDirectory() as d: |
|
out = git(['--version'], str(d))[1] |
|
if version_compare(search_version(out), '>= 2.28'): |
|
extra_cmd = ['--initial-branch', 'master'] |
|
else: |
|
extra_cmd = [] |
|
|
|
self._create_project(path) |
|
self._git(['init'] + extra_cmd, path) |
|
self._git_config(path) |
|
self._git(['add', '.'], path) |
|
self._git(['commit', '-m', 'Initial commit'], path) |
|
|
|
def _git_create_remote_repo(self, name): |
|
self._git_create_repo(self.root_dir / name) |
|
|
|
def _git_create_local_repo(self, name): |
|
self._git_create_repo(self.subprojects_dir / name) |
|
|
|
def _git_create_remote_commit(self, name, branch): |
|
self._git_remote(['checkout', branch], name) |
|
self._git_remote(['commit', '--allow-empty', '-m', f'initial {branch} commit'], name) |
|
|
|
def _git_create_remote_branch(self, name, branch): |
|
self._git_remote(['checkout', '-b', branch], name) |
|
self._git_remote(['commit', '--allow-empty', '-m', f'initial {branch} commit'], name) |
|
|
|
def _git_create_remote_tag(self, name, tag): |
|
self._git_remote(['commit', '--allow-empty', '-m', f'tag {tag} commit'], name) |
|
self._git_remote(['tag', tag], name) |
|
|
|
def _wrap_create_git(self, name, revision='master'): |
|
path = self.root_dir / name |
|
with open(str((self.subprojects_dir / name).with_suffix('.wrap')), 'w') as f: |
|
f.write(textwrap.dedent( |
|
''' |
|
[wrap-git] |
|
url={} |
|
revision={} |
|
'''.format(os.path.abspath(str(path)), revision))) |
|
|
|
def _wrap_create_file(self, name, tarball='dummy.tar.gz'): |
|
path = self.root_dir / tarball |
|
with open(str((self.subprojects_dir / name).with_suffix('.wrap')), 'w') as f: |
|
f.write(textwrap.dedent( |
|
f''' |
|
[wrap-file] |
|
source_url={os.path.abspath(str(path))} |
|
source_filename={tarball} |
|
''')) |
|
Path(self.packagecache_dir / tarball).touch() |
|
|
|
def _subprojects_cmd(self, args): |
|
return self._run(self.meson_command + ['subprojects'] + args, workdir=str(self.project_dir)) |
|
|
|
def test_git_update(self): |
|
subp_name = 'sub1' |
|
|
|
# Create a fake remote git repository and a wrap file. Checks that |
|
# "meson subprojects download" works. |
|
self._git_create_remote_repo(subp_name) |
|
self._wrap_create_git(subp_name) |
|
self._subprojects_cmd(['download']) |
|
self.assertPathExists(str(self.subprojects_dir / subp_name)) |
|
self._git_config(self.subprojects_dir / subp_name) |
|
|
|
# Create a new remote branch and update the wrap file. Checks that |
|
# "meson subprojects update --reset" checkout the new branch. |
|
self._git_create_remote_branch(subp_name, 'newbranch') |
|
self._wrap_create_git(subp_name, 'newbranch') |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_branch(subp_name), 'newbranch') |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch')) |
|
|
|
# Update remote newbranch. Checks the new commit is pulled into existing |
|
# local newbranch. Make sure it does not print spurious 'git stash' message. |
|
self._git_create_remote_commit(subp_name, 'newbranch') |
|
out = self._subprojects_cmd(['update', '--reset']) |
|
self.assertNotIn('No local changes to save', out) |
|
self.assertEqual(self._git_local_branch(subp_name), 'newbranch') |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch')) |
|
|
|
# Update remote newbranch and switch to another branch. Checks that it |
|
# switch current branch to newbranch and pull latest commit. |
|
self._git_local(['checkout', 'master'], subp_name) |
|
self._git_create_remote_commit(subp_name, 'newbranch') |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_branch(subp_name), 'newbranch') |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch')) |
|
|
|
# Stage some local changes then update. Checks that local changes got |
|
# stashed. |
|
self._create_project(self.subprojects_dir / subp_name, 'new_project_name') |
|
self._git_local(['add', '.'], subp_name) |
|
self._git_create_remote_commit(subp_name, 'newbranch') |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_branch(subp_name), 'newbranch') |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch')) |
|
self.assertTrue(self._git_local(['stash', 'list'], subp_name)) |
|
|
|
# Create a new remote tag and update the wrap file. Checks that |
|
# "meson subprojects update --reset" checkout the new tag in detached mode. |
|
self._git_create_remote_tag(subp_name, 'newtag') |
|
self._wrap_create_git(subp_name, 'newtag') |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_branch(subp_name), '') |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newtag')) |
|
|
|
# Create a new remote commit and update the wrap file with the commit id. |
|
# Checks that "meson subprojects update --reset" checkout the new commit |
|
# in detached mode. |
|
self._git_local(['checkout', 'master'], subp_name) |
|
self._git_create_remote_commit(subp_name, 'newbranch') |
|
new_commit = self._git_remote(['rev-parse', 'HEAD'], subp_name) |
|
self._wrap_create_git(subp_name, new_commit) |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_branch(subp_name), '') |
|
self.assertEqual(self._git_local_commit(subp_name), new_commit) |
|
|
|
# Create a local project not in a git repository, then update it with |
|
# a git wrap. Without --reset it should print error message and return |
|
# failure. With --reset it should delete existing project and clone the |
|
# new project. |
|
subp_name = 'sub2' |
|
self._create_project(self.subprojects_dir / subp_name) |
|
self._git_create_remote_repo(subp_name) |
|
self._wrap_create_git(subp_name) |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self._subprojects_cmd(['update']) |
|
self.assertIn('Not a git repository', cm.exception.output) |
|
self._subprojects_cmd(['update', '--reset']) |
|
self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name)) |
|
|
|
@skipIfNoExecutable('true') |
|
def test_foreach(self): |
|
self._create_project(self.subprojects_dir / 'sub_file') |
|
self._wrap_create_file('sub_file') |
|
self._git_create_local_repo('sub_git') |
|
self._wrap_create_git('sub_git') |
|
self._git_create_local_repo('sub_git_no_wrap') |
|
|
|
def ran_in(s): |
|
ret = [] |
|
prefix = 'Executing command in ' |
|
for l in s.splitlines(): |
|
if l.startswith(prefix): |
|
ret.append(l[len(prefix):]) |
|
return sorted(ret) |
|
|
|
dummy_cmd = ['true'] |
|
out = self._subprojects_cmd(['foreach'] + dummy_cmd) |
|
self.assertEqual(ran_in(out), sorted(['subprojects/sub_file', 'subprojects/sub_git', 'subprojects/sub_git_no_wrap'])) |
|
out = self._subprojects_cmd(['foreach', '--types', 'git,file'] + dummy_cmd) |
|
self.assertEqual(ran_in(out), sorted(['subprojects/sub_file', 'subprojects/sub_git'])) |
|
out = self._subprojects_cmd(['foreach', '--types', 'file'] + dummy_cmd) |
|
self.assertEqual(ran_in(out), ['subprojects/sub_file']) |
|
out = self._subprojects_cmd(['foreach', '--types', 'git'] + dummy_cmd) |
|
self.assertEqual(ran_in(out), ['subprojects/sub_git']) |
|
|
|
def test_purge(self): |
|
self._create_project(self.subprojects_dir / 'sub_file') |
|
self._wrap_create_file('sub_file') |
|
self._git_create_local_repo('sub_git') |
|
self._wrap_create_git('sub_git') |
|
|
|
def deleting(s) -> T.List[str]: |
|
ret = [] |
|
prefix = 'Deleting ' |
|
for l in s.splitlines(): |
|
if l.startswith(prefix): |
|
ret.append(l[len(prefix):]) |
|
return sorted(ret) |
|
|
|
out = self._subprojects_cmd(['purge']) |
|
self.assertEqual(deleting(out), [str(self.subprojects_dir / 'sub_file'), str(self.subprojects_dir / 'sub_git')]) |
|
out = self._subprojects_cmd(['purge', '--include-cache']) |
|
self.assertEqual(deleting(out), [str(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz'), str(self.subprojects_dir / 'sub_file'), str(self.subprojects_dir / 'sub_git')]) |
|
out = self._subprojects_cmd(['purge', '--include-cache', '--confirm']) |
|
self.assertEqual(deleting(out), [str(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz'), str(self.subprojects_dir / 'sub_file'), str(self.subprojects_dir / 'sub_git')]) |
|
self.assertFalse(Path(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz').exists()) |
|
self.assertFalse(Path(self.subprojects_dir / 'sub_file').exists()) |
|
self.assertFalse(Path(self.subprojects_dir / 'sub_git').exists()) |
|
|
|
def _clang_at_least(compiler: 'Compiler', minver: str, apple_minver: T.Optional[str]) -> bool: |
|
""" |
|
check that Clang compiler is at least a specified version, whether AppleClang or regular Clang |
|
|
|
Parameters |
|
---------- |
|
compiler: |
|
Meson compiler object |
|
minver: str |
|
Clang minimum version |
|
apple_minver: str |
|
AppleCLang minimum version |
|
|
|
Returns |
|
------- |
|
at_least: bool |
|
Clang is at least the specified version |
|
""" |
|
if isinstance(compiler, (mesonbuild.compilers.AppleClangCCompiler, |
|
mesonbuild.compilers.AppleClangCPPCompiler)): |
|
if apple_minver is None: |
|
return False |
|
return version_compare(compiler.version, apple_minver) |
|
return version_compare(compiler.version, minver) |
|
|
|
|
|
def unset_envs(): |
|
# For unit tests we must fully control all command lines |
|
# so that there are no unexpected changes coming from the |
|
# environment, for example when doing a package build. |
|
varnames = ['CPPFLAGS', 'LDFLAGS'] + list(mesonbuild.compilers.compilers.CFLAGS_MAPPING.values()) |
|
for v in varnames: |
|
if v in os.environ: |
|
del os.environ[v] |
|
|
|
def convert_args(argv): |
|
# If we got passed a list of tests, pass it on |
|
pytest_args = ['-v'] if '-v' in argv else [] |
|
test_list = [] |
|
for arg in argv: |
|
if arg.startswith('-'): |
|
if arg in ('-f', '--failfast'): |
|
arg = '--exitfirst' |
|
pytest_args.append(arg) |
|
continue |
|
# ClassName.test_name => 'ClassName and test_name' |
|
if '.' in arg: |
|
arg = ' and '.join(arg.split('.')) |
|
test_list.append(arg) |
|
if test_list: |
|
pytest_args += ['-k', ' or '.join(test_list)] |
|
return pytest_args |
|
|
|
def running_single_tests(argv, cases): |
|
''' |
|
Check whether we only got arguments for running individual tests, not |
|
entire testcases, and not all testcases (no test args). |
|
''' |
|
got_test_arg = False |
|
for arg in argv: |
|
if arg.startswith('-'): |
|
continue |
|
for case in cases: |
|
if not arg.startswith(case): |
|
continue |
|
if '.' not in arg: |
|
# Got a testcase, done |
|
return False |
|
got_test_arg = True |
|
return got_test_arg |
|
|
|
def setup_backend(): |
|
filtered = [] |
|
be = 'ninja' |
|
for a in sys.argv: |
|
if a.startswith('--backend'): |
|
be = a.split('=')[1] |
|
else: |
|
filtered.append(a) |
|
# Since we invoke the tests via unittest or xtest test runner |
|
# we need to pass the backend to use to the spawned process via |
|
# this side channel. Yes it sucks, but at least is is fully |
|
# internal to this file. |
|
os.environ['MESON_UNIT_TEST_BACKEND'] = be |
|
sys.argv = filtered |
|
|
|
def main(): |
|
unset_envs() |
|
setup_backend() |
|
cases = ['InternalTests', 'DataTests', 'AllPlatformTests', 'FailureTests', |
|
'PythonTests', 'NativeFileTests', 'RewriterTests', 'CrossFileTests', |
|
'TAPParserTests', 'SubprojectsCommandTests', |
|
|
|
'LinuxlikeTests', 'LinuxCrossArmTests', 'LinuxCrossMingwTests', |
|
'WindowsTests', 'DarwinTests'] |
|
|
|
try: |
|
import pytest # noqa: F401 |
|
# Need pytest-xdist for `-n` arg |
|
import xdist # noqa: F401 |
|
pytest_args = [] |
|
# Don't use pytest-xdist when running single unit tests since it wastes |
|
# time spawning a lot of processes to distribute tests to in that case. |
|
if not running_single_tests(sys.argv, cases): |
|
pytest_args += ['-n', 'auto'] |
|
# Let there be colors! |
|
if 'CI' in os.environ: |
|
pytest_args += ['--color=yes'] |
|
pytest_args += ['./run_unittests.py'] |
|
pytest_args += convert_args(sys.argv[1:]) |
|
# Always disable pytest-cov because we use a custom setup |
|
try: |
|
import pytest_cov # noqa: F401 |
|
print('Disabling pytest-cov') |
|
pytest_args += ['-p' 'no:cov'] |
|
except ImportError: |
|
pass |
|
return subprocess.run(python_command + ['-m', 'pytest'] + pytest_args).returncode |
|
except ImportError: |
|
print('pytest-xdist not found, using unittest instead') |
|
# Fallback to plain unittest. |
|
return unittest.main(defaultTest=cases, buffer=True) |
|
|
|
if __name__ == '__main__': |
|
setup_vsenv() |
|
print('Meson build system', mesonbuild.coredata.version, 'Unit Tests') |
|
start = time.monotonic() |
|
try: |
|
raise SystemExit(main()) |
|
finally: |
|
print('Total time: {:.3f} seconds'.format(time.monotonic() - start))
|
|
|