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.
5014 lines
223 KiB
5014 lines
223 KiB
# SPDX-License-Identifier: Apache-2.0 |
|
# Copyright 2016-2021 The Meson development team |
|
|
|
import subprocess |
|
import re |
|
import json |
|
import tempfile |
|
import textwrap |
|
import os |
|
import shutil |
|
import platform |
|
import pickle |
|
import zipfile, tarfile |
|
import sys |
|
from unittest import mock, SkipTest, skipIf, skipUnless |
|
from contextlib import contextmanager |
|
from glob import glob |
|
from pathlib import (PurePath, Path) |
|
import typing as T |
|
|
|
import mesonbuild.mlog |
|
import mesonbuild.depfile |
|
import mesonbuild.dependencies.base |
|
import mesonbuild.dependencies.factory |
|
import mesonbuild.envconfig |
|
import mesonbuild.environment |
|
import mesonbuild.coredata |
|
import mesonbuild.modules.gnome |
|
from mesonbuild.mesonlib import ( |
|
BuildDirLock, MachineChoice, is_windows, is_osx, is_cygwin, is_dragonflybsd, |
|
is_sunos, windows_proof_rmtree, python_command, version_compare, split_args, quote_arg, |
|
relpath, is_linux, git, search_version, do_conf_file, do_conf_str, default_prefix, |
|
MesonException, EnvironmentException, OptionKey, |
|
windows_proof_rm |
|
) |
|
from mesonbuild.programs import ExternalProgram |
|
|
|
from mesonbuild.compilers.mixins.clang import ClangCompiler |
|
from mesonbuild.compilers.mixins.gnu import GnuCompiler |
|
from mesonbuild.compilers.mixins.intel import IntelGnuLikeCompiler |
|
from mesonbuild.compilers.c import VisualStudioCCompiler, ClangClCCompiler |
|
from mesonbuild.compilers.cpp import VisualStudioCPPCompiler, ClangClCPPCompiler |
|
from mesonbuild.compilers import ( |
|
detect_static_linker, detect_c_compiler, compiler_from_language, |
|
detect_compiler_for |
|
) |
|
from mesonbuild.linkers import linkers |
|
|
|
from mesonbuild.dependencies.pkgconfig import PkgConfigDependency |
|
from mesonbuild.build import Target, ConfigurationData, Executable, SharedLibrary, StaticLibrary |
|
from mesonbuild import mtest |
|
import mesonbuild.modules.pkgconfig |
|
from mesonbuild.scripts import destdir_join |
|
|
|
from mesonbuild.wrap.wrap import PackageDefinition, WrapException |
|
|
|
from run_tests import ( |
|
Backend, exe_suffix, get_fake_env, get_convincing_fake_env_and_cc |
|
) |
|
|
|
from .baseplatformtests import BasePlatformTests |
|
from .helpers import * |
|
|
|
@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 |
|
|
|
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', '--no-gpg-sign', '-a', '-m', 'I am a project'], cwd=project_dir, |
|
stdout=subprocess.DEVNULL) |
|
|
|
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: |
|
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) = do_conf_str('configuration_file', in_data, confdata, variable_format = vformat) |
|
return '\n'.join(result) |
|
|
|
def check_meson_format(confdata, result): |
|
self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result) |
|
|
|
def check_cmake_format_simple(confdata, result): |
|
self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), result) |
|
|
|
def check_cmake_formats_full(confdata, result): |
|
self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result) |
|
self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result) |
|
|
|
def check_formats(confdata, result): |
|
check_meson_format(confdata, result) |
|
check_cmake_formats_full(confdata, result) |
|
|
|
confdata = ConfigurationData() |
|
# Key error as they do not exists |
|
check_formats(confdata, '/* #undef VAR */\n') |
|
|
|
# Check boolean |
|
confdata.values = {'VAR': (False, 'description')} |
|
check_meson_format(confdata, '#undef VAR\n') |
|
check_cmake_formats_full(confdata, '/* #undef VAR */\n') |
|
|
|
confdata.values = {'VAR': (True, 'description')} |
|
check_meson_format(confdata, '#define VAR\n') |
|
check_cmake_format_simple(confdata, '#define VAR\n') |
|
check_cmake_formats_full(confdata, '#define VAR 1\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') |
|
|
|
# Checking if cmakedefine behaves as it does with cmake |
|
confdata.values = {'VAR': ("var", 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '#define VAR var\n') |
|
|
|
confdata.values = {'VAR': (True, 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 1\n') |
|
|
|
confdata.values = {'VAR': (0, 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') |
|
confdata.values = {'VAR': (False, 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') |
|
|
|
confdata.values = {} |
|
self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') |
|
self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '/* #undef VAR */\n') |
|
|
|
confdata.values = {'VAR': (5, 'description')} |
|
self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), '#define VAR\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(MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'unknown_format') |
|
# More than 2 params in mesondefine |
|
self.assertRaises(MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'meson') |
|
# Mismatched line with format |
|
self.assertRaises(MesonException, conf_str, ['#cmakedefine VAR'], confdata, 'meson') |
|
self.assertRaises(MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake') |
|
self.assertRaises(MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake@') |
|
# Dict value in confdata |
|
confdata.values = {'VAR': (['value'], 'description')} |
|
self.assertRaises(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_can_be_outside_prefix(self): |
|
''' |
|
Tests that libdir is allowed to be outside prefix. |
|
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 ok too |
|
if is_windows(): |
|
args = ['--prefix', 'x:/usr', '--libdir', 'x:/opt/lib32'] |
|
else: |
|
args = ['--prefix', '/usr', '--libdir', '/opt/lib32'] |
|
self.init(testdir, extra_args=args) |
|
self.wipe() |
|
# libdir can be outside prefix even when set via mesonconf |
|
self.init(testdir) |
|
if is_windows(): |
|
self.setconf('-Dlibdir=x:/opt', will_build=False) |
|
else: |
|
self.setconf('-Dlibdir=/opt', will_build=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 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 SkipTest('https://github.com/mesonbuild/meson/issues/1526') |
|
if not isinstance(static_linker, linkers.ArLinker): |
|
raise SkipTest('static linker is not `ar`') |
|
# Configure |
|
self.init(testdir) |
|
# Get name of static library |
|
targets = self.introspect('--targets') |
|
self.assertGreaterEqual(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_replace_unencodable_xml_chars(self): |
|
''' |
|
Test that unencodable xml chars are replaced with their |
|
printable representation |
|
https://github.com/mesonbuild/meson/issues/9894 |
|
''' |
|
# Create base string(\nHello Meson\n) to see valid chars are not replaced |
|
base_string_invalid = '\n\x48\x65\x6c\x6c\x6f\x20\x4d\x65\x73\x6f\x6e\n' |
|
base_string_valid = '\nHello Meson\n' |
|
# Create invalid input from all known unencodable chars |
|
invalid_string = ( |
|
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11' |
|
'\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f' |
|
'\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f' |
|
'\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e' |
|
'\x9f\ufdd0\ufdd1\ufdd2\ufdd3\ufdd4\ufdd5\ufdd6\ufdd7\ufdd8' |
|
'\ufdd9\ufdda\ufddb\ufddc\ufddd\ufdde\ufddf\ufde0\ufde1' |
|
'\ufde2\ufde3\ufde4\ufde5\ufde6\ufde7\ufde8\ufde9\ufdea' |
|
'\ufdeb\ufdec\ufded\ufdee\ufdef\ufffe\uffff') |
|
if sys.maxunicode >= 0x10000: |
|
invalid_string = invalid_string + ( |
|
'\U0001fffe\U0001ffff\U0002fffe\U0002ffff' |
|
'\U0003fffe\U0003ffff\U0004fffe\U0004ffff' |
|
'\U0005fffe\U0005ffff\U0006fffe\U0006ffff' |
|
'\U0007fffe\U0007ffff\U0008fffe\U0008ffff' |
|
'\U0009fffe\U0009ffff\U000afffe\U000affff' |
|
'\U000bfffe\U000bffff\U000cfffe\U000cffff' |
|
'\U000dfffe\U000dffff\U000efffe\U000effff' |
|
'\U000ffffe\U000fffff\U0010fffe\U0010ffff') |
|
|
|
valid_string = base_string_valid + repr(invalid_string)[1:-1] + base_string_valid |
|
invalid_string = base_string_invalid + invalid_string + base_string_invalid |
|
fixed_string = mtest.replace_unencodable_xml_chars(invalid_string) |
|
self.assertEqual(fixed_string, valid_string) |
|
|
|
def test_replace_unencodable_xml_chars_unit(self): |
|
''' |
|
Test that unencodable xml chars are replaced with their |
|
printable representation |
|
https://github.com/mesonbuild/meson/issues/9894 |
|
''' |
|
if not shutil.which('xmllint'): |
|
raise SkipTest('xmllint not installed') |
|
testdir = os.path.join(self.unit_test_dir, '111 replace unencodable xml chars') |
|
self.init(testdir) |
|
tests_command_output = self.run_tests() |
|
junit_xml_logs = Path(self.logdir, 'testlog.junit.xml') |
|
subprocess.run(['xmllint', junit_xml_logs], check=True) |
|
# Ensure command output and JSON / text logs are not mangled. |
|
raw_output_sample = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b' |
|
assert raw_output_sample in tests_command_output |
|
text_log = Path(self.logdir, 'testlog.txt').read_text() |
|
assert raw_output_sample in text_log |
|
json_log = json.loads(Path(self.logdir, 'testlog.json').read_bytes()) |
|
assert raw_output_sample in json_log['stdout'] |
|
|
|
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 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 = { |
|
'nested_elided/sub': 'share', |
|
'new_directory': 'share/new_directory', |
|
'sub/sub1': 'share/sub1', |
|
'sub1': 'share/sub1', |
|
'sub2': 'share/sub2', |
|
'sub3': '/usr/share/sub3', |
|
'sub_elided': 'share', |
|
'subdir/sub1': 'share/sub1', |
|
'subdir/sub_elided': 'share', |
|
} |
|
|
|
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 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 read_install_logs(self): |
|
# Find logged files and directories |
|
with Path(self.builddir, 'meson-logs', 'install-log.txt').open(encoding='utf-8') as f: |
|
return list(map(lambda l: Path(l.strip()), |
|
filter(lambda l: not l.startswith('#'), |
|
f.readlines()))) |
|
|
|
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 |
|
logged = self.read_install_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, self.read_install_logs()) |
|
self.assertFalse(os.path.exists(self.installdir)) |
|
|
|
# If destdir is relative to build directory it should install |
|
# exactly the same files. |
|
rel_installpath = os.path.relpath(self.installdir, self.builddir) |
|
self._run(self.meson_command + ['install', '--dry-run', '--destdir', rel_installpath, '-C', self.builddir]) |
|
self.assertEqual(logged, self.read_install_logs()) |
|
|
|
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, '96 implicit force fallback') |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
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_force_fallback_for_nofallback(self): |
|
testdir = os.path.join(self.unit_test_dir, '31 forcefallback') |
|
self.init(testdir, extra_args=['--force-fallback-for=zlib,foo', '--wrap-mode=nofallback']) |
|
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_verbose(self): |
|
testdir = os.path.join(self.common_test_dir, '206 tap tests') |
|
self.init(testdir) |
|
self.build() |
|
out = self._run(self.mtest_command + ['--suite', 'verbose']) |
|
self.assertIn('1/1 subtest 1', out) |
|
|
|
def test_long_output(self): |
|
testdir = os.path.join(self.common_test_dir, '254 long output') |
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
# Ensure lines are found from testlog.txt when not being verbose. |
|
|
|
i = 1 |
|
with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f: |
|
line = f.readline() |
|
while line and i < 100001: |
|
if f'# Iteration {i} to stdout' in line: |
|
i += 1 |
|
line = f.readline() |
|
self.assertEqual(i, 100001) |
|
|
|
i = 1 |
|
while line: |
|
if f'# Iteration {i} to stderr' in line: |
|
i += 1 |
|
line = f.readline() |
|
self.assertEqual(i, 100001) |
|
|
|
# Ensure lines are found from both testlog.txt and console when being verbose. |
|
|
|
out = self._run(self.mtest_command + ['-v']) |
|
i = 1 |
|
with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f: |
|
line = f.readline() |
|
while line and i < 100001: |
|
if f'# Iteration {i} to stdout' in line: |
|
i += 1 |
|
line = f.readline() |
|
self.assertEqual(i, 100001) |
|
|
|
i = 1 |
|
while line: |
|
if f'# Iteration {i} to stderr' in line: |
|
i += 1 |
|
line = f.readline() |
|
self.assertEqual(i, 100001) |
|
|
|
lines = out.split('\n') |
|
line_number = 0 |
|
i = 1 |
|
while line_number < len(lines) and i < 100001: |
|
print('---> %s' % lines[line_number]) |
|
if f'# Iteration {i} to stdout' in lines[line_number]: |
|
i += 1 |
|
line_number += 1 |
|
self.assertEqual(i, 100001) |
|
|
|
line_number = 0 |
|
i = 1 |
|
while line_number < len(lines): |
|
if f'# Iteration {i} to stderr' in lines[line_number]: |
|
i += 1 |
|
line_number += 1 |
|
self.assertEqual(i, 100001) |
|
|
|
|
|
def test_testsetups(self): |
|
if not shutil.which('valgrind'): |
|
raise 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.assertNotIn('TEST_ENV is set', basic_log) |
|
self.assertNotIn('Memcheck', basic_log) |
|
self.assertIn('TEST_ENV is set', vg_log) |
|
self.assertIn('Memcheck', 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.assertNotIn('buggy', exclude_suites_log) |
|
# --suite overrides add_test_setup(exclude_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.assertIn('buggy', 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, '48 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.assertIn('ENV_A is 1', default_log) |
|
self.assertIn('ENV_B is 2', default_log) |
|
self.assertIn('ENV_C is 2', default_log) |
|
|
|
self.assertIn('ENV_A is 1', mydefault_log) |
|
self.assertIn('ENV_B is 2', mydefault_log) |
|
self.assertIn('ENV_C is 2', mydefault_log) |
|
|
|
self.assertIn('ENV_A is 1', other_log) |
|
self.assertIn('ENV_B is 3', other_log) |
|
self.assertIn('ENV_C is 2', 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_mtest_reconfigure(self): |
|
if self.backend is not Backend.ninja: |
|
raise SkipTest(f'mtest can\'t rebuild with {self.backend.name!r}') |
|
|
|
testdir = os.path.join(self.common_test_dir, '206 tap tests') |
|
self.init(testdir) |
|
self.utime(os.path.join(testdir, 'meson.build')) |
|
o = self._run(self.mtest_command + ['--list']) |
|
self.assertIn('Regenerating build files.', o) |
|
self.assertIn('test_features / xfail', o) |
|
o = self._run(self.mtest_command + ['--list']) |
|
self.assertNotIn('Regenerating build files.', o) |
|
# no real targets should have been built |
|
tester = os.path.join(self.builddir, 'tester' + exe_suffix) |
|
self.assertPathDoesNotExist(tester) |
|
# check that we don't reconfigure if --no-rebuild is passed |
|
self.utime(os.path.join(testdir, 'meson.build')) |
|
o = self._run(self.mtest_command + ['--list', '--no-rebuild']) |
|
self.assertNotIn('Regenerating build files.', o) |
|
|
|
def test_unexisting_test_name(self): |
|
testdir = os.path.join(self.unit_test_dir, '4 suite selection') |
|
self.init(testdir) |
|
self.build() |
|
|
|
self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['notatest']) |
|
|
|
def test_select_test_using_wildcards(self): |
|
testdir = os.path.join(self.unit_test_dir, '4 suite selection') |
|
self.init(testdir) |
|
self.build() |
|
|
|
o = self._run(self.mtest_command + ['--list', 'mainprj*']) |
|
self.assertIn('mainprj-failing_test', o) |
|
self.assertIn('mainprj-successful_test_no_suite', o) |
|
self.assertNotIn('subprj', o) |
|
|
|
o = self._run(self.mtest_command + ['--list', '*succ*', 'subprjm*:']) |
|
self.assertIn('mainprj-successful_test_no_suite', o) |
|
self.assertIn('subprjmix-failing_test', o) |
|
self.assertIn('subprjmix-successful_test', o) |
|
self.assertIn('subprjsucc-successful_test_no_suite', o) |
|
self.assertNotIn('subprjfail-failing_test', o) |
|
|
|
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_build_generated_pyx_directly(self): |
|
# Check that the transpile stage also includes |
|
# dependencies for the compilation stage as dependencies |
|
testdir = os.path.join("test cases/cython", '2 generated sources') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
try: |
|
detect_compiler_for(env, "cython", MachineChoice.HOST, True) |
|
except EnvironmentException: |
|
raise SkipTest("Cython is not installed") |
|
self.init(testdir) |
|
# Need to get the full target name of the pyx.c target |
|
# (which is unfortunately not provided by introspection :( ) |
|
# We'll need to dig into the generated sources |
|
targets = self.introspect('--targets') |
|
name = None |
|
for target in targets: |
|
for target_sources in target["target_sources"]: |
|
for generated_source in target_sources.get("generated_sources", []): |
|
if "includestuff.pyx.c" in generated_source: |
|
name = generated_source |
|
break |
|
# Split the path (we only want the includestuff.cpython-blahblah.so.p/includestuff.pyx.c) |
|
name = os.path.normpath(name).split(os.sep)[-2:] |
|
name = os.sep.join(name) # Glue list into a string |
|
self.build(target=name) |
|
|
|
def test_build_pyx_depfiles(self): |
|
# building regularly and then touching a depfile dependency should rebuild |
|
testdir = os.path.join("test cases/cython", '2 generated sources') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
try: |
|
cython = detect_compiler_for(env, "cython", MachineChoice.HOST, True) |
|
if not version_compare(cython.version, '>=0.29.33'): |
|
raise SkipTest('Cython is too old') |
|
except EnvironmentException: |
|
raise SkipTest("Cython is not installed") |
|
self.init(testdir) |
|
|
|
targets = self.introspect('--targets') |
|
for target in targets: |
|
if target['name'].startswith('simpleinclude'): |
|
name = target['name'] |
|
self.build() |
|
self.utime(os.path.join(testdir, 'simplestuff.pxi')) |
|
self.assertBuildRelinkedOnlyTarget(name) |
|
|
|
|
|
def test_internal_include_order(self): |
|
if mesonbuild.environment.detect_msys2_arch() and ('MESON_RSP_THRESHOLD' in os.environ): |
|
raise 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 = GnuCompiler |
|
clang = ClangCompiler |
|
intel = IntelGnuLikeCompiler |
|
msvc = (VisualStudioCCompiler, VisualStudioCPPCompiler) |
|
clangcl = (ClangClCCompiler, ClangClCPPCompiler) |
|
ar = linkers.ArLinker |
|
lib = 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, linkers.AppleDynamicLinker) |
|
elif is_sunos(): |
|
self.assertIsInstance(cc.linker, (linkers.SolarisDynamicLinker, linkers.GnuLikeDynamicLinkerMixin)) |
|
else: |
|
self.assertIsInstance(cc.linker, linkers.GnuLikeDynamicLinkerMixin) |
|
if isinstance(cc, clangcl): |
|
self.assertIsInstance(linker, lib) |
|
self.assertIsInstance(cc.linker, linkers.ClangClDynamicLinker) |
|
if isinstance(cc, clang): |
|
self.assertIsInstance(linker, ar) |
|
if is_osx(): |
|
self.assertIsInstance(cc.linker, 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, (linkers.MSVCDynamicLinker, linkers.GnuLikeDynamicLinkerMixin)) |
|
else: |
|
self.assertIsInstance(cc.linker, linkers.GnuLikeDynamicLinkerMixin) |
|
if isinstance(cc, intel): |
|
self.assertIsInstance(linker, ar) |
|
if is_osx(): |
|
self.assertIsInstance(cc.linker, linkers.AppleDynamicLinker) |
|
elif is_windows(): |
|
self.assertIsInstance(cc.linker, linkers.XilinkDynamicLinker) |
|
else: |
|
self.assertIsInstance(cc.linker, 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, 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 |
|
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 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] in ('ccache', 'sccache'): |
|
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/CXXFLAGS 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', 'CXXFLAGS'}]: |
|
env = {} |
|
for i in env_var: |
|
env[i] = f'-D{define}="{value}"' |
|
env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read' |
|
self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env) |
|
self.new_builddir() |
|
|
|
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 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 SkipTest('LTO not (yet) supported by windows clang') |
|
|
|
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']: |
|
if 'linker' in s: |
|
continue |
|
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 SkipTest('Only clang currently supports thinLTO') |
|
if cc.linker.id not in {'ld.lld', 'ld.gold', 'ld64', 'lld-link'}: |
|
raise SkipTest('thinLTO requires ld.lld, ld.gold, ld64, or lld-link') |
|
elif is_windows(): |
|
raise 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 src in t['target_sources']: |
|
self.assertTrue(expected.issubset(set(src['parameters'])), f'Incorrect values for {t["name"]}') |
|
|
|
def test_dist_git(self): |
|
if not shutil.which('git'): |
|
raise SkipTest('Git not found') |
|
if self.backend is not Backend.ninja: |
|
raise 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 SkipTest('Mercurial not found or broken.') |
|
if self.backend is not Backend.ninja: |
|
raise 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', encoding='utf-8') 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 SkipTest('Git not found') |
|
if self.backend is not Backend.ninja: |
|
raise 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', encoding='utf-8') 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', encoding='utf-8') 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', encoding='utf-8') 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") # [ignore encoding] |
|
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 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.assertIsNone(rpath) |
|
|
|
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 = set(mesonbuild.coredata.FORBIDDEN_TARGET_NAMES) |
|
# We don't actually define a target with this name |
|
targets.remove('build.ninja') |
|
# Remove this to avoid multiple entries with the same name |
|
# but different case. |
|
targets.remove('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 is_windows(): |
|
object_suffix = 'obj' |
|
shared_suffix = 'dll' |
|
elif is_cygwin(): |
|
object_suffix = 'o' |
|
shared_suffix = 'dll' |
|
elif is_osx(): |
|
object_suffix = 'o' |
|
shared_suffix = 'dylib' |
|
else: |
|
object_suffix = 'o' |
|
shared_suffix = 'so' |
|
return (cc, stlinker, object_suffix, shared_suffix) |
|
|
|
def detect_prebuild_env_versioned(self): |
|
(cc, stlinker, object_suffix, shared_suffix) = self.detect_prebuild_env() |
|
shared_suffixes = [shared_suffix] |
|
if shared_suffix == 'so': |
|
# .so may have version information integrated into the filename |
|
shared_suffixes += ['so.1', 'so.1.2.3', '1.so', '1.so.2.3'] |
|
return (cc, stlinker, object_suffix, shared_suffixes) |
|
|
|
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 = [] |
|
link_cmd = linker.get_exelist() |
|
link_cmd += linker.get_always_args() |
|
link_cmd += linker.get_std_link_args(get_fake_env(), False) |
|
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 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) |
|
|
|
if is_windows(): |
|
def cleanup() -> None: |
|
"""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) |
|
self.addCleanup(cleanup) |
|
else: |
|
self.addCleanup(os.unlink, shlibfile) |
|
|
|
# Run the test |
|
self.init(tdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_prebuilt_shared_lib_rpath(self) -> None: |
|
(cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env() |
|
tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') |
|
with tempfile.TemporaryDirectory() as d: |
|
source = os.path.join(tdir, 'alexandria.c') |
|
objectfile = os.path.join(d, 'alexandria.' + object_suffix) |
|
impfile = os.path.join(d, 'alexandria.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(d, 'alexandria.' + shared_suffix) |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(d, 'cygalexandria.' + shared_suffix) |
|
else: |
|
shlibfile = os.path.join(d, 'libalexandria.' + shared_suffix) |
|
# Ensure MSVC extra files end up in the directory that gets deleted |
|
# at the end |
|
with chdir(d): |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
|
|
# Run the test |
|
self.init(tdir, extra_args=[f'-Dsearch_dir={d}']) |
|
self.build() |
|
self.run_tests() |
|
|
|
@skipIfNoPkgconfig |
|
def test_prebuilt_shared_lib_pkg_config(self) -> None: |
|
(cc, _, object_suffix, shared_suffixes) = self.detect_prebuild_env_versioned() |
|
tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') |
|
for shared_suffix in shared_suffixes: |
|
with tempfile.TemporaryDirectory() as d: |
|
source = os.path.join(tdir, 'alexandria.c') |
|
objectfile = os.path.join(d, 'alexandria.' + object_suffix) |
|
impfile = os.path.join(d, 'alexandria.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(d, 'alexandria.' + shared_suffix) |
|
linkfile = impfile # MSVC links against the *.lib instead of the *.dll |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(d, 'cygalexandria.' + shared_suffix) |
|
linkfile = shlibfile |
|
else: |
|
shlibfile = os.path.join(d, 'libalexandria.' + shared_suffix) |
|
linkfile = shlibfile |
|
# Ensure MSVC extra files end up in the directory that gets deleted |
|
# at the end |
|
with chdir(d): |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
|
|
with open(os.path.join(d, 'alexandria.pc'), 'w', |
|
encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
Name: alexandria |
|
Description: alexandria |
|
Version: 1.0.0 |
|
Libs: {} |
|
''').format( |
|
Path(linkfile).as_posix().replace(' ', r'\ '), |
|
)) |
|
|
|
# Run the test |
|
self.init(tdir, override_envvars={'PKG_CONFIG_PATH': d}, |
|
extra_args=['-Dmethod=pkg-config']) |
|
self.build() |
|
self.run_tests() |
|
|
|
self.wipe() |
|
|
|
@skip_if_no_cmake |
|
def test_prebuilt_shared_lib_cmake(self) -> None: |
|
(cc, _, object_suffix, shared_suffixes) = self.detect_prebuild_env_versioned() |
|
tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') |
|
for shared_suffix in shared_suffixes: |
|
with tempfile.TemporaryDirectory() as d: |
|
source = os.path.join(tdir, 'alexandria.c') |
|
objectfile = os.path.join(d, 'alexandria.' + object_suffix) |
|
impfile = os.path.join(d, 'alexandria.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(d, 'alexandria.' + shared_suffix) |
|
linkfile = impfile # MSVC links against the *.lib instead of the *.dll |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(d, 'cygalexandria.' + shared_suffix) |
|
linkfile = shlibfile |
|
else: |
|
shlibfile = os.path.join(d, 'libalexandria.' + shared_suffix) |
|
linkfile = shlibfile |
|
# Ensure MSVC extra files end up in the directory that gets deleted |
|
# at the end |
|
with chdir(d): |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
|
|
with open(os.path.join(d, 'alexandriaConfig.cmake'), 'w', |
|
encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
set(alexandria_FOUND ON) |
|
set(alexandria_LIBRARIES "{}") |
|
set(alexandria_INCLUDE_DIRS "{}") |
|
''').format( |
|
re.sub(r'([\\"])', r'\\\1', linkfile), |
|
re.sub(r'([\\"])', r'\\\1', tdir), |
|
)) |
|
|
|
# Run the test |
|
self.init(tdir, override_envvars={'CMAKE_PREFIX_PATH': d}, |
|
extra_args=['-Dmethod=cmake']) |
|
self.build() |
|
self.run_tests() |
|
|
|
self.wipe() |
|
|
|
def test_prebuilt_shared_lib_rpath_same_prefix(self) -> None: |
|
(cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env() |
|
orig_tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') |
|
|
|
# Put the shared library in a location that shares a common prefix with |
|
# the source directory: |
|
# |
|
# .../ |
|
# foo-lib/ |
|
# libalexandria.so |
|
# foo/ |
|
# meson.build |
|
# ... |
|
# |
|
# This allows us to check that the .../foo-lib/libalexandria.so path is |
|
# preserved correctly when meson processes it. |
|
with tempfile.TemporaryDirectory() as d: |
|
libdir = os.path.join(d, 'foo-lib') |
|
os.mkdir(libdir) |
|
|
|
source = os.path.join(orig_tdir, 'alexandria.c') |
|
objectfile = os.path.join(libdir, 'alexandria.' + object_suffix) |
|
impfile = os.path.join(libdir, 'alexandria.lib') |
|
if cc.get_argument_syntax() == 'msvc': |
|
shlibfile = os.path.join(libdir, 'alexandria.' + shared_suffix) |
|
elif is_cygwin(): |
|
shlibfile = os.path.join(libdir, 'cygalexandria.' + shared_suffix) |
|
else: |
|
shlibfile = os.path.join(libdir, 'libalexandria.' + shared_suffix) |
|
# Ensure MSVC extra files end up in the directory that gets deleted |
|
# at the end |
|
with chdir(libdir): |
|
self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) |
|
|
|
tdir = os.path.join(d, 'foo') |
|
shutil.copytree(orig_tdir, tdir) |
|
|
|
# Run the test |
|
self.init(tdir, extra_args=[f'-Dsearch_dir={libdir}']) |
|
self.build() |
|
self.run_tests() |
|
|
|
def test_underscore_prefix_detection_list(self) -> None: |
|
''' |
|
Test the underscore detection hardcoded lookup list |
|
against what was detected in the binary. |
|
''' |
|
env, cc = get_convincing_fake_env_and_cc(self.builddir, self.prefix) |
|
expected_uscore = cc._symbols_have_underscore_prefix_searchbin(env) |
|
list_uscore = cc._symbols_have_underscore_prefix_list(env) |
|
if list_uscore is not None: |
|
self.assertEqual(list_uscore, expected_uscore) |
|
else: |
|
raise SkipTest('No match in underscore prefix list for this platform.') |
|
|
|
def test_underscore_prefix_detection_define(self) -> None: |
|
''' |
|
Test the underscore detection based on compiler-defined preprocessor macro |
|
against what was detected in the binary. |
|
''' |
|
env, cc = get_convincing_fake_env_and_cc(self.builddir, self.prefix) |
|
expected_uscore = cc._symbols_have_underscore_prefix_searchbin(env) |
|
define_uscore = cc._symbols_have_underscore_prefix_define(env) |
|
if define_uscore is not None: |
|
self.assertEqual(define_uscore, expected_uscore) |
|
else: |
|
raise SkipTest('Did not find the underscore prefix define __USER_LABEL_PREFIX__') |
|
|
|
@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 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('libanswer', env, kwargs) |
|
# Ensure link_args are properly quoted |
|
libdir = PurePath(prefix) / PurePath(libdir) |
|
link_args = ['-L' + libdir.as_posix(), '-lanswer'] |
|
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)) |
|
|
|
@skipIfNoPkgconfig |
|
def test_pkgconfig_relocatable(self): |
|
''' |
|
Test that it generates relocatable pkgconfig when module |
|
option pkgconfig.relocatable=true. |
|
''' |
|
testdir_rel = os.path.join(self.common_test_dir, '44 pkgconfig-gen') |
|
self.init(testdir_rel, extra_args=['-Dpkgconfig.relocatable=true']) |
|
|
|
def check_pcfile(name, *, relocatable, levels=2): |
|
with open(os.path.join(self.privatedir, name), encoding='utf-8') as f: |
|
pcfile = f.read() |
|
# The pkgconfig module always uses posix path regardless of platform |
|
prefix_rel = PurePath('${pcfiledir}', *(['..'] * levels)).as_posix() |
|
(self.assertIn if relocatable else self.assertNotIn)( |
|
f'prefix={prefix_rel}\n', |
|
pcfile) |
|
|
|
check_pcfile('libvartest.pc', relocatable=True) |
|
check_pcfile('libvartest2.pc', relocatable=True) |
|
|
|
self.wipe() |
|
self.init(testdir_rel, extra_args=['-Dpkgconfig.relocatable=false']) |
|
|
|
check_pcfile('libvartest.pc', relocatable=False) |
|
check_pcfile('libvartest2.pc', relocatable=False) |
|
|
|
self.wipe() |
|
testdir_abs = os.path.join(self.unit_test_dir, '106 pkgconfig relocatable with absolute path') |
|
self.init(testdir_abs) |
|
|
|
check_pcfile('libsimple.pc', relocatable=True, levels=3) |
|
|
|
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'], |
|
'choices': ['foo', 'bar', 'oink', 'boink'], |
|
'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'], |
|
'choices': ['foo', 'bar', 'oink', 'boink'], |
|
'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': [], |
|
'choices': ['foo', 'bar', 'oink', 'boink'], |
|
'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 test_executable_names(self): |
|
testdir = os.path.join(self.unit_test_dir, '121 executable suffix') |
|
self.init(testdir) |
|
self.build() |
|
exe1 = os.path.join(self.builddir, 'foo' + exe_suffix) |
|
exe2 = os.path.join(self.builddir, 'foo.bin') |
|
self.assertPathExists(exe1) |
|
self.assertPathExists(exe2) |
|
self.assertNotEqual(exe1, exe2) |
|
|
|
# Wipe and run the compile command against the target names |
|
self.init(testdir, extra_args=['--wipe']) |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo']) |
|
self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo.bin']) |
|
self.assertPathExists(exe1) |
|
self.assertPathExists(exe2) |
|
self.assertNotEqual(exe1, exe2) |
|
|
|
|
|
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, '83 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'] == 'array': |
|
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'] == 'array': |
|
self.assertEqual(item['value'], ['b', 'c']) |
|
self.assertEqual(item['choices'], ['b', 'c', 'd']) |
|
|
|
def test_options_listed_in_build_options(self) -> None: |
|
"""Detect when changed options become listed in build options.""" |
|
testdir = os.path.join(self.unit_test_dir, '113 list build options') |
|
|
|
out = self.init(testdir) |
|
for line in out.splitlines(): |
|
if line.startswith('Message: Build options:'): |
|
self.assertNotIn('-Dauto_features=auto', line) |
|
self.assertNotIn('-Doptional=auto', line) |
|
|
|
self.wipe() |
|
self.mac_ci_delay() |
|
|
|
out = self.init(testdir, extra_args=['-Dauto_features=disabled', '-Doptional=enabled']) |
|
for line in out.splitlines(): |
|
if line.startswith('Message: Build options:'): |
|
self.assertIn('-Dauto_features=disabled', line) |
|
self.assertIn('-Doptional=enabled', line) |
|
|
|
self.setconf('-Doptional=disabled') |
|
out = self.build() |
|
for line in out.splitlines(): |
|
if line.startswith('Message: Build options:'): |
|
self.assertIn('-Dauto_features=disabled', line) |
|
self.assertNotIn('-Doptional=enabled', line) |
|
self.assertIn('-Doptional=disabled', line) |
|
|
|
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.new_builddir() # Ensure builddir is not parent or workdir |
|
self.init(workdir) |
|
self.build() |
|
|
|
def test_subproject_promotion_wrap(self): |
|
testdir = os.path.join(self.unit_test_dir, '43 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 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.", |
|
]: |
|
with self.subTest(expected): |
|
self.assertRegex(out, re.escape(expected)) |
|
|
|
for wd in [ |
|
self.src_root, |
|
self.builddir, |
|
os.getcwd(), |
|
]: |
|
with self.subTest(wd): |
|
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'), |
|
('57 bad option argument', 'meson_options.txt'), |
|
('95 subdir parse error', os.path.join('subdir', 'meson.build')), |
|
('96 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') |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.init(tdir) |
|
self.assertIn('ERROR: compiler.has_header_symbol got unknown keyword arguments "prefixxx"', cm.exception.output) |
|
|
|
def test_templates(self): |
|
ninja = mesonbuild.environment.detect_ninja() |
|
if ninja is None: |
|
raise 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', 'vala']: |
|
try: |
|
comp = detect_compiler_for(env, l, MachineChoice.HOST, True) |
|
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'): |
|
with self.subTest(f'Language: {lang}; type: {target_type}'): |
|
if is_windows() and lang == 'fortran' and target_type == 'library': |
|
# non-Gfortran Windows Fortran compilers do not do shared libraries in a Fortran standard way |
|
# see "test cases/fortran/6 dynamic" |
|
fc = detect_compiler_for(env, 'fortran', MachineChoice.HOST, True) |
|
if fc.get_id() in {'intel-cl', 'pgi'}: |
|
continue |
|
# 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', encoding='utf-8') 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', encoding='utf-8') 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.') |
|
|
|
@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 = ('''DEPRECATION: target prog links against shared module mymod, which is incorrect. |
|
This will be an error in the future, so please use shared_library() for mymod instead. |
|
If shared_module() was used for mymod because it has references to undefined symbols, |
|
use shared_library() with `override_options: ['b_lundef=false']` instead.''') |
|
self.assertIn(msg, out) |
|
|
|
def test_mixed_language_linker_check(self): |
|
testdir = os.path.join(self.unit_test_dir, '97 compiler.links file arg') |
|
self.init(testdir) |
|
cmds = self.get_meson_log_compiler_checks() |
|
self.assertEqual(len(cmds), 5) |
|
# Path to the compilers, gleaned from cc.compiles tests |
|
cc = cmds[0][0] |
|
cxx = cmds[1][0] |
|
# cc.links |
|
self.assertEqual(cmds[2][0], cc) |
|
# cxx.links with C source |
|
self.assertEqual(cmds[3][0], cc) |
|
self.assertEqual(cmds[4][0], cxx) |
|
if self.backend is Backend.ninja: |
|
# updating the file to check causes a reconfigure |
|
# |
|
# only the ninja backend is competent enough to detect reconfigured |
|
# no-op builds without build targets |
|
self.utime(os.path.join(testdir, 'test.c')) |
|
self.assertReconfiguredBuildIsNoop() |
|
|
|
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.setconf('--warnlevel=everything') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, 'everything') |
|
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.setconf('-Dwarning_level=everything') |
|
obj = mesonbuild.coredata.load(self.builddir) |
|
self.assertEqual(obj.options[OptionKey('warning_level')].value, 'everything') |
|
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 fail on unknown options |
|
with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm: |
|
self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo']) |
|
self.assertNotEqual(0, cm.exception.returncode) |
|
self.assertIn(msg, cm.exception.output) |
|
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, '40 featurenew subprojects') |
|
out = self.init(testdir) |
|
# Parent project warns correctly |
|
self.assertRegex(out, "WARNING: Project targets '>=0.45'.*'0.47.0': dict") |
|
# Subprojects warn correctly |
|
self.assertRegex(out, r"foo\| .*WARNING: Project targets '>=0.40'.*'0.44.0': disabler") |
|
self.assertRegex(out, r"baz\| .*WARNING: Project targets '!=0.40'.*'0.44.0': disabler") |
|
# Subproject has a new-enough meson_version, no warning |
|
self.assertNotRegex(out, "WARNING: Project targets.*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'') |
|
|
|
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', encoding='utf-8') |
|
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 |
|
@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, |
|
'45 native dep pkgconfig var') |
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as crossfile: |
|
crossfile.write(textwrap.dedent( |
|
'''[binaries] |
|
pkg-config = '{}' |
|
|
|
[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_files = [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 |
|
@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, |
|
'45 native dep pkgconfig var') |
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as crossfile: |
|
crossfile.write(textwrap.dedent( |
|
'''[binaries] |
|
pkg-config = '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_files = [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): |
|
# 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) |
|
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, '47 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', encoding='utf-8').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() |
|
self.init(testdir, extra_args=['--wipe'], workdir=self.builddir) |
|
|
|
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': 'sub_static', |
|
'name': 'sub_static', |
|
'version': 'undefined' |
|
}, |
|
{ |
|
'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 SkipTest(f'Clang-format is for now only supported on Ninja, not {self.backend.name}') |
|
testdir = os.path.join(self.unit_test_dir, '53 clang-format') |
|
|
|
# Ensure that test project is in git even when running meson from tarball. |
|
srcdir = os.path.join(self.builddir, 'src') |
|
shutil.copytree(testdir, srcdir) |
|
git_init(srcdir) |
|
testdir = srcdir |
|
self.new_builddir() |
|
|
|
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') |
|
includefile = os.path.join(testdir, '.clang-format-include') |
|
try: |
|
shutil.copyfile(badfile, testfile) |
|
shutil.copyfile(badheader, testheader) |
|
self.init(testdir) |
|
self.assertNotEqual(Path(testfile).read_text(encoding='utf-8'), |
|
Path(goodfile).read_text(encoding='utf-8')) |
|
self.assertNotEqual(Path(testheader).read_text(encoding='utf-8'), |
|
Path(goodheader).read_text(encoding='utf-8')) |
|
|
|
# test files are not in git so this should do nothing |
|
self.run_target('clang-format') |
|
self.assertNotEqual(Path(testfile).read_text(encoding='utf-8'), |
|
Path(goodfile).read_text(encoding='utf-8')) |
|
self.assertNotEqual(Path(testheader).read_text(encoding='utf-8'), |
|
Path(goodheader).read_text(encoding='utf-8')) |
|
|
|
# Add an include file to reformat everything |
|
with open(includefile, 'w', encoding='utf-8') as f: |
|
f.write('*') |
|
self.run_target('clang-format') |
|
self.assertEqual(Path(testheader).read_text(encoding='utf-8'), |
|
Path(goodheader).read_text(encoding='utf-8')) |
|
finally: |
|
if os.path.exists(testfile): |
|
os.unlink(testfile) |
|
if os.path.exists(testheader): |
|
os.unlink(testheader) |
|
if os.path.exists(includefile): |
|
os.unlink(includefile) |
|
|
|
@skipIfNoExecutable('clang-tidy') |
|
def test_clang_tidy(self): |
|
if self.backend is not Backend.ninja: |
|
raise SkipTest(f'Clang-tidy is for now only supported on Ninja, not {self.backend.name}') |
|
if shutil.which('c++') is None: |
|
raise SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.') |
|
if is_osx(): |
|
raise SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.') |
|
testdir = os.path.join(self.unit_test_dir, '68 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) |
|
|
|
@skipIfNoExecutable('clang-tidy') |
|
def test_clang_tidy_fix(self): |
|
if self.backend is not Backend.ninja: |
|
raise SkipTest(f'Clang-tidy is for now only supported on Ninja, not {self.backend.name}') |
|
if shutil.which('c++') is None: |
|
raise SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.') |
|
if is_osx(): |
|
raise SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.') |
|
testdir = os.path.join(self.unit_test_dir, '68 clang-tidy') |
|
|
|
# Ensure that test project is in git even when running meson from tarball. |
|
srcdir = os.path.join(self.builddir, 'src') |
|
shutil.copytree(testdir, srcdir) |
|
git_init(srcdir) |
|
testdir = srcdir |
|
self.new_builddir() |
|
|
|
dummydir = os.path.join(testdir, 'dummydir.h') |
|
testfile = os.path.join(testdir, 'cttest.cpp') |
|
fixedfile = os.path.join(testdir, 'cttest_fixed.cpp') |
|
self.init(testdir, override_envvars={'CXX': 'c++'}) |
|
# Make sure test files are different |
|
self.assertNotEqual(Path(testfile).read_text(encoding='utf-8'), |
|
Path(fixedfile).read_text(encoding='utf-8')) |
|
out = self.run_target('clang-tidy-fix') |
|
self.assertIn('cttest.cpp:4:20', out) |
|
self.assertNotIn(dummydir, out) |
|
# Make sure the test file is fixed |
|
self.assertEqual(Path(testfile).read_text(encoding='utf-8'), |
|
Path(fixedfile).read_text(encoding='utf-8')) |
|
|
|
def test_identity_cross(self): |
|
testdir = os.path.join(self.unit_test_dir, '69 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_files = [os.path.join(self.builddir, "crossfile")] |
|
self.assertTrue(os.path.exists(self.meson_cross_files[0])) |
|
# 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, '58 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, '58 introspect buildoptions') |
|
self._run(self.mconf_command + [testdir]) |
|
|
|
def test_introspect_buildoptions_cross_only(self): |
|
testdir = os.path.join(self.unit_test_dir, '82 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, '56 introspection') |
|
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'), encoding='utf-8') 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, '56 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), |
|
('compilers', dict), |
|
('dependencies', list), |
|
('install_plan', dict), |
|
('installed', dict), |
|
('machines', 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), |
|
('extra_paths', list), |
|
] |
|
|
|
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), |
|
('type', str), |
|
('version', str), |
|
('compile_args', list), |
|
('link_args', list), |
|
('include_directories', list), |
|
('sources', list), |
|
('extra_files', list), |
|
('dependencies', list), |
|
('depends', list), |
|
('meson_variables', 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)), |
|
('dependencies', list), |
|
('depends', list), |
|
('install_filename', (list, None)), |
|
('installed', bool), |
|
('vs_module_defs', (str, None)), |
|
('win_subsystem', (str, None)), |
|
] |
|
|
|
targets_sources_typelist = [ |
|
('language', str), |
|
('compiler', list), |
|
('parameters', list), |
|
('sources', list), |
|
('generated_sources', list), |
|
('unity_sources', (list, None)), |
|
] |
|
|
|
target_sources_linker_typelist = [ |
|
('linker', list), |
|
('parameters', 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, encoding='utf-8') 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.get('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']: |
|
if 'compiler' in j: |
|
assertKeyTypes(targets_sources_typelist, j) |
|
self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]]) |
|
else: |
|
assertKeyTypes(target_sources_linker_typelist, j) |
|
self.assertDictEqual(targets_to_find, {}) |
|
|
|
def test_introspect_file_dump_equals_all(self): |
|
testdir = os.path.join(self.unit_test_dir, '56 introspection') |
|
self.init(testdir) |
|
res_all = self.introspect('--all') |
|
res_file = {} |
|
|
|
root_keylist = [ |
|
'benchmarks', |
|
'buildoptions', |
|
'buildsystem_files', |
|
'compilers', |
|
'dependencies', |
|
'installed', |
|
'install_plan', |
|
'machines', |
|
'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, encoding='utf-8') 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, '56 introspection') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'meson-info.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile, encoding='utf-8') 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, '56 introspection') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'intro-buildoptions.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile, encoding='utf-8') 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, encoding='utf-8') 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, '56 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, encoding='utf-8') 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']] |
|
for k in ('install_filename', 'dependencies', 'win_subsystem'): |
|
if k in i: |
|
del i[k] |
|
|
|
sources = [] |
|
for j in i['target_sources']: |
|
sources += j.get('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, '56 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)], |
|
'FormatStringNode': [('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, '56 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, '62 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, '63 cmake parser') |
|
self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')]) |
|
|
|
def test_alias_target(self): |
|
testdir = os.path.join(self.unit_test_dir, '64 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')) |
|
out = self.run_target('aliased-run') |
|
self.assertIn('a run target was here', out) |
|
|
|
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, '71 summary') |
|
out = self.init(testdir, extra_args=['-Denabled_opt=enabled', f'-Dpython={sys.executable}']) |
|
expected = textwrap.dedent(r''' |
|
Some Subproject 2.0 |
|
|
|
string : bar |
|
integer: 1 |
|
boolean: true |
|
|
|
subsub undefined |
|
|
|
Something: Some value |
|
|
|
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 |
|
comma list : a, b, c |
|
|
|
Stuff |
|
missing prog : NO |
|
existing prog : ''' + ExternalProgram('python3', [sys.executable], silent=True).path + ''' |
|
missing dep : NO |
|
external dep : YES 1.2.3 |
|
internal dep : YES |
|
disabler : NO |
|
|
|
Plugins |
|
long comma list: alpha, alphacolor, apetag, audiofx, audioparsers, auparse, |
|
autodetect, avi |
|
|
|
Subprojects |
|
sub : YES |
|
sub2 : NO Problem encountered: This subproject failed |
|
subsub : YES (from sub2) |
|
|
|
User defined options |
|
backend : ''' + self.backend_name + ''' |
|
libdir : lib |
|
prefix : /usr |
|
enabled_opt : enabled |
|
python : ''' + sys.executable + ''' |
|
''') |
|
expected_lines = expected.split('\n')[1:] |
|
out_start = out.find(expected_lines[0]) |
|
out_lines = out[out_start:].split('\n')[:len(expected_lines)] |
|
for e, o in zip(expected_lines, out_lines): |
|
if e.startswith(' external dep'): |
|
self.assertRegex(o, r'^ external dep : (YES [0-9.]*|NO)$') |
|
else: |
|
self.assertEqual(o, e) |
|
|
|
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, '73 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 SkipTest('lxml required, but not found.') |
|
|
|
schema = et.XMLSchema(et.parse(str(Path(self.src_root) / '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 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 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']) |
|
# Python's argument parser might put the command list to its own line. Or it might not. |
|
self.assertTrue(help_output.startswith('usage: ')) |
|
lines = help_output.split('\n') |
|
line1 = lines[0] |
|
line2 = lines[1] |
|
if '{' in line1: |
|
cmndline = line1 |
|
else: |
|
self.assertIn('{', line2) |
|
cmndline = line2 |
|
cmndstr = cmndline.split('{')[1] |
|
self.assertIn('}', cmndstr) |
|
help_commands = set(cmndstr.split('}')[0].split(',')) |
|
self.assertTrue(len(help_commands) > 0, 'Must detect some command names.') |
|
|
|
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 SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise 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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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 SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise 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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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 SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise 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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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 SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise 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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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 SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise 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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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_coverage_escaping(self): |
|
if mesonbuild.environment.detect_msys2_arch(): |
|
raise SkipTest('Skipped due to problems with coverage on MSYS2') |
|
gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr() |
|
if not gcovr_exe: |
|
raise SkipTest('gcovr not found, or too old') |
|
testdir = os.path.join(self.common_test_dir, '243 escape++') |
|
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 SkipTest('llvm-cov not found') |
|
if cc.get_id() == 'msvc': |
|
raise 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_cross_file_constants(self): |
|
with temp_filename() as crossfile1, temp_filename() as crossfile2: |
|
with open(crossfile1, 'w', encoding='utf-8') as f: |
|
f.write(textwrap.dedent( |
|
''' |
|
[constants] |
|
compiler = 'gcc' |
|
''')) |
|
with open(crossfile2, 'w', encoding='utf-8') 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'] |
|
rel_to_src = '@GLOBAL_SOURCE_ROOT@' / 'tool' |
|
rel_to_file = '@DIRNAME@' / 'tool' |
|
no_escaping = '@@DIRNAME@@' / 'tool' |
|
|
|
[binaries] |
|
c = toolchain / compiler |
|
''')) |
|
|
|
values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2], self.builddir) |
|
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']) |
|
self.assertEqual(values['properties']['rel_to_src'], os.path.join(self.builddir, 'tool')) |
|
self.assertEqual(values['properties']['rel_to_file'], os.path.join(os.path.dirname(crossfile2), 'tool')) |
|
self.assertEqual(values['properties']['no_escaping'], os.path.join(f'@{os.path.dirname(crossfile2)}@', 'tool')) |
|
|
|
@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, '80 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', encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-git] |
|
url = {} |
|
patch_directory = wrap_git_builddef |
|
revision = master |
|
'''.format(upstream_uri))) |
|
out = self.init(srcdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
# Make sure the warning does not occur on the first init. |
|
out_of_date_warning = 'revision may be out of date' |
|
self.assertNotIn(out_of_date_warning, out) |
|
|
|
# Change the wrap's revisions, reconfigure, and make sure it does |
|
# warn on the reconfigure. |
|
with open(os.path.join(srcdir, 'subprojects', 'wrap_git.wrap'), 'w', encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-git] |
|
url = {} |
|
patch_directory = wrap_git_builddef |
|
revision = not-master |
|
'''.format(upstream_uri))) |
|
out = self.init(srcdir, extra_args='--reconfigure') |
|
self.assertIn(out_of_date_warning, out) |
|
|
|
def test_extract_objects_custom_target_no_warning(self): |
|
testdir = os.path.join(self.common_test_dir, '22 object extraction') |
|
|
|
out = self.init(testdir) |
|
self.assertNotRegex(out, "WARNING:.*can't be converted to File object") |
|
|
|
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() |
|
|
|
@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, '77 nostdlib') |
|
machinefile = os.path.join(self.builddir, 'machine.txt') |
|
with open(machinefile, 'w', encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
[properties] |
|
c_stdlib = 'mylibc' |
|
''')) |
|
|
|
# Test native C stdlib |
|
self.meson_native_files = [machinefile] |
|
self.init(testdir) |
|
self.build() |
|
|
|
# Test cross C stdlib |
|
self.new_builddir() |
|
self.meson_native_files = [] |
|
self.meson_cross_files = [machinefile] |
|
self.init(testdir) |
|
self.build() |
|
|
|
def test_meson_version_compare(self): |
|
testdir = os.path.join(self.unit_test_dir, '81 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', encoding='utf-8') 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', encoding='utf-8') 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', encoding='utf-8') 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'): |
|
PackageDefinition(redirect_wrap) |
|
|
|
# Correct redirect |
|
with open(redirect_wrap, 'w', encoding='utf-8') as f: |
|
f.write(textwrap.dedent(''' |
|
[wrap-redirect] |
|
filename = foo/subprojects/real.wrap |
|
''')) |
|
with open(real_wrap, 'w', encoding='utf-8') 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, '84 nested subproject regenerate depends') |
|
cmakefile = Path(testdir) / 'subprojects' / 'sub2' / 'CMakeLists.txt' |
|
self.init(testdir) |
|
self.build() |
|
with cmakefile.open('a', encoding='utf-8'): |
|
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, '88 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/bar', |
|
'share/bar/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, '90 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()) |
|
|
|
cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump'] |
|
o = self._run(cmd) |
|
expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) |
|
self.assertIn(f'TEST_C="{expected}"', o) |
|
self.assertIn('export TEST_C', o) |
|
|
|
cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', '--dump-format', 'sh'] |
|
o = self._run(cmd) |
|
expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) |
|
self.assertIn(f'TEST_C="{expected}"', o) |
|
self.assertNotIn('export', o) |
|
|
|
cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', '--dump-format', 'vscode'] |
|
o = self._run(cmd) |
|
expected = os.pathsep.join(['/prefix', '/suffix']) |
|
self.assertIn(f'TEST_C="{expected}"', o) |
|
self.assertNotIn('export', o) |
|
|
|
fname = os.path.join(self.builddir, 'dump.env') |
|
cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', fname] |
|
o = self._run(cmd) |
|
self.assertEqual(o, '') |
|
o = Path(fname).read_text(encoding='utf-8') |
|
expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) |
|
self.assertIn(f'TEST_C="{expected}"', o) |
|
self.assertIn('export TEST_C', o) |
|
|
|
def test_clang_format_check(self): |
|
if self.backend is not Backend.ninja: |
|
raise SkipTest(f'Skipping clang-format tests with {self.backend.name} backend') |
|
if not shutil.which('clang-format'): |
|
raise 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 = detect_compiler_for(env, 'c', MachineChoice.HOST, True) |
|
cc_type = type(cc) |
|
|
|
# Test a compiler that acts as a linker |
|
with mock.patch.object(cc_type, 'INVOKES_LINKER', True): |
|
cc = detect_compiler_for(env, 'c', MachineChoice.HOST, True) |
|
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 = detect_compiler_for(env, 'c', MachineChoice.HOST, True) |
|
link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) |
|
self.assertEqual(sorted(link_args), sorted(['-flto'])) |
|
|
|
def test_install_tag(self) -> None: |
|
testdir = os.path.join(self.unit_test_dir, '99 install all targets') |
|
self.init(testdir) |
|
self.build() |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
|
|
def shared_lib_name(name): |
|
if cc.get_id() in {'msvc', 'clang-cl'}: |
|
return f'bin/{name}.dll' |
|
elif is_windows(): |
|
return f'bin/lib{name}.dll' |
|
elif is_cygwin(): |
|
return f'bin/cyg{name}.dll' |
|
elif is_osx(): |
|
return f'lib/lib{name}.dylib' |
|
return f'lib/lib{name}.so' |
|
|
|
def exe_name(name): |
|
if is_windows() or is_cygwin(): |
|
return f'{name}.exe' |
|
return name |
|
|
|
installpath = Path(self.installdir) |
|
|
|
expected_common = { |
|
installpath, |
|
Path(installpath, 'usr'), |
|
} |
|
|
|
expected_devel = expected_common | { |
|
Path(installpath, 'usr/include'), |
|
Path(installpath, 'usr/include/bar-devel.h'), |
|
Path(installpath, 'usr/include/bar2-devel.h'), |
|
Path(installpath, 'usr/include/foo1-devel.h'), |
|
Path(installpath, 'usr/include/foo2-devel.h'), |
|
Path(installpath, 'usr/include/foo3-devel.h'), |
|
Path(installpath, 'usr/include/out-devel.h'), |
|
Path(installpath, 'usr/lib'), |
|
Path(installpath, 'usr/lib/libstatic.a'), |
|
Path(installpath, 'usr/lib/libboth.a'), |
|
Path(installpath, 'usr/lib/libboth2.a'), |
|
Path(installpath, 'usr/include/ct-header1.h'), |
|
Path(installpath, 'usr/include/ct-header3.h'), |
|
Path(installpath, 'usr/include/subdir-devel'), |
|
Path(installpath, 'usr/include/custom_files'), |
|
Path(installpath, 'usr/include/custom_files/data.txt'), |
|
} |
|
|
|
if cc.get_id() in {'msvc', 'clang-cl'}: |
|
expected_devel |= { |
|
Path(installpath, 'usr/bin'), |
|
Path(installpath, 'usr/bin/app.pdb'), |
|
Path(installpath, 'usr/bin/app2.pdb'), |
|
Path(installpath, 'usr/bin/both.pdb'), |
|
Path(installpath, 'usr/bin/both2.pdb'), |
|
Path(installpath, 'usr/bin/bothcustom.pdb'), |
|
Path(installpath, 'usr/bin/shared.pdb'), |
|
Path(installpath, 'usr/bin/versioned_shared-1.pdb'), |
|
Path(installpath, 'usr/lib/both.lib'), |
|
Path(installpath, 'usr/lib/both2.lib'), |
|
Path(installpath, 'usr/lib/bothcustom.lib'), |
|
Path(installpath, 'usr/lib/shared.lib'), |
|
Path(installpath, 'usr/lib/versioned_shared.lib'), |
|
Path(installpath, 'usr/otherbin'), |
|
Path(installpath, 'usr/otherbin/app-otherdir.pdb'), |
|
} |
|
elif is_windows() or is_cygwin(): |
|
expected_devel |= { |
|
Path(installpath, 'usr/lib/libboth.dll.a'), |
|
Path(installpath, 'usr/lib/libboth2.dll.a'), |
|
Path(installpath, 'usr/lib/libshared.dll.a'), |
|
Path(installpath, 'usr/lib/libbothcustom.dll.a'), |
|
Path(installpath, 'usr/lib/libversioned_shared.dll.a'), |
|
} |
|
else: |
|
expected_devel |= { |
|
Path(installpath, 'usr/' + shared_lib_name('versioned_shared')), |
|
} |
|
|
|
expected_runtime = expected_common | { |
|
Path(installpath, 'usr/bin'), |
|
Path(installpath, 'usr/bin/' + exe_name('app')), |
|
Path(installpath, 'usr/otherbin'), |
|
Path(installpath, 'usr/otherbin/' + exe_name('app-otherdir')), |
|
Path(installpath, 'usr/bin/' + exe_name('app2')), |
|
Path(installpath, 'usr/' + shared_lib_name('shared')), |
|
Path(installpath, 'usr/' + shared_lib_name('both')), |
|
Path(installpath, 'usr/' + shared_lib_name('both2')), |
|
} |
|
|
|
if is_windows() or is_cygwin(): |
|
expected_runtime |= { |
|
Path(installpath, 'usr/' + shared_lib_name('versioned_shared-1')), |
|
} |
|
elif is_osx(): |
|
expected_runtime |= { |
|
Path(installpath, 'usr/' + shared_lib_name('versioned_shared.1')), |
|
} |
|
else: |
|
expected_runtime |= { |
|
Path(installpath, 'usr/' + shared_lib_name('versioned_shared') + '.1'), |
|
Path(installpath, 'usr/' + shared_lib_name('versioned_shared') + '.1.2.3'), |
|
} |
|
|
|
expected_custom = expected_common | { |
|
Path(installpath, 'usr/share'), |
|
Path(installpath, 'usr/share/bar-custom.txt'), |
|
Path(installpath, 'usr/share/foo-custom.h'), |
|
Path(installpath, 'usr/share/out1-custom.txt'), |
|
Path(installpath, 'usr/share/out2-custom.txt'), |
|
Path(installpath, 'usr/share/out3-custom.txt'), |
|
Path(installpath, 'usr/share/custom_files'), |
|
Path(installpath, 'usr/share/custom_files/data.txt'), |
|
Path(installpath, 'usr/share/excludes'), |
|
Path(installpath, 'usr/share/excludes/installed.txt'), |
|
Path(installpath, 'usr/lib'), |
|
Path(installpath, 'usr/lib/libbothcustom.a'), |
|
Path(installpath, 'usr/' + shared_lib_name('bothcustom')), |
|
} |
|
|
|
if is_windows() or is_cygwin(): |
|
expected_custom |= {Path(installpath, 'usr/bin')} |
|
else: |
|
expected_runtime |= {Path(installpath, 'usr/lib')} |
|
|
|
expected_runtime_custom = expected_runtime | expected_custom |
|
|
|
expected_all = expected_devel | expected_runtime | expected_custom | { |
|
Path(installpath, 'usr/share/foo-notag.h'), |
|
Path(installpath, 'usr/share/bar-notag.txt'), |
|
Path(installpath, 'usr/share/out1-notag.txt'), |
|
Path(installpath, 'usr/share/out2-notag.txt'), |
|
Path(installpath, 'usr/share/out3-notag.txt'), |
|
Path(installpath, 'usr/share/foo2.h'), |
|
Path(installpath, 'usr/share/out1.txt'), |
|
Path(installpath, 'usr/share/out2.txt'), |
|
Path(installpath, 'usr/share/subproject'), |
|
Path(installpath, 'usr/share/subproject/aaa.txt'), |
|
Path(installpath, 'usr/share/subproject/bbb.txt'), |
|
} |
|
|
|
def do_install(tags, expected_files, expected_scripts): |
|
cmd = self.meson_command + ['install', '--dry-run', '--destdir', self.installdir] |
|
cmd += ['--tags', tags] if tags else [] |
|
stdout = self._run(cmd, workdir=self.builddir) |
|
installed = self.read_install_logs() |
|
self.assertEqual(sorted(expected_files), sorted(installed)) |
|
self.assertEqual(expected_scripts, stdout.count('Running custom install script')) |
|
|
|
do_install('devel', expected_devel, 0) |
|
do_install('runtime', expected_runtime, 0) |
|
do_install('custom', expected_custom, 1) |
|
do_install('runtime,custom', expected_runtime_custom, 1) |
|
do_install(None, expected_all, 2) |
|
|
|
|
|
def test_install_script_dry_run(self): |
|
testdir = os.path.join(self.common_test_dir, '53 install script') |
|
self.init(testdir) |
|
self.build() |
|
|
|
cmd = self.meson_command + ['install', '--dry-run', '--destdir', self.installdir] |
|
outputs = self._run(cmd, workdir=self.builddir) |
|
|
|
installpath = Path(self.installdir) |
|
self.assertFalse((installpath / 'usr/diiba/daaba/file.dat').exists()) |
|
self.assertIn("DRYRUN: Writing file file.dat", outputs) |
|
|
|
|
|
def test_introspect_install_plan(self): |
|
testdir = os.path.join(self.unit_test_dir, '99 install all targets') |
|
introfile = os.path.join(self.builddir, 'meson-info', 'intro-install_plan.json') |
|
self.init(testdir) |
|
self.assertPathExists(introfile) |
|
with open(introfile, encoding='utf-8') as fp: |
|
res = json.load(fp) |
|
|
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
|
|
def output_name(name, type_): |
|
target = type_(name=name, subdir=None, subproject=None, |
|
for_machine=MachineChoice.HOST, sources=[], |
|
structured_sources=None, |
|
objects=[], environment=env, compilers=env.coredata.compilers[MachineChoice.HOST], |
|
kwargs={}) |
|
target.process_compilers_late() |
|
return target.filename |
|
|
|
shared_lib_name = lambda name: output_name(name, SharedLibrary) |
|
static_lib_name = lambda name: output_name(name, StaticLibrary) |
|
exe_name = lambda name: output_name(name, Executable) |
|
|
|
expected = { |
|
'targets': { |
|
f'{self.builddir}/out1-notag.txt': { |
|
'destination': '{datadir}/out1-notag.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out2-notag.txt': { |
|
'destination': '{datadir}/out2-notag.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/libstatic.a': { |
|
'destination': '{libdir_static}/libstatic.a', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + exe_name('app'): { |
|
'destination': '{bindir}/' + exe_name('app'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + exe_name('app-otherdir'): { |
|
'destination': '{prefix}/otherbin/' + exe_name('app-otherdir'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/' + exe_name('app2'): { |
|
'destination': '{bindir}/' + exe_name('app2'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + shared_lib_name('shared'): { |
|
'destination': '{libdir_shared}/' + shared_lib_name('shared'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + shared_lib_name('both'): { |
|
'destination': '{libdir_shared}/' + shared_lib_name('both'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + static_lib_name('both'): { |
|
'destination': '{libdir_static}/' + static_lib_name('both'), |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + shared_lib_name('bothcustom'): { |
|
'destination': '{libdir_shared}/' + shared_lib_name('bothcustom'), |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/' + static_lib_name('bothcustom'): { |
|
'destination': '{libdir_static}/' + static_lib_name('bothcustom'), |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/' + shared_lib_name('both2'): { |
|
'destination': '{libdir_shared}/' + shared_lib_name('both2'), |
|
'tag': 'runtime', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/' + static_lib_name('both2'): { |
|
'destination': '{libdir_static}/' + static_lib_name('both2'), |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out1-custom.txt': { |
|
'destination': '{datadir}/out1-custom.txt', |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out2-custom.txt': { |
|
'destination': '{datadir}/out2-custom.txt', |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out3-custom.txt': { |
|
'destination': '{datadir}/out3-custom.txt', |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/out1.txt': { |
|
'destination': '{datadir}/out1.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/out2.txt': { |
|
'destination': '{datadir}/out2.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out-devel.h': { |
|
'destination': '{includedir}/out-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/out3-notag.txt': { |
|
'destination': '{datadir}/out3-notag.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
}, |
|
'configure': { |
|
f'{self.builddir}/foo-notag.h': { |
|
'destination': '{datadir}/foo-notag.h', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/foo2-devel.h': { |
|
'destination': '{includedir}/foo2-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/foo-custom.h': { |
|
'destination': '{datadir}/foo-custom.h', |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{self.builddir}/subdir/foo2.h': { |
|
'destination': '{datadir}/foo2.h', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
}, |
|
'data': { |
|
f'{testdir}/bar-notag.txt': { |
|
'destination': '{datadir}/bar-notag.txt', |
|
'tag': None, |
|
'subproject': None, |
|
}, |
|
f'{testdir}/bar-devel.h': { |
|
'destination': '{includedir}/bar-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{testdir}/bar-custom.txt': { |
|
'destination': '{datadir}/bar-custom.txt', |
|
'tag': 'custom', |
|
'subproject': None, |
|
}, |
|
f'{testdir}/subdir/bar2-devel.h': { |
|
'destination': '{includedir}/bar2-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{testdir}/subprojects/subproject/aaa.txt': { |
|
'destination': '{datadir}/subproject/aaa.txt', |
|
'tag': None, |
|
'subproject': 'subproject', |
|
}, |
|
f'{testdir}/subprojects/subproject/bbb.txt': { |
|
'destination': '{datadir}/subproject/bbb.txt', |
|
'tag': 'data', |
|
'subproject': 'subproject', |
|
}, |
|
}, |
|
'headers': { |
|
f'{testdir}/foo1-devel.h': { |
|
'destination': '{includedir}/foo1-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
f'{testdir}/subdir/foo3-devel.h': { |
|
'destination': '{includedir}/foo3-devel.h', |
|
'tag': 'devel', |
|
'subproject': None, |
|
}, |
|
}, |
|
'install_subdirs': { |
|
f'{testdir}/custom_files': { |
|
'destination': '{datadir}/custom_files', |
|
'tag': 'custom', |
|
'subproject': None, |
|
'exclude_dirs': [], |
|
'exclude_files': [], |
|
}, |
|
f'{testdir}/excludes': { |
|
'destination': '{datadir}/excludes', |
|
'tag': 'custom', |
|
'subproject': None, |
|
'exclude_dirs': ['excluded'], |
|
'exclude_files': ['excluded.txt'], |
|
} |
|
} |
|
} |
|
|
|
fix_path = lambda path: os.path.sep.join(path.split('/')) |
|
expected_fixed = { |
|
data_type: { |
|
fix_path(source): { |
|
key: fix_path(value) if key == 'destination' else value |
|
for key, value in attributes.items() |
|
} |
|
for source, attributes in files.items() |
|
} |
|
for data_type, files in expected.items() |
|
} |
|
|
|
for data_type, files in expected_fixed.items(): |
|
for file, details in files.items(): |
|
with self.subTest(key='{}.{}'.format(data_type, file)): |
|
self.assertEqual(res[data_type][file], details) |
|
|
|
@skip_if_not_language('rust') |
|
@unittest.skipIf(not shutil.which('clippy-driver'), 'Test requires clippy-driver') |
|
def test_rust_clippy(self) -> None: |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Rust is only supported with ninja currently') |
|
# When clippy is used, we should get an exception since a variable named |
|
# "foo" is used, but is on our denylist |
|
testdir = os.path.join(self.rust_test_dir, '1 basic') |
|
self.init(testdir, extra_args=['--werror'], override_envvars={'RUSTC': 'clippy-driver'}) |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.build() |
|
self.assertTrue('error: use of a blacklisted/placeholder name `foo`' in cm.exception.stdout or |
|
'error: use of a disallowed/placeholder name `foo`' in cm.exception.stdout) |
|
|
|
@skip_if_not_language('rust') |
|
def test_rust_rlib_linkage(self) -> None: |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Rust is only supported with ninja currently') |
|
template = textwrap.dedent('''\ |
|
use std::process::exit; |
|
|
|
pub fn fun() {{ |
|
exit({}); |
|
}} |
|
''') |
|
|
|
testdir = os.path.join(self.unit_test_dir, '102 rlib linkage') |
|
gen_file = os.path.join(testdir, 'lib.rs') |
|
with open(gen_file, 'w', encoding='utf-8') as f: |
|
f.write(template.format(0)) |
|
self.addCleanup(windows_proof_rm, gen_file) |
|
|
|
self.init(testdir) |
|
self.build() |
|
self.run_tests() |
|
|
|
with open(gen_file, 'w', encoding='utf-8') as f: |
|
f.write(template.format(39)) |
|
|
|
self.build() |
|
with self.assertRaises(subprocess.CalledProcessError) as cm: |
|
self.run_tests() |
|
self.assertEqual(cm.exception.returncode, 1) |
|
self.assertIn('exit status 39', cm.exception.stdout) |
|
|
|
@skip_if_not_language('rust') |
|
def test_bindgen_drops_invalid(self) -> None: |
|
if self.backend is not Backend.ninja: |
|
raise unittest.SkipTest('Rust is only supported with ninja currently') |
|
testdir = os.path.join(self.rust_test_dir, '12 bindgen') |
|
env = get_fake_env(testdir, self.builddir, self.prefix) |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
# bindgen understands compiler args that clang understands, but not |
|
# flags by other compilers |
|
if cc.get_id() == 'gcc': |
|
bad_arg = '-fdse' |
|
elif cc.get_id() == 'msvc': |
|
bad_arg = '/fastfail' |
|
else: |
|
raise unittest.SkipTest('Test only supports GCC and MSVC') |
|
self.init(testdir, extra_args=[f"-Dc_args=['-DCMD_ARG', '{bad_arg}']"]) |
|
intro = self.introspect(['--targets']) |
|
for i in intro: |
|
if i['type'] == 'custom' and i['id'].startswith('rustmod-bindgen'): |
|
args = i['target_sources'][0]['compiler'] |
|
self.assertIn('-DCMD_ARG', args) |
|
self.assertIn('-DPROJECT_ARG', args) |
|
self.assertIn('-DGLOBAL_ARG', args) |
|
self.assertNotIn(bad_arg, args) |
|
self.assertNotIn('-mtls-dialect=gnu2', args) |
|
self.assertNotIn('/fp:fast', args) |
|
return |
|
|
|
def test_custom_target_name(self): |
|
testdir = os.path.join(self.unit_test_dir, '100 custom target name') |
|
self.init(testdir) |
|
out = self.build() |
|
if self.backend is Backend.ninja: |
|
self.assertIn('Generating file.txt with a custom command', out) |
|
self.assertIn('Generating subdir/file.txt with a custom command', out) |
|
|
|
def test_symlinked_subproject(self): |
|
testdir = os.path.join(self.unit_test_dir, '107 subproject symlink') |
|
subproject_dir = os.path.join(testdir, 'subprojects') |
|
subproject = os.path.join(testdir, 'symlinked_subproject') |
|
symlinked_subproject = os.path.join(testdir, 'subprojects', 'symlinked_subproject') |
|
if not os.path.exists(subproject_dir): |
|
os.mkdir(subproject_dir) |
|
try: |
|
os.symlink(subproject, symlinked_subproject) |
|
except OSError: |
|
raise SkipTest("Symlinks are not available on this machine") |
|
self.addCleanup(os.remove, symlinked_subproject) |
|
|
|
self.init(testdir) |
|
self.build() |
|
|
|
def test_configure_same_noop(self): |
|
testdir = os.path.join(self.unit_test_dir, '109 configure same noop') |
|
args = [ |
|
'-Dstring=val', |
|
'-Dboolean=true', |
|
'-Dcombo=two', |
|
'-Dinteger=7', |
|
'-Darray=[\'three\']', |
|
'-Dfeature=disabled', |
|
'--buildtype=plain', |
|
'--prefix=/abc', |
|
] |
|
self.init(testdir, extra_args=args) |
|
|
|
filename = Path(self.privatedir) / 'coredata.dat' |
|
|
|
olddata = filename.read_bytes() |
|
oldmtime = os.path.getmtime(filename) |
|
|
|
for opt in ('-Dstring=val', '--buildtype=plain', '-Dfeature=disabled', '-Dprefix=/abc'): |
|
self.setconf([opt]) |
|
newdata = filename.read_bytes() |
|
newmtime = os.path.getmtime(filename) |
|
self.assertEqual(oldmtime, newmtime) |
|
self.assertEqual(olddata, newdata) |
|
olddata = newdata |
|
oldmtime = newmtime |
|
|
|
for opt in ('-Dstring=abc', '--buildtype=release', '-Dfeature=enabled', '-Dprefix=/def'): |
|
self.setconf([opt]) |
|
newdata = filename.read_bytes() |
|
newmtime = os.path.getmtime(filename) |
|
self.assertGreater(newmtime, oldmtime) |
|
self.assertNotEqual(olddata, newdata) |
|
olddata = newdata |
|
oldmtime = newmtime |
|
|
|
def test_c_cpp_stds(self): |
|
testdir = os.path.join(self.unit_test_dir, '115 c cpp stds') |
|
self.init(testdir) |
|
# Invalid values should fail whatever compiler we have |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.setconf('-Dc_std=invalid') |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.setconf('-Dc_std=c89,invalid') |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.setconf('-Dc_std=c++11') |
|
env = get_fake_env() |
|
cc = detect_c_compiler(env, MachineChoice.HOST) |
|
if cc.get_id() == 'msvc': |
|
# default_option should have selected those |
|
self.assertEqual(self.getconf('c_std'), 'c89') |
|
self.assertEqual(self.getconf('cpp_std'), 'vc++11') |
|
# This is deprecated but works for C |
|
self.setconf('-Dc_std=gnu99') |
|
self.assertEqual(self.getconf('c_std'), 'c99') |
|
# C++ however never accepted that fallback |
|
with self.assertRaises(subprocess.CalledProcessError): |
|
self.setconf('-Dcpp_std=gnu++11') |
|
# The first supported std should be selected |
|
self.setconf('-Dcpp_std=gnu++11,vc++11,c++11') |
|
self.assertEqual(self.getconf('cpp_std'), 'vc++11') |
|
elif cc.get_id() == 'gcc': |
|
# default_option should have selected those |
|
self.assertEqual(self.getconf('c_std'), 'gnu89') |
|
self.assertEqual(self.getconf('cpp_std'), 'gnu++98') |
|
# The first supported std should be selected |
|
self.setconf('-Dcpp_std=c++11,gnu++11,vc++11') |
|
self.assertEqual(self.getconf('cpp_std'), 'c++11')
|
|
|