summaryrefslogtreecommitdiffstats
path: root/unittests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
commit7b6e527f440cd7e6f8be2b07cee320ee6ca18786 (patch)
tree4a2738d69fa2814659fdadddf5826282e73d81f4 /unittests
parentInitial commit. (diff)
downloadmeson-7b6e527f440cd7e6f8be2b07cee320ee6ca18786.tar.xz
meson-7b6e527f440cd7e6f8be2b07cee320ee6ca18786.zip
Adding upstream version 1.0.1.upstream/1.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'unittests')
-rw-r--r--unittests/allplatformstests.py4499
-rw-r--r--unittests/baseplatformtests.py483
-rw-r--r--unittests/darwintests.py150
-rw-r--r--unittests/datatests.py242
-rw-r--r--unittests/failuretests.py392
-rw-r--r--unittests/helpers.py206
-rw-r--r--unittests/internaltests.py1648
-rw-r--r--unittests/linuxcrosstests.py192
-rw-r--r--unittests/linuxliketests.py1830
-rw-r--r--unittests/machinefiletests.py953
-rw-r--r--unittests/platformagnostictests.py123
-rw-r--r--unittests/pythontests.py62
-rw-r--r--unittests/rewritetests.py398
-rw-r--r--unittests/subprojectscommandtests.py300
-rw-r--r--unittests/taptests.py294
-rw-r--r--unittests/windowstests.py400
16 files changed, 12172 insertions, 0 deletions
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
new file mode 100644
index 0000000..d8c74e6
--- /dev/null
+++ b/unittests/allplatformstests.py
@@ -0,0 +1,4499 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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, ExecutableSerialisation, EnvironmentVariables,
+ windows_proof_rm
+)
+
+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.dependencies import PkgConfigDependency
+from mesonbuild.build import Target, ConfigurationData, Executable, SharedLibrary, StaticLibrary
+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_formats(confdata, result):
+ self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result)
+ self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result)
+ self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result)
+
+ confdata = ConfigurationData()
+ # Key error as they do not exists
+ check_formats(confdata, '/* #undef VAR */\n')
+
+ # Check boolean
+ confdata.values = {'VAR': (False, 'description')}
+ check_formats(confdata, '#undef VAR\n')
+ confdata.values = {'VAR': (True, 'description')}
+ check_formats(confdata, '#define VAR\n')
+
+ # Check string
+ confdata.values = {'VAR': ('value', 'description')}
+ check_formats(confdata, '#define VAR value\n')
+
+ # Check integer
+ confdata.values = {'VAR': (10, 'description')}
+ check_formats(confdata, '#define VAR 10\n')
+
+ # Check multiple string with cmake formats
+ confdata.values = {'VAR': ('value', 'description')}
+ self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n')
+ self.assertEqual(conf_str(['#define VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value')
+ self.assertEqual(conf_str(['#cmakedefine VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value\n')
+ self.assertEqual(conf_str(['#define VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value')
+
+ # Handles meson format exceptions
+ # Unknown format
+ self.assertRaises(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, mesonbuild.linkers.ArLinker):
+ raise SkipTest('static linker is not `ar`')
+ # Configure
+ self.init(testdir)
+ # Get name of static library
+ targets = self.introspect('--targets')
+ self.assertEqual(len(targets), 1)
+ libname = targets[0]['filename'][0]
+ # Build and get contents of static library
+ self.build()
+ before = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
+ # Filter out non-object-file contents
+ before = [f for f in before if f.endswith(('.o', '.obj'))]
+ # Static library should contain only one object
+ self.assertEqual(len(before), 1, msg=before)
+ # Change the source to be built into the static library
+ self.setconf('-Dsource=libfile2.c')
+ self.build()
+ after = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
+ # Filter out non-object-file contents
+ after = [f for f in after if f.endswith(('.o', '.obj'))]
+ # Static library should contain only one object
+ self.assertEqual(len(after), 1, msg=after)
+ # and the object must have changed
+ self.assertNotEqual(before, after)
+
+ def test_static_compile_order(self):
+ '''
+ Test that the order of files in a compiler command-line while compiling
+ and linking statically is deterministic. This can't be an ordinary test
+ case because we need to inspect the compiler database.
+ https://github.com/mesonbuild/meson/pull/951
+ '''
+ testdir = os.path.join(self.common_test_dir, '5 linkstatic')
+ self.init(testdir)
+ compdb = self.get_compdb()
+ # Rules will get written out in this order
+ self.assertTrue(compdb[0]['file'].endswith("libfile.c"))
+ self.assertTrue(compdb[1]['file'].endswith("libfile2.c"))
+ self.assertTrue(compdb[2]['file'].endswith("libfile3.c"))
+ self.assertTrue(compdb[3]['file'].endswith("libfile4.c"))
+ # FIXME: We don't have access to the linker command
+
+ def test_run_target_files_path(self):
+ '''
+ Test that run_targets are run from the correct directory
+ https://github.com/mesonbuild/meson/issues/957
+ '''
+ testdir = os.path.join(self.common_test_dir, '51 run target')
+ self.init(testdir)
+ self.run_target('check_exists')
+ self.run_target('check-env')
+ self.run_target('check-env-ct')
+
+ def test_run_target_subdir(self):
+ '''
+ Test that run_targets are run from the correct directory
+ https://github.com/mesonbuild/meson/issues/957
+ '''
+ testdir = os.path.join(self.common_test_dir, '51 run target')
+ self.init(testdir)
+ self.run_target('textprinter')
+
+ def test_install_introspection(self):
+ '''
+ Tests that the Meson introspection API exposes install filenames correctly
+ https://github.com/mesonbuild/meson/issues/829
+ '''
+ if self.backend is not Backend.ninja:
+ raise SkipTest(f'{self.backend.name!r} backend can\'t install files')
+ testdir = os.path.join(self.common_test_dir, '8 install')
+ self.init(testdir)
+ intro = self.introspect('--targets')
+ if intro[0]['type'] == 'executable':
+ intro = intro[::-1]
+ self.assertPathListEqual(intro[0]['install_filename'], ['/usr/lib/libstat.a'])
+ self.assertPathListEqual(intro[1]['install_filename'], ['/usr/bin/prog' + exe_suffix])
+
+ def test_install_subdir_introspection(self):
+ '''
+ Test that the Meson introspection API also contains subdir install information
+ https://github.com/mesonbuild/meson/issues/5556
+ '''
+ testdir = os.path.join(self.common_test_dir, '59 install subdir')
+ self.init(testdir)
+ intro = self.introspect('--installed')
+ expected = {
+ 'sub2': 'share/sub2',
+ 'subdir/sub1': 'share/sub1',
+ 'subdir/sub_elided': 'share',
+ 'sub1': 'share/sub1',
+ 'sub/sub1': 'share/sub1',
+ 'sub_elided': 'share',
+ 'nested_elided/sub': 'share',
+ 'new_directory': 'share/new_directory',
+ }
+
+ self.assertEqual(len(intro), len(expected))
+
+ # Convert expected to PurePath
+ expected_converted = {PurePath(os.path.join(testdir, key)): PurePath(os.path.join(self.prefix, val)) for key, val in expected.items()}
+ intro_converted = {PurePath(key): PurePath(val) for key, val in intro.items()}
+
+ for src, dst in expected_converted.items():
+ self.assertIn(src, intro_converted)
+ self.assertEqual(dst, intro_converted[src])
+
+ def test_install_introspection_multiple_outputs(self):
+ '''
+ Tests that the Meson introspection API exposes multiple install filenames correctly without crashing
+ https://github.com/mesonbuild/meson/pull/4555
+
+ Reverted to the first file only because of https://github.com/mesonbuild/meson/pull/4547#discussion_r244173438
+ TODO Change the format to a list officially in a followup PR
+ '''
+ if self.backend is not Backend.ninja:
+ raise 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, '95 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(xclude_suites)
+ self._run(self.mtest_command + ['--setup=good', '--suite', 'buggy'])
+ with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f:
+ include_suites_log = f.read()
+ self.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_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)
+ 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["generated_sources"]:
+ if "includestuff.pyx.c" in generated_source:
+ name = generated_source
+ break
+ # Split the path (we only want the includestuff.cpython-blahblahblah)
+ name = os.path.normpath(name).split("/")[-2:]
+ name = "/".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)
+ 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 = mesonbuild.linkers.ArLinker
+ lib = mesonbuild.linkers.VisualStudioLinker
+ langs = [('c', 'CC'), ('cpp', 'CXX')]
+ if not is_windows() and platform.machine().lower() != 'e2k':
+ langs += [('objc', 'OBJC'), ('objcpp', 'OBJCXX')]
+ testdir = os.path.join(self.unit_test_dir, '5 compiler detection')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ for lang, evar in langs:
+ # Detect with evar and do sanity checks on that
+ if evar in os.environ:
+ ecc = compiler_from_language(env, lang, MachineChoice.HOST)
+ self.assertTrue(ecc.version)
+ elinker = detect_static_linker(env, ecc)
+ # Pop it so we don't use it for the next detection
+ evalue = os.environ.pop(evar)
+ # Very rough/strict heuristics. Would never work for actual
+ # compiler detection, but should be ok for the tests.
+ ebase = os.path.basename(evalue)
+ if ebase.startswith('g') or ebase.endswith(('-gcc', '-g++')):
+ self.assertIsInstance(ecc, gnu)
+ self.assertIsInstance(elinker, ar)
+ elif 'clang-cl' in ebase:
+ self.assertIsInstance(ecc, clangcl)
+ self.assertIsInstance(elinker, lib)
+ elif 'clang' in ebase:
+ self.assertIsInstance(ecc, clang)
+ self.assertIsInstance(elinker, ar)
+ elif ebase.startswith('ic'):
+ self.assertIsInstance(ecc, intel)
+ self.assertIsInstance(elinker, ar)
+ elif ebase.startswith('cl'):
+ self.assertIsInstance(ecc, msvc)
+ self.assertIsInstance(elinker, lib)
+ else:
+ raise AssertionError(f'Unknown compiler {evalue!r}')
+ # Check that we actually used the evalue correctly as the compiler
+ self.assertEqual(ecc.get_exelist(), split_args(evalue))
+ # Do auto-detection of compiler based on platform, PATH, etc.
+ cc = compiler_from_language(env, lang, MachineChoice.HOST)
+ self.assertTrue(cc.version)
+ linker = detect_static_linker(env, cc)
+ # Check compiler type
+ if isinstance(cc, gnu):
+ self.assertIsInstance(linker, ar)
+ if is_osx():
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
+ elif is_sunos():
+ self.assertIsInstance(cc.linker, (mesonbuild.linkers.SolarisDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin))
+ else:
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
+ if isinstance(cc, clangcl):
+ self.assertIsInstance(linker, lib)
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.ClangClDynamicLinker)
+ if isinstance(cc, clang):
+ self.assertIsInstance(linker, ar)
+ if is_osx():
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
+ elif is_windows():
+ # This is clang, not clang-cl. This can be either an
+ # ld-like linker of link.exe-like linker (usually the
+ # former for msys2, the latter otherwise)
+ self.assertIsInstance(cc.linker, (mesonbuild.linkers.MSVCDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin))
+ else:
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
+ if isinstance(cc, intel):
+ self.assertIsInstance(linker, ar)
+ if is_osx():
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
+ elif is_windows():
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.XilinkDynamicLinker)
+ else:
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuDynamicLinker)
+ if isinstance(cc, msvc):
+ self.assertTrue(is_windows())
+ self.assertIsInstance(linker, lib)
+ self.assertEqual(cc.id, 'msvc')
+ self.assertTrue(hasattr(cc, 'is_64'))
+ self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker)
+ # If we're on Windows CI, we know what the compiler will be
+ if 'arch' in os.environ:
+ if os.environ['arch'] == 'x64':
+ self.assertTrue(cc.is_64)
+ else:
+ self.assertFalse(cc.is_64)
+ # Set evar ourselves to a wrapper script that just calls the same
+ # exelist + some argument. This is meant to test that setting
+ # something like `ccache gcc -pipe` or `distcc ccache gcc` works.
+ wrapper = os.path.join(testdir, 'compiler wrapper.py')
+ wrappercc = python_command + [wrapper] + cc.get_exelist() + ['-DSOME_ARG']
+ os.environ[evar] = ' '.join(quote_arg(w) for w in wrappercc)
+
+ # Check static linker too
+ wrapperlinker = python_command + [wrapper] + linker.get_exelist() + linker.get_always_args()
+ os.environ['AR'] = ' '.join(quote_arg(w) for w in wrapperlinker)
+
+ # Need a new env to re-run environment loading
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+
+ wcc = compiler_from_language(env, lang, MachineChoice.HOST)
+ wlinker = detect_static_linker(env, wcc)
+ # Pop it so we don't use it for the next detection
+ 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] == 'ccache':
+ compiler = split[1]
+ else:
+ compiler = split[0]
+ # Classify commands
+ if 'Ic-asm' in cmd['command']:
+ if cmd['file'].endswith('.S'):
+ commands['c-asm']['asm'] = compiler
+ elif cmd['file'].endswith('.c'):
+ commands['c-asm']['c'] = compiler
+ else:
+ raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
+ elif 'Icpp-asm' in cmd['command']:
+ if cmd['file'].endswith('.S'):
+ commands['cpp-asm']['asm'] = compiler
+ elif cmd['file'].endswith('.cpp'):
+ commands['cpp-asm']['cpp'] = compiler
+ else:
+ raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
+ elif 'Ic-cpp-asm' in cmd['command']:
+ if cmd['file'].endswith('.S'):
+ commands['c-cpp-asm']['asm'] = compiler
+ elif cmd['file'].endswith('.c'):
+ commands['c-cpp-asm']['c'] = compiler
+ elif cmd['file'].endswith('.cpp'):
+ commands['c-cpp-asm']['cpp'] = compiler
+ else:
+ raise AssertionError('{!r} found in c-cpp-asm?'.format(cmd['command']))
+ elif 'Icpp-c-asm' in cmd['command']:
+ if cmd['file'].endswith('.S'):
+ commands['cpp-c-asm']['asm'] = compiler
+ elif cmd['file'].endswith('.c'):
+ commands['cpp-c-asm']['c'] = compiler
+ elif cmd['file'].endswith('.cpp'):
+ commands['cpp-c-asm']['cpp'] = compiler
+ else:
+ raise AssertionError('{!r} found in cpp-c-asm?'.format(cmd['command']))
+ else:
+ raise AssertionError('Unknown command {!r} found'.format(cmd['command']))
+ # Check that .S files are always built with the C compiler
+ self.assertEqual(commands['c-asm']['asm'], commands['c-asm']['c'])
+ self.assertEqual(commands['c-asm']['asm'], commands['cpp-asm']['asm'])
+ self.assertEqual(commands['cpp-asm']['asm'], commands['c-cpp-asm']['c'])
+ self.assertEqual(commands['c-cpp-asm']['asm'], commands['c-cpp-asm']['c'])
+ self.assertEqual(commands['cpp-c-asm']['asm'], commands['cpp-c-asm']['c'])
+ self.assertNotEqual(commands['cpp-asm']['asm'], commands['cpp-asm']['cpp'])
+ self.assertNotEqual(commands['c-cpp-asm']['c'], commands['c-cpp-asm']['cpp'])
+ self.assertNotEqual(commands['cpp-c-asm']['c'], commands['cpp-c-asm']['cpp'])
+ # Check that the c-asm target is always linked with the C linker
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ with open(build_ninja, encoding='utf-8') as f:
+ contents = f.read()
+ m = re.search('build c-asm.*: c_LINKER', contents)
+ self.assertIsNotNone(m, msg=contents)
+
+ def test_preprocessor_checks_CPPFLAGS(self):
+ '''
+ Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but
+ not LDFLAGS.
+ '''
+ testdir = os.path.join(self.common_test_dir, '132 get define')
+ define = 'MESON_TEST_DEFINE_VALUE'
+ # NOTE: this list can't have \n, ' or "
+ # \n is never substituted by the GNU pre-processor via a -D define
+ # ' and " confuse split_args() even when they are escaped
+ # % and # confuse the MSVC preprocessor
+ # !, ^, *, and < confuse lcc preprocessor
+ value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`'
+ for env_var in ['CPPFLAGS', 'CFLAGS']:
+ env = {}
+ env[env_var] = f'-D{define}="{value}"'
+ env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read'
+ self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env)
+
+ def test_custom_target_exe_data_deterministic(self):
+ testdir = os.path.join(self.common_test_dir, '109 custom target capture')
+ self.init(testdir)
+ meson_exe_dat1 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
+ self.wipe()
+ self.init(testdir)
+ meson_exe_dat2 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
+ self.assertListEqual(meson_exe_dat1, meson_exe_dat2)
+
+ def test_noop_changes_cause_no_rebuilds(self):
+ '''
+ Test that no-op changes to the build files such as mtime do not cause
+ a rebuild of anything.
+ '''
+ testdir = os.path.join(self.common_test_dir, '6 linkshared')
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Changing mtime of meson.build should not rebuild anything
+ self.utime(os.path.join(testdir, 'meson.build'))
+ self.assertReconfiguredBuildIsNoop()
+ # Changing mtime of libefile.c should rebuild the library, but not relink the executable
+ self.utime(os.path.join(testdir, 'libfile.c'))
+ self.assertBuildRelinkedOnlyTarget('mylib')
+
+ def test_source_changes_cause_rebuild(self):
+ '''
+ Test that changes to sources and headers cause rebuilds, but not
+ changes to unused files (as determined by the dependency file) in the
+ input files list.
+ '''
+ testdir = os.path.join(self.common_test_dir, '19 header in file list')
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Changing mtime of header.h should rebuild everything
+ self.utime(os.path.join(testdir, 'header.h'))
+ self.assertBuildRelinkedOnlyTarget('prog')
+
+ def test_custom_target_changes_cause_rebuild(self):
+ '''
+ Test that in a custom target, changes to the input files, the
+ ExternalProgram, and any File objects on the command-line cause
+ a rebuild.
+ '''
+ testdir = os.path.join(self.common_test_dir, '57 custom header generator')
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Changing mtime of these should rebuild everything
+ for f in ('input.def', 'makeheader.py', 'somefile.txt'):
+ self.utime(os.path.join(testdir, f))
+ self.assertBuildRelinkedOnlyTarget('prog')
+
+ def test_source_generator_program_cause_rebuild(self):
+ '''
+ Test that changes to generator programs in the source tree cause
+ a rebuild.
+ '''
+ testdir = os.path.join(self.common_test_dir, '90 gen extra')
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Changing mtime of generator should rebuild the executable
+ self.utime(os.path.join(testdir, 'srcgen.py'))
+ self.assertRebuiltTarget('basic')
+
+ def test_static_library_lto(self):
+ '''
+ Test that static libraries can be built with LTO and linked to
+ executables. On Linux, this requires the use of gcc-ar.
+ https://github.com/mesonbuild/meson/issues/1646
+ '''
+ testdir = os.path.join(self.common_test_dir, '5 linkstatic')
+
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ if detect_c_compiler(env, MachineChoice.HOST).get_id() == 'clang' and is_windows():
+ raise 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']:
+ 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 s in t['target_sources']:
+ self.assertTrue(expected.issubset(set(s['parameters'])), f'Incorrect values for {t["name"]}')
+
+ def test_dist_git(self):
+ if not shutil.which('git'):
+ raise 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 = mesonbuild.coredata.FORBIDDEN_TARGET_NAMES
+ # We don't actually define a target with this name
+ targets.pop('build.ninja')
+ # Remove this to avoid multiple entries with the same name
+ # but different case.
+ targets.pop('PHONY')
+ for i in targets:
+ self.assertPathExists(os.path.join(testdir, i))
+
+ def detect_prebuild_env(self):
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ stlinker = detect_static_linker(env, cc)
+ if 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 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()
+
+ 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, '105 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 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_subproject_promotion(self):
+ testdir = os.path.join(self.unit_test_dir, '12 promote')
+ workdir = os.path.join(self.builddir, 'work')
+ shutil.copytree(testdir, workdir)
+ spdir = os.path.join(workdir, 'subprojects')
+ s3dir = os.path.join(spdir, 's3')
+ scommondir = os.path.join(spdir, 'scommon')
+ self.assertFalse(os.path.isdir(s3dir))
+ subprocess.check_call(self.wrap_command + ['promote', 's3'],
+ cwd=workdir,
+ stdout=subprocess.DEVNULL)
+ self.assertTrue(os.path.isdir(s3dir))
+ self.assertFalse(os.path.isdir(scommondir))
+ self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'scommon'],
+ cwd=workdir,
+ stderr=subprocess.DEVNULL), 0)
+ self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'invalid/path/to/scommon'],
+ cwd=workdir,
+ stderr=subprocess.DEVNULL), 0)
+ self.assertFalse(os.path.isdir(scommondir))
+ subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/scommon'], cwd=workdir)
+ self.assertTrue(os.path.isdir(scommondir))
+ promoted_wrap = os.path.join(spdir, 'athing.wrap')
+ self.assertFalse(os.path.isfile(promoted_wrap))
+ subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir)
+ self.assertTrue(os.path.isfile(promoted_wrap))
+ self.init(workdir)
+ self.build()
+
+ def test_subproject_promotion_wrap(self):
+ testdir = os.path.join(self.unit_test_dir, '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'),
+ ('59 bad option argument', 'meson_options.txt'),
+ ('97 subdir parse error', os.path.join('subdir', 'meson.build')),
+ ('98 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']:
+ try:
+ comp = detect_compiler_for(env, l, MachineChoice.HOST)
+ with tempfile.TemporaryDirectory() as d:
+ comp.sanity_check(d, env)
+ langs.append(l)
+ except EnvironmentException:
+ pass
+
+ # The D template fails under mac CI and we don't know why.
+ # Patches welcome
+ if is_osx():
+ langs = [l for l in langs if l != 'd']
+
+ for lang in langs:
+ for target_type in ('executable', 'library'):
+ 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)
+ 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_libary() 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, '96 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) as crossfile:
+ crossfile.write(textwrap.dedent(
+ '''[binaries]
+ pkgconfig = '{}'
+
+ [properties]
+
+ [host_machine]
+ system = 'linux'
+ cpu_family = 'arm'
+ cpu = 'armv7'
+ endian = 'little'
+ '''.format(os.path.join(testdir, 'cross_pkgconfig.py'))))
+ crossfile.flush()
+ self.meson_cross_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) as crossfile:
+ crossfile.write(textwrap.dedent(
+ '''[binaries]
+ pkgconfig = 'pkg-config'
+
+ [properties]
+ pkg_config_libdir = ['{}']
+
+ [host_machine]
+ system = 'linux'
+ cpu_family = 'arm'
+ cpu = 'armv7'
+ endian = 'little'
+ '''.format(os.path.join(testdir, 'cross_pkgconfig'))))
+ crossfile.flush()
+ self.meson_cross_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)
+
+ 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),
+ ('dependencies', list),
+ ('installed', dict),
+ ('projectinfo', dict),
+ ('targets', list),
+ ('tests', list),
+ ]
+
+ test_keylist = [
+ ('cmd', list),
+ ('env', dict),
+ ('name', str),
+ ('timeout', int),
+ ('suite', list),
+ ('is_parallel', bool),
+ ('protocol', str),
+ ('depends', list),
+ ('workdir', (str, None)),
+ ('priority', int),
+ ]
+
+ buildoptions_keylist = [
+ ('name', str),
+ ('section', str),
+ ('type', str),
+ ('description', str),
+ ('machine', str),
+ ('choices', (list, None)),
+ ('value', (str, int, bool, list)),
+ ]
+
+ buildoptions_typelist = [
+ ('combo', str, [('choices', list)]),
+ ('string', str, []),
+ ('boolean', bool, []),
+ ('integer', int, []),
+ ('array', list, []),
+ ]
+
+ buildoptions_sections = ['core', 'backend', 'base', 'compiler', 'directory', 'user', 'test']
+ buildoptions_machines = ['any', 'build', 'host']
+
+ dependencies_typelist = [
+ ('name', str),
+ ('version', str),
+ ('compile_args', list),
+ ('link_args', list),
+ ]
+
+ targets_typelist = [
+ ('name', str),
+ ('id', str),
+ ('type', str),
+ ('defined_in', str),
+ ('filename', list),
+ ('build_by_default', bool),
+ ('target_sources', list),
+ ('extra_files', list),
+ ('subproject', (str, None)),
+ ('install_filename', (list, None)),
+ ('installed', bool),
+ ]
+
+ targets_sources_typelist = [
+ ('language', str),
+ ('compiler', list),
+ ('parameters', list),
+ ('sources', list),
+ ('generated_sources', list),
+ ]
+
+ # First load all files
+ res = {}
+ for i in root_keylist:
+ curr = os.path.join(infodir, 'intro-{}.json'.format(i[0]))
+ self.assertPathExists(curr)
+ with open(curr, 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['sources']})
+
+ # Check Tests and benchmarks
+ tests_to_find = ['test case 1', 'test case 2', 'benchmark 1']
+ deps_to_find = {'test case 1': [src_to_id['t1.cpp']],
+ 'test case 2': [src_to_id['t2.cpp'], src_to_id['t3.cpp']],
+ 'benchmark 1': [out_to_id['file2'], out_to_id['file3'], out_to_id['file4'], src_to_id['t3.cpp']]}
+ for i in res['benchmarks'] + res['tests']:
+ assertKeyTypes(test_keylist, i)
+ if i['name'] in tests_to_find:
+ tests_to_find.remove(i['name'])
+ self.assertEqual(sorted(i['depends']),
+ sorted(deps_to_find[i['name']]))
+ self.assertListEqual(tests_to_find, [])
+
+ # Check buildoptions
+ buildopts_to_find = {'cpp_std': 'c++11'}
+ for i in res['buildoptions']:
+ assertKeyTypes(buildoptions_keylist, i)
+ valid_type = False
+ for j in buildoptions_typelist:
+ if i['type'] == j[0]:
+ self.assertIsInstance(i['value'], j[1])
+ assertKeyTypes(j[2], i, strict=False)
+ valid_type = True
+ break
+
+ self.assertIn(i['section'], buildoptions_sections)
+ self.assertIn(i['machine'], buildoptions_machines)
+ self.assertTrue(valid_type)
+ if i['name'] in buildopts_to_find:
+ self.assertEqual(i['value'], buildopts_to_find[i['name']])
+ buildopts_to_find.pop(i['name'], None)
+ self.assertDictEqual(buildopts_to_find, {})
+
+ # Check buildsystem_files
+ bs_files = ['meson.build', 'meson_options.txt', 'sharedlib/meson.build', 'staticlib/meson.build']
+ bs_files = [os.path.join(testdir, x) for x in bs_files]
+ self.assertPathListEqual(list(sorted(res['buildsystem_files'])), list(sorted(bs_files)))
+
+ # Check dependencies
+ dependencies_to_find = ['threads']
+ for i in res['dependencies']:
+ assertKeyTypes(dependencies_typelist, i)
+ if i['name'] in dependencies_to_find:
+ dependencies_to_find.remove(i['name'])
+ self.assertListEqual(dependencies_to_find, [])
+
+ # Check projectinfo
+ self.assertDictEqual(res['projectinfo'], {'version': '1.2.3', 'descriptive_name': 'introspection', 'subproject_dir': 'subprojects', 'subprojects': []})
+
+ # Check targets
+ targets_to_find = {
+ 'sharedTestLib': ('shared library', True, False, 'sharedlib/meson.build',
+ [os.path.join(testdir, 'sharedlib', 'shared.cpp')]),
+ 'staticTestLib': ('static library', True, False, 'staticlib/meson.build',
+ [os.path.join(testdir, 'staticlib', 'static.c')]),
+ 'custom target test 1': ('custom', False, False, 'meson.build',
+ [os.path.join(testdir, 'cp.py')]),
+ 'custom target test 2': ('custom', False, False, 'meson.build',
+ name_to_out['custom target test 1']),
+ 'test1': ('executable', True, True, 'meson.build',
+ [os.path.join(testdir, 't1.cpp')]),
+ 'test2': ('executable', True, False, 'meson.build',
+ [os.path.join(testdir, 't2.cpp')]),
+ 'test3': ('executable', True, False, 'meson.build',
+ [os.path.join(testdir, 't3.cpp')]),
+ 'custom target test 3': ('custom', False, False, 'meson.build',
+ name_to_out['test3']),
+ }
+ for i in res['targets']:
+ assertKeyTypes(targets_typelist, i)
+ if i['name'] in targets_to_find:
+ tgt = targets_to_find[i['name']]
+ self.assertEqual(i['type'], tgt[0])
+ self.assertEqual(i['build_by_default'], tgt[1])
+ self.assertEqual(i['installed'], tgt[2])
+ self.assertPathEqual(i['defined_in'], os.path.join(testdir, tgt[3]))
+ targets_to_find.pop(i['name'], None)
+ for j in i['target_sources']:
+ assertKeyTypes(targets_sources_typelist, j)
+ self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]])
+ self.assertDictEqual(targets_to_find, {})
+
+ def test_introspect_file_dump_equals_all(self):
+ testdir = os.path.join(self.unit_test_dir, '56 introspection')
+ self.init(testdir)
+ res_all = self.introspect('--all')
+ res_file = {}
+
+ root_keylist = [
+ 'benchmarks',
+ 'buildoptions',
+ 'buildsystem_files',
+ 'dependencies',
+ 'installed',
+ 'install_plan',
+ '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']]
+ if 'install_filename' in i:
+ del i['install_filename']
+
+ sources = []
+ for j in i['target_sources']:
+ sources += j['sources']
+ i['target_sources'] = [{
+ 'language': 'unknown',
+ 'compiler': [],
+ 'parameters': [],
+ 'sources': sources,
+ 'generated_sources': []
+ }]
+
+ self.maxDiff = None
+ self.assertListEqual(res_nb, res_wb)
+
+ def test_introspect_ast_source(self):
+ testdir = os.path.join(self.unit_test_dir, '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'])
+ 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 : ''' + sys.executable + '''
+ 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
+
+ User defined options
+ backend : ''' + self.backend.name + '''
+ libdir : lib
+ prefix : /usr
+ enabled_opt : enabled
+ ''')
+ 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']
+
+ [binaries]
+ c = toolchain / compiler
+ '''))
+
+ values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2])
+ self.assertEqual(values['binaries']['c'], '/toolchain/gcc')
+ self.assertEqual(values['properties']['c_args'],
+ ['--sysroot=/toolchain/sysroot', '-DSOMETHING'])
+ self.assertEqual(values['properties']['cpp_args'],
+ ['--sysroot=/toolchain/sysroot', '-DSOMETHING', '-DSOMETHING_ELSE'])
+
+ @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, '91 install skip subprojects')
+ self.init(testdir)
+ self.build()
+
+ main_expected = [
+ '',
+ 'share',
+ 'include',
+ 'foo',
+ 'bin',
+ 'share/foo',
+ 'share/foo/foo.dat',
+ 'include/foo.h',
+ 'foo/foofile',
+ 'bin/foo' + exe_suffix,
+ ]
+ bar_expected = [
+ 'bar',
+ 'share/foo/bar.dat',
+ 'include/bar.h',
+ 'bin/bar' + exe_suffix,
+ 'bar/barfile'
+ ]
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() == 'msvc':
+ main_expected.append('bin/foo.pdb')
+ bar_expected.append('bin/bar.pdb')
+ prefix = destdir_join(self.installdir, self.prefix)
+ main_expected = [Path(prefix, p) for p in main_expected]
+ bar_expected = [Path(prefix, p) for p in bar_expected]
+ all_expected = main_expected + bar_expected
+
+ def check_installed_files(extra_args, expected):
+ args = ['install', '--destdir', self.installdir] + extra_args
+ self._run(self.meson_command + args, workdir=self.builddir)
+ all_files = [p for p in Path(self.installdir).rglob('*')]
+ self.assertEqual(sorted(expected), sorted(all_files))
+ windows_proof_rmtree(self.installdir)
+
+ check_installed_files([], all_expected)
+ check_installed_files(['--skip-subprojects'], main_expected)
+ check_installed_files(['--skip-subprojects', 'bar'], main_expected)
+ check_installed_files(['--skip-subprojects', 'another'], all_expected)
+
+ def test_adding_subproject_to_configure_project(self) -> None:
+ srcdir = os.path.join(self.unit_test_dir, '92 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())
+
+ 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, '93 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, '94 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)
+ 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)
+ 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)
+ 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, '98 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/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'),
+ }
+
+ 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_introspect_install_plan(self):
+ testdir = os.path.join(self.unit_test_dir, '98 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()
+ 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,
+ },
+ f'{self.builddir}/out2-notag.txt': {
+ 'destination': '{datadir}/out2-notag.txt',
+ 'tag': None,
+ },
+ f'{self.builddir}/libstatic.a': {
+ 'destination': '{libdir_static}/libstatic.a',
+ 'tag': 'devel',
+ },
+ f'{self.builddir}/' + exe_name('app'): {
+ 'destination': '{bindir}/' + exe_name('app'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/' + exe_name('app-otherdir'): {
+ 'destination': '{prefix}/otherbin/' + exe_name('app-otherdir'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/subdir/' + exe_name('app2'): {
+ 'destination': '{bindir}/' + exe_name('app2'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/' + shared_lib_name('shared'): {
+ 'destination': '{libdir_shared}/' + shared_lib_name('shared'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/' + shared_lib_name('both'): {
+ 'destination': '{libdir_shared}/' + shared_lib_name('both'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/' + static_lib_name('both'): {
+ 'destination': '{libdir_static}/' + static_lib_name('both'),
+ 'tag': 'devel',
+ },
+ f'{self.builddir}/' + shared_lib_name('bothcustom'): {
+ 'destination': '{libdir_shared}/' + shared_lib_name('bothcustom'),
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/' + static_lib_name('bothcustom'): {
+ 'destination': '{libdir_static}/' + static_lib_name('bothcustom'),
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/subdir/' + shared_lib_name('both2'): {
+ 'destination': '{libdir_shared}/' + shared_lib_name('both2'),
+ 'tag': 'runtime',
+ },
+ f'{self.builddir}/subdir/' + static_lib_name('both2'): {
+ 'destination': '{libdir_static}/' + static_lib_name('both2'),
+ 'tag': 'devel',
+ },
+ f'{self.builddir}/out1-custom.txt': {
+ 'destination': '{datadir}/out1-custom.txt',
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/out2-custom.txt': {
+ 'destination': '{datadir}/out2-custom.txt',
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/out3-custom.txt': {
+ 'destination': '{datadir}/out3-custom.txt',
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/subdir/out1.txt': {
+ 'destination': '{datadir}/out1.txt',
+ 'tag': None,
+ },
+ f'{self.builddir}/subdir/out2.txt': {
+ 'destination': '{datadir}/out2.txt',
+ 'tag': None,
+ },
+ f'{self.builddir}/out-devel.h': {
+ 'destination': '{includedir}/out-devel.h',
+ 'tag': 'devel',
+ },
+ f'{self.builddir}/out3-notag.txt': {
+ 'destination': '{datadir}/out3-notag.txt',
+ 'tag': None,
+ },
+ },
+ 'configure': {
+ f'{self.builddir}/foo-notag.h': {
+ 'destination': '{datadir}/foo-notag.h',
+ 'tag': None,
+ },
+ f'{self.builddir}/foo2-devel.h': {
+ 'destination': '{includedir}/foo2-devel.h',
+ 'tag': 'devel',
+ },
+ f'{self.builddir}/foo-custom.h': {
+ 'destination': '{datadir}/foo-custom.h',
+ 'tag': 'custom',
+ },
+ f'{self.builddir}/subdir/foo2.h': {
+ 'destination': '{datadir}/foo2.h',
+ 'tag': None,
+ },
+ },
+ 'data': {
+ f'{testdir}/bar-notag.txt': {
+ 'destination': '{datadir}/bar-notag.txt',
+ 'tag': None,
+ },
+ f'{testdir}/bar-devel.h': {
+ 'destination': '{includedir}/bar-devel.h',
+ 'tag': 'devel',
+ },
+ f'{testdir}/bar-custom.txt': {
+ 'destination': '{datadir}/bar-custom.txt',
+ 'tag': 'custom',
+ },
+ f'{testdir}/subdir/bar2-devel.h': {
+ 'destination': '{includedir}/bar2-devel.h',
+ 'tag': 'devel',
+ },
+ },
+ 'headers': {
+ f'{testdir}/foo1-devel.h': {
+ 'destination': '{includedir}/foo1-devel.h',
+ 'tag': 'devel',
+ },
+ f'{testdir}/subdir/foo3-devel.h': {
+ 'destination': '{includedir}/foo3-devel.h',
+ 'tag': 'devel',
+ },
+ },
+ 'install_subdirs': {
+ f'{testdir}/custom_files': {
+ 'destination': '{datadir}/custom_files',
+ 'tag': 'custom'
+ }
+ }
+ }
+
+ 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, '101 rlib linkage')
+ gen_file = os.path.join(testdir, 'lib.rs')
+ with open(gen_file, 'w') 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') 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)
+
+ def test_custom_target_name(self):
+ testdir = os.path.join(self.unit_test_dir, '99 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, '106 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)
+ os.symlink(subproject, symlinked_subproject)
+ 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, '108 configure same noop')
+ self.init(testdir, extra_args=['-Dopt=val'])
+
+ filename = os.path.join(self.privatedir, 'coredata.dat')
+ oldmtime = os.path.getmtime(filename)
+ self.setconf(["-Dopt=val"])
+ newmtime = os.path.getmtime(filename)
+ self.assertEqual(oldmtime, newmtime)
+
+ def test_scripts_loaded_modules(self):
+ '''
+ Simulate a wrapped command, as done for custom_target() that capture
+ output. The script will print all python modules loaded and we verify
+ that it contains only an acceptable subset. Loading too many modules
+ slows down the build when many custom targets get wrapped.
+ '''
+ es = ExecutableSerialisation(python_command + ['-c', 'exit(0)'], env=EnvironmentVariables())
+ p = Path(self.builddir, 'exe.dat')
+ with p.open('wb') as f:
+ pickle.dump(es, f)
+ cmd = self.meson_command + ['--internal', 'test_loaded_modules', '--unpickle', str(p)]
+ p = subprocess.run(cmd, stdout=subprocess.PIPE)
+ all_modules = json.loads(p.stdout.splitlines()[0])
+ meson_modules = [m for m in all_modules if 'meson' in m]
+ expected_meson_modules = [
+ 'mesonbuild',
+ 'mesonbuild._pathlib',
+ 'mesonbuild.utils',
+ 'mesonbuild.utils.core',
+ 'mesonbuild.mesonmain',
+ 'mesonbuild.mlog',
+ 'mesonbuild.scripts',
+ 'mesonbuild.scripts.meson_exe',
+ 'mesonbuild.scripts.test_loaded_modules'
+ ]
+ self.assertEqual(sorted(expected_meson_modules), sorted(meson_modules))
diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py
new file mode 100644
index 0000000..d83ef3f
--- /dev/null
+++ b/unittests/baseplatformtests.py
@@ -0,0 +1,483 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from pathlib import PurePath
+from unittest import mock, TestCase, SkipTest
+import json
+import io
+import os
+import re
+import subprocess
+import sys
+import tempfile
+import typing as T
+
+import mesonbuild.mlog
+import mesonbuild.depfile
+import mesonbuild.dependencies.base
+import mesonbuild.dependencies.factory
+import mesonbuild.compilers
+import mesonbuild.envconfig
+import mesonbuild.environment
+import mesonbuild.coredata
+import mesonbuild.modules.gnome
+from mesonbuild.mesonlib import (
+ is_cygwin, join_args, windows_proof_rmtree, python_command
+)
+import mesonbuild.modules.pkgconfig
+
+
+from run_tests import (
+ Backend, ensure_backend_detects_changes, get_backend_commands,
+ get_builddir_target_args, get_meson_script, run_configure_inprocess,
+ run_mtest_inprocess
+)
+
+
+class BasePlatformTests(TestCase):
+ prefix = '/usr'
+ libdir = 'lib'
+
+ def setUp(self):
+ super().setUp()
+ self.maxDiff = None
+ src_root = str(PurePath(__file__).parents[1])
+ self.src_root = src_root
+ # Get the backend
+ self.backend = getattr(Backend, os.environ['MESON_UNIT_TEST_BACKEND'])
+ self.meson_args = ['--backend=' + self.backend.name]
+ self.meson_native_files = []
+ self.meson_cross_files = []
+ self.meson_command = python_command + [get_meson_script()]
+ self.setup_command = self.meson_command + ['setup'] + self.meson_args
+ self.mconf_command = self.meson_command + ['configure']
+ self.mintro_command = self.meson_command + ['introspect']
+ self.wrap_command = self.meson_command + ['wrap']
+ self.rewrite_command = self.meson_command + ['rewrite']
+ # Backend-specific build commands
+ self.build_command, self.clean_command, self.test_command, self.install_command, \
+ self.uninstall_command = get_backend_commands(self.backend)
+ # Test directories
+ self.common_test_dir = os.path.join(src_root, 'test cases/common')
+ self.rust_test_dir = os.path.join(src_root, 'test cases/rust')
+ self.vala_test_dir = os.path.join(src_root, 'test cases/vala')
+ self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks')
+ self.unit_test_dir = os.path.join(src_root, 'test cases/unit')
+ self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite')
+ self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike')
+ self.objc_test_dir = os.path.join(src_root, 'test cases/objc')
+ self.objcpp_test_dir = os.path.join(src_root, 'test cases/objcpp')
+
+ # Misc stuff
+ self.orig_env = os.environ.copy()
+ if self.backend is Backend.ninja:
+ self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do']
+ else:
+ # VS doesn't have a stable output when no changes are done
+ # XCode backend is untested with unit tests, help welcome!
+ self.no_rebuild_stdout = [f'UNKNOWN BACKEND {self.backend.name!r}']
+
+ self.builddirs = []
+ self.new_builddir()
+
+ def change_builddir(self, newdir):
+ self.builddir = newdir
+ self.privatedir = os.path.join(self.builddir, 'meson-private')
+ self.logdir = os.path.join(self.builddir, 'meson-logs')
+ self.installdir = os.path.join(self.builddir, 'install')
+ self.distdir = os.path.join(self.builddir, 'meson-dist')
+ self.mtest_command = self.meson_command + ['test', '-C', self.builddir]
+ self.builddirs.append(self.builddir)
+
+ def new_builddir(self):
+ # Keep builddirs inside the source tree so that virus scanners
+ # don't complain
+ newdir = tempfile.mkdtemp(dir=os.getcwd())
+ # In case the directory is inside a symlinked directory, find the real
+ # path otherwise we might not find the srcdir from inside the builddir.
+ newdir = os.path.realpath(newdir)
+ self.change_builddir(newdir)
+
+ def new_builddir_in_tempdir(self):
+ # Can't keep the builddir inside the source tree for the umask tests:
+ # https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523
+ # And we can't do this for all tests because it causes the path to be
+ # a short-path which breaks other tests:
+ # https://github.com/mesonbuild/meson/pull/9497
+ newdir = tempfile.mkdtemp()
+ # In case the directory is inside a symlinked directory, find the real
+ # path otherwise we might not find the srcdir from inside the builddir.
+ newdir = os.path.realpath(newdir)
+ self.change_builddir(newdir)
+
+ def _open_meson_log(self) -> io.TextIOWrapper:
+ log = os.path.join(self.logdir, 'meson-log.txt')
+ return open(log, encoding='utf-8')
+
+ def _get_meson_log(self) -> T.Optional[str]:
+ try:
+ with self._open_meson_log() as f:
+ return f.read()
+ except FileNotFoundError as e:
+ print(f"{e.filename!r} doesn't exist", file=sys.stderr)
+ return None
+
+ def _print_meson_log(self) -> None:
+ log = self._get_meson_log()
+ if log:
+ print(log)
+
+ def tearDown(self):
+ for path in self.builddirs:
+ try:
+ windows_proof_rmtree(path)
+ except FileNotFoundError:
+ pass
+ os.environ.clear()
+ os.environ.update(self.orig_env)
+ super().tearDown()
+
+ def _run(self, command, *, workdir=None, override_envvars: T.Optional[T.Mapping[str, str]] = None, stderr=True):
+ '''
+ Run a command while printing the stdout and stderr to stdout,
+ and also return a copy of it
+ '''
+ # If this call hangs CI will just abort. It is very hard to distinguish
+ # between CI issue and test bug in that case. Set timeout and fail loud
+ # instead.
+ if override_envvars is None:
+ env = None
+ else:
+ env = os.environ.copy()
+ env.update(override_envvars)
+
+ p = subprocess.run(command, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT if stderr else subprocess.PIPE,
+ env=env,
+ encoding='utf-8',
+ text=True, cwd=workdir, timeout=60 * 5)
+ print('$', join_args(command))
+ print('stdout:')
+ print(p.stdout)
+ if not stderr:
+ print('stderr:')
+ print(p.stderr)
+ if p.returncode != 0:
+ if 'MESON_SKIP_TEST' in p.stdout:
+ raise SkipTest('Project requested skipping.')
+ raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout)
+ return p.stdout
+
+ def init(self, srcdir, *,
+ extra_args=None,
+ default_args=True,
+ inprocess=False,
+ override_envvars: T.Optional[T.Mapping[str, str]] = None,
+ workdir=None,
+ allow_fail: bool = False) -> str:
+ """Call `meson setup`
+
+ :param allow_fail: If set to true initialization is allowed to fail.
+ When it does the log will be returned instead of stdout.
+ :return: the value of stdout on success, or the meson log on failure
+ when :param allow_fail: is true
+ """
+ self.assertPathExists(srcdir)
+ if extra_args is None:
+ extra_args = []
+ if not isinstance(extra_args, list):
+ extra_args = [extra_args]
+ args = [srcdir, self.builddir]
+ if default_args:
+ args += ['--prefix', self.prefix]
+ if self.libdir:
+ args += ['--libdir', self.libdir]
+ for f in self.meson_native_files:
+ args += ['--native-file', f]
+ for f in self.meson_cross_files:
+ args += ['--cross-file', f]
+ self.privatedir = os.path.join(self.builddir, 'meson-private')
+ if inprocess:
+ try:
+ returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args, override_envvars)
+ except Exception as e:
+ if not allow_fail:
+ self._print_meson_log()
+ raise
+ out = self._get_meson_log() # Best we can do here
+ err = '' # type checkers can't figure out that on this path returncode will always be 0
+ returncode = 0
+ finally:
+ # Close log file to satisfy Windows file locking
+ mesonbuild.mlog.shutdown()
+ mesonbuild.mlog.log_dir = None
+ mesonbuild.mlog.log_file = None
+
+ if 'MESON_SKIP_TEST' in out:
+ raise SkipTest('Project requested skipping.')
+ if returncode != 0:
+ self._print_meson_log()
+ print('Stdout:\n')
+ print(out)
+ print('Stderr:\n')
+ print(err)
+ if not allow_fail:
+ raise RuntimeError('Configure failed')
+ else:
+ try:
+ out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir)
+ except SkipTest:
+ raise SkipTest('Project requested skipping: ' + srcdir)
+ except Exception:
+ if not allow_fail:
+ self._print_meson_log()
+ raise
+ out = self._get_meson_log() # best we can do here
+ return out
+
+ def build(self, target=None, *, extra_args=None, override_envvars=None, stderr=True):
+ if extra_args is None:
+ extra_args = []
+ # Add arguments for building the target (if specified),
+ # and using the build dir (if required, with VS)
+ args = get_builddir_target_args(self.backend, self.builddir, target)
+ return self._run(self.build_command + args + extra_args, workdir=self.builddir, override_envvars=override_envvars, stderr=stderr)
+
+ def clean(self, *, override_envvars=None):
+ dir_args = get_builddir_target_args(self.backend, self.builddir, None)
+ self._run(self.clean_command + dir_args, workdir=self.builddir, override_envvars=override_envvars)
+
+ def run_tests(self, *, inprocess=False, override_envvars=None):
+ if not inprocess:
+ return self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars)
+ else:
+ with mock.patch.dict(os.environ, override_envvars):
+ return run_mtest_inprocess(['-C', self.builddir])[1]
+
+ def install(self, *, use_destdir=True, override_envvars=None):
+ if self.backend is not Backend.ninja:
+ raise SkipTest(f'{self.backend.name!r} backend can\'t install files')
+ if use_destdir:
+ destdir = {'DESTDIR': self.installdir}
+ if override_envvars is None:
+ override_envvars = destdir
+ else:
+ override_envvars.update(destdir)
+ return self._run(self.install_command, workdir=self.builddir, override_envvars=override_envvars)
+
+ def uninstall(self, *, override_envvars=None):
+ self._run(self.uninstall_command, workdir=self.builddir, override_envvars=override_envvars)
+
+ def run_target(self, target, *, override_envvars=None):
+ '''
+ Run a Ninja target while printing the stdout and stderr to stdout,
+ and also return a copy of it
+ '''
+ return self.build(target=target, override_envvars=override_envvars)
+
+ def setconf(self, arg, will_build=True):
+ if not isinstance(arg, list):
+ arg = [arg]
+ if will_build:
+ ensure_backend_detects_changes(self.backend)
+ self._run(self.mconf_command + arg + [self.builddir])
+
+ def wipe(self):
+ windows_proof_rmtree(self.builddir)
+
+ def utime(self, f):
+ ensure_backend_detects_changes(self.backend)
+ os.utime(f)
+
+ def get_compdb(self):
+ if self.backend is not Backend.ninja:
+ raise SkipTest(f'Compiler db not available with {self.backend.name} backend')
+ try:
+ with open(os.path.join(self.builddir, 'compile_commands.json'), encoding='utf-8') as ifile:
+ contents = json.load(ifile)
+ except FileNotFoundError:
+ raise SkipTest('Compiler db not found')
+ # If Ninja is using .rsp files, generate them, read their contents, and
+ # replace it as the command for all compile commands in the parsed json.
+ if len(contents) > 0 and contents[0]['command'].endswith('.rsp'):
+ # Pretend to build so that the rsp files are generated
+ self.build(extra_args=['-d', 'keeprsp', '-n'])
+ for each in contents:
+ # Extract the actual command from the rsp file
+ compiler, rsp = each['command'].split(' @')
+ rsp = os.path.join(self.builddir, rsp)
+ # Replace the command with its contents
+ with open(rsp, encoding='utf-8') as f:
+ each['command'] = compiler + ' ' + f.read()
+ return contents
+
+ def get_meson_log_raw(self):
+ with self._open_meson_log() as f:
+ return f.read()
+
+ def get_meson_log(self):
+ with self._open_meson_log() as f:
+ return f.readlines()
+
+ def get_meson_log_compiler_checks(self):
+ '''
+ Fetch a list command-lines run by meson for compiler checks.
+ Each command-line is returned as a list of arguments.
+ '''
+ prefix = 'Command line:'
+ with self._open_meson_log() as log:
+ cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)]
+ return cmds
+
+ def get_meson_log_sanitychecks(self):
+ '''
+ Same as above, but for the sanity checks that were run
+ '''
+ prefix = 'Sanity check compiler command line:'
+ with self._open_meson_log() as log:
+ cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)]
+ return cmds
+
+ def introspect(self, args):
+ if isinstance(args, str):
+ args = [args]
+ out = subprocess.check_output(self.mintro_command + args + [self.builddir],
+ universal_newlines=True)
+ return json.loads(out)
+
+ def introspect_directory(self, directory, args):
+ if isinstance(args, str):
+ args = [args]
+ out = subprocess.check_output(self.mintro_command + args + [directory],
+ universal_newlines=True)
+ try:
+ obj = json.loads(out)
+ except Exception as e:
+ print(out)
+ raise e
+ return obj
+
+ def assertPathEqual(self, path1, path2):
+ '''
+ Handles a lot of platform-specific quirks related to paths such as
+ separator, case-sensitivity, etc.
+ '''
+ self.assertEqual(PurePath(path1), PurePath(path2))
+
+ def assertPathListEqual(self, pathlist1, pathlist2):
+ self.assertEqual(len(pathlist1), len(pathlist2))
+ worklist = list(zip(pathlist1, pathlist2))
+ for i in worklist:
+ if i[0] is None:
+ self.assertEqual(i[0], i[1])
+ else:
+ self.assertPathEqual(i[0], i[1])
+
+ def assertPathBasenameEqual(self, path, basename):
+ msg = f'{path!r} does not end with {basename!r}'
+ # We cannot use os.path.basename because it returns '' when the path
+ # ends with '/' for some silly reason. This is not how the UNIX utility
+ # `basename` works.
+ path_basename = PurePath(path).parts[-1]
+ self.assertEqual(PurePath(path_basename), PurePath(basename), msg)
+
+ def assertReconfiguredBuildIsNoop(self):
+ 'Assert that we reconfigured and then there was nothing to do'
+ ret = self.build(stderr=False)
+ self.assertIn('The Meson build system', ret)
+ if self.backend is Backend.ninja:
+ for line in ret.split('\n'):
+ if line in self.no_rebuild_stdout:
+ break
+ else:
+ raise AssertionError('build was reconfigured, but was not no-op')
+ elif self.backend is Backend.vs:
+ # Ensure that some target said that no rebuild was done
+ # XXX: Note CustomBuild did indeed rebuild, because of the regen checker!
+ self.assertIn('ClCompile:\n All outputs are up-to-date.', ret)
+ self.assertIn('Link:\n All outputs are up-to-date.', ret)
+ # Ensure that no targets were built
+ self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE))
+ self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE))
+ elif self.backend is Backend.xcode:
+ raise SkipTest('Please help us fix this test on the xcode backend')
+ else:
+ raise RuntimeError(f'Invalid backend: {self.backend.name!r}')
+
+ def assertBuildIsNoop(self):
+ ret = self.build(stderr=False)
+ if self.backend is Backend.ninja:
+ self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout)
+ elif self.backend is Backend.vs:
+ # Ensure that some target of each type said that no rebuild was done
+ # We always have at least one CustomBuild target for the regen checker
+ self.assertIn('CustomBuild:\n All outputs are up-to-date.', ret)
+ self.assertIn('ClCompile:\n All outputs are up-to-date.', ret)
+ self.assertIn('Link:\n All outputs are up-to-date.', ret)
+ # Ensure that no targets were built
+ self.assertNotRegex(ret, re.compile('CustomBuild:\n [^\n]*cl', flags=re.IGNORECASE))
+ self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE))
+ self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE))
+ elif self.backend is Backend.xcode:
+ raise SkipTest('Please help us fix this test on the xcode backend')
+ else:
+ raise RuntimeError(f'Invalid backend: {self.backend.name!r}')
+
+ def assertRebuiltTarget(self, target):
+ ret = self.build()
+ if self.backend is Backend.ninja:
+ self.assertIn(f'Linking target {target}', ret)
+ elif self.backend is Backend.vs:
+ # Ensure that this target was rebuilt
+ linkre = re.compile('Link:\n [^\n]*link[^\n]*' + target, flags=re.IGNORECASE)
+ self.assertRegex(ret, linkre)
+ elif self.backend is Backend.xcode:
+ raise SkipTest('Please help us fix this test on the xcode backend')
+ else:
+ raise RuntimeError(f'Invalid backend: {self.backend.name!r}')
+
+ @staticmethod
+ def get_target_from_filename(filename):
+ base = os.path.splitext(filename)[0]
+ if base.startswith(('lib', 'cyg')):
+ return base[3:]
+ return base
+
+ def assertBuildRelinkedOnlyTarget(self, target):
+ ret = self.build()
+ if self.backend is Backend.ninja:
+ linked_targets = []
+ for line in ret.split('\n'):
+ if 'Linking target' in line:
+ fname = line.rsplit('target ')[-1]
+ linked_targets.append(self.get_target_from_filename(fname))
+ self.assertEqual(linked_targets, [target])
+ elif self.backend is Backend.vs:
+ # Ensure that this target was rebuilt
+ linkre = re.compile(r'Link:\n [^\n]*link.exe[^\n]*/OUT:".\\([^"]*)"', flags=re.IGNORECASE)
+ matches = linkre.findall(ret)
+ self.assertEqual(len(matches), 1, msg=matches)
+ self.assertEqual(self.get_target_from_filename(matches[0]), target)
+ elif self.backend is Backend.xcode:
+ raise SkipTest('Please help us fix this test on the xcode backend')
+ else:
+ raise RuntimeError(f'Invalid backend: {self.backend.name!r}')
+
+ def assertPathExists(self, path):
+ m = f'Path {path!r} should exist'
+ self.assertTrue(os.path.exists(path), msg=m)
+
+ def assertPathDoesNotExist(self, path):
+ m = f'Path {path!r} should not exist'
+ self.assertFalse(os.path.exists(path), msg=m)
diff --git a/unittests/darwintests.py b/unittests/darwintests.py
new file mode 100644
index 0000000..6bf15aa
--- /dev/null
+++ b/unittests/darwintests.py
@@ -0,0 +1,150 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import re
+import os
+import unittest
+
+from mesonbuild.mesonlib import (
+ MachineChoice, is_osx
+)
+from mesonbuild.compilers import (
+ detect_c_compiler
+)
+
+
+from run_tests import (
+ get_fake_env
+)
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+@unittest.skipUnless(is_osx(), "requires Darwin")
+class DarwinTests(BasePlatformTests):
+ '''
+ Tests that should run on macOS
+ '''
+
+ def setUp(self):
+ super().setUp()
+ self.platform_test_dir = os.path.join(self.src_root, 'test cases/osx')
+
+ def test_apple_bitcode(self):
+ '''
+ Test that -fembed-bitcode is correctly added while compiling and
+ -bitcode_bundle is added while linking when b_bitcode is true and not
+ when it is false. This can't be an ordinary test case because we need
+ to inspect the compiler database.
+ '''
+ testdir = os.path.join(self.platform_test_dir, '7 bitcode')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.id != 'clang':
+ raise unittest.SkipTest('Not using Clang on OSX')
+ # Try with bitcode enabled
+ out = self.init(testdir, extra_args='-Db_bitcode=true')
+ # Warning was printed
+ self.assertRegex(out, 'WARNING:.*b_bitcode')
+ # Compiler options were added
+ for compdb in self.get_compdb():
+ if 'module' in compdb['file']:
+ self.assertNotIn('-fembed-bitcode', compdb['command'])
+ else:
+ self.assertIn('-fembed-bitcode', compdb['command'])
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ # Linker options were added
+ with open(build_ninja, encoding='utf-8') as f:
+ contents = f.read()
+ m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
+ self.assertIsNotNone(m, msg=contents)
+ # Try with bitcode disabled
+ self.setconf('-Db_bitcode=false')
+ # Regenerate build
+ self.build()
+ for compdb in self.get_compdb():
+ self.assertNotIn('-fembed-bitcode', compdb['command'])
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ with open(build_ninja, encoding='utf-8') as f:
+ contents = f.read()
+ m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
+ self.assertIsNone(m, msg=contents)
+
+ def test_apple_bitcode_modules(self):
+ '''
+ Same as above, just for shared_module()
+ '''
+ testdir = os.path.join(self.common_test_dir, '148 shared module resolving symbol in executable')
+ # Ensure that it builds even with bitcode enabled
+ self.init(testdir, extra_args='-Db_bitcode=true')
+ self.build()
+ self.run_tests()
+
+ def _get_darwin_versions(self, fname):
+ fname = os.path.join(self.builddir, fname)
+ out = subprocess.check_output(['otool', '-L', fname], universal_newlines=True)
+ m = re.match(r'.*version (.*), current version (.*)\)', out.split('\n')[1])
+ self.assertIsNotNone(m, msg=out)
+ return m.groups()
+
+ @skipIfNoPkgconfig
+ def test_library_versioning(self):
+ '''
+ Ensure that compatibility_version and current_version are set correctly
+ '''
+ testdir = os.path.join(self.platform_test_dir, '2 library versions')
+ self.init(testdir)
+ self.build()
+ targets = {}
+ for t in self.introspect('--targets'):
+ targets[t['name']] = t['filename'][0] if isinstance(t['filename'], list) else t['filename']
+ self.assertEqual(self._get_darwin_versions(targets['some']), ('7.0.0', '7.0.0'))
+ self.assertEqual(self._get_darwin_versions(targets['noversion']), ('0.0.0', '0.0.0'))
+ self.assertEqual(self._get_darwin_versions(targets['onlyversion']), ('1.0.0', '1.0.0'))
+ self.assertEqual(self._get_darwin_versions(targets['onlysoversion']), ('5.0.0', '5.0.0'))
+ self.assertEqual(self._get_darwin_versions(targets['intver']), ('2.0.0', '2.0.0'))
+ self.assertEqual(self._get_darwin_versions(targets['stringver']), ('2.3.0', '2.3.0'))
+ self.assertEqual(self._get_darwin_versions(targets['stringlistver']), ('2.4.0', '2.4.0'))
+ self.assertEqual(self._get_darwin_versions(targets['intstringver']), ('1111.0.0', '2.5.0'))
+ self.assertEqual(self._get_darwin_versions(targets['stringlistvers']), ('2.6.0', '2.6.1'))
+
+ def test_duplicate_rpath(self):
+ testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
+ # We purposely pass a duplicate rpath to Meson, in order
+ # to ascertain that Meson does not call install_name_tool
+ # with duplicate -delete_rpath arguments, which would
+ # lead to erroring out on installation
+ env = {"LDFLAGS": "-Wl,-rpath,/foo/bar"}
+ self.init(testdir, override_envvars=env)
+ self.build()
+ self.install()
+
+ def test_removing_unused_linker_args(self):
+ testdir = os.path.join(self.common_test_dir, '104 has arg')
+ env = {'CFLAGS': '-L/tmp -L /var/tmp -headerpad_max_install_names -Wl,-export_dynamic -framework Foundation'}
+ self.init(testdir, override_envvars=env)
+
+ def test_objc_versions(self):
+ # Objective-C always uses the C standard version.
+ # Objecttive-C++ always uses the C++ standard version.
+ # This is what most people seem to want and in addition
+ # it is the only setup supported by Xcode.
+ testdir = os.path.join(self.objc_test_dir, '1 simple')
+ self.init(testdir)
+ self.assertIn('-std=c99', self.get_compdb()[0]['command'])
+ self.wipe()
+ testdir = os.path.join(self.objcpp_test_dir, '1 simple')
+ self.init(testdir)
+ self.assertIn('-std=c++14', self.get_compdb()[0]['command'])
diff --git a/unittests/datatests.py b/unittests/datatests.py
new file mode 100644
index 0000000..9a46ec4
--- /dev/null
+++ b/unittests/datatests.py
@@ -0,0 +1,242 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import unittest
+from itertools import chain
+from pathlib import Path
+
+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.interpreter import Interpreter
+from mesonbuild.ast import AstInterpreter
+from mesonbuild.mesonlib import (
+ MachineChoice, OptionKey
+)
+from mesonbuild.compilers import (
+ detect_c_compiler, detect_cpp_compiler
+)
+import mesonbuild.modules.pkgconfig
+
+
+from run_tests import (
+ FakeBuild, get_fake_env
+)
+
+from .helpers import *
+
+@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
+class DataTests(unittest.TestCase):
+
+ def test_snippets(self):
+ hashcounter = re.compile('^ *(#)+')
+ snippet_dir = Path('docs/markdown/snippets')
+ self.assertTrue(snippet_dir.is_dir())
+ for f in snippet_dir.glob('*'):
+ self.assertTrue(f.is_file())
+ if f.parts[-1].endswith('~'):
+ continue
+ if f.suffix == '.md':
+ in_code_block = False
+ with f.open(encoding='utf-8') as snippet:
+ for line in snippet:
+ if line.startswith(' '):
+ continue
+ if line.startswith('```'):
+ in_code_block = not in_code_block
+ if in_code_block:
+ continue
+ m = re.match(hashcounter, line)
+ if m:
+ self.assertEqual(len(m.group(0)), 2, 'All headings in snippets must have two hash symbols: ' + f.name)
+ self.assertFalse(in_code_block, 'Unclosed code block.')
+ else:
+ if f.name != 'add_release_note_snippets_here':
+ self.assertTrue(False, 'A file without .md suffix in snippets dir: ' + f.name)
+
+ def test_compiler_options_documented(self):
+ '''
+ Test that C and C++ compiler options and base options are documented in
+ Builtin-Options.md. Only tests the default compiler for the current
+ platform on the CI.
+ '''
+ md = None
+ with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f:
+ md = f.read()
+ self.assertIsNotNone(md)
+ env = get_fake_env()
+ # FIXME: Support other compilers
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ cpp = detect_cpp_compiler(env, MachineChoice.HOST)
+ for comp in (cc, cpp):
+ for opt in comp.get_options():
+ self.assertIn(str(opt), md)
+ for opt in comp.base_options:
+ self.assertIn(str(opt), md)
+ self.assertNotIn('b_unknown', md)
+
+ @staticmethod
+ def _get_section_content(name, sections, md):
+ for section in sections:
+ if section and section.group(1) == name:
+ try:
+ next_section = next(sections)
+ end = next_section.start()
+ except StopIteration:
+ end = len(md)
+ # Extract the content for this section
+ return md[section.end():end]
+ raise RuntimeError(f'Could not find "{name}" heading')
+
+ def test_builtin_options_documented(self):
+ '''
+ Test that universal options and base options are documented in
+ Builtin-Options.md.
+ '''
+ from itertools import tee
+ md = None
+ with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f:
+ md = f.read()
+ self.assertIsNotNone(md)
+
+ found_entries = set()
+ sections = re.finditer(r"^## (.+)$", md, re.MULTILINE)
+ # Extract the content for this section
+ u_subcontents = []
+ content = self._get_section_content("Universal options", sections, md)
+ subsections = tee(re.finditer(r"^### (.+)$", content, re.MULTILINE))
+ u_subcontents.append(self._get_section_content("Directories", subsections[0], content))
+ u_subcontents.append(self._get_section_content("Core options", subsections[1], content))
+
+ mod_subcontents = []
+ content = self._get_section_content("Module options", sections, md)
+ subsections = tee(re.finditer(r"^### (.+)$", content, re.MULTILINE))
+ for idx, mod in enumerate(['Pkgconfig', 'Python']):
+ mod_subcontents.append(self._get_section_content(f'{mod} module', subsections[idx], content))
+ for subcontent in u_subcontents + mod_subcontents:
+ # Find the option names
+ options = set()
+ # Match either a table row or a table heading separator: | ------ |
+ rows = re.finditer(r"^\|(?: (\w+) .* | *-+ *)\|", subcontent, re.MULTILINE)
+ # Skip the header of the first table
+ next(rows)
+ # Skip the heading separator of the first table
+ next(rows)
+ for m in rows:
+ value = m.group(1)
+ # End when the `buildtype` table starts
+ if value is None:
+ break
+ options.add(value)
+ self.assertEqual(len(found_entries & options), 0)
+ found_entries |= options
+
+ self.assertEqual(found_entries, {
+ *(str(k.evolve(module=None)) for k in mesonbuild.coredata.BUILTIN_OPTIONS),
+ *(str(k.evolve(module=None)) for k in mesonbuild.coredata.BUILTIN_OPTIONS_PER_MACHINE),
+ })
+
+ # Check that `buildtype` table inside `Core options` matches how
+ # setting of builtin options behaves
+ #
+ # Find all tables inside this subsection
+ tables = re.finditer(r"^\| (\w+) .* \|\n\| *[-|\s]+ *\|$", u_subcontents[1], re.MULTILINE)
+ # Get the table we want using the header of the first column
+ table = self._get_section_content('buildtype', tables, u_subcontents[1])
+ # Get table row data
+ rows = re.finditer(r"^\|(?: (\w+)\s+\| (\w+)\s+\| (\w+) .* | *-+ *)\|", table, re.MULTILINE)
+ env = get_fake_env()
+ for m in rows:
+ buildtype, debug, opt = m.groups()
+ if debug == 'true':
+ debug = True
+ elif debug == 'false':
+ debug = False
+ else:
+ raise RuntimeError(f'Invalid debug value {debug!r} in row:\n{m.group()}')
+ env.coredata.set_option(OptionKey('buildtype'), buildtype)
+ self.assertEqual(env.coredata.options[OptionKey('buildtype')].value, buildtype)
+ self.assertEqual(env.coredata.options[OptionKey('optimization')].value, opt)
+ self.assertEqual(env.coredata.options[OptionKey('debug')].value, debug)
+
+ def test_cpu_families_documented(self):
+ with open("docs/markdown/Reference-tables.md", encoding='utf-8') as f:
+ md = f.read()
+ self.assertIsNotNone(md)
+
+ sections = re.finditer(r"^## (.+)$", md, re.MULTILINE)
+ content = self._get_section_content("CPU families", sections, md)
+ # Find the list entries
+ arches = [m.group(1) for m in re.finditer(r"^\| (\w+) +\|", content, re.MULTILINE)]
+ # Drop the header
+ arches = set(arches[1:])
+ self.assertEqual(arches, set(mesonbuild.environment.known_cpu_families))
+
+ def test_markdown_files_in_sitemap(self):
+ '''
+ Test that each markdown files in docs/markdown is referenced in sitemap.txt
+ '''
+ with open("docs/sitemap.txt", encoding='utf-8') as f:
+ md = f.read()
+ self.assertIsNotNone(md)
+ toc = list(m.group(1) for m in re.finditer(r"^\s*(\w.*)$", md, re.MULTILINE))
+ markdownfiles = [f.name for f in Path("docs/markdown").iterdir() if f.is_file() and f.suffix == '.md']
+ exceptions = ['_Sidebar.md']
+ for f in markdownfiles:
+ if f not in exceptions and not f.startswith('_include'):
+ self.assertIn(f, toc)
+
+ def test_modules_in_navbar(self):
+ '''
+ Test that each module is referenced in navbar_links.html
+ '''
+ with open("docs/theme/extra/templates/navbar_links.html", encoding='utf-8') as f:
+ html = f.read().lower()
+ self.assertIsNotNone(html)
+ for f in Path('mesonbuild/modules').glob('*.py'):
+ if f.name in {'modtest.py', 'qt.py', '__init__.py'}:
+ continue
+ name = f'{f.stem}-module.html'
+ name = name.replace('unstable_', '')
+ name = name.replace('python3', 'python-3')
+ name = name.replace('_', '-')
+ self.assertIn(name, html)
+
+ def test_vim_syntax_highlighting(self):
+ '''
+ Ensure that vim syntax highlighting files were updated for new
+ functions in the global namespace in build files.
+ '''
+ env = get_fake_env()
+ interp = Interpreter(FakeBuild(env), mock=True)
+ with open('data/syntax-highlighting/vim/syntax/meson.vim', encoding='utf-8') as f:
+ res = re.search(r'syn keyword mesonBuiltin(\s+\\\s\w+)+', f.read(), re.MULTILINE)
+ defined = set([a.strip() for a in res.group().split('\\')][1:])
+ self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys())))
+
+ def test_all_functions_defined_in_ast_interpreter(self):
+ '''
+ Ensure that the all functions defined in the Interpreter are also defined
+ in the AstInterpreter (and vice versa).
+ '''
+ env = get_fake_env()
+ interp = Interpreter(FakeBuild(env), mock=True)
+ astint = AstInterpreter('.', '', '')
+ self.assertEqual(set(interp.funcs.keys()), set(astint.funcs.keys()))
diff --git a/unittests/failuretests.py b/unittests/failuretests.py
new file mode 100644
index 0000000..54a6c58
--- /dev/null
+++ b/unittests/failuretests.py
@@ -0,0 +1,392 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import tempfile
+import os
+import shutil
+import unittest
+from contextlib import contextmanager
+
+from mesonbuild.mesonlib import (
+ MachineChoice, is_windows, is_osx, windows_proof_rmtree, windows_proof_rm
+)
+from mesonbuild.compilers import (
+ detect_objc_compiler, detect_objcpp_compiler
+)
+from mesonbuild.mesonlib import EnvironmentException, MesonException
+from mesonbuild.programs import ExternalProgram
+
+
+from run_tests import (
+ get_fake_env
+)
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+@contextmanager
+def no_pkgconfig():
+ '''
+ A context manager that overrides shutil.which and ExternalProgram to force
+ them to return None for pkg-config to simulate it not existing.
+ '''
+ old_which = shutil.which
+ old_search = ExternalProgram._search
+
+ def new_search(self, name, search_dir):
+ if name == 'pkg-config':
+ return [None]
+ return old_search(self, name, search_dir)
+
+ def new_which(cmd, *kwargs):
+ if cmd == 'pkg-config':
+ return None
+ return old_which(cmd, *kwargs)
+
+ shutil.which = new_which
+ ExternalProgram._search = new_search
+ try:
+ yield
+ finally:
+ shutil.which = old_which
+ ExternalProgram._search = old_search
+
+class FailureTests(BasePlatformTests):
+ '''
+ Tests that test failure conditions. Build files here should be dynamically
+ generated and static tests should go into `test cases/failing*`.
+ This is useful because there can be many ways in which a particular
+ function can fail, and creating failing tests for all of them is tedious
+ and slows down testing.
+ '''
+ dnf = "[Dd]ependency.*not found(:.*)?"
+ nopkg = '[Pp]kg-config.*not found'
+
+ def setUp(self):
+ super().setUp()
+ self.srcdir = os.path.realpath(tempfile.mkdtemp())
+ self.mbuild = os.path.join(self.srcdir, 'meson.build')
+ self.moptions = os.path.join(self.srcdir, 'meson_options.txt')
+
+ def tearDown(self):
+ super().tearDown()
+ windows_proof_rmtree(self.srcdir)
+
+ def assertMesonRaises(self, contents, match, *,
+ extra_args=None,
+ langs=None,
+ meson_version=None,
+ options=None,
+ override_envvars=None):
+ '''
+ Assert that running meson configure on the specified @contents raises
+ a error message matching regex @match.
+ '''
+ if langs is None:
+ langs = []
+ with open(self.mbuild, 'w', encoding='utf-8') as f:
+ f.write("project('failure test', 'c', 'cpp'")
+ if meson_version:
+ f.write(f", meson_version: '{meson_version}'")
+ f.write(")\n")
+ for lang in langs:
+ f.write(f"add_languages('{lang}', required : false)\n")
+ f.write(contents)
+ if options is not None:
+ with open(self.moptions, 'w', encoding='utf-8') as f:
+ f.write(options)
+ o = {'MESON_FORCE_BACKTRACE': '1'}
+ if override_envvars is None:
+ override_envvars = o
+ else:
+ override_envvars.update(o)
+ # Force tracebacks so we can detect them properly
+ with self.assertRaisesRegex(MesonException, match, msg=contents):
+ # Must run in-process or we'll get a generic CalledProcessError
+ self.init(self.srcdir, extra_args=extra_args,
+ inprocess=True,
+ override_envvars = override_envvars)
+
+ def obtainMesonOutput(self, contents, match, extra_args, langs, meson_version=None):
+ if langs is None:
+ langs = []
+ with open(self.mbuild, 'w', encoding='utf-8') as f:
+ f.write("project('output test', 'c', 'cpp'")
+ if meson_version:
+ f.write(f", meson_version: '{meson_version}'")
+ f.write(")\n")
+ for lang in langs:
+ f.write(f"add_languages('{lang}', required : false)\n")
+ f.write(contents)
+ # Run in-process for speed and consistency with assertMesonRaises
+ return self.init(self.srcdir, extra_args=extra_args, inprocess=True)
+
+ def assertMesonOutputs(self, contents, match, extra_args=None, langs=None, meson_version=None):
+ '''
+ Assert that running meson configure on the specified @contents outputs
+ something that matches regex @match.
+ '''
+ out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
+ self.assertRegex(out, match)
+
+ def assertMesonDoesNotOutput(self, contents, match, extra_args=None, langs=None, meson_version=None):
+ '''
+ Assert that running meson configure on the specified @contents does not output
+ something that matches regex @match.
+ '''
+ out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
+ self.assertNotRegex(out, match)
+
+ @skipIfNoPkgconfig
+ def test_dependency(self):
+ if subprocess.call(['pkg-config', '--exists', 'zlib']) != 0:
+ raise unittest.SkipTest('zlib not found with pkg-config')
+ a = (("dependency('zlib', method : 'fail')", "'fail' is invalid"),
+ ("dependency('zlib', static : '1')", "[Ss]tatic.*boolean"),
+ ("dependency('zlib', version : 1)", "Item must be a list or one of <class 'str'>"),
+ ("dependency('zlib', required : 1)", "[Rr]equired.*boolean"),
+ ("dependency('zlib', method : 1)", "[Mm]ethod.*string"),
+ ("dependency('zlibfail')", self.dnf),)
+ for contents, match in a:
+ self.assertMesonRaises(contents, match)
+
+ def test_apple_frameworks_dependency(self):
+ if not is_osx():
+ raise unittest.SkipTest('only run on macOS')
+ self.assertMesonRaises("dependency('appleframeworks')",
+ "requires at least one module")
+
+ def test_extraframework_dependency_method(self):
+ code = "dependency('metal', method : 'extraframework')"
+ if not is_osx():
+ self.assertMesonRaises(code, self.dnf)
+ else:
+ # metal framework is always available on macOS
+ self.assertMesonOutputs(code, '[Dd]ependency.*metal.*found.*YES')
+
+ def test_sdl2_notfound_dependency(self):
+ # Want to test failure, so skip if available
+ if shutil.which('sdl2-config'):
+ raise unittest.SkipTest('sdl2-config found')
+ self.assertMesonRaises("dependency('sdl2', method : 'sdlconfig')", self.dnf)
+ if shutil.which('pkg-config'):
+ self.assertMesonRaises("dependency('sdl2', method : 'pkg-config')", self.dnf)
+ with no_pkgconfig():
+ # Look for pkg-config, cache it, then
+ # Use cached pkg-config without erroring out, then
+ # Use cached pkg-config to error out
+ code = "dependency('foobarrr', method : 'pkg-config', required : false)\n" \
+ "dependency('foobarrr2', method : 'pkg-config', required : false)\n" \
+ "dependency('sdl2', method : 'pkg-config')"
+ self.assertMesonRaises(code, self.nopkg)
+
+ def test_gnustep_notfound_dependency(self):
+ # Want to test failure, so skip if available
+ if shutil.which('gnustep-config'):
+ raise unittest.SkipTest('gnustep-config found')
+ self.assertMesonRaises("dependency('gnustep')",
+ f"(requires a Objc compiler|{self.dnf})",
+ langs = ['objc'])
+
+ def test_wx_notfound_dependency(self):
+ # Want to test failure, so skip if available
+ if shutil.which('wx-config-3.0') or shutil.which('wx-config') or shutil.which('wx-config-gtk3'):
+ raise unittest.SkipTest('wx-config, wx-config-3.0 or wx-config-gtk3 found')
+ self.assertMesonRaises("dependency('wxwidgets')", self.dnf)
+ self.assertMesonOutputs("dependency('wxwidgets', required : false)",
+ "Run-time dependency .*WxWidgets.* found: .*NO.*")
+
+ def test_wx_dependency(self):
+ if not shutil.which('wx-config-3.0') and not shutil.which('wx-config') and not shutil.which('wx-config-gtk3'):
+ raise unittest.SkipTest('Neither wx-config, wx-config-3.0 nor wx-config-gtk3 found')
+ self.assertMesonRaises("dependency('wxwidgets', modules : 1)",
+ "module argument is not a string")
+
+ def test_llvm_dependency(self):
+ self.assertMesonRaises("dependency('llvm', modules : 'fail')",
+ f"(required.*fail|{self.dnf})")
+
+ def test_boost_notfound_dependency(self):
+ # Can be run even if Boost is found or not
+ self.assertMesonRaises("dependency('boost', modules : 1)",
+ "module.*not a string")
+ self.assertMesonRaises("dependency('boost', modules : 'fail')",
+ f"(fail.*not found|{self.dnf})")
+
+ def test_boost_BOOST_ROOT_dependency(self):
+ # Test BOOST_ROOT; can be run even if Boost is found or not
+ self.assertMesonRaises("dependency('boost')",
+ f"(boost_root.*absolute|{self.dnf})",
+ override_envvars = {'BOOST_ROOT': 'relative/path'})
+
+ def test_dependency_invalid_method(self):
+ code = '''zlib_dep = dependency('zlib', required : false)
+ zlib_dep.get_configtool_variable('foo')
+ '''
+ self.assertMesonRaises(code, ".* is not a config-tool dependency")
+ code = '''zlib_dep = dependency('zlib', required : false)
+ dep = declare_dependency(dependencies : zlib_dep)
+ dep.get_pkgconfig_variable('foo')
+ '''
+ self.assertMesonRaises(code, "Method.*pkgconfig.*is invalid.*internal")
+ code = '''zlib_dep = dependency('zlib', required : false)
+ dep = declare_dependency(dependencies : zlib_dep)
+ dep.get_configtool_variable('foo')
+ '''
+ self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal")
+
+ def test_objc_cpp_detection(self):
+ '''
+ Test that when we can't detect objc or objcpp, we fail gracefully.
+ '''
+ env = get_fake_env()
+ try:
+ detect_objc_compiler(env, MachineChoice.HOST)
+ detect_objcpp_compiler(env, MachineChoice.HOST)
+ except EnvironmentException:
+ code = "add_languages('objc')\nadd_languages('objcpp')"
+ self.assertMesonRaises(code, "Unknown compiler")
+ return
+ raise unittest.SkipTest("objc and objcpp found, can't test detection failure")
+
+ def test_subproject_variables(self):
+ '''
+ Test that:
+ 1. The correct message is outputted when a not-required dep is not
+ found and the fallback subproject is also not found.
+ 2. A not-required fallback dependency is not found because the
+ subproject failed to parse.
+ 3. A not-found not-required dep with a fallback subproject outputs the
+ correct message when the fallback subproject is found but the
+ variable inside it is not.
+ 4. A fallback dependency is found from the subproject parsed in (3)
+ 5. A wrap file from a subproject is used but fails because it does not
+ contain required keys.
+ '''
+ tdir = os.path.join(self.unit_test_dir, '20 subproj dep variables')
+ stray_file = os.path.join(tdir, 'subprojects/subsubproject.wrap')
+ if os.path.exists(stray_file):
+ windows_proof_rm(stray_file)
+ out = self.init(tdir, inprocess=True)
+ self.assertRegex(out, r"Neither a subproject directory nor a .*nosubproj.wrap.* file was found")
+ self.assertRegex(out, r'Function does not take positional arguments.')
+ self.assertRegex(out, r'Dependency .*somenotfounddep.* from subproject .*subprojects/somesubproj.* found: .*NO.*')
+ self.assertRegex(out, r'Dependency .*zlibproxy.* from subproject .*subprojects.*somesubproj.* found: .*YES.*')
+ self.assertRegex(out, r'Missing key .*source_filename.* in subsubproject.wrap')
+ windows_proof_rm(stray_file)
+
+ def test_exception_exit_status(self):
+ '''
+ Test exit status on python exception
+ '''
+ tdir = os.path.join(self.unit_test_dir, '21 exit status')
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ self.init(tdir, inprocess=False, override_envvars = {'MESON_UNIT_TEST': '1', 'MESON_FORCE_BACKTRACE': ''})
+ self.assertEqual(cm.exception.returncode, 2)
+ self.wipe()
+
+ def test_dict_requires_key_value_pairs(self):
+ self.assertMesonRaises("dict = {3, 'foo': 'bar'}",
+ 'Only key:value pairs are valid in dict construction.')
+ self.assertMesonRaises("{'foo': 'bar', 3}",
+ 'Only key:value pairs are valid in dict construction.')
+
+ def test_dict_forbids_duplicate_keys(self):
+ self.assertMesonRaises("dict = {'a': 41, 'a': 42}",
+ 'Duplicate dictionary key: a.*')
+
+ def test_dict_forbids_integer_key(self):
+ self.assertMesonRaises("dict = {3: 'foo'}",
+ 'Key must be a string.*')
+
+ def test_using_too_recent_feature(self):
+ # Here we use a dict, which was introduced in 0.47.0
+ self.assertMesonOutputs("dict = {}",
+ ".*WARNING.*Project targets.*but.*",
+ meson_version='>= 0.46.0')
+
+ def test_using_recent_feature(self):
+ # Same as above, except the meson version is now appropriate
+ self.assertMesonDoesNotOutput("dict = {}",
+ ".*WARNING.*Project targets.*but.*",
+ meson_version='>= 0.47')
+
+ def test_using_too_recent_feature_dependency(self):
+ self.assertMesonOutputs("dependency('pcap', required: false)",
+ ".*WARNING.*Project targets.*but.*",
+ meson_version='>= 0.41.0')
+
+ def test_vcs_tag_featurenew_build_always_stale(self):
+ 'https://github.com/mesonbuild/meson/issues/3904'
+ vcs_tag = '''version_data = configuration_data()
+ version_data.set('PROJVER', '@VCS_TAG@')
+ vf = configure_file(output : 'version.h.in', configuration: version_data)
+ f = vcs_tag(input : vf, output : 'version.h')
+ '''
+ msg = '.*WARNING:.*feature.*build_always_stale.*custom_target.*'
+ self.assertMesonDoesNotOutput(vcs_tag, msg, meson_version='>=0.43')
+
+ def test_missing_subproject_not_required_and_required(self):
+ self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
+ "sub2 = subproject('not-found-subproject', required: true)",
+ """.*Subproject "subprojects/not-found-subproject" required but not found.*""")
+
+ def test_get_variable_on_not_found_project(self):
+ self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
+ "sub1.get_variable('naaa')",
+ """Subproject "subprojects/not-found-subproject" disabled can't get_variable on it.""")
+
+ def test_version_checked_before_parsing_options(self):
+ '''
+ https://github.com/mesonbuild/meson/issues/5281
+ '''
+ options = "option('some-option', type: 'foo', value: '')"
+ match = 'Meson version is.*but project requires >=2000'
+ self.assertMesonRaises("", match, meson_version='>=2000', options=options)
+
+ def test_assert_default_message(self):
+ self.assertMesonRaises("k1 = 'a'\n" +
+ "assert({\n" +
+ " k1: 1,\n" +
+ "}['a'] == 2)\n",
+ r"Assert failed: {k1 : 1}\['a'\] == 2")
+
+ def test_wrap_nofallback(self):
+ self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])",
+ r"Dependency 'notfound' is required but not found.",
+ extra_args=['--wrap-mode=nofallback'])
+
+ def test_message(self):
+ self.assertMesonOutputs("message('Array:', ['a', 'b'])",
+ r"Message:.* Array: \['a', 'b'\]")
+
+ def test_warning(self):
+ self.assertMesonOutputs("warning('Array:', ['a', 'b'])",
+ r"WARNING:.* Array: \['a', 'b'\]")
+
+ def test_override_dependency_twice(self):
+ self.assertMesonRaises("meson.override_dependency('foo', declare_dependency())\n" +
+ "meson.override_dependency('foo', declare_dependency())",
+ """Tried to override dependency 'foo' which has already been resolved or overridden""")
+
+ @unittest.skipIf(is_windows(), 'zlib is not available on Windows')
+ def test_override_resolved_dependency(self):
+ self.assertMesonRaises("dependency('zlib')\n" +
+ "meson.override_dependency('zlib', declare_dependency())",
+ """Tried to override dependency 'zlib' which has already been resolved or overridden""")
+
+ def test_error_func(self):
+ self.assertMesonRaises("error('a', 'b', ['c', ['d', {'e': 'f'}]], 'g')",
+ r"Problem encountered: a b \['c', \['d', {'e' : 'f'}\]\] g")
diff --git a/unittests/helpers.py b/unittests/helpers.py
new file mode 100644
index 0000000..d3d1560
--- /dev/null
+++ b/unittests/helpers.py
@@ -0,0 +1,206 @@
+import subprocess
+import os
+import shutil
+import unittest
+import functools
+import re
+import typing as T
+import zipfile
+from pathlib import Path
+from contextlib import contextmanager
+
+from mesonbuild.compilers import detect_c_compiler, compiler_from_language
+from mesonbuild.mesonlib import (
+ MachineChoice, is_osx, is_cygwin, EnvironmentException, OptionKey, MachineChoice,
+ OrderedSet
+)
+from run_tests import get_fake_env
+
+
+def is_ci():
+ if os.environ.get('MESON_CI_JOBNAME') not in {None, 'thirdparty'}:
+ return True
+ return False
+
+def skip_if_not_base_option(feature):
+ """Skip tests if The compiler does not support a given base option.
+
+ for example, ICC doesn't currently support b_sanitize.
+ """
+ def actual(f):
+ @functools.wraps(f)
+ def wrapped(*args, **kwargs):
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ key = OptionKey(feature)
+ if key not in cc.base_options:
+ raise unittest.SkipTest(
+ f'{feature} not available with {cc.id}')
+ return f(*args, **kwargs)
+ return wrapped
+ return actual
+
+def skipIfNoPkgconfig(f):
+ '''
+ Skip this test if no pkg-config is found, unless we're on CI.
+ This allows users to run our test suite without having
+ pkg-config installed on, f.ex., macOS, while ensuring that our CI does not
+ silently skip the test because of misconfiguration.
+
+ Note: Yes, we provide pkg-config even while running Windows CI
+ '''
+ @functools.wraps(f)
+ def wrapped(*args, **kwargs):
+ if not is_ci() and shutil.which('pkg-config') is None:
+ raise unittest.SkipTest('pkg-config not found')
+ return f(*args, **kwargs)
+ return wrapped
+
+def skipIfNoPkgconfigDep(depname):
+ '''
+ Skip this test if the given pkg-config dep is not found, unless we're on CI.
+ '''
+ def wrapper(func):
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ if not is_ci() and shutil.which('pkg-config') is None:
+ raise unittest.SkipTest('pkg-config not found')
+ if not is_ci() and subprocess.call(['pkg-config', '--exists', depname]) != 0:
+ raise unittest.SkipTest(f'pkg-config dependency {depname} not found.')
+ return func(*args, **kwargs)
+ return wrapped
+ return wrapper
+
+def skip_if_no_cmake(f):
+ '''
+ Skip this test if no cmake is found, unless we're on CI.
+ This allows users to run our test suite without having
+ cmake installed on, f.ex., macOS, while ensuring that our CI does not
+ silently skip the test because of misconfiguration.
+ '''
+ @functools.wraps(f)
+ def wrapped(*args, **kwargs):
+ if not is_ci() and shutil.which('cmake') is None:
+ raise unittest.SkipTest('cmake not found')
+ return f(*args, **kwargs)
+ return wrapped
+
+def skip_if_not_language(lang: str):
+ def wrapper(func):
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ try:
+ compiler_from_language(get_fake_env(), lang, MachineChoice.HOST)
+ except EnvironmentException:
+ raise unittest.SkipTest(f'No {lang} compiler found.')
+ return func(*args, **kwargs)
+ return wrapped
+ return wrapper
+
+def skip_if_env_set(key):
+ '''
+ Skip a test if a particular env is set, except when running under CI
+ '''
+ def wrapper(func):
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ old = None
+ if key in os.environ:
+ if not is_ci():
+ raise unittest.SkipTest(f'Env var {key!r} set, skipping')
+ old = os.environ.pop(key)
+ try:
+ return func(*args, **kwargs)
+ finally:
+ if old is not None:
+ os.environ[key] = old
+ return wrapped
+ return wrapper
+
+def skipIfNoExecutable(exename):
+ '''
+ Skip this test if the given executable is not found.
+ '''
+ def wrapper(func):
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ if shutil.which(exename) is None:
+ raise unittest.SkipTest(exename + ' not found')
+ return func(*args, **kwargs)
+ return wrapped
+ return wrapper
+
+def is_tarball():
+ if not os.path.isdir('docs'):
+ return True
+ return False
+
+@contextmanager
+def chdir(path: str):
+ curdir = os.getcwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(curdir)
+
+def get_dynamic_section_entry(fname: str, entry: str) -> T.Optional[str]:
+ if is_cygwin() or is_osx():
+ raise unittest.SkipTest('Test only applicable to ELF platforms')
+
+ try:
+ raw_out = subprocess.check_output(['readelf', '-d', fname],
+ universal_newlines=True)
+ except FileNotFoundError:
+ # FIXME: Try using depfixer.py:Elf() as a fallback
+ raise unittest.SkipTest('readelf not found')
+ pattern = re.compile(entry + r': \[(.*?)\]')
+ for line in raw_out.split('\n'):
+ m = pattern.search(line)
+ if m is not None:
+ return str(m.group(1))
+ return None # The file did not contain the specified entry.
+
+def get_soname(fname: str) -> T.Optional[str]:
+ return get_dynamic_section_entry(fname, 'soname')
+
+def get_rpath(fname: str) -> T.Optional[str]:
+ raw = get_dynamic_section_entry(fname, r'(?:rpath|runpath)')
+ # Get both '' and None here
+ if not raw:
+ return None
+ # nix/nixos adds a bunch of stuff to the rpath out of necessity that we
+ # don't check for, so clear those
+ final = ':'.join([e for e in raw.split(':') if not e.startswith('/nix')])
+ # If we didn't end up anything but nix paths, return None here
+ if not final:
+ return None
+ return final
+
+def get_classpath(fname: str) -> T.Optional[str]:
+ with zipfile.ZipFile(fname) as zip:
+ with zip.open('META-INF/MANIFEST.MF') as member:
+ contents = member.read().decode().strip()
+ lines = []
+ for line in contents.splitlines():
+ if line.startswith(' '):
+ # continuation line
+ lines[-1] += line[1:]
+ else:
+ lines.append(line)
+ manifest = {
+ k.lower(): v.strip() for k, v in [l.split(':', 1) for l in lines]
+ }
+ return manifest.get('class-path')
+
+def get_path_without_cmd(cmd: str, path: str) -> str:
+ pathsep = os.pathsep
+ paths = OrderedSet([Path(p).resolve() for p in path.split(pathsep)])
+ while True:
+ full_path = shutil.which(cmd, path=path)
+ if full_path is None:
+ break
+ dirname = Path(full_path).resolve().parent
+ paths.discard(dirname)
+ path = pathsep.join([str(p) for p in paths])
+ return path
diff --git a/unittests/internaltests.py b/unittests/internaltests.py
new file mode 100644
index 0000000..79a3217
--- /dev/null
+++ b/unittests/internaltests.py
@@ -0,0 +1,1648 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from configparser import ConfigParser
+from pathlib import Path
+from unittest import mock
+import contextlib
+import io
+import json
+import operator
+import os
+import pickle
+import stat
+import subprocess
+import tempfile
+import typing as T
+import unittest
+
+import mesonbuild.mlog
+import mesonbuild.depfile
+import mesonbuild.dependencies.base
+import mesonbuild.dependencies.factory
+import mesonbuild.envconfig
+import mesonbuild.environment
+import mesonbuild.modules.gnome
+from mesonbuild import coredata
+from mesonbuild.compilers.c import ClangCCompiler, GnuCCompiler
+from mesonbuild.compilers.cpp import VisualStudioCPPCompiler
+from mesonbuild.compilers.d import DmdDCompiler
+from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder
+from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo
+from mesonbuild.mesonlib import (
+ LibType, MachineChoice, PerMachine, Version, is_windows, is_osx,
+ is_cygwin, is_openbsd, search_version, MesonException, OptionKey,
+ OptionType
+)
+from mesonbuild.interpreter.type_checking import in_set_validator, NoneType
+from mesonbuild.dependencies import PkgConfigDependency
+from mesonbuild.programs import ExternalProgram
+import mesonbuild.modules.pkgconfig
+
+
+from run_tests import (
+ FakeCompilerOptions, get_fake_env, get_fake_options
+)
+
+from .helpers import *
+
+class InternalTests(unittest.TestCase):
+
+ def test_version_number(self):
+ self.assertEqual(search_version('foobar 1.2.3'), '1.2.3')
+ self.assertEqual(search_version('1.2.3'), '1.2.3')
+ self.assertEqual(search_version('foobar 2016.10.28 1.2.3'), '1.2.3')
+ self.assertEqual(search_version('2016.10.28 1.2.3'), '1.2.3')
+ self.assertEqual(search_version('foobar 2016.10.128'), '2016.10.128')
+ self.assertEqual(search_version('2016.10.128'), '2016.10.128')
+ self.assertEqual(search_version('2016.10'), '2016.10')
+ self.assertEqual(search_version('2016.10 1.2.3'), '1.2.3')
+ self.assertEqual(search_version('oops v1.2.3'), '1.2.3')
+ self.assertEqual(search_version('2016.oops 1.2.3'), '1.2.3')
+ self.assertEqual(search_version('2016.x'), 'unknown version')
+ self.assertEqual(search_version(r'something version is \033[32;2m1.2.0\033[0m.'), '1.2.0')
+
+ # Literal output of mvn
+ self.assertEqual(search_version(r'''\
+ \033[1mApache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)\033[0m
+ Maven home: /nix/store/g84a9wnid2h1d3z2wfydy16dky73wh7i-apache-maven-3.8.1/maven
+ Java version: 11.0.10, vendor: Oracle Corporation, runtime: /nix/store/afsnl4ahmm9svvl7s1a0cj41vw4nkmz4-openjdk-11.0.10+9/lib/openjdk
+ Default locale: en_US, platform encoding: UTF-8
+ OS name: "linux", version: "5.12.17", arch: "amd64", family: "unix"'''),
+ '3.8.1')
+
+ def test_mode_symbolic_to_bits(self):
+ modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
+ self.assertEqual(modefunc('---------'), 0)
+ self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
+ self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
+ self.assertEqual(modefunc('------r--'), stat.S_IROTH)
+ self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
+ self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
+ self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
+ self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
+ self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
+ self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
+ self.assertEqual(modefunc('--S------'), stat.S_ISUID)
+ self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
+ self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
+ self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
+ self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
+ self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
+ self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
+ self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
+ self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
+ # We could keep listing combinations exhaustively but that seems
+ # tedious and pointless. Just test a few more.
+ self.assertEqual(modefunc('rwxr-xr-x'),
+ stat.S_IRWXU |
+ stat.S_IRGRP | stat.S_IXGRP |
+ stat.S_IROTH | stat.S_IXOTH)
+ self.assertEqual(modefunc('rw-r--r--'),
+ stat.S_IRUSR | stat.S_IWUSR |
+ stat.S_IRGRP |
+ stat.S_IROTH)
+ self.assertEqual(modefunc('rwsr-x---'),
+ stat.S_IRWXU | stat.S_ISUID |
+ stat.S_IRGRP | stat.S_IXGRP)
+
+ def test_compiler_args_class_none_flush(self):
+ cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, False, mock.Mock())
+ a = cc.compiler_args(['-I.'])
+ #first we are checking if the tree construction deduplicates the correct -I argument
+ a += ['-I..']
+ a += ['-I./tests/']
+ a += ['-I./tests2/']
+ #think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes:
+ # assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.'])
+ a += ['-I.']
+ a += ['-I.', '-I./tests/']
+ self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..'])
+
+ #then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one
+ a += ['-I.', '-I./tests2/']
+ self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..'])
+
+ def test_compiler_args_class_d(self):
+ d = DmdDCompiler([], 'fake', MachineChoice.HOST, 'info', 'arch')
+ # check include order is kept when deduplicating
+ a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird'])
+ a += ['-Ifirst']
+ self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird'])
+
+ def test_compiler_args_class_clike(self):
+ cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, False, mock.Mock())
+ # Test that empty initialization works
+ a = cc.compiler_args()
+ self.assertEqual(a, [])
+ # Test that list initialization works
+ a = cc.compiler_args(['-I.', '-I..'])
+ self.assertEqual(a, ['-I.', '-I..'])
+ # Test that there is no de-dup on initialization
+ self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.'])
+
+ ## Test that appending works
+ a.append('-I..')
+ self.assertEqual(a, ['-I..', '-I.'])
+ a.append('-O3')
+ self.assertEqual(a, ['-I..', '-I.', '-O3'])
+
+ ## Test that in-place addition works
+ a += ['-O2', '-O2']
+ self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2'])
+ # Test that removal works
+ a.remove('-O2')
+ self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2'])
+ # Test that de-dup happens on addition
+ a += ['-Ifoo', '-Ifoo']
+ self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
+
+ # .extend() is just +=, so we don't test it
+
+ ## Test that addition works
+ # Test that adding a list with just one old arg works and yields the same array
+ a = a + ['-Ifoo']
+ self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
+ # Test that adding a list with one arg new and one old works
+ a = a + ['-Ifoo', '-Ibaz']
+ self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2'])
+ # Test that adding args that must be prepended and appended works
+ a = a + ['-Ibar', '-Wall']
+ self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
+
+ ## Test that reflected addition works
+ # Test that adding to a list with just one old arg works and yields the same array
+ a = ['-Ifoo'] + a
+ self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
+ # Test that adding to a list with just one new arg that is not pre-pended works
+ a = ['-Werror'] + a
+ self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall'])
+ # Test that adding to a list with two new args preserves the order
+ a = ['-Ldir', '-Lbah'] + a
+ self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
+ # Test that adding to a list with old args does nothing
+ a = ['-Ibar', '-Ibaz', '-Ifoo'] + a
+ self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
+
+ ## Test that adding libraries works
+ l = cc.compiler_args(['-Lfoodir', '-lfoo'])
+ self.assertEqual(l, ['-Lfoodir', '-lfoo'])
+ # Adding a library and a libpath appends both correctly
+ l += ['-Lbardir', '-lbar']
+ self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
+ # Adding the same library again does nothing
+ l += ['-lbar']
+ self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
+
+ ## Test that 'direct' append and extend works
+ l = cc.compiler_args(['-Lfoodir', '-lfoo'])
+ self.assertEqual(l, ['-Lfoodir', '-lfoo'])
+ # Direct-adding a library and a libpath appends both correctly
+ l.extend_direct(['-Lbardir', '-lbar'])
+ self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar'])
+ # Direct-adding the same library again still adds it
+ l.append_direct('-lbar')
+ self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar'])
+ # Direct-adding with absolute path deduplicates
+ l.append_direct('/libbaz.a')
+ self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
+ # Adding libbaz again does nothing
+ l.append_direct('/libbaz.a')
+ self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
+
+
+ def test_compiler_args_class_visualstudio(self):
+ linker = mesonbuild.linkers.MSVCDynamicLinker(MachineChoice.HOST, [])
+ # Version just needs to be > 19.0.0
+ cc = VisualStudioCPPCompiler([], [], '20.00', MachineChoice.HOST, False, mock.Mock(), 'x64', linker=linker)
+
+ a = cc.compiler_args(cc.get_always_args())
+ self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/utf-8', '/Zc:__cplusplus'])
+
+ # Ensure /source-charset: removes /utf-8
+ a.append('/source-charset:utf-8')
+ self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/source-charset:utf-8'])
+
+ # Ensure /execution-charset: removes /utf-8
+ a = cc.compiler_args(cc.get_always_args() + ['/execution-charset:utf-8'])
+ self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/execution-charset:utf-8'])
+
+ # Ensure /validate-charset- removes /utf-8
+ a = cc.compiler_args(cc.get_always_args() + ['/validate-charset-'])
+ self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/validate-charset-'])
+
+
+ def test_compiler_args_class_gnuld(self):
+ ## Test --start/end-group
+ linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
+ gcc = GnuCCompiler([], [], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
+ ## Ensure that the fake compiler is never called by overriding the relevant function
+ gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
+ ## Test that 'direct' append and extend works
+ l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
+ # Direct-adding a library and a libpath appends both correctly
+ l.extend_direct(['-Lbardir', '-lbar'])
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group'])
+ # Direct-adding the same library again still adds it
+ l.append_direct('-lbar')
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group'])
+ # Direct-adding with absolute path deduplicates
+ l.append_direct('/libbaz.a')
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
+ # Adding libbaz again does nothing
+ l.append_direct('/libbaz.a')
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
+ # Adding a non-library argument doesn't include it in the group
+ l += ['-Lfoo', '-Wl,--export-dynamic']
+ self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group', '-Wl,--export-dynamic'])
+ # -Wl,-lfoo is detected as a library and gets added to the group
+ l.append('-Wl,-ldl')
+ self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group'])
+
+ def test_compiler_args_remove_system(self):
+ ## Test --start/end-group
+ linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
+ gcc = GnuCCompiler([], [], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
+ ## Ensure that the fake compiler is never called by overriding the relevant function
+ gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
+ ## Test that 'direct' append and extend works
+ l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
+ ## Test that to_native removes all system includes
+ l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include']
+ self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group', '-DSOMETHING_IMPORTANT=1'])
+
+ def test_string_templates_substitution(self):
+ dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict
+ substfunc = mesonbuild.mesonlib.substitute_values
+ ME = mesonbuild.mesonlib.MesonException
+
+ # Identity
+ self.assertEqual(dictfunc([], []), {})
+
+ # One input, no outputs
+ inputs = ['bar/foo.c.in']
+ outputs = []
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
+ '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
+ # Check dictionary
+ self.assertEqual(ret, d)
+ # Check substitutions
+ cmd = ['some', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), cmd)
+ cmd = ['@INPUT@.out', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
+ cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings']
+ self.assertEqual(substfunc(cmd, d),
+ [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:])
+ cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
+ self.assertEqual(substfunc(cmd, d),
+ inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
+ cmd = ['@OUTPUT@']
+ self.assertRaises(ME, substfunc, cmd, d)
+
+ # One input, one output
+ inputs = ['bar/foo.c.in']
+ outputs = ['out.c']
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
+ '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
+ '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
+ # Check dictionary
+ self.assertEqual(ret, d)
+ # Check substitutions
+ cmd = ['some', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), cmd)
+ cmd = ['@INPUT@.out', '@OUTPUT@', 'strings']
+ self.assertEqual(substfunc(cmd, d),
+ [inputs[0] + '.out'] + outputs + cmd[2:])
+ cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@']
+ self.assertEqual(substfunc(cmd, d),
+ [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs)
+ cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
+ self.assertEqual(substfunc(cmd, d),
+ inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
+
+ # One input, one output with a subdir
+ outputs = ['dir/out.c']
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
+ '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
+ '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
+ # Check dictionary
+ self.assertEqual(ret, d)
+
+ # Two inputs, no outputs
+ inputs = ['bar/foo.c.in', 'baz/foo.c.in']
+ outputs = []
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]}
+ # Check dictionary
+ self.assertEqual(ret, d)
+ # Check substitutions
+ cmd = ['some', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), cmd)
+ cmd = ['@INPUT@', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), inputs + cmd[1:])
+ cmd = ['@INPUT0@.out', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
+ cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings']
+ self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
+ cmd = ['@INPUT0@', '@INPUT1@', 'strings']
+ self.assertEqual(substfunc(cmd, d), inputs + cmd[2:])
+ # Many inputs, can't use @INPUT@ like this
+ cmd = ['@INPUT@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Not enough inputs
+ cmd = ['@INPUT2@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Too many inputs
+ cmd = ['@PLAINNAME@']
+ self.assertRaises(ME, substfunc, cmd, d)
+ cmd = ['@BASENAME@']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # No outputs
+ cmd = ['@OUTPUT@']
+ self.assertRaises(ME, substfunc, cmd, d)
+ cmd = ['@OUTPUT0@']
+ self.assertRaises(ME, substfunc, cmd, d)
+ cmd = ['@OUTDIR@']
+ self.assertRaises(ME, substfunc, cmd, d)
+
+ # Two inputs, one output
+ outputs = ['dir/out.c']
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
+ '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
+ # Check dictionary
+ self.assertEqual(ret, d)
+ # Check substitutions
+ cmd = ['some', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), cmd)
+ cmd = ['@OUTPUT@', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
+ cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:])
+ cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings']
+ self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
+ # Many inputs, can't use @INPUT@ like this
+ cmd = ['@INPUT@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Not enough inputs
+ cmd = ['@INPUT2@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Not enough outputs
+ cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+
+ # Two inputs, two outputs
+ outputs = ['dir/out.c', 'dir/out2.c']
+ ret = dictfunc(inputs, outputs)
+ d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
+ '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
+ '@OUTDIR@': 'dir'}
+ # Check dictionary
+ self.assertEqual(ret, d)
+ # Check substitutions
+ cmd = ['some', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), cmd)
+ cmd = ['@OUTPUT@', 'ordinary', 'strings']
+ self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
+ cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
+ self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
+ cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@']
+ self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
+ # Many inputs, can't use @INPUT@ like this
+ cmd = ['@INPUT@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Not enough inputs
+ cmd = ['@INPUT2@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Not enough outputs
+ cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+ # Many outputs, can't use @OUTPUT@ like this
+ cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
+ self.assertRaises(ME, substfunc, cmd, d)
+
+ def test_needs_exe_wrapper_override(self):
+ config = ConfigParser()
+ config['binaries'] = {
+ 'c': '\'/usr/bin/gcc\'',
+ }
+ config['host_machine'] = {
+ 'system': '\'linux\'',
+ 'cpu_family': '\'arm\'',
+ 'cpu': '\'armv7\'',
+ 'endian': '\'little\'',
+ }
+ # Can not be used as context manager because we need to
+ # open it a second time and this is not possible on
+ # Windows.
+ configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
+ configfilename = configfile.name
+ config.write(configfile)
+ configfile.flush()
+ configfile.close()
+ opts = get_fake_options()
+ opts.cross_file = (configfilename,)
+ env = get_fake_env(opts=opts)
+ detected_value = env.need_exe_wrapper()
+ os.unlink(configfilename)
+
+ desired_value = not detected_value
+ config['properties'] = {
+ 'needs_exe_wrapper': 'true' if desired_value else 'false'
+ }
+
+ configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
+ configfilename = configfile.name
+ config.write(configfile)
+ configfile.close()
+ opts = get_fake_options()
+ opts.cross_file = (configfilename,)
+ env = get_fake_env(opts=opts)
+ forced_value = env.need_exe_wrapper()
+ os.unlink(configfilename)
+
+ self.assertEqual(forced_value, desired_value)
+
+ def test_listify(self):
+ listify = mesonbuild.mesonlib.listify
+ # Test sanity
+ self.assertEqual([1], listify(1))
+ self.assertEqual([], listify([]))
+ self.assertEqual([1], listify([1]))
+ # Test flattening
+ self.assertEqual([1, 2, 3], listify([1, [2, 3]]))
+ self.assertEqual([1, 2, 3], listify([1, [2, [3]]]))
+ self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False))
+ # Test flattening and unholdering
+ class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
+ def __init__(self, val: int) -> None:
+ self._val = val
+ class MockInterpreter:
+ def __init__(self) -> None:
+ self.subproject = ''
+ self.environment = None
+ heldObj1 = TestHeldObj(1)
+ holder1 = ObjectHolder(heldObj1, MockInterpreter())
+ self.assertEqual([holder1], listify(holder1))
+ self.assertEqual([holder1], listify([holder1]))
+ self.assertEqual([holder1, 2], listify([holder1, 2]))
+ self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]]))
+
+ def test_extract_as_list(self):
+ extract = mesonbuild.mesonlib.extract_as_list
+ # Test sanity
+ kwargs = {'sources': [1, 2, 3]}
+ self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
+ self.assertEqual(kwargs, {'sources': [1, 2, 3]})
+ self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True))
+ self.assertEqual(kwargs, {})
+
+ class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
+ pass
+ class MockInterpreter:
+ def __init__(self) -> None:
+ self.subproject = ''
+ self.environment = None
+ heldObj = TestHeldObj()
+
+ # Test unholding
+ holder3 = ObjectHolder(heldObj, MockInterpreter())
+ kwargs = {'sources': [1, 2, holder3]}
+ self.assertEqual(kwargs, {'sources': [1, 2, holder3]})
+
+ # flatten nested lists
+ kwargs = {'sources': [1, [2, [3]]]}
+ self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
+
+ def _test_all_naming(self, cc, env, patterns, platform):
+ shr = patterns[platform]['shared']
+ stc = patterns[platform]['static']
+ shrstc = shr + tuple(x for x in stc if x not in shr)
+ stcshr = stc + tuple(x for x in shr if x not in stc)
+ p = cc.get_library_naming(env, LibType.SHARED)
+ self.assertEqual(p, shr)
+ p = cc.get_library_naming(env, LibType.STATIC)
+ self.assertEqual(p, stc)
+ p = cc.get_library_naming(env, LibType.PREFER_STATIC)
+ self.assertEqual(p, stcshr)
+ p = cc.get_library_naming(env, LibType.PREFER_SHARED)
+ self.assertEqual(p, shrstc)
+ # Test find library by mocking up openbsd
+ if platform != 'openbsd':
+ return
+ with tempfile.TemporaryDirectory() as tmpdir:
+ for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1']:
+ libpath = Path(tmpdir) / i
+ libpath.write_text('', encoding='utf-8')
+ found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED)
+ self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0')
+
+ def test_find_library_patterns(self):
+ '''
+ Unit test for the library search patterns used by find_library()
+ '''
+ unix_static = ('lib{}.a', '{}.a')
+ msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib')
+ # This is the priority list of pattern matching for library searching
+ patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'),
+ 'static': unix_static},
+ 'linux': {'shared': ('lib{}.so', '{}.so'),
+ 'static': unix_static},
+ 'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'),
+ 'static': unix_static},
+ 'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll',
+ 'lib{}.dll.a', '{}.dll', '{}.dll.a'),
+ 'static': ('cyg{}.a',) + unix_static},
+ 'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'),
+ 'static': msvc_static},
+ 'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll',
+ '{}.dll.a', '{}.lib', '{}.dll'),
+ 'static': msvc_static}}
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if is_osx():
+ self._test_all_naming(cc, env, patterns, 'darwin')
+ elif is_cygwin():
+ self._test_all_naming(cc, env, patterns, 'cygwin')
+ elif is_windows():
+ if cc.get_argument_syntax() == 'msvc':
+ self._test_all_naming(cc, env, patterns, 'windows-msvc')
+ else:
+ self._test_all_naming(cc, env, patterns, 'windows-mingw')
+ elif is_openbsd():
+ self._test_all_naming(cc, env, patterns, 'openbsd')
+ else:
+ self._test_all_naming(cc, env, patterns, 'linux')
+ env.machines.host.system = 'openbsd'
+ self._test_all_naming(cc, env, patterns, 'openbsd')
+ env.machines.host.system = 'darwin'
+ self._test_all_naming(cc, env, patterns, 'darwin')
+ env.machines.host.system = 'cygwin'
+ self._test_all_naming(cc, env, patterns, 'cygwin')
+ env.machines.host.system = 'windows'
+ self._test_all_naming(cc, env, patterns, 'windows-mingw')
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_parse_libs(self):
+ '''
+ Unit test for parsing of pkg-config output to search for libraries
+
+ https://github.com/mesonbuild/meson/issues/3951
+ '''
+ def create_static_lib(name):
+ if not is_osx():
+ name.open('w', encoding='utf-8').close()
+ return
+ src = name.with_suffix('.c')
+ out = name.with_suffix('.o')
+ with src.open('w', encoding='utf-8') as f:
+ f.write('int meson_foobar (void) { return 0; }')
+ subprocess.check_call(['clang', '-c', str(src), '-o', str(out)])
+ subprocess.check_call(['ar', 'csr', str(name), str(out)])
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True)
+ env = get_fake_env()
+ compiler = detect_c_compiler(env, MachineChoice.HOST)
+ env.coredata.compilers.host = {'c': compiler}
+ env.coredata.options[OptionKey('link_args', lang='c')] = FakeCompilerOptions()
+ p1 = Path(tmpdir) / '1'
+ p2 = Path(tmpdir) / '2'
+ p1.mkdir()
+ p2.mkdir()
+ # libfoo.a is in one prefix
+ create_static_lib(p1 / 'libfoo.a')
+ # libbar.a is in both prefixes
+ create_static_lib(p1 / 'libbar.a')
+ create_static_lib(p2 / 'libbar.a')
+ # Ensure that we never statically link to these
+ create_static_lib(p1 / 'libpthread.a')
+ create_static_lib(p1 / 'libm.a')
+ create_static_lib(p1 / 'libc.a')
+ create_static_lib(p1 / 'libdl.a')
+ create_static_lib(p1 / 'librt.a')
+
+ def fake_call_pkgbin(self, args, env=None):
+ if '--libs' not in args:
+ return 0, '', ''
+ if args[-1] == 'foo':
+ return 0, f'-L{p2.as_posix()} -lfoo -L{p1.as_posix()} -lbar', ''
+ if args[-1] == 'bar':
+ return 0, f'-L{p2.as_posix()} -lbar', ''
+ if args[-1] == 'internal':
+ return 0, f'-L{p1.as_posix()} -lpthread -lm -lc -lrt -ldl', ''
+
+ old_call = PkgConfigDependency._call_pkgbin
+ old_check = PkgConfigDependency.check_pkgconfig
+ PkgConfigDependency._call_pkgbin = fake_call_pkgbin
+ PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin
+ # Test begins
+ try:
+ kwargs = {'required': True, 'silent': True}
+ foo_dep = PkgConfigDependency('foo', env, kwargs)
+ self.assertEqual(foo_dep.get_link_args(),
+ [(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()])
+ bar_dep = PkgConfigDependency('bar', env, kwargs)
+ self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()])
+ internal_dep = PkgConfigDependency('internal', env, kwargs)
+ if compiler.get_argument_syntax() == 'msvc':
+ self.assertEqual(internal_dep.get_link_args(), [])
+ else:
+ link_args = internal_dep.get_link_args()
+ for link_arg in link_args:
+ for lib in ('pthread', 'm', 'c', 'dl', 'rt'):
+ self.assertNotIn(f'lib{lib}.a', link_arg, msg=link_args)
+ finally:
+ # Test ends
+ PkgConfigDependency._call_pkgbin = old_call
+ PkgConfigDependency.check_pkgconfig = old_check
+ # Reset dependency class to ensure that in-process configure doesn't mess up
+ PkgConfigDependency.pkgbin_cache = {}
+ PkgConfigDependency.class_pkgbin = PerMachine(None, None)
+
+ def test_version_compare(self):
+ comparefunc = mesonbuild.mesonlib.version_compare_many
+ for (a, b, result) in [
+ ('0.99.beta19', '>= 0.99.beta14', True),
+ ]:
+ self.assertEqual(comparefunc(a, b)[0], result)
+
+ for (a, b, op) in [
+ # examples from https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison
+ ("1.0010", "1.9", operator.gt),
+ ("1.05", "1.5", operator.eq),
+ ("1.0", "1", operator.gt),
+ ("2.50", "2.5", operator.gt),
+ ("fc4", "fc.4", operator.eq),
+ ("FC5", "fc4", operator.lt),
+ ("2a", "2.0", operator.lt),
+ ("1.0", "1.fc4", operator.gt),
+ ("3.0.0_fc", "3.0.0.fc", operator.eq),
+ # from RPM tests
+ ("1.0", "1.0", operator.eq),
+ ("1.0", "2.0", operator.lt),
+ ("2.0", "1.0", operator.gt),
+ ("2.0.1", "2.0.1", operator.eq),
+ ("2.0", "2.0.1", operator.lt),
+ ("2.0.1", "2.0", operator.gt),
+ ("2.0.1a", "2.0.1a", operator.eq),
+ ("2.0.1a", "2.0.1", operator.gt),
+ ("2.0.1", "2.0.1a", operator.lt),
+ ("5.5p1", "5.5p1", operator.eq),
+ ("5.5p1", "5.5p2", operator.lt),
+ ("5.5p2", "5.5p1", operator.gt),
+ ("5.5p10", "5.5p10", operator.eq),
+ ("5.5p1", "5.5p10", operator.lt),
+ ("5.5p10", "5.5p1", operator.gt),
+ ("10xyz", "10.1xyz", operator.lt),
+ ("10.1xyz", "10xyz", operator.gt),
+ ("xyz10", "xyz10", operator.eq),
+ ("xyz10", "xyz10.1", operator.lt),
+ ("xyz10.1", "xyz10", operator.gt),
+ ("xyz.4", "xyz.4", operator.eq),
+ ("xyz.4", "8", operator.lt),
+ ("8", "xyz.4", operator.gt),
+ ("xyz.4", "2", operator.lt),
+ ("2", "xyz.4", operator.gt),
+ ("5.5p2", "5.6p1", operator.lt),
+ ("5.6p1", "5.5p2", operator.gt),
+ ("5.6p1", "6.5p1", operator.lt),
+ ("6.5p1", "5.6p1", operator.gt),
+ ("6.0.rc1", "6.0", operator.gt),
+ ("6.0", "6.0.rc1", operator.lt),
+ ("10b2", "10a1", operator.gt),
+ ("10a2", "10b2", operator.lt),
+ ("1.0aa", "1.0aa", operator.eq),
+ ("1.0a", "1.0aa", operator.lt),
+ ("1.0aa", "1.0a", operator.gt),
+ ("10.0001", "10.0001", operator.eq),
+ ("10.0001", "10.1", operator.eq),
+ ("10.1", "10.0001", operator.eq),
+ ("10.0001", "10.0039", operator.lt),
+ ("10.0039", "10.0001", operator.gt),
+ ("4.999.9", "5.0", operator.lt),
+ ("5.0", "4.999.9", operator.gt),
+ ("20101121", "20101121", operator.eq),
+ ("20101121", "20101122", operator.lt),
+ ("20101122", "20101121", operator.gt),
+ ("2_0", "2_0", operator.eq),
+ ("2.0", "2_0", operator.eq),
+ ("2_0", "2.0", operator.eq),
+ ("a", "a", operator.eq),
+ ("a+", "a+", operator.eq),
+ ("a+", "a_", operator.eq),
+ ("a_", "a+", operator.eq),
+ ("+a", "+a", operator.eq),
+ ("+a", "_a", operator.eq),
+ ("_a", "+a", operator.eq),
+ ("+_", "+_", operator.eq),
+ ("_+", "+_", operator.eq),
+ ("_+", "_+", operator.eq),
+ ("+", "_", operator.eq),
+ ("_", "+", operator.eq),
+ # other tests
+ ('0.99.beta19', '0.99.beta14', operator.gt),
+ ("1.0.0", "2.0.0", operator.lt),
+ (".0.0", "2.0.0", operator.lt),
+ ("alpha", "beta", operator.lt),
+ ("1.0", "1.0.0", operator.lt),
+ ("2.456", "2.1000", operator.lt),
+ ("2.1000", "3.111", operator.lt),
+ ("2.001", "2.1", operator.eq),
+ ("2.34", "2.34", operator.eq),
+ ("6.1.2", "6.3.8", operator.lt),
+ ("1.7.3.0", "2.0.0", operator.lt),
+ ("2.24.51", "2.25", operator.lt),
+ ("2.1.5+20120813+gitdcbe778", "2.1.5", operator.gt),
+ ("3.4.1", "3.4b1", operator.gt),
+ ("041206", "200090325", operator.lt),
+ ("0.6.2+git20130413", "0.6.2", operator.gt),
+ ("2.6.0+bzr6602", "2.6.0", operator.gt),
+ ("2.6.0", "2.6b2", operator.gt),
+ ("2.6.0+bzr6602", "2.6b2x", operator.gt),
+ ("0.6.7+20150214+git3a710f9", "0.6.7", operator.gt),
+ ("15.8b", "15.8.0.1", operator.lt),
+ ("1.2rc1", "1.2.0", operator.lt),
+ ]:
+ ver_a = Version(a)
+ ver_b = Version(b)
+ if op is operator.eq:
+ for o, name in [(op, 'eq'), (operator.ge, 'ge'), (operator.le, 'le')]:
+ self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
+ if op is operator.lt:
+ for o, name in [(op, 'lt'), (operator.le, 'le'), (operator.ne, 'ne')]:
+ self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
+ for o, name in [(operator.gt, 'gt'), (operator.ge, 'ge'), (operator.eq, 'eq')]:
+ self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
+ if op is operator.gt:
+ for o, name in [(op, 'gt'), (operator.ge, 'ge'), (operator.ne, 'ne')]:
+ self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
+ for o, name in [(operator.lt, 'lt'), (operator.le, 'le'), (operator.eq, 'eq')]:
+ self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
+
+ def test_msvc_toolset_version(self):
+ '''
+ Ensure that the toolset version returns the correct value for this MSVC
+ '''
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() != 'msvc':
+ raise unittest.SkipTest('Test only applies to MSVC-like compilers')
+ toolset_ver = cc.get_toolset_version()
+ self.assertIsNotNone(toolset_ver)
+ # Visual Studio 2015 and older versions do not define VCToolsVersion
+ # TODO: ICL doesn't set this in the VSC2015 profile either
+ if cc.id == 'msvc' and int(''.join(cc.version.split('.')[0:2])) < 1910:
+ return
+ if 'VCToolsVersion' in os.environ:
+ vctools_ver = os.environ['VCToolsVersion']
+ else:
+ self.assertIn('VCINSTALLDIR', os.environ)
+ # See https://devblogs.microsoft.com/cppblog/finding-the-visual-c-compiler-tools-in-visual-studio-2017/
+ vctools_ver = (Path(os.environ['VCINSTALLDIR']) / 'Auxiliary' / 'Build' / 'Microsoft.VCToolsVersion.default.txt').read_text(encoding='utf-8')
+ self.assertTrue(vctools_ver.startswith(toolset_ver),
+ msg=f'{vctools_ver!r} does not start with {toolset_ver!r}')
+
+ def test_split_args(self):
+ split_args = mesonbuild.mesonlib.split_args
+ join_args = mesonbuild.mesonlib.join_args
+ if is_windows():
+ test_data = [
+ # examples from https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
+ (r'"a b c" d e', ['a b c', 'd', 'e'], True),
+ (r'"ab\"c" "\\" d', ['ab"c', '\\', 'd'], False),
+ (r'a\\\b d"e f"g h', [r'a\\\b', 'de fg', 'h'], False),
+ (r'a\\\"b c d', [r'a\"b', 'c', 'd'], False),
+ (r'a\\\\"b c" d e', [r'a\\b c', 'd', 'e'], False),
+ # other basics
+ (r'""', [''], True),
+ (r'a b c d "" e', ['a', 'b', 'c', 'd', '', 'e'], True),
+ (r"'a b c' d e", ["'a", 'b', "c'", 'd', 'e'], True),
+ (r"'a&b&c' d e", ["'a&b&c'", 'd', 'e'], True),
+ (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], True),
+ (r"'a & b & c d e'", ["'a", '&', 'b', '&', 'c', 'd', "e'"], True),
+ ('a b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),
+ # more illustrative tests
+ (r'cl test.cpp /O1 /Fe:test.exe', ['cl', 'test.cpp', '/O1', '/Fe:test.exe'], True),
+ (r'cl "test.cpp /O1 /Fe:test.exe"', ['cl', 'test.cpp /O1 /Fe:test.exe'], True),
+ (r'cl /DNAME=\"Bob\" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], False),
+ (r'cl "/DNAME=\"Bob\"" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], True),
+ (r'cl /DNAME=\"Bob, Alice\" test.cpp', ['cl', '/DNAME="Bob,', 'Alice"', 'test.cpp'], False),
+ (r'cl "/DNAME=\"Bob, Alice\"" test.cpp', ['cl', '/DNAME="Bob, Alice"', 'test.cpp'], True),
+ (r'cl C:\path\with\backslashes.cpp', ['cl', r'C:\path\with\backslashes.cpp'], True),
+ (r'cl C:\\path\\with\\double\\backslashes.cpp', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], True),
+ (r'cl "C:\\path\\with\\double\\backslashes.cpp"', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], False),
+ (r'cl C:\path with spaces\test.cpp', ['cl', r'C:\path', 'with', r'spaces\test.cpp'], False),
+ (r'cl "C:\path with spaces\test.cpp"', ['cl', r'C:\path with spaces\test.cpp'], True),
+ (r'cl /DPATH="C:\path\with\backslashes test.cpp', ['cl', r'/DPATH=C:\path\with\backslashes test.cpp'], False),
+ (r'cl /DPATH=\"C:\\ends\\with\\backslashes\\\" test.cpp', ['cl', r'/DPATH="C:\\ends\\with\\backslashes\"', 'test.cpp'], False),
+ (r'cl /DPATH="C:\\ends\\with\\backslashes\\" test.cpp', ['cl', '/DPATH=C:\\\\ends\\\\with\\\\backslashes\\', 'test.cpp'], False),
+ (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\"', 'test.cpp'], True),
+ (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\ test.cpp'], False),
+ (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\"', 'test.cpp'], True),
+ ]
+ else:
+ test_data = [
+ (r"'a b c' d e", ['a b c', 'd', 'e'], True),
+ (r"a/b/c d e", ['a/b/c', 'd', 'e'], True),
+ (r"a\b\c d e", [r'abc', 'd', 'e'], False),
+ (r"a\\b\\c d e", [r'a\b\c', 'd', 'e'], False),
+ (r'"a b c" d e', ['a b c', 'd', 'e'], False),
+ (r'"a\\b\\c\\" d e', ['a\\b\\c\\', 'd', 'e'], False),
+ (r"'a\b\c\' d e", ['a\\b\\c\\', 'd', 'e'], True),
+ (r"'a&b&c' d e", ['a&b&c', 'd', 'e'], True),
+ (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], False),
+ (r"'a & b & c d e'", ['a & b & c d e'], True),
+ (r"abd'e f'g h", [r'abde fg', 'h'], False),
+ ('a b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),
+
+ ('g++ -DNAME="Bob" test.cpp', ['g++', '-DNAME=Bob', 'test.cpp'], False),
+ ("g++ '-DNAME=\"Bob\"' test.cpp", ['g++', '-DNAME="Bob"', 'test.cpp'], True),
+ ('g++ -DNAME="Bob, Alice" test.cpp', ['g++', '-DNAME=Bob, Alice', 'test.cpp'], False),
+ ("g++ '-DNAME=\"Bob, Alice\"' test.cpp", ['g++', '-DNAME="Bob, Alice"', 'test.cpp'], True),
+ ]
+
+ for (cmd, expected, roundtrip) in test_data:
+ self.assertEqual(split_args(cmd), expected)
+ if roundtrip:
+ self.assertEqual(join_args(expected), cmd)
+
+ def test_quote_arg(self):
+ split_args = mesonbuild.mesonlib.split_args
+ quote_arg = mesonbuild.mesonlib.quote_arg
+ if is_windows():
+ test_data = [
+ ('', '""'),
+ ('arg1', 'arg1'),
+ ('/option1', '/option1'),
+ ('/Ovalue', '/Ovalue'),
+ ('/OBob&Alice', '/OBob&Alice'),
+ ('/Ovalue with spaces', r'"/Ovalue with spaces"'),
+ (r'/O"value with spaces"', r'"/O\"value with spaces\""'),
+ (r'/OC:\path with spaces\test.exe', r'"/OC:\path with spaces\test.exe"'),
+ ('/LIBPATH:C:\\path with spaces\\ends\\with\\backslashes\\', r'"/LIBPATH:C:\path with spaces\ends\with\backslashes\\"'),
+ ('/LIBPATH:"C:\\path with spaces\\ends\\with\\backslashes\\\\"', r'"/LIBPATH:\"C:\path with spaces\ends\with\backslashes\\\\\""'),
+ (r'/DMSG="Alice said: \"Let\'s go\""', r'"/DMSG=\"Alice said: \\\"Let\'s go\\\"\""'),
+ ]
+ else:
+ test_data = [
+ ('arg1', 'arg1'),
+ ('--option1', '--option1'),
+ ('-O=value', '-O=value'),
+ ('-O=Bob&Alice', "'-O=Bob&Alice'"),
+ ('-O=value with spaces', "'-O=value with spaces'"),
+ ('-O="value with spaces"', '\'-O=\"value with spaces\"\''),
+ ('-O=/path with spaces/test', '\'-O=/path with spaces/test\''),
+ ('-DMSG="Alice said: \\"Let\'s go\\""', "'-DMSG=\"Alice said: \\\"Let'\"'\"'s go\\\"\"'"),
+ ]
+
+ for (arg, expected) in test_data:
+ self.assertEqual(quote_arg(arg), expected)
+ self.assertEqual(split_args(expected)[0], arg)
+
+ def test_depfile(self):
+ for (f, target, expdeps) in [
+ # empty, unknown target
+ ([''], 'unknown', set()),
+ # simple target & deps
+ (['meson/foo.o : foo.c foo.h'], 'meson/foo.o', set({'foo.c', 'foo.h'})),
+ (['meson/foo.o: foo.c foo.h'], 'foo.c', set()),
+ # get all deps
+ (['meson/foo.o: foo.c foo.h',
+ 'foo.c: gen.py'], 'meson/foo.o', set({'foo.c', 'foo.h', 'gen.py'})),
+ (['meson/foo.o: foo.c foo.h',
+ 'foo.c: gen.py'], 'foo.c', set({'gen.py'})),
+ # linue continuation, multiple targets
+ (['foo.o \\', 'foo.h: bar'], 'foo.h', set({'bar'})),
+ (['foo.o \\', 'foo.h: bar'], 'foo.o', set({'bar'})),
+ # \\ handling
+ (['foo: Program\\ F\\iles\\\\X'], 'foo', set({'Program Files\\X'})),
+ # $ handling
+ (['f$o.o: c/b'], 'f$o.o', set({'c/b'})),
+ (['f$$o.o: c/b'], 'f$o.o', set({'c/b'})),
+ # cycles
+ (['a: b', 'b: a'], 'a', set({'a', 'b'})),
+ (['a: b', 'b: a'], 'b', set({'a', 'b'})),
+ ]:
+ d = mesonbuild.depfile.DepFile(f)
+ deps = d.get_all_dependencies(target)
+ self.assertEqual(sorted(deps), sorted(expdeps))
+
+ def test_log_once(self):
+ f = io.StringIO()
+ with mock.patch('mesonbuild.mlog.log_file', f), \
+ mock.patch('mesonbuild.mlog._logged_once', set()):
+ mesonbuild.mlog.log_once('foo')
+ mesonbuild.mlog.log_once('foo')
+ actual = f.getvalue().strip()
+ self.assertEqual(actual, 'foo', actual)
+
+ def test_log_once_ansi(self):
+ f = io.StringIO()
+ with mock.patch('mesonbuild.mlog.log_file', f), \
+ mock.patch('mesonbuild.mlog._logged_once', set()):
+ mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo'))
+ mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo'))
+ actual = f.getvalue().strip()
+ self.assertEqual(actual.count('foo'), 1, actual)
+
+ mesonbuild.mlog.log_once('foo')
+ actual = f.getvalue().strip()
+ self.assertEqual(actual.count('foo'), 1, actual)
+
+ f.truncate()
+
+ mesonbuild.mlog.warning('bar', once=True)
+ mesonbuild.mlog.warning('bar', once=True)
+ actual = f.getvalue().strip()
+ self.assertEqual(actual.count('bar'), 1, actual)
+
+ def test_sort_libpaths(self):
+ sort_libpaths = mesonbuild.dependencies.base.sort_libpaths
+ self.assertEqual(sort_libpaths(
+ ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'],
+ ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
+ ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
+ self.assertEqual(sort_libpaths(
+ ['/usr/local/lib', '/home/mesonuser/.local/lib', '/usr/lib'],
+ ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
+ ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
+ self.assertEqual(sort_libpaths(
+ ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
+ ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
+ ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
+ self.assertEqual(sort_libpaths(
+ ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
+ ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/libdata/pkgconfig']),
+ ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
+
+ def test_dependency_factory_order(self):
+ b = mesonbuild.dependencies.base
+ F = mesonbuild.dependencies.factory
+ with tempfile.TemporaryDirectory() as tmpdir:
+ with chdir(tmpdir):
+ env = get_fake_env()
+ env.scratch_dir = tmpdir
+
+ f = F.DependencyFactory(
+ 'test_dep',
+ methods=[b.DependencyMethods.PKGCONFIG, b.DependencyMethods.CMAKE]
+ )
+ actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})]
+ self.assertListEqual([m.type_name for m in actual], ['pkgconfig', 'cmake'])
+
+ f = F.DependencyFactory(
+ 'test_dep',
+ methods=[b.DependencyMethods.CMAKE, b.DependencyMethods.PKGCONFIG]
+ )
+ actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})]
+ self.assertListEqual([m.type_name for m in actual], ['cmake', 'pkgconfig'])
+
+ def test_validate_json(self) -> None:
+ """Validate the json schema for the test cases."""
+ try:
+ from jsonschema import validate, ValidationError
+ except ImportError:
+ if is_ci():
+ raise
+ raise unittest.SkipTest('Python jsonschema module not found.')
+
+ schema = json.loads(Path('data/test.schema.json').read_text(encoding='utf-8'))
+
+ errors = [] # type: T.Tuple[str, Exception]
+ for p in Path('test cases').glob('**/test.json'):
+ try:
+ validate(json.loads(p.read_text(encoding='utf-8')), schema=schema)
+ except ValidationError as e:
+ errors.append((p.resolve(), e))
+
+ for f, e in errors:
+ print(f'Failed to validate: "{f}"')
+ print(str(e))
+
+ self.assertFalse(errors)
+
+ def test_typed_pos_args_types(self) -> None:
+ @typed_pos_args('foo', str, int, bool)
+ def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
+ self.assertIsInstance(args, tuple)
+ self.assertIsInstance(args[0], str)
+ self.assertIsInstance(args[1], int)
+ self.assertIsInstance(args[2], bool)
+
+ _(None, mock.Mock(), ['string', 1, False], None)
+
+ def test_typed_pos_args_types_invalid(self) -> None:
+ @typed_pos_args('foo', str, int, bool)
+ def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 1.0, False], None)
+ self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been "int"')
+
+ def test_typed_pos_args_types_wrong_number(self) -> None:
+ @typed_pos_args('foo', str, int, bool)
+ def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 1], None)
+ self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 2.')
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 1, True, True], None)
+ self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 4.')
+
+ def test_typed_pos_args_varargs(self) -> None:
+ @typed_pos_args('foo', str, varargs=str)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertIsInstance(args, tuple)
+ self.assertIsInstance(args[0], str)
+ self.assertIsInstance(args[1], list)
+ self.assertIsInstance(args[1][0], str)
+ self.assertIsInstance(args[1][1], str)
+
+ _(None, mock.Mock(), ['string', 'var', 'args'], None)
+
+ def test_typed_pos_args_varargs_not_given(self) -> None:
+ @typed_pos_args('foo', str, varargs=str)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertIsInstance(args, tuple)
+ self.assertIsInstance(args[0], str)
+ self.assertIsInstance(args[1], list)
+ self.assertEqual(args[1], [])
+
+ _(None, mock.Mock(), ['string'], None)
+
+ def test_typed_pos_args_varargs_invalid(self) -> None:
+ @typed_pos_args('foo', str, varargs=str)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 'var', 'args', 0], None)
+ self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"')
+
+ def test_typed_pos_args_varargs_invalid_mulitple_types(self) -> None:
+ @typed_pos_args('foo', str, varargs=(str, list))
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 'var', 'args', 0], None)
+ self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: "str", "list"')
+
+ def test_typed_pos_args_max_varargs(self) -> None:
+ @typed_pos_args('foo', str, varargs=str, max_varargs=5)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertIsInstance(args, tuple)
+ self.assertIsInstance(args[0], str)
+ self.assertIsInstance(args[1], list)
+ self.assertIsInstance(args[1][0], str)
+ self.assertIsInstance(args[1][1], str)
+
+ _(None, mock.Mock(), ['string', 'var', 'args'], None)
+
+ def test_typed_pos_args_max_varargs_exceeded(self) -> None:
+ @typed_pos_args('foo', str, varargs=str, max_varargs=1)
+ def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 'var', 'args'], None)
+ self.assertEqual(str(cm.exception), 'foo takes between 1 and 2 arguments, but got 3.')
+
+ def test_typed_pos_args_min_varargs(self) -> None:
+ @typed_pos_args('foo', varargs=str, max_varargs=2, min_varargs=1)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertIsInstance(args, tuple)
+ self.assertIsInstance(args[0], list)
+ self.assertIsInstance(args[0][0], str)
+ self.assertIsInstance(args[0][1], str)
+
+ _(None, mock.Mock(), ['string', 'var'], None)
+
+ def test_typed_pos_args_min_varargs_not_met(self) -> None:
+ @typed_pos_args('foo', str, varargs=str, min_varargs=1)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string'], None)
+ self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.')
+
+ def test_typed_pos_args_min_and_max_varargs_exceeded(self) -> None:
+ @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
+ def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', 'var', 'args', 'bar'], None)
+ self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 4.')
+
+ def test_typed_pos_args_min_and_max_varargs_not_met(self) -> None:
+ @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
+ def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string'], None)
+ self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 1.')
+
+ def test_typed_pos_args_variadic_and_optional(self) -> None:
+ @typed_pos_args('foo', str, optargs=[str], varargs=str, min_varargs=0)
+ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(AssertionError) as cm:
+ _(None, mock.Mock(), ['string'], None)
+ self.assertEqual(
+ str(cm.exception),
+ 'varargs and optargs not supported together as this would be ambiguous')
+
+ def test_typed_pos_args_min_optargs_not_met(self) -> None:
+ @typed_pos_args('foo', str, str, optargs=[str])
+ def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string'], None)
+ self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.')
+
+ def test_typed_pos_args_min_optargs_max_exceeded(self) -> None:
+ @typed_pos_args('foo', str, optargs=[str])
+ def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
+ self.assertTrue(False) # should not be reachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), ['string', '1', '2'], None)
+ self.assertEqual(str(cm.exception), 'foo takes at most 2 arguments, but got 3.')
+
+ def test_typed_pos_args_optargs_not_given(self) -> None:
+ @typed_pos_args('foo', str, optargs=[str])
+ def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
+ self.assertEqual(len(args), 2)
+ self.assertIsInstance(args[0], str)
+ self.assertEqual(args[0], 'string')
+ self.assertIsNone(args[1])
+
+ _(None, mock.Mock(), ['string'], None)
+
+ def test_typed_pos_args_optargs_some_given(self) -> None:
+ @typed_pos_args('foo', str, optargs=[str, int])
+ def _(obj, node, args: T.Tuple[str, T.Optional[str], T.Optional[int]], kwargs) -> None:
+ self.assertEqual(len(args), 3)
+ self.assertIsInstance(args[0], str)
+ self.assertEqual(args[0], 'string')
+ self.assertIsInstance(args[1], str)
+ self.assertEqual(args[1], '1')
+ self.assertIsNone(args[2])
+
+ _(None, mock.Mock(), ['string', '1'], None)
+
+ def test_typed_pos_args_optargs_all_given(self) -> None:
+ @typed_pos_args('foo', str, optargs=[str])
+ def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
+ self.assertEqual(len(args), 2)
+ self.assertIsInstance(args[0], str)
+ self.assertEqual(args[0], 'string')
+ self.assertIsInstance(args[1], str)
+
+ _(None, mock.Mock(), ['string', '1'], None)
+
+ def test_typed_kwarg_basic(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', str, default='')
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertIsInstance(kwargs['input'], str)
+ self.assertEqual(kwargs['input'], 'foo')
+
+ _(None, mock.Mock(), [], {'input': 'foo'})
+
+ def test_typed_kwarg_missing_required(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', str, required=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertTrue(False) # should be unreachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), [], {})
+ self.assertEqual(str(cm.exception), 'testfunc is missing required keyword argument "input"')
+
+ def test_typed_kwarg_missing_optional(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', (str, type(None))),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Optional[str]]) -> None:
+ self.assertIsNone(kwargs['input'])
+
+ _(None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_default(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', str, default='default'),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertEqual(kwargs['input'], 'default')
+
+ _(None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_container_valid(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str), default=[], required=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
+ self.assertEqual(kwargs['input'], ['str'])
+
+ _(None, mock.Mock(), [], {'input': ['str']})
+
+ def test_typed_kwarg_container_invalid(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str), required=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
+ self.assertTrue(False) # should be unreachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), [], {'input': {}})
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type dict[] but should have been array[str]")
+
+ def test_typed_kwarg_contained_invalid(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(dict, str), required=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Dict[str, str]]) -> None:
+ self.assertTrue(False) # should be unreachable
+
+ with self.assertRaises(InvalidArguments) as cm:
+ _(None, mock.Mock(), [], {'input': {'key': 1, 'bar': 2}})
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type dict[int] but should have been dict[str]")
+
+ def test_typed_kwarg_container_listify(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str), default=[], listify=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
+ self.assertEqual(kwargs['input'], ['str'])
+
+ _(None, mock.Mock(), [], {'input': 'str'})
+
+ def test_typed_kwarg_container_default_copy(self) -> None:
+ default: T.List[str] = []
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=default),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
+ self.assertIsNot(kwargs['input'], default)
+
+ _(None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_container_pairs(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str, pairs=True), listify=True),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
+ self.assertEqual(kwargs['input'], ['a', 'b'])
+
+ _(None, mock.Mock(), [], {'input': ['a', 'b']})
+
+ with self.assertRaises(MesonException) as cm:
+ _(None, mock.Mock(), [], {'input': ['a']})
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type array[str] but should have been array[str] that has even size")
+
+ def test_typed_kwarg_since(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', str, since='1.0', since_message='Its awesome, use it',
+ deprecated='2.0', deprecated_message='Its terrible, dont use it')
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertIsInstance(kwargs['input'], str)
+ self.assertEqual(kwargs['input'], 'foo')
+
+ with self.subTest('use before available'), \
+ mock.patch('sys.stdout', io.StringIO()) as out, \
+ mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '0.1'}):
+ # With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning
+ _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
+ self.assertRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc. Its awesome, use it')
+ self.assertNotRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc. Its terrible, dont use it')
+
+ with self.subTest('no warnings should be triggered'), \
+ mock.patch('sys.stdout', io.StringIO()) as out, \
+ mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '1.5'}):
+ # With Meson 1.5 it shouldn't trigger any warning
+ _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
+ self.assertNotRegex(out.getvalue(), r'WARNING:.*')
+
+ with self.subTest('use after deprecated'), \
+ mock.patch('sys.stdout', io.StringIO()) as out, \
+ mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '2.0'}):
+ # With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning
+ _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
+ self.assertRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc. Its terrible, dont use it')
+ self.assertNotRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc. Its awesome, use it')
+
+ def test_typed_kwarg_validator(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', str, default='', validator=lambda x: 'invalid!' if x != 'foo' else None)
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ pass
+
+ # Should be valid
+ _(None, mock.Mock(), tuple(), dict(input='foo'))
+
+ with self.assertRaises(MesonException) as cm:
+ _(None, mock.Mock(), tuple(), dict(input='bar'))
+ self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" invalid!")
+
+ def test_typed_kwarg_convertor(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('native', bool, default=False, convertor=lambda n: MachineChoice.BUILD if n else MachineChoice.HOST)
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, MachineChoice]) -> None:
+ assert isinstance(kwargs['native'], MachineChoice)
+
+ _(None, mock.Mock(), tuple(), dict(native=True))
+
+ @mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '1.0'})
+ def test_typed_kwarg_since_values(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=[], deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}),
+ KwargInfo('output', ContainerTypeInfo(dict, str), default={}, deprecated_values={'foo': '0.9', 'foo2': ('0.9', 'dont use it')}, since_values={'bar': '1.1', 'bar2': ('1.1', 'use this')}),
+ KwargInfo('install_dir', (bool, str, NoneType), deprecated_values={False: '0.9'}),
+ KwargInfo(
+ 'mode',
+ (str, type(None)),
+ validator=in_set_validator({'clean', 'build', 'rebuild', 'deprecated', 'since'}),
+ deprecated_values={'deprecated': '1.0'},
+ since_values={'since': '1.1'}),
+ KwargInfo('dict', (ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), default={},
+ since_values={list: '1.9'}),
+ KwargInfo('new_dict', (ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), default={},
+ since_values={dict: '1.1'}),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ pass
+
+ with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'input': ['foo']})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""")
+
+ with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'input': ['bar']})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""")
+
+ with self.subTest('deprecated dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'output': {'foo': 'a'}})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo".*""")
+
+ with self.subTest('deprecated dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'output': {'foo2': 'a'}})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo2". dont use it.*""")
+
+ with self.subTest('new dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'output': {'bar': 'b'}})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar".*""")
+
+ with self.subTest('new dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'output': {'bar2': 'a'}})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar2". use this.*""")
+
+ with self.subTest('non string union'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'install_dir': False})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "install_dir" value "False".*""")
+
+ with self.subTest('deprecated string union'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'mode': 'deprecated'})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '1.0': "testfunc" keyword argument "mode" value "deprecated".*""")
+
+ with self.subTest('new string union'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'mode': 'since'})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "mode" value "since".*""")
+
+ with self.subTest('new container'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'dict': ['a=b']})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.9': "testfunc" keyword argument "dict" of type list.*""")
+
+ with self.subTest('new container set to default'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {'new_dict': {}})
+ self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "new_dict" of type dict.*""")
+
+ with self.subTest('new container default'), mock.patch('sys.stdout', io.StringIO()) as out:
+ _(None, mock.Mock(subproject=''), [], {})
+ self.assertNotRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "new_dict" of type dict.*""")
+
+ def test_typed_kwarg_evolve(self) -> None:
+ k = KwargInfo('foo', str, required=True, default='foo')
+ v = k.evolve(default='bar')
+ self.assertEqual(k.name, 'foo')
+ self.assertEqual(k.name, v.name)
+ self.assertEqual(k.types, str)
+ self.assertEqual(k.types, v.types)
+ self.assertEqual(k.required, True)
+ self.assertEqual(k.required, v.required)
+ self.assertEqual(k.default, 'foo')
+ self.assertEqual(v.default, 'bar')
+
+ def test_typed_kwarg_default_type(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('no_default', (str, ContainerTypeInfo(list, str), NoneType)),
+ KwargInfo('str_default', (str, ContainerTypeInfo(list, str)), default=''),
+ KwargInfo('list_default', (str, ContainerTypeInfo(list, str)), default=['']),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertEqual(kwargs['no_default'], None)
+ self.assertEqual(kwargs['str_default'], '')
+ self.assertEqual(kwargs['list_default'], [''])
+ _(None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_invalid_default_type(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('invalid_default', (str, ContainerTypeInfo(list, str), NoneType), default=42),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ pass
+ self.assertRaises(AssertionError, _, None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_container_in_tuple(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', (str, ContainerTypeInfo(list, str))),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertEqual(kwargs['input'], args[0])
+ _(None, mock.Mock(), [''], {'input': ''})
+ _(None, mock.Mock(), [['']], {'input': ['']})
+ self.assertRaises(InvalidArguments, _, None, mock.Mock(), [], {'input': 42})
+
+ def test_detect_cpu_family(self) -> None:
+ """Test the various cpu families that we detect and normalize.
+
+ This is particularly useful as both documentation, and to keep testing
+ platforms that are less common.
+ """
+
+ @contextlib.contextmanager
+ def mock_trial(value: str) -> T.Iterable[None]:
+ """Mock all of the ways we could get the trial at once."""
+ mocked = mock.Mock(return_value=value)
+
+ with mock.patch('mesonbuild.environment.detect_windows_arch', mocked), \
+ mock.patch('mesonbuild.environment.platform.processor', mocked), \
+ mock.patch('mesonbuild.environment.platform.machine', mocked):
+ yield
+
+ cases = [
+ ('x86', 'x86'),
+ ('i386', 'x86'),
+ ('bepc', 'x86'), # Haiku
+ ('earm', 'arm'), # NetBSD
+ ('arm', 'arm'),
+ ('ppc64', 'ppc64'),
+ ('powerpc64', 'ppc64'),
+ ('powerpc', 'ppc'),
+ ('ppc', 'ppc'),
+ ('macppc', 'ppc'),
+ ('power macintosh', 'ppc'),
+ ('mips64el', 'mips64'),
+ ('mips64', 'mips64'),
+ ('mips', 'mips'),
+ ('mipsel', 'mips'),
+ ('ip30', 'mips64'),
+ ('ip35', 'mips64'),
+ ('parisc64', 'parisc'),
+ ('sun4u', 'sparc64'),
+ ('sun4v', 'sparc64'),
+ ('amd64', 'x86_64'),
+ ('x64', 'x86_64'),
+ ('i86pc', 'x86_64'), # Solaris
+ ('aarch64', 'aarch64'),
+ ('aarch64_be', 'aarch64'),
+ ]
+
+ with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=False)):
+ for test, expected in cases:
+ with self.subTest(test, has_define=False), mock_trial(test):
+ actual = mesonbuild.environment.detect_cpu_family({})
+ self.assertEqual(actual, expected)
+
+ with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=True)):
+ for test, expected in [('x86_64', 'x86'), ('aarch64', 'arm'), ('ppc', 'ppc64')]:
+ with self.subTest(test, has_define=True), mock_trial(test):
+ actual = mesonbuild.environment.detect_cpu_family({})
+ self.assertEqual(actual, expected)
+
+ def test_detect_cpu(self) -> None:
+
+ @contextlib.contextmanager
+ def mock_trial(value: str) -> T.Iterable[None]:
+ """Mock all of the ways we could get the trial at once."""
+ mocked = mock.Mock(return_value=value)
+
+ with mock.patch('mesonbuild.environment.detect_windows_arch', mocked), \
+ mock.patch('mesonbuild.environment.platform.processor', mocked), \
+ mock.patch('mesonbuild.environment.platform.machine', mocked):
+ yield
+
+ cases = [
+ ('amd64', 'x86_64'),
+ ('x64', 'x86_64'),
+ ('i86pc', 'x86_64'),
+ ('earm', 'arm'),
+ ('mips64el', 'mips64'),
+ ('mips64', 'mips64'),
+ ('mips', 'mips'),
+ ('mipsel', 'mips'),
+ ('aarch64', 'aarch64'),
+ ('aarch64_be', 'aarch64'),
+ ]
+
+ with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=False)):
+ for test, expected in cases:
+ with self.subTest(test, has_define=False), mock_trial(test):
+ actual = mesonbuild.environment.detect_cpu({})
+ self.assertEqual(actual, expected)
+
+ with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=True)):
+ for test, expected in [('x86_64', 'i686'), ('aarch64', 'arm'), ('ppc', 'ppc64')]:
+ with self.subTest(test, has_define=True), mock_trial(test):
+ actual = mesonbuild.environment.detect_cpu({})
+ self.assertEqual(actual, expected)
+
+ def test_interpreter_unpicklable(self) -> None:
+ build = mock.Mock()
+ build.environment = mock.Mock()
+ build.environment.get_source_dir = mock.Mock(return_value='')
+ with mock.patch('mesonbuild.interpreter.Interpreter._redetect_machines', mock.Mock()), \
+ self.assertRaises(mesonbuild.mesonlib.MesonBugException):
+ i = mesonbuild.interpreter.Interpreter(build, mock=True)
+ pickle.dumps(i)
+
+ def test_major_versions_differ(self) -> None:
+ # Return True when going to next major release, when going to dev cycle,
+ # when going to rc cycle or when going out of rc cycle.
+ self.assertTrue(coredata.major_versions_differ('0.59.0', '0.60.0'))
+ self.assertTrue(coredata.major_versions_differ('0.59.0', '0.59.99'))
+ self.assertTrue(coredata.major_versions_differ('0.59.0', '0.60.0.rc1'))
+ self.assertTrue(coredata.major_versions_differ('0.59.99', '0.60.0.rc1'))
+ self.assertTrue(coredata.major_versions_differ('0.60.0.rc1', '0.60.0'))
+ # Return False when going to next point release or when staying in dev/rc cycle.
+ self.assertFalse(coredata.major_versions_differ('0.60.0', '0.60.0'))
+ self.assertFalse(coredata.major_versions_differ('0.60.0', '0.60.1'))
+ self.assertFalse(coredata.major_versions_differ('0.59.99', '0.59.99'))
+ self.assertFalse(coredata.major_versions_differ('0.60.0.rc1', '0.60.0.rc2'))
+
+ def test_option_key_from_string(self) -> None:
+ cases = [
+ ('c_args', OptionKey('args', lang='c', _type=OptionType.COMPILER)),
+ ('build.cpp_args', OptionKey('args', machine=MachineChoice.BUILD, lang='cpp', _type=OptionType.COMPILER)),
+ ('prefix', OptionKey('prefix', _type=OptionType.BUILTIN)),
+ ('made_up', OptionKey('made_up', _type=OptionType.PROJECT)),
+
+ # TODO: the from_String method should be splitting the prefix off of
+ # these, as we have the type already, but it doesn't. For now have a
+ # test so that we don't change the behavior un-intentionally
+ ('b_lto', OptionKey('b_lto', _type=OptionType.BASE)),
+ ('backend_startup_project', OptionKey('backend_startup_project', _type=OptionType.BACKEND)),
+ ]
+
+ for raw, expected in cases:
+ with self.subTest(raw):
+ self.assertEqual(OptionKey.from_string(raw), expected)
diff --git a/unittests/linuxcrosstests.py b/unittests/linuxcrosstests.py
new file mode 100644
index 0000000..28bf415
--- /dev/null
+++ b/unittests/linuxcrosstests.py
@@ -0,0 +1,192 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import unittest
+import platform
+
+from mesonbuild.mesonlib import (
+ is_windows, is_cygwin
+)
+from mesonbuild.mesonlib import MesonException
+
+
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+class BaseLinuxCrossTests(BasePlatformTests):
+ # Don't pass --libdir when cross-compiling. We have tests that
+ # check whether meson auto-detects it correctly.
+ libdir = None
+
+
+def should_run_cross_arm_tests():
+ return shutil.which('arm-linux-gnueabihf-gcc') and not platform.machine().lower().startswith('arm')
+
+@unittest.skipUnless(not is_windows() and should_run_cross_arm_tests(), "requires ability to cross compile to ARM")
+class LinuxCrossArmTests(BaseLinuxCrossTests):
+ '''
+ Tests that cross-compilation to Linux/ARM works
+ '''
+
+ def setUp(self):
+ super().setUp()
+ self.meson_cross_files = [os.path.join(self.src_root, 'cross', 'ubuntu-armhf.txt')]
+
+ def test_cflags_cross_environment_pollution(self):
+ '''
+ Test that the CFLAGS environment variable does not pollute the cross
+ environment. This can't be an ordinary test case because we need to
+ inspect the compiler database.
+ '''
+ testdir = os.path.join(self.common_test_dir, '3 static')
+ self.init(testdir, override_envvars={'CFLAGS': '-DBUILD_ENVIRONMENT_ONLY'})
+ compdb = self.get_compdb()
+ self.assertNotIn('-DBUILD_ENVIRONMENT_ONLY', compdb[0]['command'])
+
+ def test_cross_file_overrides_always_args(self):
+ '''
+ Test that $lang_args in cross files always override get_always_args().
+ Needed for overriding the default -D_FILE_OFFSET_BITS=64 on some
+ architectures such as some Android versions and Raspbian.
+ https://github.com/mesonbuild/meson/issues/3049
+ https://github.com/mesonbuild/meson/issues/3089
+ '''
+ testdir = os.path.join(self.unit_test_dir, '33 cross file overrides always args')
+ self.meson_cross_files = [os.path.join(testdir, 'ubuntu-armhf-overrides.txt')]
+ self.init(testdir)
+ compdb = self.get_compdb()
+ self.assertRegex(compdb[0]['command'], '-D_FILE_OFFSET_BITS=64.*-U_FILE_OFFSET_BITS')
+ self.build()
+
+ def test_cross_libdir(self):
+ # When cross compiling "libdir" should default to "lib"
+ # rather than "lib/x86_64-linux-gnu" or something like that.
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+ self.init(testdir)
+ for i in self.introspect('--buildoptions'):
+ if i['name'] == 'libdir':
+ self.assertEqual(i['value'], 'lib')
+ return
+ self.assertTrue(False, 'Option libdir not in introspect data.')
+
+ def test_cross_libdir_subproject(self):
+ # Guard against a regression where calling "subproject"
+ # would reset the value of libdir to its default value.
+ testdir = os.path.join(self.unit_test_dir, '75 subdir libdir')
+ self.init(testdir, extra_args=['--libdir=fuf'])
+ for i in self.introspect('--buildoptions'):
+ if i['name'] == 'libdir':
+ self.assertEqual(i['value'], 'fuf')
+ return
+ self.assertTrue(False, 'Libdir specified on command line gets reset.')
+
+ def test_std_remains(self):
+ # C_std defined in project options must be in effect also when cross compiling.
+ testdir = os.path.join(self.unit_test_dir, '50 noncross options')
+ self.init(testdir)
+ compdb = self.get_compdb()
+ self.assertRegex(compdb[0]['command'], '-std=c99')
+ self.build()
+
+ @skipIfNoPkgconfig
+ def test_pkg_config_option(self):
+ if not shutil.which('arm-linux-gnueabihf-pkg-config'):
+ raise unittest.SkipTest('Cross-pkgconfig not found.')
+ testdir = os.path.join(self.unit_test_dir, '57 pkg_config_path option')
+ self.init(testdir, extra_args=[
+ '-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'),
+ '-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'),
+ ])
+
+ def test_run_native_test(self):
+ '''
+ https://github.com/mesonbuild/meson/issues/7997
+ check run native test in crossbuild without exe wrapper
+ '''
+ testdir = os.path.join(self.unit_test_dir, '87 run native test')
+ stamp_file = os.path.join(self.builddir, 'native_test_has_run.stamp')
+ self.init(testdir)
+ self.build()
+ self.assertPathDoesNotExist(stamp_file)
+ self.run_tests()
+ self.assertPathExists(stamp_file)
+
+
+def should_run_cross_mingw_tests():
+ return shutil.which('x86_64-w64-mingw32-gcc') and not (is_windows() or is_cygwin())
+
+@unittest.skipUnless(not is_windows() and should_run_cross_mingw_tests(), "requires ability to cross compile with MinGW")
+class LinuxCrossMingwTests(BaseLinuxCrossTests):
+ '''
+ Tests that cross-compilation to Windows/MinGW works
+ '''
+
+ def setUp(self):
+ super().setUp()
+ self.meson_cross_files = [os.path.join(self.src_root, 'cross', 'linux-mingw-w64-64bit.txt')]
+
+ def test_exe_wrapper_behaviour(self):
+ '''
+ Test that an exe wrapper that isn't found doesn't cause compiler sanity
+ checks and compiler checks to fail, but causes configure to fail if it
+ requires running a cross-built executable (custom_target or run_target)
+ and causes the tests to be skipped if they are run.
+ '''
+ testdir = os.path.join(self.unit_test_dir, '36 exe_wrapper behaviour')
+ # Configures, builds, and tests fine by default
+ self.init(testdir)
+ self.build()
+ self.run_tests()
+ self.wipe()
+ os.mkdir(self.builddir)
+ # Change cross file to use a non-existing exe_wrapper and it should fail
+ self.meson_cross_files = [os.path.join(testdir, 'broken-cross.txt')]
+ # Force tracebacks so we can detect them properly
+ env = {'MESON_FORCE_BACKTRACE': '1'}
+ error_message = "An exe_wrapper is needed but was not found. Please define one in cross file and check the command and/or add it to PATH."
+
+ with self.assertRaises(MesonException) as cm:
+ # Must run in-process or we'll get a generic CalledProcessError
+ self.init(testdir, extra_args='-Drun-target=false',
+ inprocess=True,
+ override_envvars=env)
+ self.assertEqual(str(cm.exception), error_message)
+
+ with self.assertRaises(MesonException) as cm:
+ # Must run in-process or we'll get a generic CalledProcessError
+ self.init(testdir, extra_args='-Dcustom-target=false',
+ inprocess=True,
+ override_envvars=env)
+ self.assertEqual(str(cm.exception), error_message)
+
+ self.init(testdir, extra_args=['-Dcustom-target=false', '-Drun-target=false'],
+ override_envvars=env)
+ self.build()
+
+ with self.assertRaises(MesonException) as cm:
+ # Must run in-process or we'll get a generic CalledProcessError
+ self.run_tests(inprocess=True, override_envvars=env)
+ self.assertEqual(str(cm.exception),
+ "The exe_wrapper defined in the cross file 'broken' was not found. Please check the command and/or add it to PATH.")
+
+ @skipIfNoPkgconfig
+ def test_cross_pkg_config_option(self):
+ testdir = os.path.join(self.unit_test_dir, '57 pkg_config_path option')
+ self.init(testdir, extra_args=[
+ '-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'),
+ '-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'),
+ ])
diff --git a/unittests/linuxliketests.py b/unittests/linuxliketests.py
new file mode 100644
index 0000000..50c6b62
--- /dev/null
+++ b/unittests/linuxliketests.py
@@ -0,0 +1,1830 @@
+# Copyright 2016-2022 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import stat
+import subprocess
+import re
+import tempfile
+import textwrap
+import os
+import shutil
+import hashlib
+from unittest import mock, skipUnless, SkipTest
+from glob import glob
+from pathlib import 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 (
+ MachineChoice, is_windows, is_osx, is_cygwin, is_openbsd, is_haiku,
+ is_sunos, windows_proof_rmtree, version_compare, is_linux,
+ OptionKey, EnvironmentException
+)
+from mesonbuild.compilers import (
+ detect_c_compiler, detect_cpp_compiler, compiler_from_language,
+)
+from mesonbuild.compilers.c import AppleClangCCompiler
+from mesonbuild.compilers.cpp import AppleClangCPPCompiler
+from mesonbuild.compilers.objc import AppleClangObjCCompiler
+from mesonbuild.compilers.objcpp import AppleClangObjCPPCompiler
+from mesonbuild.dependencies import PkgConfigDependency
+import mesonbuild.modules.pkgconfig
+
+PKG_CONFIG = os.environ.get('PKG_CONFIG', 'pkg-config')
+
+
+from run_tests import (
+ get_fake_env
+)
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+def _prepend_pkg_config_path(path: str) -> str:
+ """Prepend a string value to pkg_config_path
+
+ :param path: The path to prepend
+ :return: The path, followed by any PKG_CONFIG_PATH already in the environment
+ """
+ pkgconf = os.environ.get('PKG_CONFIG_PATH')
+ if pkgconf:
+ return f'{path}{os.path.pathsep}{pkgconf}'
+ return path
+
+
+def _clang_at_least(compiler: 'Compiler', minver: str, apple_minver: T.Optional[str]) -> bool:
+ """
+ check that Clang compiler is at least a specified version, whether AppleClang or regular Clang
+
+ Parameters
+ ----------
+ compiler:
+ Meson compiler object
+ minver: str
+ Clang minimum version
+ apple_minver: str
+ AppleCLang minimum version
+
+ Returns
+ -------
+ at_least: bool
+ Clang is at least the specified version
+ """
+ if isinstance(compiler, (AppleClangCCompiler, AppleClangCPPCompiler)):
+ if apple_minver is None:
+ return False
+ return version_compare(compiler.version, apple_minver)
+ return version_compare(compiler.version, minver)
+
+@skipUnless(not is_windows(), "requires something Unix-like")
+class LinuxlikeTests(BasePlatformTests):
+ '''
+ Tests that should run on Linux, macOS, and *BSD
+ '''
+
+ def test_basic_soname(self):
+ '''
+ Test that the soname is set correctly for shared libraries. This can't
+ be an ordinary test case because we need to run `readelf` and actually
+ check the soname.
+ https://github.com/mesonbuild/meson/issues/785
+ '''
+ testdir = os.path.join(self.common_test_dir, '4 shared')
+ self.init(testdir)
+ self.build()
+ lib1 = os.path.join(self.builddir, 'libmylib.so')
+ soname = get_soname(lib1)
+ self.assertEqual(soname, 'libmylib.so')
+
+ def test_custom_soname(self):
+ '''
+ Test that the soname is set correctly for shared libraries when
+ a custom prefix and/or suffix is used. This can't be an ordinary test
+ case because we need to run `readelf` and actually check the soname.
+ https://github.com/mesonbuild/meson/issues/785
+ '''
+ testdir = os.path.join(self.common_test_dir, '24 library versions')
+ self.init(testdir)
+ self.build()
+ lib1 = os.path.join(self.builddir, 'prefixsomelib.suffix')
+ soname = get_soname(lib1)
+ self.assertEqual(soname, 'prefixsomelib.suffix')
+
+ def test_pic(self):
+ '''
+ Test that -fPIC is correctly added to static libraries when b_staticpic
+ is true and not when it is false. This can't be an ordinary test case
+ because we need to inspect the compiler database.
+ '''
+ if is_windows() or is_cygwin() or is_osx():
+ raise SkipTest('PIC not relevant')
+
+ testdir = os.path.join(self.common_test_dir, '3 static')
+ self.init(testdir)
+ compdb = self.get_compdb()
+ self.assertIn('-fPIC', compdb[0]['command'])
+ self.setconf('-Db_staticpic=false')
+ # Regenerate build
+ self.build()
+ compdb = self.get_compdb()
+ self.assertNotIn('-fPIC', compdb[0]['command'])
+
+ @mock.patch.dict(os.environ)
+ def test_pkgconfig_gen(self):
+ '''
+ Test that generated pkg-config files can be found and have the correct
+ version and link args. This can't be an ordinary test case because we
+ need to run pkg-config outside of a Meson build file.
+ https://github.com/mesonbuild/meson/issues/889
+ '''
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
+ self.init(testdir)
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ kwargs = {'required': True, 'silent': True}
+ os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir
+ foo_dep = PkgConfigDependency('libfoo', env, kwargs)
+ self.assertTrue(foo_dep.found())
+ self.assertEqual(foo_dep.get_version(), '1.0')
+ self.assertIn('-lfoo', foo_dep.get_link_args())
+ self.assertEqual(foo_dep.get_pkgconfig_variable('foo', [], None), 'bar')
+ self.assertPathEqual(foo_dep.get_pkgconfig_variable('datadir', [], None), '/usr/data')
+
+ libhello_nolib = PkgConfigDependency('libhello_nolib', env, kwargs)
+ self.assertTrue(libhello_nolib.found())
+ self.assertEqual(libhello_nolib.get_link_args(), [])
+ self.assertEqual(libhello_nolib.get_compile_args(), [])
+ self.assertEqual(libhello_nolib.get_pkgconfig_variable('foo', [], None), 'bar')
+ self.assertEqual(libhello_nolib.get_pkgconfig_variable('prefix', [], None), self.prefix)
+ if version_compare(PkgConfigDependency.check_pkgconfig(env, libhello_nolib.pkgbin),">=0.29.1"):
+ self.assertEqual(libhello_nolib.get_pkgconfig_variable('escaped_var', [], None), r'hello\ world')
+ self.assertEqual(libhello_nolib.get_pkgconfig_variable('unescaped_var', [], None), 'hello world')
+
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_id() in {'gcc', 'clang'}:
+ for name in {'ct', 'ct0'}:
+ ct_dep = PkgConfigDependency(name, env, kwargs)
+ self.assertTrue(ct_dep.found())
+ self.assertIn('-lct', ct_dep.get_link_args(raw=True))
+
+ def test_pkgconfig_gen_deps(self):
+ '''
+ Test that generated pkg-config files correctly handle dependencies
+ '''
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
+ self.init(testdir)
+ privatedir1 = self.privatedir
+
+ self.new_builddir()
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies')
+ self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': privatedir1})
+ privatedir2 = self.privatedir
+
+ env = {
+ 'PKG_CONFIG_LIBDIR': os.pathsep.join([privatedir1, privatedir2]),
+ 'PKG_CONFIG_SYSTEM_LIBRARY_PATH': '/usr/lib',
+ }
+ self._run([PKG_CONFIG, 'dependency-test', '--validate'], override_envvars=env)
+
+ # pkg-config strips some duplicated flags so we have to parse the
+ # generated file ourself.
+ expected = {
+ 'Requires': 'libexposed',
+ 'Requires.private': 'libfoo >= 1.0',
+ 'Libs': '-L${libdir} -llibmain -pthread -lcustom',
+ 'Libs.private': '-lcustom2 -L${libdir} -llibinternal',
+ 'Cflags': '-I${includedir} -pthread -DCUSTOM',
+ }
+ if is_osx() or is_haiku():
+ expected['Cflags'] = expected['Cflags'].replace('-pthread ', '')
+ with open(os.path.join(privatedir2, 'dependency-test.pc'), encoding='utf-8') as f:
+ matched_lines = 0
+ for line in f:
+ parts = line.split(':', 1)
+ if parts[0] in expected:
+ key = parts[0]
+ val = parts[1].strip()
+ expected_val = expected[key]
+ self.assertEqual(expected_val, val)
+ matched_lines += 1
+ self.assertEqual(len(expected), matched_lines)
+
+ cmd = [PKG_CONFIG, 'requires-test']
+ out = self._run(cmd + ['--print-requires'], override_envvars=env).strip().split('\n')
+ if not is_openbsd():
+ self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
+ else:
+ self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
+
+ cmd = [PKG_CONFIG, 'requires-private-test']
+ out = self._run(cmd + ['--print-requires-private'], override_envvars=env).strip().split('\n')
+ if not is_openbsd():
+ self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
+ else:
+ self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
+
+ cmd = [PKG_CONFIG, 'pub-lib-order']
+ out = self._run(cmd + ['--libs'], override_envvars=env).strip().split()
+ self.assertEqual(out, ['-llibmain2', '-llibinternal'])
+
+ # See common/44 pkgconfig-gen/meson.build for description of the case this test
+ with open(os.path.join(privatedir1, 'simple2.pc'), encoding='utf-8') as f:
+ content = f.read()
+ self.assertIn('Libs: -L${libdir} -lsimple2 -lsimple1', content)
+ self.assertIn('Libs.private: -lz', content)
+
+ with open(os.path.join(privatedir1, 'simple3.pc'), encoding='utf-8') as f:
+ content = f.read()
+ self.assertEqual(1, content.count('-lsimple3'))
+
+ with open(os.path.join(privatedir1, 'simple5.pc'), encoding='utf-8') as f:
+ content = f.read()
+ self.assertNotIn('-lstat2', content)
+
+ @mock.patch.dict(os.environ)
+ def test_pkgconfig_uninstalled(self):
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
+ self.init(testdir)
+ self.build()
+
+ os.environ['PKG_CONFIG_LIBDIR'] = os.path.join(self.builddir, 'meson-uninstalled')
+ if is_cygwin():
+ os.environ['PATH'] += os.pathsep + self.builddir
+
+ self.new_builddir()
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies')
+ self.init(testdir)
+ self.build()
+ self.run_tests()
+
+ def test_pkg_unfound(self):
+ testdir = os.path.join(self.unit_test_dir, '23 unfound pkgconfig')
+ self.init(testdir)
+ with open(os.path.join(self.privatedir, 'somename.pc'), encoding='utf-8') as f:
+ pcfile = f.read()
+ self.assertNotIn('blub_blob_blib', pcfile)
+
+ def test_symlink_builddir(self) -> None:
+ '''
+ Test using a symlink as either the builddir for "setup" or
+ the argument for "-C".
+ '''
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+
+ symdir = f'{self.builddir}-symlink'
+ os.symlink(self.builddir, symdir)
+ self.addCleanup(os.unlink, symdir)
+ self.change_builddir(symdir)
+
+ self.init(testdir)
+ self.build()
+ self._run(self.mtest_command)
+
+ def test_vala_c_warnings(self):
+ '''
+ Test that no warnings are emitted for C code generated by Vala. This
+ can't be an ordinary test case because we need to inspect the compiler
+ database.
+ https://github.com/mesonbuild/meson/issues/864
+ '''
+ if not shutil.which('valac'):
+ raise SkipTest('valac not installed.')
+ testdir = os.path.join(self.vala_test_dir, '5 target glib')
+ self.init(testdir)
+ compdb = self.get_compdb()
+ vala_command = None
+ c_command = None
+ for each in compdb:
+ if each['file'].endswith('GLib.Thread.c'):
+ vala_command = each['command']
+ elif each['file'].endswith('GLib.Thread.vala'):
+ continue
+ elif each['file'].endswith('retcode.c'):
+ c_command = each['command']
+ else:
+ m = 'Unknown file {!r} in vala_c_warnings test'.format(each['file'])
+ raise AssertionError(m)
+ self.assertIsNotNone(vala_command)
+ self.assertIsNotNone(c_command)
+ # -w suppresses all warnings, should be there in Vala but not in C
+ self.assertIn(" -w ", vala_command)
+ self.assertNotIn(" -w ", c_command)
+ # -Wall enables all warnings, should be there in C but not in Vala
+ self.assertNotIn(" -Wall ", vala_command)
+ self.assertIn(" -Wall ", c_command)
+ # -Werror converts warnings to errors, should always be there since it's
+ # injected by an unrelated piece of code and the project has werror=true
+ self.assertIn(" -Werror ", vala_command)
+ self.assertIn(" -Werror ", c_command)
+
+ @skipIfNoPkgconfig
+ def test_qtdependency_pkgconfig_detection(self):
+ '''
+ Test that qt4 and qt5 detection with pkgconfig works.
+ '''
+ # Verify Qt4 or Qt5 can be found with pkg-config
+ qt4 = subprocess.call([PKG_CONFIG, '--exists', 'QtCore'])
+ qt5 = subprocess.call([PKG_CONFIG, '--exists', 'Qt5Core'])
+ testdir = os.path.join(self.framework_test_dir, '4 qt')
+ self.init(testdir, extra_args=['-Dmethod=pkg-config'])
+ # Confirm that the dependency was found with pkg-config
+ mesonlog = self.get_meson_log_raw()
+ if qt4 == 0:
+ self.assertRegex(mesonlog,
+ r'Run-time dependency qt4 \(modules: Core\) found: YES 4.* \(pkg-config\)')
+ if qt5 == 0:
+ self.assertRegex(mesonlog,
+ r'Run-time dependency qt5 \(modules: Core\) found: YES 5.* \(pkg-config\)')
+
+ @skip_if_not_base_option('b_sanitize')
+ def test_generate_gir_with_address_sanitizer(self):
+ if is_cygwin():
+ raise SkipTest('asan not available on Cygwin')
+ if is_openbsd():
+ raise SkipTest('-fsanitize=address is not supported on OpenBSD')
+
+ testdir = os.path.join(self.framework_test_dir, '7 gnome')
+ self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
+ self.build()
+
+ def test_qt5dependency_qmake_detection(self):
+ '''
+ Test that qt5 detection with qmake works. This can't be an ordinary
+ test case because it involves setting the environment.
+ '''
+ # Verify that qmake is for Qt5
+ if not shutil.which('qmake-qt5'):
+ if not shutil.which('qmake'):
+ raise SkipTest('QMake not found')
+ output = subprocess.getoutput('qmake --version')
+ if 'Qt version 5' not in output:
+ raise SkipTest('Qmake found, but it is not for Qt 5.')
+ # Disable pkg-config codepath and force searching with qmake/qmake-qt5
+ testdir = os.path.join(self.framework_test_dir, '4 qt')
+ self.init(testdir, extra_args=['-Dmethod=qmake'])
+ # Confirm that the dependency was found with qmake
+ mesonlog = self.get_meson_log_raw()
+ self.assertRegex(mesonlog,
+ r'Run-time dependency qt5 \(modules: Core\) found: YES .* \(qmake\)\n')
+
+ def test_qt6dependency_qmake_detection(self):
+ '''
+ Test that qt6 detection with qmake works. This can't be an ordinary
+ test case because it involves setting the environment.
+ '''
+ # Verify that qmake is for Qt6
+ if not shutil.which('qmake6'):
+ if not shutil.which('qmake'):
+ raise SkipTest('QMake not found')
+ output = subprocess.getoutput('qmake --version')
+ if 'Qt version 6' not in output:
+ raise SkipTest('Qmake found, but it is not for Qt 6.')
+ # Disable pkg-config codepath and force searching with qmake/qmake-qt6
+ testdir = os.path.join(self.framework_test_dir, '4 qt')
+ self.init(testdir, extra_args=['-Dmethod=qmake'])
+ # Confirm that the dependency was found with qmake
+ mesonlog = self.get_meson_log_raw()
+ self.assertRegex(mesonlog,
+ r'Run-time dependency qt6 \(modules: Core\) found: YES .* \(qmake\)\n')
+
+ def glob_sofiles_without_privdir(self, g):
+ files = glob(g)
+ return [f for f in files if not f.endswith('.p')]
+
+ def _test_soname_impl(self, libpath, install):
+ if is_cygwin() or is_osx():
+ raise SkipTest('Test only applicable to ELF and linuxlike sonames')
+
+ testdir = os.path.join(self.unit_test_dir, '1 soname')
+ self.init(testdir)
+ self.build()
+ if install:
+ self.install()
+
+ # File without aliases set.
+ nover = os.path.join(libpath, 'libnover.so')
+ self.assertPathExists(nover)
+ self.assertFalse(os.path.islink(nover))
+ self.assertEqual(get_soname(nover), 'libnover.so')
+ self.assertEqual(len(self.glob_sofiles_without_privdir(nover[:-3] + '*')), 1)
+
+ # File with version set
+ verset = os.path.join(libpath, 'libverset.so')
+ self.assertPathExists(verset + '.4.5.6')
+ self.assertEqual(os.readlink(verset), 'libverset.so.4')
+ self.assertEqual(get_soname(verset), 'libverset.so.4')
+ self.assertEqual(len(self.glob_sofiles_without_privdir(verset[:-3] + '*')), 3)
+
+ # File with soversion set
+ soverset = os.path.join(libpath, 'libsoverset.so')
+ self.assertPathExists(soverset + '.1.2.3')
+ self.assertEqual(os.readlink(soverset), 'libsoverset.so.1.2.3')
+ self.assertEqual(get_soname(soverset), 'libsoverset.so.1.2.3')
+ self.assertEqual(len(self.glob_sofiles_without_privdir(soverset[:-3] + '*')), 2)
+
+ # File with version and soversion set to same values
+ settosame = os.path.join(libpath, 'libsettosame.so')
+ self.assertPathExists(settosame + '.7.8.9')
+ self.assertEqual(os.readlink(settosame), 'libsettosame.so.7.8.9')
+ self.assertEqual(get_soname(settosame), 'libsettosame.so.7.8.9')
+ self.assertEqual(len(self.glob_sofiles_without_privdir(settosame[:-3] + '*')), 2)
+
+ # File with version and soversion set to different values
+ bothset = os.path.join(libpath, 'libbothset.so')
+ self.assertPathExists(bothset + '.1.2.3')
+ self.assertEqual(os.readlink(bothset), 'libbothset.so.1.2.3')
+ self.assertEqual(os.readlink(bothset + '.1.2.3'), 'libbothset.so.4.5.6')
+ self.assertEqual(get_soname(bothset), 'libbothset.so.1.2.3')
+ self.assertEqual(len(self.glob_sofiles_without_privdir(bothset[:-3] + '*')), 3)
+
+ # A shared_module that is not linked to anything
+ module = os.path.join(libpath, 'libsome_module.so')
+ self.assertPathExists(module)
+ self.assertFalse(os.path.islink(module))
+ self.assertEqual(get_soname(module), None)
+
+ # A shared_module that is not linked to an executable with link_with:
+ module = os.path.join(libpath, 'liblinked_module1.so')
+ self.assertPathExists(module)
+ self.assertFalse(os.path.islink(module))
+ self.assertEqual(get_soname(module), 'liblinked_module1.so')
+
+ # A shared_module that is not linked to an executable with dependencies:
+ module = os.path.join(libpath, 'liblinked_module2.so')
+ self.assertPathExists(module)
+ self.assertFalse(os.path.islink(module))
+ self.assertEqual(get_soname(module), 'liblinked_module2.so')
+
+ def test_soname(self):
+ self._test_soname_impl(self.builddir, False)
+
+ def test_installed_soname(self):
+ libdir = self.installdir + os.path.join(self.prefix, self.libdir)
+ self._test_soname_impl(libdir, True)
+
+ def test_compiler_check_flags_order(self):
+ '''
+ Test that compiler check flags override all other flags. This can't be
+ an ordinary test case because it needs the environment to be set.
+ '''
+ testdir = os.path.join(self.common_test_dir, '36 has function')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cpp = detect_cpp_compiler(env, MachineChoice.HOST)
+ Oflag = '-O3'
+ OflagCPP = Oflag
+ if cpp.get_id() in ('clang', 'gcc'):
+ # prevent developers from adding "int main(int argc, char **argv)"
+ # to small Meson checks unless these parameters are actually used
+ OflagCPP += ' -Werror=unused-parameter'
+ env = {'CFLAGS': Oflag,
+ 'CXXFLAGS': OflagCPP}
+ self.init(testdir, override_envvars=env)
+ cmds = self.get_meson_log_compiler_checks()
+ for cmd in cmds:
+ if cmd[0] == 'ccache':
+ cmd = cmd[1:]
+ # Verify that -I flags from the `args` kwarg are first
+ # This is set in the '36 has function' test case
+ self.assertEqual(cmd[1], '-I/tmp')
+ # Verify that -O3 set via the environment is overridden by -O0
+ Oargs = [arg for arg in cmd if arg.startswith('-O')]
+ self.assertEqual(Oargs, [Oflag, '-O0'])
+
+ def _test_stds_impl(self, testdir: str, compiler: 'Compiler') -> None:
+ has_cpp17 = (compiler.get_id() not in {'clang', 'gcc'} or
+ compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=5.0.0', '>=9.1') or
+ compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=5.0.0'))
+ has_cpp2a_c17 = (compiler.get_id() not in {'clang', 'gcc'} or
+ compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=6.0.0', '>=10.0') or
+ compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
+ has_cpp20 = (compiler.get_id() not in {'clang', 'gcc'} or
+ compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=10.0.0', None) or
+ compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=10.0.0'))
+ has_c18 = (compiler.get_id() not in {'clang', 'gcc'} or
+ compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=8.0.0', '>=11.0') or
+ compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
+ # Check that all the listed -std=xxx options for this compiler work just fine when used
+ # https://en.wikipedia.org/wiki/Xcode#Latest_versions
+ # https://www.gnu.org/software/gcc/projects/cxx-status.html
+ key = OptionKey('std', lang=compiler.language)
+ for v in compiler.get_options()[key].choices:
+ # we do it like this to handle gnu++17,c++17 and gnu17,c17 cleanly
+ # thus, C++ first
+ if '++17' in v and not has_cpp17:
+ continue
+ elif '++2a' in v and not has_cpp2a_c17: # https://en.cppreference.com/w/cpp/compiler_support
+ continue
+ elif '++20' in v and not has_cpp20:
+ continue
+ # now C
+ elif '17' in v and not has_cpp2a_c17:
+ continue
+ elif '18' in v and not has_c18:
+ continue
+ self.init(testdir, extra_args=[f'-D{key!s}={v}'])
+ cmd = self.get_compdb()[0]['command']
+ # c++03 and gnu++03 are not understood by ICC, don't try to look for them
+ skiplist = frozenset([
+ ('intel', 'c++03'),
+ ('intel', 'gnu++03')])
+ if v != 'none' and not (compiler.get_id(), v) in skiplist:
+ cmd_std = f" -std={v} "
+ self.assertIn(cmd_std, cmd)
+ try:
+ self.build()
+ except Exception:
+ print(f'{key!s} was {v!r}')
+ raise
+ self.wipe()
+ # Check that an invalid std option in CFLAGS/CPPFLAGS fails
+ # Needed because by default ICC ignores invalid options
+ cmd_std = '-std=FAIL'
+ if compiler.language == 'c':
+ env_flag_name = 'CFLAGS'
+ elif compiler.language == 'cpp':
+ env_flag_name = 'CXXFLAGS'
+ else:
+ raise NotImplementedError(f'Language {compiler.language} not defined.')
+ env = {}
+ env[env_flag_name] = cmd_std
+ with self.assertRaises((subprocess.CalledProcessError, EnvironmentException),
+ msg='C compiler should have failed with -std=FAIL'):
+ self.init(testdir, override_envvars = env)
+ # ICC won't fail in the above because additional flags are needed to
+ # make unknown -std=... options errors.
+ self.build()
+
+ def test_compiler_c_stds(self):
+ '''
+ Test that C stds specified for this compiler can all be used. Can't be
+ an ordinary test because it requires passing options to meson.
+ '''
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ self._test_stds_impl(testdir, cc)
+
+ def test_compiler_cpp_stds(self):
+ '''
+ Test that C++ stds specified for this compiler can all be used. Can't
+ be an ordinary test because it requires passing options to meson.
+ '''
+ testdir = os.path.join(self.common_test_dir, '2 cpp')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cpp = detect_cpp_compiler(env, MachineChoice.HOST)
+ self._test_stds_impl(testdir, cpp)
+
+ def test_unity_subproj(self):
+ testdir = os.path.join(self.common_test_dir, '42 subproject')
+ self.init(testdir, extra_args='--unity=subprojects')
+ pdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/simpletest*.p'))
+ self.assertEqual(len(pdirs), 1)
+ self.assertPathExists(os.path.join(pdirs[0], 'simpletest-unity0.c'))
+ sdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/*sublib*.p'))
+ self.assertEqual(len(sdirs), 1)
+ self.assertPathExists(os.path.join(sdirs[0], 'sublib-unity0.c'))
+ self.assertPathDoesNotExist(os.path.join(self.builddir, 'user@exe/user-unity.c'))
+ self.build()
+
+ def test_installed_modes(self):
+ '''
+ Test that files installed by these tests have the correct permissions.
+ Can't be an ordinary test because our installed_files.txt is very basic.
+ '''
+ if is_cygwin():
+ self.new_builddir_in_tempdir()
+ # Test file modes
+ testdir = os.path.join(self.common_test_dir, '12 data')
+ self.init(testdir)
+ self.install()
+
+ f = os.path.join(self.installdir, 'etc', 'etcfile.dat')
+ found_mode = stat.filemode(os.stat(f).st_mode)
+ want_mode = 'rw-------'
+ self.assertEqual(want_mode, found_mode[1:])
+
+ f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh')
+ statf = os.stat(f)
+ found_mode = stat.filemode(statf.st_mode)
+ want_mode = 'rwxr-sr-x'
+ self.assertEqual(want_mode, found_mode[1:])
+ if os.getuid() == 0:
+ # The chown failed nonfatally if we're not root
+ self.assertEqual(0, statf.st_uid)
+ self.assertEqual(0, statf.st_gid)
+
+ f = os.path.join(self.installdir, 'usr', 'share', 'progname',
+ 'fileobject_datafile.dat')
+ orig = os.path.join(testdir, 'fileobject_datafile.dat')
+ statf = os.stat(f)
+ statorig = os.stat(orig)
+ found_mode = stat.filemode(statf.st_mode)
+ orig_mode = stat.filemode(statorig.st_mode)
+ self.assertEqual(orig_mode[1:], found_mode[1:])
+ self.assertEqual(os.getuid(), statf.st_uid)
+ if os.getuid() == 0:
+ # The chown failed nonfatally if we're not root
+ self.assertEqual(0, statf.st_gid)
+
+ self.wipe()
+ # Test directory modes
+ testdir = os.path.join(self.common_test_dir, '59 install subdir')
+ self.init(testdir)
+ self.install()
+
+ f = os.path.join(self.installdir, 'usr', 'share', 'sub1', 'second.dat')
+ statf = os.stat(f)
+ found_mode = stat.filemode(statf.st_mode)
+ want_mode = 'rwxr-x--x'
+ self.assertEqual(want_mode, found_mode[1:])
+ if os.getuid() == 0:
+ # The chown failed nonfatally if we're not root
+ self.assertEqual(0, statf.st_uid)
+
+ def test_installed_modes_extended(self):
+ '''
+ Test that files are installed with correct permissions using install_mode.
+ '''
+ if is_cygwin():
+ self.new_builddir_in_tempdir()
+ testdir = os.path.join(self.common_test_dir, '190 install_mode')
+ self.init(testdir)
+ self.build()
+ self.install()
+
+ for fsobj, want_mode in [
+ ('bin', 'drwxr-x---'),
+ ('bin/runscript.sh', '-rwxr-sr-x'),
+ ('bin/trivialprog', '-rwxr-sr-x'),
+ ('include', 'drwxr-x---'),
+ ('include/config.h', '-rw-rwSr--'),
+ ('include/rootdir.h', '-r--r--r--'),
+ ('lib', 'drwxr-x---'),
+ ('lib/libstat.a', '-rw---Sr--'),
+ ('share', 'drwxr-x---'),
+ ('share/man', 'drwxr-x---'),
+ ('share/man/man1', 'drwxr-x---'),
+ ('share/man/man1/foo.1', '-r--r--r--'),
+ ('share/sub1', 'drwxr-x---'),
+ ('share/sub1/second.dat', '-rwxr-x--x'),
+ ('subdir', 'drwxr-x---'),
+ ('subdir/data.dat', '-rw-rwSr--'),
+ ]:
+ f = os.path.join(self.installdir, 'usr', *fsobj.split('/'))
+ found_mode = stat.filemode(os.stat(f).st_mode)
+ self.assertEqual(want_mode, found_mode,
+ msg=('Expected file %s to have mode %s but found %s instead.' %
+ (fsobj, want_mode, found_mode)))
+ # Ensure that introspect --installed works on all types of files
+ # FIXME: also verify the files list
+ self.introspect('--installed')
+
+ def test_install_umask(self):
+ '''
+ Test that files are installed with correct permissions using default
+ install umask of 022, regardless of the umask at time the worktree
+ was checked out or the build was executed.
+ '''
+ if is_cygwin():
+ self.new_builddir_in_tempdir()
+ # Copy source tree to a temporary directory and change permissions
+ # there to simulate a checkout with umask 002.
+ orig_testdir = os.path.join(self.unit_test_dir, '26 install umask')
+ # Create a new testdir under tmpdir.
+ tmpdir = os.path.realpath(tempfile.mkdtemp())
+ self.addCleanup(windows_proof_rmtree, tmpdir)
+ testdir = os.path.join(tmpdir, '26 install umask')
+ # Copy the tree using shutil.copyfile, which will use the current umask
+ # instead of preserving permissions of the old tree.
+ save_umask = os.umask(0o002)
+ self.addCleanup(os.umask, save_umask)
+ shutil.copytree(orig_testdir, testdir, copy_function=shutil.copyfile)
+ # Preserve the executable status of subdir/sayhello though.
+ os.chmod(os.path.join(testdir, 'subdir', 'sayhello'), 0o775)
+ self.init(testdir)
+ # Run the build under a 027 umask now.
+ os.umask(0o027)
+ self.build()
+ # And keep umask 027 for the install step too.
+ self.install()
+
+ for executable in [
+ 'bin/prog',
+ 'share/subdir/sayhello',
+ ]:
+ f = os.path.join(self.installdir, 'usr', *executable.split('/'))
+ found_mode = stat.filemode(os.stat(f).st_mode)
+ want_mode = '-rwxr-xr-x'
+ self.assertEqual(want_mode, found_mode,
+ msg=('Expected file %s to have mode %s but found %s instead.' %
+ (executable, want_mode, found_mode)))
+
+ for directory in [
+ 'usr',
+ 'usr/bin',
+ 'usr/include',
+ 'usr/share',
+ 'usr/share/man',
+ 'usr/share/man/man1',
+ 'usr/share/subdir',
+ ]:
+ f = os.path.join(self.installdir, *directory.split('/'))
+ found_mode = stat.filemode(os.stat(f).st_mode)
+ want_mode = 'drwxr-xr-x'
+ self.assertEqual(want_mode, found_mode,
+ msg=('Expected directory %s to have mode %s but found %s instead.' %
+ (directory, want_mode, found_mode)))
+
+ for datafile in [
+ 'include/sample.h',
+ 'share/datafile.cat',
+ 'share/file.dat',
+ 'share/man/man1/prog.1',
+ 'share/subdir/datafile.dog',
+ ]:
+ f = os.path.join(self.installdir, 'usr', *datafile.split('/'))
+ found_mode = stat.filemode(os.stat(f).st_mode)
+ want_mode = '-rw-r--r--'
+ self.assertEqual(want_mode, found_mode,
+ msg=('Expected file %s to have mode %s but found %s instead.' %
+ (datafile, want_mode, found_mode)))
+
+ def test_cpp_std_override(self):
+ testdir = os.path.join(self.unit_test_dir, '6 std override')
+ self.init(testdir)
+ compdb = self.get_compdb()
+ # Don't try to use -std=c++03 as a check for the
+ # presence of a compiler flag, as ICC does not
+ # support it.
+ for i in compdb:
+ if 'prog98' in i['file']:
+ c98_comp = i['command']
+ if 'prog11' in i['file']:
+ c11_comp = i['command']
+ if 'progp' in i['file']:
+ plain_comp = i['command']
+ self.assertNotEqual(len(plain_comp), 0)
+ self.assertIn('-std=c++98', c98_comp)
+ self.assertNotIn('-std=c++11', c98_comp)
+ self.assertIn('-std=c++11', c11_comp)
+ self.assertNotIn('-std=c++98', c11_comp)
+ self.assertNotIn('-std=c++98', plain_comp)
+ self.assertNotIn('-std=c++11', plain_comp)
+ # Now werror
+ self.assertIn('-Werror', plain_comp)
+ self.assertNotIn('-Werror', c98_comp)
+
+ def test_run_installed(self):
+ if is_cygwin() or is_osx():
+ raise SkipTest('LD_LIBRARY_PATH and RPATH not applicable')
+
+ testdir = os.path.join(self.unit_test_dir, '7 run installed')
+ self.init(testdir)
+ self.build()
+ self.install()
+ installed_exe = os.path.join(self.installdir, 'usr/bin/prog')
+ installed_libdir = os.path.join(self.installdir, 'usr/foo')
+ installed_lib = os.path.join(installed_libdir, 'libfoo.so')
+ self.assertTrue(os.path.isfile(installed_exe))
+ self.assertTrue(os.path.isdir(installed_libdir))
+ self.assertTrue(os.path.isfile(installed_lib))
+ # Must fail when run without LD_LIBRARY_PATH to ensure that
+ # rpath has been properly stripped rather than pointing to the builddir.
+ self.assertNotEqual(subprocess.call(installed_exe, stderr=subprocess.DEVNULL), 0)
+ # When LD_LIBRARY_PATH is set it should start working.
+ # For some reason setting LD_LIBRARY_PATH in os.environ fails
+ # when all tests are run (but works when only this test is run),
+ # but doing this explicitly works.
+ env = os.environ.copy()
+ env['LD_LIBRARY_PATH'] = ':'.join([installed_libdir, env.get('LD_LIBRARY_PATH', '')])
+ self.assertEqual(subprocess.call(installed_exe, env=env), 0)
+ # Ensure that introspect --installed works
+ installed = self.introspect('--installed')
+ for v in installed.values():
+ self.assertTrue('prog' in v or 'foo' in v)
+
+ @skipIfNoPkgconfig
+ def test_order_of_l_arguments(self):
+ testdir = os.path.join(self.unit_test_dir, '8 -L -l order')
+ self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir})
+ # NOTE: .pc file has -Lfoo -lfoo -Lbar -lbar but pkg-config reorders
+ # the flags before returning them to -Lfoo -Lbar -lfoo -lbar
+ # but pkgconf seems to not do that. Sigh. Support both.
+ expected_order = [('-L/me/first', '-lfoo1'),
+ ('-L/me/second', '-lfoo2'),
+ ('-L/me/first', '-L/me/second'),
+ ('-lfoo1', '-lfoo2'),
+ ('-L/me/second', '-L/me/third'),
+ ('-L/me/third', '-L/me/fourth',),
+ ('-L/me/third', '-lfoo3'),
+ ('-L/me/fourth', '-lfoo4'),
+ ('-lfoo3', '-lfoo4'),
+ ]
+ with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as ifile:
+ for line in ifile:
+ if expected_order[0][0] in line:
+ for first, second in expected_order:
+ self.assertLess(line.index(first), line.index(second))
+ return
+ raise RuntimeError('Linker entries not found in the Ninja file.')
+
+ def test_introspect_dependencies(self):
+ '''
+ Tests that mesonintrospect --dependencies returns expected output.
+ '''
+ testdir = os.path.join(self.framework_test_dir, '7 gnome')
+ self.init(testdir)
+ glib_found = False
+ gobject_found = False
+ deps = self.introspect('--dependencies')
+ self.assertIsInstance(deps, list)
+ for dep in deps:
+ self.assertIsInstance(dep, dict)
+ self.assertIn('name', dep)
+ self.assertIn('compile_args', dep)
+ self.assertIn('link_args', dep)
+ if dep['name'] == 'glib-2.0':
+ glib_found = True
+ elif dep['name'] == 'gobject-2.0':
+ gobject_found = True
+ self.assertTrue(glib_found)
+ self.assertTrue(gobject_found)
+ if subprocess.call([PKG_CONFIG, '--exists', 'glib-2.0 >= 2.56.2']) != 0:
+ raise SkipTest('glib >= 2.56.2 needed for the rest')
+ targets = self.introspect('--targets')
+ docbook_target = None
+ for t in targets:
+ if t['name'] == 'generated-gdbus-docbook':
+ docbook_target = t
+ break
+ self.assertIsInstance(docbook_target, dict)
+ self.assertEqual(os.path.basename(t['filename'][0]), 'generated-gdbus-doc-' + os.path.basename(t['target_sources'][0]['sources'][0]))
+
+ def test_introspect_installed(self):
+ testdir = os.path.join(self.linuxlike_test_dir, '7 library versions')
+ self.init(testdir)
+
+ install = self.introspect('--installed')
+ install = {os.path.basename(k): v for k, v in install.items()}
+ print(install)
+ if is_osx():
+ the_truth = {
+ 'libmodule.dylib': '/usr/lib/libmodule.dylib',
+ 'libnoversion.dylib': '/usr/lib/libnoversion.dylib',
+ 'libonlysoversion.5.dylib': '/usr/lib/libonlysoversion.5.dylib',
+ 'libonlysoversion.dylib': '/usr/lib/libonlysoversion.dylib',
+ 'libonlyversion.1.dylib': '/usr/lib/libonlyversion.1.dylib',
+ 'libonlyversion.dylib': '/usr/lib/libonlyversion.dylib',
+ 'libsome.0.dylib': '/usr/lib/libsome.0.dylib',
+ 'libsome.dylib': '/usr/lib/libsome.dylib',
+ }
+ the_truth_2 = {'/usr/lib/libsome.dylib',
+ '/usr/lib/libsome.0.dylib',
+ }
+ else:
+ the_truth = {
+ 'libmodule.so': '/usr/lib/libmodule.so',
+ 'libnoversion.so': '/usr/lib/libnoversion.so',
+ 'libonlysoversion.so': '/usr/lib/libonlysoversion.so',
+ 'libonlysoversion.so.5': '/usr/lib/libonlysoversion.so.5',
+ 'libonlyversion.so': '/usr/lib/libonlyversion.so',
+ 'libonlyversion.so.1': '/usr/lib/libonlyversion.so.1',
+ 'libonlyversion.so.1.4.5': '/usr/lib/libonlyversion.so.1.4.5',
+ 'libsome.so': '/usr/lib/libsome.so',
+ 'libsome.so.0': '/usr/lib/libsome.so.0',
+ 'libsome.so.1.2.3': '/usr/lib/libsome.so.1.2.3',
+ }
+ the_truth_2 = {'/usr/lib/libsome.so',
+ '/usr/lib/libsome.so.0',
+ '/usr/lib/libsome.so.1.2.3'}
+ self.assertDictEqual(install, the_truth)
+
+ targets = self.introspect('--targets')
+ for t in targets:
+ if t['name'] != 'some':
+ continue
+ self.assertSetEqual(the_truth_2, set(t['install_filename']))
+
+ def test_build_rpath(self):
+ if is_cygwin():
+ raise SkipTest('Windows PE/COFF binaries do not use RPATH')
+ testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
+ self.init(testdir)
+ self.build()
+ build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
+ self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
+ build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx'))
+ self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
+ self.install()
+ install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog'))
+ self.assertEqual(install_rpath, '/baz')
+ install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx'))
+ self.assertEqual(install_rpath, 'baz')
+
+ @skipIfNoPkgconfig
+ def test_build_rpath_pkgconfig(self):
+ '''
+ Test that current build artefacts (libs) are found first on the rpath,
+ manually specified rpath comes second and additional rpath elements (from
+ pkg-config files) come last
+ '''
+ if is_cygwin():
+ raise SkipTest('Windows PE/COFF binaries do not use RPATH')
+ testdir = os.path.join(self.unit_test_dir, '89 pkgconfig build rpath order')
+ self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir})
+ self.build()
+ build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
+ self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy')
+ build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx'))
+ self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy')
+ self.install()
+ install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog'))
+ self.assertEqual(install_rpath, '/baz:/foo/dummy')
+ install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx'))
+ self.assertEqual(install_rpath, 'baz:/foo/dummy')
+
+ def test_global_rpath(self):
+ if is_cygwin():
+ raise SkipTest('Windows PE/COFF binaries do not use RPATH')
+ if is_osx():
+ raise SkipTest('Global RPATHs via LDFLAGS not yet supported on MacOS (does anybody need it?)')
+
+ testdir = os.path.join(self.unit_test_dir, '79 global-rpath')
+ oldinstalldir = self.installdir
+
+ # Build and install an external library without DESTDIR.
+ # The external library generates a .pc file without an rpath.
+ yonder_dir = os.path.join(testdir, 'yonder')
+ yonder_prefix = os.path.join(oldinstalldir, 'yonder')
+ yonder_libdir = os.path.join(yonder_prefix, self.libdir)
+ self.prefix = yonder_prefix
+ self.installdir = yonder_prefix
+ self.init(yonder_dir)
+ self.build()
+ self.install(use_destdir=False)
+
+ # Since rpath has multiple valid formats we need to
+ # test that they are all properly used.
+ rpath_formats = [
+ ('-Wl,-rpath=', False),
+ ('-Wl,-rpath,', False),
+ ('-Wl,--just-symbols=', True),
+ ('-Wl,--just-symbols,', True),
+ ('-Wl,-R', False),
+ ('-Wl,-R,', False)
+ ]
+ for rpath_format, exception in rpath_formats:
+ # Build an app that uses that installed library.
+ # Supply the rpath to the installed library via LDFLAGS
+ # (as systems like buildroot and guix are wont to do)
+ # and verify install preserves that rpath.
+ self.new_builddir()
+ env = {'LDFLAGS': rpath_format + yonder_libdir,
+ 'PKG_CONFIG_PATH': os.path.join(yonder_libdir, 'pkgconfig')}
+ if exception:
+ with self.assertRaises(subprocess.CalledProcessError):
+ self.init(testdir, override_envvars=env)
+ continue
+ self.init(testdir, override_envvars=env)
+ self.build()
+ self.install(use_destdir=False)
+ got_rpath = get_rpath(os.path.join(yonder_prefix, 'bin/rpathified'))
+ self.assertEqual(got_rpath, yonder_libdir, rpath_format)
+
+ @skip_if_not_base_option('b_sanitize')
+ def test_pch_with_address_sanitizer(self):
+ if is_cygwin():
+ raise SkipTest('asan not available on Cygwin')
+ if is_openbsd():
+ raise SkipTest('-fsanitize=address is not supported on OpenBSD')
+
+ testdir = os.path.join(self.common_test_dir, '13 pch')
+ self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
+ self.build()
+ compdb = self.get_compdb()
+ for i in compdb:
+ self.assertIn("-fsanitize=address", i["command"])
+
+ def test_cross_find_program(self):
+ testdir = os.path.join(self.unit_test_dir, '11 cross prog')
+ crossfile = tempfile.NamedTemporaryFile(mode='w')
+ print(os.path.join(testdir, 'some_cross_tool.py'))
+
+ tool_path = os.path.join(testdir, 'some_cross_tool.py')
+
+ crossfile.write(textwrap.dedent(f'''\
+ [binaries]
+ c = '{shutil.which('gcc' if is_sunos() else 'cc')}'
+ ar = '{shutil.which('ar')}'
+ strip = '{shutil.which('strip')}'
+ sometool.py = ['{tool_path}']
+ someothertool.py = '{tool_path}'
+
+ [properties]
+
+ [host_machine]
+ system = 'linux'
+ cpu_family = 'arm'
+ cpu = 'armv7' # Not sure if correct.
+ endian = 'little'
+ '''))
+ crossfile.flush()
+ self.meson_cross_files = [crossfile.name]
+ self.init(testdir)
+
+ def test_reconfigure(self):
+ testdir = os.path.join(self.unit_test_dir, '13 reconfigure')
+ self.init(testdir, extra_args=['-Db_coverage=true'], default_args=False)
+ self.build('reconfigure')
+
+ def test_vala_generated_source_buildir_inside_source_tree(self):
+ '''
+ Test that valac outputs generated C files in the expected location when
+ the builddir is a subdir of the source tree.
+ '''
+ if not shutil.which('valac'):
+ raise SkipTest('valac not installed.')
+
+ testdir = os.path.join(self.vala_test_dir, '8 generated sources')
+ newdir = os.path.join(self.builddir, 'srctree')
+ shutil.copytree(testdir, newdir)
+ testdir = newdir
+ # New builddir
+ builddir = os.path.join(testdir, 'subdir/_build')
+ os.makedirs(builddir, exist_ok=True)
+ self.change_builddir(builddir)
+ self.init(testdir)
+ self.build()
+
+ def test_old_gnome_module_codepaths(self):
+ '''
+ A lot of code in the GNOME module is conditional on the version of the
+ glib tools that are installed, and breakages in the old code can slip
+ by once the CI has a newer glib version. So we force the GNOME module
+ to pretend that it's running on an ancient glib so the fallback code is
+ also tested.
+ '''
+ testdir = os.path.join(self.framework_test_dir, '7 gnome')
+ with mock.patch('mesonbuild.modules.gnome.GnomeModule._get_native_glib_version', mock.Mock(return_value='2.20')):
+ env = {'MESON_UNIT_TEST_PRETEND_GLIB_OLD': "1"}
+ self.init(testdir,
+ inprocess=True,
+ override_envvars=env)
+ self.build(override_envvars=env)
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_usage(self):
+ testdir1 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency')
+ testdir2 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependee')
+ if subprocess.call([PKG_CONFIG, '--cflags', 'glib-2.0'],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL) != 0:
+ raise SkipTest('Glib 2.0 dependency not available.')
+ with tempfile.TemporaryDirectory() as tempdirname:
+ self.init(testdir1, extra_args=['--prefix=' + tempdirname, '--libdir=lib'], default_args=False)
+ self.install(use_destdir=False)
+ shutil.rmtree(self.builddir)
+ os.mkdir(self.builddir)
+ pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
+ self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'libpkgdep.pc')))
+ lib_dir = os.path.join(tempdirname, 'lib')
+ myenv = os.environ.copy()
+ myenv['PKG_CONFIG_PATH'] = pkg_dir
+ # Private internal libraries must not leak out.
+ pkg_out = subprocess.check_output([PKG_CONFIG, '--static', '--libs', 'libpkgdep'], env=myenv)
+ self.assertNotIn(b'libpkgdep-int', pkg_out, 'Internal library leaked out.')
+ # Dependencies must not leak to cflags when building only a shared library.
+ pkg_out = subprocess.check_output([PKG_CONFIG, '--cflags', 'libpkgdep'], env=myenv)
+ self.assertNotIn(b'glib', pkg_out, 'Internal dependency leaked to headers.')
+ # Test that the result is usable.
+ self.init(testdir2, override_envvars=myenv)
+ self.build(override_envvars=myenv)
+ myenv = os.environ.copy()
+ myenv['LD_LIBRARY_PATH'] = ':'.join([lib_dir, myenv.get('LD_LIBRARY_PATH', '')])
+ if is_cygwin():
+ bin_dir = os.path.join(tempdirname, 'bin')
+ myenv['PATH'] = bin_dir + os.pathsep + myenv['PATH']
+ self.assertTrue(os.path.isdir(lib_dir))
+ test_exe = os.path.join(self.builddir, 'pkguser')
+ self.assertTrue(os.path.isfile(test_exe))
+ subprocess.check_call(test_exe, env=myenv)
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_relative_paths(self):
+ testdir = os.path.join(self.unit_test_dir, '61 pkgconfig relative paths')
+ pkg_dir = os.path.join(testdir, 'pkgconfig')
+ self.assertPathExists(os.path.join(pkg_dir, 'librelativepath.pc'))
+
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='')
+ kwargs = {'required': True, 'silent': True}
+ relative_path_dep = PkgConfigDependency('librelativepath', env, kwargs)
+ self.assertTrue(relative_path_dep.found())
+
+ # Ensure link_args are properly quoted
+ libpath = Path(self.builddir) / '../relativepath/lib'
+ link_args = ['-L' + libpath.as_posix(), '-lrelativepath']
+ self.assertEqual(relative_path_dep.get_link_args(), link_args)
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_duplicate_path_entries(self):
+ testdir = os.path.join(self.unit_test_dir, '111 pkgconfig duplicate path entries')
+ pkg_dir = os.path.join(testdir, 'pkgconfig')
+
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='')
+
+ # Regression test: This used to modify the value of `pkg_config_path`
+ # option, adding the meson-uninstalled directory to it.
+ PkgConfigDependency.setup_env({}, env, MachineChoice.HOST, uninstalled=True)
+
+ pkg_config_path = env.coredata.options[OptionKey('pkg_config_path')].value
+ self.assertEqual(pkg_config_path, [pkg_dir])
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_internal_libraries(self):
+ '''
+ '''
+ with tempfile.TemporaryDirectory() as tempdirname:
+ # build library
+ testdirbase = os.path.join(self.unit_test_dir, '32 pkgconfig use libraries')
+ testdirlib = os.path.join(testdirbase, 'lib')
+ self.init(testdirlib, extra_args=['--prefix=' + tempdirname,
+ '--libdir=lib',
+ '--default-library=static'], default_args=False)
+ self.build()
+ self.install(use_destdir=False)
+
+ # build user of library
+ pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
+ self.new_builddir()
+ self.init(os.path.join(testdirbase, 'app'),
+ override_envvars={'PKG_CONFIG_PATH': pkg_dir})
+ self.build()
+
+ @skipIfNoPkgconfig
+ def test_static_archive_stripping(self):
+ '''
+ Check that Meson produces valid static archives with --strip enabled
+ '''
+ with tempfile.TemporaryDirectory() as tempdirname:
+ testdirbase = os.path.join(self.unit_test_dir, '65 static archive stripping')
+
+ # build lib
+ self.new_builddir()
+ testdirlib = os.path.join(testdirbase, 'lib')
+ testlibprefix = os.path.join(tempdirname, 'libprefix')
+ self.init(testdirlib, extra_args=['--prefix=' + testlibprefix,
+ '--libdir=lib',
+ '--default-library=static',
+ '--buildtype=debug',
+ '--strip'], default_args=False)
+ self.build()
+ self.install(use_destdir=False)
+
+ # build executable (uses lib, fails if static archive has been stripped incorrectly)
+ pkg_dir = os.path.join(testlibprefix, 'lib/pkgconfig')
+ self.new_builddir()
+ self.init(os.path.join(testdirbase, 'app'),
+ override_envvars={'PKG_CONFIG_PATH': pkg_dir})
+ self.build()
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_formatting(self):
+ testdir = os.path.join(self.unit_test_dir, '38 pkgconfig format')
+ self.init(testdir)
+ myenv = os.environ.copy()
+ myenv['PKG_CONFIG_PATH'] = _prepend_pkg_config_path(self.privatedir)
+ stdo = subprocess.check_output([PKG_CONFIG, '--libs-only-l', 'libsomething'], env=myenv)
+ deps = [b'-lgobject-2.0', b'-lgio-2.0', b'-lglib-2.0', b'-lsomething']
+ if is_windows() or is_cygwin() or is_osx() or is_openbsd():
+ # On Windows, libintl is a separate library
+ deps.append(b'-lintl')
+ self.assertEqual(set(deps), set(stdo.split()))
+
+ @skipIfNoPkgconfig
+ @skip_if_not_language('cs')
+ def test_pkgconfig_csharp_library(self):
+ testdir = os.path.join(self.unit_test_dir, '49 pkgconfig csharp library')
+ self.init(testdir)
+ myenv = os.environ.copy()
+ myenv['PKG_CONFIG_PATH'] = _prepend_pkg_config_path(self.privatedir)
+ stdo = subprocess.check_output([PKG_CONFIG, '--libs', 'libsomething'], env=myenv)
+
+ self.assertEqual("-r/usr/lib/libsomething.dll", str(stdo.decode('ascii')).strip())
+
+ @skipIfNoPkgconfig
+ def test_pkgconfig_link_order(self):
+ '''
+ Test that libraries are listed before their dependencies.
+ '''
+ testdir = os.path.join(self.unit_test_dir, '52 pkgconfig static link order')
+ self.init(testdir)
+ myenv = os.environ.copy()
+ myenv['PKG_CONFIG_PATH'] = _prepend_pkg_config_path(self.privatedir)
+ stdo = subprocess.check_output([PKG_CONFIG, '--libs', 'libsomething'], env=myenv)
+ deps = stdo.split()
+ self.assertLess(deps.index(b'-lsomething'), deps.index(b'-ldependency'))
+
+ def test_deterministic_dep_order(self):
+ '''
+ Test that the dependencies are always listed in a deterministic order.
+ '''
+ testdir = os.path.join(self.unit_test_dir, '42 dep order')
+ self.init(testdir)
+ with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
+ for line in bfile:
+ if 'build myexe:' in line or 'build myexe.exe:' in line:
+ self.assertIn('liblib1.a liblib2.a', line)
+ return
+ raise RuntimeError('Could not find the build rule')
+
+ def test_deterministic_rpath_order(self):
+ '''
+ Test that the rpaths are always listed in a deterministic order.
+ '''
+ if is_cygwin():
+ raise SkipTest('rpath are not used on Cygwin')
+ testdir = os.path.join(self.unit_test_dir, '41 rpath order')
+ self.init(testdir)
+ if is_osx():
+ rpathre = re.compile(r'-rpath,.*/subprojects/sub1.*-rpath,.*/subprojects/sub2')
+ else:
+ rpathre = re.compile(r'-rpath,\$\$ORIGIN/subprojects/sub1:\$\$ORIGIN/subprojects/sub2')
+ with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
+ for line in bfile:
+ if '-rpath' in line:
+ self.assertRegex(line, rpathre)
+ return
+ raise RuntimeError('Could not find the rpath')
+
+ def test_override_with_exe_dep(self):
+ '''
+ Test that we produce the correct dependencies when a program is overridden with an executable.
+ '''
+ testdir = os.path.join(self.src_root, 'test cases', 'native', '9 override with exe')
+ self.init(testdir)
+ with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
+ for line in bfile:
+ if 'main1.c:' in line or 'main2.c:' in line:
+ self.assertIn('| subprojects/sub/foobar', line)
+
+ @skipIfNoPkgconfig
+ def test_usage_external_library(self):
+ '''
+ Test that uninstalled usage of an external library (from the system or
+ PkgConfigDependency) works. On macOS, this workflow works out of the
+ box. On Linux, BSDs, Windows, etc, you need to set extra arguments such
+ as LD_LIBRARY_PATH, etc, so this test is skipped.
+
+ The system library is found with cc.find_library() and pkg-config deps.
+ '''
+ oldprefix = self.prefix
+ # Install external library so we can find it
+ testdir = os.path.join(self.unit_test_dir, '39 external, internal library rpath', 'external library')
+ # install into installdir without using DESTDIR
+ installdir = self.installdir
+ self.prefix = installdir
+ self.init(testdir)
+ self.prefix = oldprefix
+ self.build()
+ self.install(use_destdir=False)
+ ## New builddir for the consumer
+ self.new_builddir()
+ env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir),
+ 'PKG_CONFIG_PATH': _prepend_pkg_config_path(os.path.join(installdir, self.libdir, 'pkgconfig'))}
+ testdir = os.path.join(self.unit_test_dir, '39 external, internal library rpath', 'built library')
+ # install into installdir without using DESTDIR
+ self.prefix = self.installdir
+ self.init(testdir, override_envvars=env)
+ self.prefix = oldprefix
+ self.build(override_envvars=env)
+ # test uninstalled
+ self.run_tests(override_envvars=env)
+ if not (is_osx() or is_linux()):
+ return
+ # test running after installation
+ self.install(use_destdir=False)
+ prog = os.path.join(self.installdir, 'bin', 'prog')
+ self._run([prog])
+ if not is_osx():
+ # Rest of the workflow only works on macOS
+ return
+ out = self._run(['otool', '-L', prog])
+ self.assertNotIn('@rpath', out)
+ ## New builddir for testing that DESTDIR is not added to install_name
+ self.new_builddir()
+ # install into installdir with DESTDIR
+ self.init(testdir, override_envvars=env)
+ self.build(override_envvars=env)
+ # test running after installation
+ self.install(override_envvars=env)
+ prog = self.installdir + os.path.join(self.prefix, 'bin', 'prog')
+ lib = self.installdir + os.path.join(self.prefix, 'lib', 'libbar_built.dylib')
+ for f in prog, lib:
+ out = self._run(['otool', '-L', f])
+ # Ensure that the otool output does not contain self.installdir
+ self.assertNotRegex(out, self.installdir + '.*dylib ')
+
+ @skipIfNoPkgconfig
+ def test_link_arg_fullname(self):
+ '''
+ Test for support of -l:libfullname.a
+ see: https://github.com/mesonbuild/meson/issues/9000
+ https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a
+ '''
+ testdir = os.path.join(self.unit_test_dir, '97 link full name','libtestprovider')
+ oldprefix = self.prefix
+ # install into installdir without using DESTDIR
+ installdir = self.installdir
+ self.prefix = installdir
+ self.init(testdir)
+ self.prefix=oldprefix
+ self.build()
+ self.install(use_destdir=False)
+
+ self.new_builddir()
+ env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir),
+ 'PKG_CONFIG_PATH': _prepend_pkg_config_path(os.path.join(installdir, self.libdir, 'pkgconfig'))}
+ testdir = os.path.join(self.unit_test_dir, '97 link full name','proguser')
+ self.init(testdir,override_envvars=env)
+
+ # test for link with full path
+ with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
+ for line in bfile:
+ if 'build dprovidertest:' in line:
+ self.assertIn('/libtestprovider.a', line)
+
+ if is_osx():
+ # macOS's ld do not supports `--whole-archive`, skip build & run
+ return
+
+ self.build(override_envvars=env)
+
+ # skip test if pkg-config is too old.
+ # before v0.28, Libs flags like -Wl will not kept in context order with -l flags.
+ # see https://gitlab.freedesktop.org/pkg-config/pkg-config/-/blob/master/NEWS
+ pkgconfigver = subprocess.check_output([PKG_CONFIG, '--version'])
+ if b'0.28' > pkgconfigver:
+ raise SkipTest('pkg-config is too old to be correctly done this.')
+ self.run_tests()
+
+ @skipIfNoPkgconfig
+ def test_usage_pkgconfig_prefixes(self):
+ '''
+ Build and install two external libraries, to different prefixes,
+ then build and install a client program that finds them via pkgconfig,
+ and verify the installed client program runs.
+ '''
+ oldinstalldir = self.installdir
+
+ # Build and install both external libraries without DESTDIR
+ val1dir = os.path.join(self.unit_test_dir, '74 pkgconfig prefixes', 'val1')
+ val1prefix = os.path.join(oldinstalldir, 'val1')
+ self.prefix = val1prefix
+ self.installdir = val1prefix
+ self.init(val1dir)
+ self.build()
+ self.install(use_destdir=False)
+ self.new_builddir()
+
+ env1 = {}
+ env1['PKG_CONFIG_PATH'] = os.path.join(val1prefix, self.libdir, 'pkgconfig')
+ val2dir = os.path.join(self.unit_test_dir, '74 pkgconfig prefixes', 'val2')
+ val2prefix = os.path.join(oldinstalldir, 'val2')
+ self.prefix = val2prefix
+ self.installdir = val2prefix
+ self.init(val2dir, override_envvars=env1)
+ self.build()
+ self.install(use_destdir=False)
+ self.new_builddir()
+
+ # Build, install, and run the client program
+ env2 = {}
+ env2['PKG_CONFIG_PATH'] = os.path.join(val2prefix, self.libdir, 'pkgconfig')
+ testdir = os.path.join(self.unit_test_dir, '74 pkgconfig prefixes', 'client')
+ testprefix = os.path.join(oldinstalldir, 'client')
+ self.prefix = testprefix
+ self.installdir = testprefix
+ self.init(testdir, override_envvars=env2)
+ self.build()
+ self.install(use_destdir=False)
+ prog = os.path.join(self.installdir, 'bin', 'client')
+ env3 = {}
+ if is_cygwin():
+ env3['PATH'] = os.path.join(val1prefix, 'bin') + \
+ os.pathsep + \
+ os.path.join(val2prefix, 'bin') + \
+ os.pathsep + os.environ['PATH']
+ out = self._run([prog], override_envvars=env3).strip()
+ # Expected output is val1 + val2 = 3
+ self.assertEqual(out, '3')
+
+ def install_subdir_invalid_symlinks(self, testdir, subdir_path):
+ '''
+ Test that installation of broken symlinks works fine.
+ https://github.com/mesonbuild/meson/issues/3914
+ '''
+ testdir = os.path.join(self.common_test_dir, testdir)
+ subdir = os.path.join(testdir, subdir_path)
+ with chdir(subdir):
+ # Can't distribute broken symlinks in the source tree because it breaks
+ # the creation of zipapps. Create it dynamically and run the test by
+ # hand.
+ src = '../../nonexistent.txt'
+ os.symlink(src, 'invalid-symlink.txt')
+ try:
+ self.init(testdir)
+ self.build()
+ self.install()
+ install_path = subdir_path.split(os.path.sep)[-1]
+ link = os.path.join(self.installdir, 'usr', 'share', install_path, 'invalid-symlink.txt')
+ self.assertTrue(os.path.islink(link), msg=link)
+ self.assertEqual(src, os.readlink(link))
+ self.assertFalse(os.path.isfile(link), msg=link)
+ finally:
+ os.remove(os.path.join(subdir, 'invalid-symlink.txt'))
+
+ def test_install_subdir_symlinks(self):
+ self.install_subdir_invalid_symlinks('59 install subdir', os.path.join('sub', 'sub1'))
+
+ def test_install_subdir_symlinks_with_default_umask(self):
+ self.install_subdir_invalid_symlinks('190 install_mode', 'sub2')
+
+ def test_install_subdir_symlinks_with_default_umask_and_mode(self):
+ self.install_subdir_invalid_symlinks('190 install_mode', 'sub1')
+
+ @skipIfNoPkgconfigDep('gmodule-2.0')
+ def test_ldflag_dedup(self):
+ testdir = os.path.join(self.unit_test_dir, '51 ldflagdedup')
+ if is_cygwin() or is_osx():
+ raise SkipTest('Not applicable on Cygwin or OSX.')
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ linker = cc.linker
+ if not linker.export_dynamic_args(env):
+ raise SkipTest('Not applicable for linkers without --export-dynamic')
+ self.init(testdir)
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ max_count = 0
+ search_term = '-Wl,--export-dynamic'
+ with open(build_ninja, encoding='utf-8') as f:
+ for line in f:
+ max_count = max(max_count, line.count(search_term))
+ self.assertEqual(max_count, 1, 'Export dynamic incorrectly deduplicated.')
+
+ def test_compiler_libs_static_dedup(self):
+ testdir = os.path.join(self.unit_test_dir, '55 dedup compiler libs')
+ self.init(testdir)
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ with open(build_ninja, encoding='utf-8') as f:
+ lines = f.readlines()
+ for lib in ('-ldl', '-lm', '-lc', '-lrt'):
+ for line in lines:
+ if lib not in line:
+ continue
+ # Assert that
+ self.assertEqual(len(line.split(lib)), 2, msg=(lib, line))
+
+ @skipIfNoPkgconfig
+ def test_noncross_options(self):
+ # C_std defined in project options must be in effect also when native compiling.
+ testdir = os.path.join(self.unit_test_dir, '50 noncross options')
+ self.init(testdir, extra_args=['-Dpkg_config_path=' + testdir])
+ compdb = self.get_compdb()
+ self.assertEqual(len(compdb), 2)
+ self.assertRegex(compdb[0]['command'], '-std=c99')
+ self.assertRegex(compdb[1]['command'], '-std=c99')
+ self.build()
+
+ def test_identity_cross(self):
+ testdir = os.path.join(self.unit_test_dir, '60 identity cross')
+
+ constantsfile = tempfile.NamedTemporaryFile(mode='w')
+ constantsfile.write(textwrap.dedent('''\
+ [constants]
+ py_ext = '.py'
+ '''))
+ constantsfile.flush()
+
+ nativefile = tempfile.NamedTemporaryFile(mode='w')
+ nativefile.write(textwrap.dedent('''\
+ [binaries]
+ c = ['{}' + py_ext]
+ '''.format(os.path.join(testdir, 'build_wrapper'))))
+ nativefile.flush()
+ self.meson_native_files = [constantsfile.name, nativefile.name]
+
+ crossfile = tempfile.NamedTemporaryFile(mode='w')
+ crossfile.write(textwrap.dedent('''\
+ [binaries]
+ c = ['{}' + py_ext]
+ '''.format(os.path.join(testdir, 'host_wrapper'))))
+ crossfile.flush()
+ self.meson_cross_files = [constantsfile.name, crossfile.name]
+
+ # TODO should someday be explicit about build platform only here
+ self.init(testdir)
+
+ def test_identity_cross_env(self):
+ testdir = os.path.join(self.unit_test_dir, '60 identity cross')
+ env = {
+ 'CC_FOR_BUILD': '"' + os.path.join(testdir, 'build_wrapper.py') + '"',
+ 'CC': '"' + os.path.join(testdir, 'host_wrapper.py') + '"',
+ }
+ crossfile = tempfile.NamedTemporaryFile(mode='w')
+ crossfile.write('')
+ crossfile.flush()
+ self.meson_cross_files = [crossfile.name]
+ # TODO should someday be explicit about build platform only here
+ self.init(testdir, override_envvars=env)
+
+ @skipIfNoPkgconfig
+ def test_static_link(self):
+ if is_cygwin():
+ raise SkipTest("Cygwin doesn't support LD_LIBRARY_PATH.")
+
+ # Build some libraries and install them
+ testdir = os.path.join(self.unit_test_dir, '66 static link/lib')
+ libdir = os.path.join(self.installdir, self.libdir)
+ oldprefix = self.prefix
+ self.prefix = self.installdir
+ self.init(testdir)
+ self.install(use_destdir=False)
+
+ # Test that installed libraries works
+ self.new_builddir()
+ self.prefix = oldprefix
+ meson_args = [f'-Dc_link_args=-L{libdir}',
+ '--fatal-meson-warnings']
+ testdir = os.path.join(self.unit_test_dir, '66 static link')
+ env = {'PKG_CONFIG_LIBDIR': os.path.join(libdir, 'pkgconfig')}
+ self.init(testdir, extra_args=meson_args, override_envvars=env)
+ self.build()
+ self.run_tests()
+
+ def _check_ld(self, check: str, name: str, lang: str, expected: str) -> None:
+ if is_sunos():
+ raise SkipTest('Solaris currently cannot override the linker.')
+ if not shutil.which(check):
+ raise SkipTest(f'Could not find {check}.')
+ envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']]
+
+ # Also test a deprecated variable if there is one.
+ if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP:
+ envvars.append(
+ mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld'])
+
+ for envvar in envvars:
+ with mock.patch.dict(os.environ, {envvar: name}):
+ env = get_fake_env()
+ comp = compiler_from_language(env, lang, MachineChoice.HOST)
+ if isinstance(comp, (AppleClangCCompiler, AppleClangCPPCompiler,
+ AppleClangObjCCompiler, AppleClangObjCPPCompiler)):
+ raise SkipTest('AppleClang is currently only supported with ld64')
+ if lang != 'rust' and comp.use_linker_args('bfd', '') == []:
+ raise SkipTest(
+ f'Compiler {comp.id} does not support using alternative linkers')
+ self.assertEqual(comp.linker.id, expected)
+
+ def test_ld_environment_variable_bfd(self):
+ self._check_ld('ld.bfd', 'bfd', 'c', 'ld.bfd')
+
+ def test_ld_environment_variable_gold(self):
+ self._check_ld('ld.gold', 'gold', 'c', 'ld.gold')
+
+ def test_ld_environment_variable_lld(self):
+ self._check_ld('ld.lld', 'lld', 'c', 'ld.lld')
+
+ @skip_if_not_language('rust')
+ @skipIfNoExecutable('ld.gold') # need an additional check here because _check_ld checks for gcc
+ def test_ld_environment_variable_rust(self):
+ self._check_ld('gcc', 'gcc -fuse-ld=gold', 'rust', 'ld.gold')
+
+ def test_ld_environment_variable_cpp(self):
+ self._check_ld('ld.gold', 'gold', 'cpp', 'ld.gold')
+
+ @skip_if_not_language('objc')
+ def test_ld_environment_variable_objc(self):
+ self._check_ld('ld.gold', 'gold', 'objc', 'ld.gold')
+
+ @skip_if_not_language('objcpp')
+ def test_ld_environment_variable_objcpp(self):
+ self._check_ld('ld.gold', 'gold', 'objcpp', 'ld.gold')
+
+ @skip_if_not_language('fortran')
+ def test_ld_environment_variable_fortran(self):
+ self._check_ld('ld.gold', 'gold', 'fortran', 'ld.gold')
+
+ @skip_if_not_language('d')
+ def test_ld_environment_variable_d(self):
+ # At least for me, ldc defaults to gold, and gdc defaults to bfd, so
+ # let's pick lld, which isn't the default for either (currently)
+ if is_osx():
+ expected = 'ld64'
+ else:
+ expected = 'ld.lld'
+ self._check_ld('ld.lld', 'lld', 'd', expected)
+
+ def compute_sha256(self, filename):
+ with open(filename, 'rb') as f:
+ return hashlib.sha256(f.read()).hexdigest()
+
+ def test_wrap_with_file_url(self):
+ testdir = os.path.join(self.unit_test_dir, '72 wrap file url')
+ source_filename = os.path.join(testdir, 'subprojects', 'foo.tar.xz')
+ patch_filename = os.path.join(testdir, 'subprojects', 'foo-patch.tar.xz')
+ wrap_filename = os.path.join(testdir, 'subprojects', 'foo.wrap')
+ source_hash = self.compute_sha256(source_filename)
+ patch_hash = self.compute_sha256(patch_filename)
+ wrap = textwrap.dedent("""\
+ [wrap-file]
+ directory = foo
+
+ source_url = http://server.invalid/foo
+ source_fallback_url = file://{}
+ source_filename = foo.tar.xz
+ source_hash = {}
+
+ patch_url = http://server.invalid/foo
+ patch_fallback_url = file://{}
+ patch_filename = foo-patch.tar.xz
+ patch_hash = {}
+ """.format(source_filename, source_hash, patch_filename, patch_hash))
+ with open(wrap_filename, 'w', encoding='utf-8') as f:
+ f.write(wrap)
+ self.init(testdir)
+ self.build()
+ self.run_tests()
+
+ windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'packagecache'))
+ windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'foo'))
+ os.unlink(wrap_filename)
+
+ def test_no_rpath_for_static(self):
+ testdir = os.path.join(self.common_test_dir, '5 linkstatic')
+ self.init(testdir)
+ self.build()
+ build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
+ self.assertIsNone(build_rpath)
+
+ def test_lookup_system_after_broken_fallback(self):
+ # Just to generate libfoo.pc so we can test system dependency lookup.
+ testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
+ self.init(testdir)
+ privatedir = self.privatedir
+
+ # Write test project where the first dependency() returns not-found
+ # because 'broken' subproject does not exit, but that should not prevent
+ # the 2nd dependency() to lookup on system.
+ self.new_builddir()
+ with tempfile.TemporaryDirectory() as d:
+ with open(os.path.join(d, 'meson.build'), 'w', encoding='utf-8') as f:
+ f.write(textwrap.dedent('''\
+ project('test')
+ dependency('notfound', fallback: 'broken', required: false)
+ dependency('libfoo', fallback: 'broken', required: true)
+ '''))
+ self.init(d, override_envvars={'PKG_CONFIG_LIBDIR': privatedir})
+
+ def test_as_link_whole(self):
+ testdir = os.path.join(self.unit_test_dir, '76 as link whole')
+ self.init(testdir)
+ with open(os.path.join(self.privatedir, 'bar1.pc'), encoding='utf-8') as f:
+ content = f.read()
+ self.assertIn('-lfoo', content)
+ with open(os.path.join(self.privatedir, 'bar2.pc'), encoding='utf-8') as f:
+ content = f.read()
+ self.assertNotIn('-lfoo', content)
+
+ def test_prelinking(self):
+ # Prelinking currently only works on recently new GNU toolchains.
+ # Skip everything else. When support for other toolchains is added,
+ # remove limitations as necessary.
+ if is_osx():
+ raise SkipTest('Prelinking not supported on Darwin.')
+ if 'clang' in os.environ.get('CC', 'dummy'):
+ raise SkipTest('Prelinking not supported with Clang.')
+ testdir = os.path.join(self.unit_test_dir, '86 prelinking')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.id == "gcc" and not version_compare(cc.version, '>=9'):
+ raise SkipTest('Prelinking not supported with gcc 8 or older.')
+ self.init(testdir)
+ self.build()
+ outlib = os.path.join(self.builddir, 'libprelinked.a')
+ ar = shutil.which('ar')
+ self.assertPathExists(outlib)
+ self.assertIsNotNone(ar)
+ p = subprocess.run([ar, 't', outlib],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ text=True, timeout=1)
+ obj_files = p.stdout.strip().split('\n')
+ self.assertEqual(len(obj_files), 1)
+ self.assertTrue(obj_files[0].endswith('-prelink.o'))
+
+ def do_one_test_with_nativefile(self, testdir, args):
+ testdir = os.path.join(self.common_test_dir, testdir)
+ with tempfile.TemporaryDirectory() as d:
+ p = Path(d) / 'nativefile'
+ with p.open('wt', encoding='utf-8') as f:
+ f.write(f'''[binaries]
+ c = {args}
+ ''')
+ self.init(testdir, extra_args=['--native-file=' + str(p)])
+ self.build()
+
+ def test_cmake_multilib(self):
+ '''
+ Test that the cmake module handles multilib paths correctly.
+ '''
+ # Verify that "gcc -m32" works
+ try:
+ self.do_one_test_with_nativefile('1 trivial', "['gcc', '-m32']")
+ except subprocess.CalledProcessError as e:
+ raise SkipTest('Not GCC, or GCC does not have the -m32 option')
+ self.wipe()
+
+ # Verify that cmake works
+ try:
+ self.do_one_test_with_nativefile('../cmake/1 basic', "['gcc']")
+ except subprocess.CalledProcessError as e:
+ raise SkipTest('Could not build basic cmake project')
+ self.wipe()
+
+ # If so, we can test that cmake works with "gcc -m32"
+ self.do_one_test_with_nativefile('../cmake/1 basic', "['gcc', '-m32']")
+
+ @skipUnless(is_linux() or is_osx(), 'Test only applicable to Linux and macOS')
+ def test_install_strip(self):
+ testdir = os.path.join(self.unit_test_dir, '103 strip')
+ self.init(testdir)
+ self.build()
+
+ destdir = self.installdir + self.prefix
+ if is_linux():
+ lib = os.path.join(destdir, self.libdir, 'liba.so')
+ else:
+ lib = os.path.join(destdir, self.libdir, 'liba.dylib')
+ install_cmd = self.meson_command + ['install', '--destdir', self.installdir]
+
+ # Check we have debug symbols by default
+ self._run(install_cmd, workdir=self.builddir)
+ if is_linux():
+ # file can detect stripped libraries on linux
+ stdout = self._run(['file', '-b', lib])
+ self.assertIn('not stripped', stdout)
+ else:
+ # on macOS we need to query dsymutil instead.
+ # Alternatively, check if __dyld_private is defined
+ # in the output of nm liba.dylib, but that is not
+ # 100% reliable, it needs linking to an external library
+ stdout = self._run(['dsymutil', '--dump-debug-map', lib])
+ self.assertIn('symbols:', stdout)
+
+ # Check debug symbols got removed with --strip
+ self._run(install_cmd + ['--strip'], workdir=self.builddir)
+ if is_linux():
+ stdout = self._run(['file', '-b', lib])
+ self.assertNotIn('not stripped', stdout)
+ else:
+ stdout = self._run(['dsymutil', '--dump-debug-map', lib])
+ self.assertNotIn('symbols:', stdout)
+
+ def test_isystem_default_removal_with_symlink(self):
+ env = get_fake_env()
+ cpp = detect_cpp_compiler(env, MachineChoice.HOST)
+ default_dirs = cpp.get_default_include_dirs()
+ default_symlinks = []
+ with tempfile.TemporaryDirectory() as tmpdir:
+ for i in range(len(default_dirs)):
+ symlink = f'{tmpdir}/default_dir{i}'
+ default_symlinks.append(symlink)
+ os.symlink(default_dirs[i], symlink)
+ self.assertFalse(cpp.compiler_args([f'-isystem{symlink}' for symlink in default_symlinks]).to_native())
+
+ def test_freezing(self):
+ testdir = os.path.join(self.unit_test_dir, '109 freeze')
+ self.init(testdir)
+ self.build()
+ with self.assertRaises(subprocess.CalledProcessError) as e:
+ self.run_tests()
+ self.assertNotIn('Traceback', e.exception.output)
diff --git a/unittests/machinefiletests.py b/unittests/machinefiletests.py
new file mode 100644
index 0000000..bf109b2
--- /dev/null
+++ b/unittests/machinefiletests.py
@@ -0,0 +1,953 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import tempfile
+import textwrap
+import os
+import shutil
+import functools
+import threading
+import sys
+from itertools import chain
+from unittest import mock, skipIf, SkipTest
+from pathlib import Path
+import typing as T
+
+import mesonbuild.mlog
+import mesonbuild.depfile
+import mesonbuild.dependencies.factory
+import mesonbuild.envconfig
+import mesonbuild.environment
+import mesonbuild.coredata
+import mesonbuild.modules.gnome
+from mesonbuild.mesonlib import (
+ MachineChoice, is_windows, is_osx, is_cygwin, is_haiku, is_sunos
+)
+from mesonbuild.compilers import (
+ detect_swift_compiler, compiler_from_language
+)
+import mesonbuild.modules.pkgconfig
+
+
+from run_tests import (
+ Backend,
+ get_fake_env
+)
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+@functools.lru_cache()
+def is_real_gnu_compiler(path):
+ '''
+ Check if the gcc we have is a real gcc and not a macOS wrapper around clang
+ '''
+ if not path:
+ return False
+ out = subprocess.check_output([path, '--version'], universal_newlines=True, stderr=subprocess.STDOUT)
+ return 'Free Software Foundation' in out
+
+class NativeFileTests(BasePlatformTests):
+
+ def setUp(self):
+ super().setUp()
+ self.testcase = os.path.join(self.unit_test_dir, '46 native file binary')
+ self.current_config = 0
+ self.current_wrapper = 0
+
+ def helper_create_native_file(self, values):
+ """Create a config file as a temporary file.
+
+ values should be a nested dictionary structure of {section: {key:
+ value}}
+ """
+ filename = os.path.join(self.builddir, f'generated{self.current_config}.config')
+ self.current_config += 1
+ with open(filename, 'wt', encoding='utf-8') as f:
+ for section, entries in values.items():
+ f.write(f'[{section}]\n')
+ for k, v in entries.items():
+ if isinstance(v, (bool, int, float)):
+ f.write(f"{k}={v}\n")
+ elif isinstance(v, list):
+ f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v])))
+ else:
+ f.write(f"{k}='{v}'\n")
+ return filename
+
+ def helper_create_binary_wrapper(self, binary, dir_=None, extra_args=None, **kwargs):
+ """Creates a wrapper around a binary that overrides specific values."""
+ filename = os.path.join(dir_ or self.builddir, f'binary_wrapper{self.current_wrapper}.py')
+ extra_args = extra_args or {}
+ self.current_wrapper += 1
+ if is_haiku():
+ chbang = '#!/bin/env python3'
+ else:
+ chbang = '#!/usr/bin/env python3'
+
+ with open(filename, 'wt', encoding='utf-8') as f:
+ f.write(textwrap.dedent('''\
+ {}
+ import argparse
+ import subprocess
+ import sys
+
+ def main():
+ parser = argparse.ArgumentParser()
+ '''.format(chbang)))
+ for name in chain(extra_args, kwargs):
+ f.write(' parser.add_argument("-{0}", "--{0}", action="store_true")\n'.format(name))
+ f.write(' args, extra_args = parser.parse_known_args()\n')
+ for name, value in chain(extra_args.items(), kwargs.items()):
+ f.write(f' if args.{name}:\n')
+ f.write(' print("{}", file=sys.{})\n'.format(value, kwargs.get('outfile', 'stdout')))
+ f.write(' sys.exit(0)\n')
+ f.write(textwrap.dedent('''
+ ret = subprocess.run(
+ ["{}"] + extra_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ print(ret.stdout.decode('utf-8'))
+ print(ret.stderr.decode('utf-8'), file=sys.stderr)
+ sys.exit(ret.returncode)
+
+ if __name__ == '__main__':
+ main()
+ '''.format(binary)))
+
+ if not is_windows():
+ os.chmod(filename, 0o755)
+ return filename
+
+ # On windows we need yet another level of indirection, as cmd cannot
+ # invoke python files itself, so instead we generate a .bat file, which
+ # invokes our python wrapper
+ batfile = os.path.join(self.builddir, f'binary_wrapper{self.current_wrapper}.bat')
+ with open(batfile, 'wt', encoding='utf-8') as f:
+ f.write(fr'@{sys.executable} {filename} %*')
+ return batfile
+
+ def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST):
+ """Helper for generating tests for overriding compilers for langaugages
+ with more than one implementation, such as C, C++, ObjC, ObjC++, and D.
+ """
+ env = get_fake_env()
+ getter = lambda: compiler_from_language(env, lang, for_machine)
+ cc = getter()
+ binary, newid = cb(cc)
+ env.binaries[for_machine].binaries[lang] = binary
+ compiler = getter()
+ self.assertEqual(compiler.id, newid)
+
+ def test_multiple_native_files_override(self):
+ wrapper = self.helper_create_binary_wrapper('bash', version='foo')
+ config = self.helper_create_native_file({'binaries': {'bash': wrapper}})
+ wrapper = self.helper_create_binary_wrapper('bash', version='12345')
+ config2 = self.helper_create_native_file({'binaries': {'bash': wrapper}})
+ self.init(self.testcase, extra_args=[
+ '--native-file', config, '--native-file', config2,
+ '-Dcase=find_program'])
+
+ # This test hangs on cygwin.
+ @skipIf(os.name != 'posix' or is_cygwin(), 'Uses fifos, which are not available on non Unix OSes.')
+ def test_native_file_is_pipe(self):
+ fifo = os.path.join(self.builddir, 'native.file')
+ os.mkfifo(fifo)
+ with tempfile.TemporaryDirectory() as d:
+ wrapper = self.helper_create_binary_wrapper('bash', d, version='12345')
+
+ def filler():
+ with open(fifo, 'w', encoding='utf-8') as f:
+ f.write('[binaries]\n')
+ f.write(f"bash = '{wrapper}'\n")
+
+ thread = threading.Thread(target=filler)
+ thread.start()
+
+ self.init(self.testcase, extra_args=['--native-file', fifo, '-Dcase=find_program'])
+
+ thread.join()
+ os.unlink(fifo)
+
+ self.init(self.testcase, extra_args=['--wipe'])
+
+ def test_multiple_native_files(self):
+ wrapper = self.helper_create_binary_wrapper('bash', version='12345')
+ config = self.helper_create_native_file({'binaries': {'bash': wrapper}})
+ wrapper = self.helper_create_binary_wrapper('python')
+ config2 = self.helper_create_native_file({'binaries': {'python': wrapper}})
+ self.init(self.testcase, extra_args=[
+ '--native-file', config, '--native-file', config2,
+ '-Dcase=find_program'])
+
+ def _simple_test(self, case, binary, entry=None):
+ wrapper = self.helper_create_binary_wrapper(binary, version='12345')
+ config = self.helper_create_native_file({'binaries': {entry or binary: wrapper}})
+ self.init(self.testcase, extra_args=['--native-file', config, f'-Dcase={case}'])
+
+ def test_find_program(self):
+ self._simple_test('find_program', 'bash')
+
+ def test_config_tool_dep(self):
+ # Do the skip at this level to avoid screwing up the cache
+ if mesonbuild.environment.detect_msys2_arch():
+ raise SkipTest('Skipped due to problems with LLVM on MSYS2')
+ if not shutil.which('llvm-config'):
+ raise SkipTest('No llvm-installed, cannot test')
+ self._simple_test('config_dep', 'llvm-config')
+
+ def test_python3_module(self):
+ self._simple_test('python3', 'python3')
+
+ def test_python_module(self):
+ if is_windows():
+ # Bat adds extra crap to stdout, so the version check logic in the
+ # python module breaks. This is fine on other OSes because they
+ # don't need the extra indirection.
+ raise SkipTest('bat indirection breaks internal sanity checks.')
+ elif is_osx():
+ binary = 'python'
+ else:
+ binary = 'python2'
+
+ # We not have python2, check for it
+ for v in ['2', '2.7', '-2.7']:
+ rc = subprocess.call(['pkg-config', '--cflags', f'python{v}'],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ if rc == 0:
+ break
+ else:
+ raise SkipTest('Not running Python 2 tests because dev packages not installed.')
+ self._simple_test('python', binary, entry='python')
+
+ @skipIf(is_windows(), 'Setting up multiple compilers on windows is hard')
+ @skip_if_env_set('CC')
+ def test_c_compiler(self):
+ def cb(comp):
+ if comp.id == 'gcc':
+ if not shutil.which('clang'):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'clang', 'clang'
+ if not is_real_gnu_compiler(shutil.which('gcc')):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'gcc', 'gcc'
+ self.helper_for_compiler('c', cb)
+
+ @skipIf(is_windows(), 'Setting up multiple compilers on windows is hard')
+ @skip_if_env_set('CXX')
+ def test_cpp_compiler(self):
+ def cb(comp):
+ if comp.id == 'gcc':
+ if not shutil.which('clang++'):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'clang++', 'clang'
+ if not is_real_gnu_compiler(shutil.which('g++')):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'g++', 'gcc'
+ self.helper_for_compiler('cpp', cb)
+
+ @skip_if_not_language('objc')
+ @skip_if_env_set('OBJC')
+ def test_objc_compiler(self):
+ def cb(comp):
+ if comp.id == 'gcc':
+ if not shutil.which('clang'):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'clang', 'clang'
+ if not is_real_gnu_compiler(shutil.which('gcc')):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'gcc', 'gcc'
+ self.helper_for_compiler('objc', cb)
+
+ @skip_if_not_language('objcpp')
+ @skip_if_env_set('OBJCXX')
+ def test_objcpp_compiler(self):
+ def cb(comp):
+ if comp.id == 'gcc':
+ if not shutil.which('clang++'):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'clang++', 'clang'
+ if not is_real_gnu_compiler(shutil.which('g++')):
+ raise SkipTest('Only one compiler found, cannot test.')
+ return 'g++', 'gcc'
+ self.helper_for_compiler('objcpp', cb)
+
+ @skip_if_not_language('d')
+ @skip_if_env_set('DC')
+ def test_d_compiler(self):
+ def cb(comp):
+ if comp.id == 'dmd':
+ if shutil.which('ldc'):
+ return 'ldc', 'ldc'
+ elif shutil.which('gdc'):
+ return 'gdc', 'gdc'
+ else:
+ raise SkipTest('No alternative dlang compiler found.')
+ if shutil.which('dmd'):
+ return 'dmd', 'dmd'
+ raise SkipTest('No alternative dlang compiler found.')
+ self.helper_for_compiler('d', cb)
+
+ @skip_if_not_language('cs')
+ @skip_if_env_set('CSC')
+ def test_cs_compiler(self):
+ def cb(comp):
+ if comp.id == 'csc':
+ if not shutil.which('mcs'):
+ raise SkipTest('No alternate C# implementation.')
+ return 'mcs', 'mcs'
+ if not shutil.which('csc'):
+ raise SkipTest('No alternate C# implementation.')
+ return 'csc', 'csc'
+ self.helper_for_compiler('cs', cb)
+
+ @skip_if_not_language('fortran')
+ @skip_if_env_set('FC')
+ def test_fortran_compiler(self):
+ def cb(comp):
+ if comp.id == 'lcc':
+ if shutil.which('lfortran'):
+ return 'lfortran', 'lcc'
+ raise SkipTest('No alternate Fortran implementation.')
+ elif comp.id == 'gcc':
+ if shutil.which('ifort'):
+ # There is an ICC for windows (windows build, linux host),
+ # but we don't support that ATM so lets not worry about it.
+ if is_windows():
+ return 'ifort', 'intel-cl'
+ return 'ifort', 'intel'
+ elif shutil.which('flang'):
+ return 'flang', 'flang'
+ elif shutil.which('pgfortran'):
+ return 'pgfortran', 'pgi'
+ # XXX: there are several other fortran compilers meson
+ # supports, but I don't have any of them to test with
+ raise SkipTest('No alternate Fortran implementation.')
+ if not shutil.which('gfortran'):
+ raise SkipTest('No alternate Fortran implementation.')
+ return 'gfortran', 'gcc'
+ self.helper_for_compiler('fortran', cb)
+
+ def _single_implementation_compiler(self, lang: str, binary: str, version_str: str, version: str) -> None:
+ """Helper for languages with a single (supported) implementation.
+
+ Builds a wrapper around the compiler to override the version.
+ """
+ wrapper = self.helper_create_binary_wrapper(binary, version=version_str)
+ env = get_fake_env()
+ env.binaries.host.binaries[lang] = [wrapper]
+ compiler = compiler_from_language(env, lang, MachineChoice.HOST)
+ self.assertEqual(compiler.version, version)
+
+ @skip_if_not_language('vala')
+ @skip_if_env_set('VALAC')
+ def test_vala_compiler(self):
+ self._single_implementation_compiler(
+ 'vala', 'valac', 'Vala 1.2345', '1.2345')
+
+ @skip_if_not_language('rust')
+ @skip_if_env_set('RUSTC')
+ def test_rust_compiler(self):
+ self._single_implementation_compiler(
+ 'rust', 'rustc', 'rustc 1.2345', '1.2345')
+
+ @skip_if_not_language('java')
+ def test_java_compiler(self):
+ self._single_implementation_compiler(
+ 'java', 'javac', 'javac 9.99.77', '9.99.77')
+
+ @skip_if_not_language('java')
+ def test_java_classpath(self):
+ if self.backend is not Backend.ninja:
+ raise SkipTest('Jar is only supported with Ninja')
+ testdir = os.path.join(self.unit_test_dir, '110 classpath')
+ self.init(testdir)
+ self.build()
+ one_build_path = get_classpath(os.path.join(self.builddir, 'one.jar'))
+ self.assertIsNone(one_build_path)
+ two_build_path = get_classpath(os.path.join(self.builddir, 'two.jar'))
+ self.assertEqual(two_build_path, 'one.jar')
+ self.install()
+ one_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/one.jar'))
+ self.assertIsNone(one_install_path)
+ two_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/two.jar'))
+ self.assertIsNone(two_install_path)
+
+ @skip_if_not_language('swift')
+ def test_swift_compiler(self):
+ wrapper = self.helper_create_binary_wrapper(
+ 'swiftc', version='Swift 1.2345', outfile='stderr',
+ extra_args={'Xlinker': 'macosx_version. PROJECT:ld - 1.2.3'})
+ env = get_fake_env()
+ env.binaries.host.binaries['swift'] = [wrapper]
+ compiler = detect_swift_compiler(env, MachineChoice.HOST)
+ self.assertEqual(compiler.version, '1.2345')
+
+ def test_native_file_dirs(self):
+ testcase = os.path.join(self.unit_test_dir, '59 native file override')
+ self.init(testcase, default_args=False,
+ extra_args=['--native-file', os.path.join(testcase, 'nativefile')])
+
+ def test_native_file_dirs_overridden(self):
+ testcase = os.path.join(self.unit_test_dir, '59 native file override')
+ self.init(testcase, default_args=False,
+ extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
+ '-Ddef_libdir=liblib', '-Dlibdir=liblib'])
+
+ def test_compile_sys_path(self):
+ """Compiling with a native file stored in a system path works.
+
+ There was a bug which caused the paths to be stored incorrectly and
+ would result in ninja invoking meson in an infinite loop. This tests
+ for that by actually invoking ninja.
+ """
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+
+ # It really doesn't matter what's in the native file, just that it exists
+ config = self.helper_create_native_file({'binaries': {'bash': 'false'}})
+
+ self.init(testcase, extra_args=['--native-file', config])
+ self.build()
+
+ def test_user_options(self):
+ testcase = os.path.join(self.common_test_dir, '40 options')
+ for opt, value in [('testoption', 'some other val'), ('other_one', True),
+ ('combo_opt', 'one'), ('array_opt', ['two']),
+ ('integer_opt', 0),
+ ('CaseSenSiTivE', 'SOME other Value'),
+ ('CASESENSITIVE', 'some other Value')]:
+ config = self.helper_create_native_file({'project options': {opt: value}})
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ self.init(testcase, extra_args=['--native-file', config])
+ self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option')
+
+ def test_user_options_command_line_overrides(self):
+ testcase = os.path.join(self.common_test_dir, '40 options')
+ config = self.helper_create_native_file({'project options': {'other_one': True}})
+ self.init(testcase, extra_args=['--native-file', config, '-Dother_one=false'])
+
+ def test_user_options_subproject(self):
+ testcase = os.path.join(self.unit_test_dir, '78 user options for subproject')
+
+ s = os.path.join(testcase, 'subprojects')
+ if not os.path.exists(s):
+ os.mkdir(s)
+ s = os.path.join(s, 'sub')
+ if not os.path.exists(s):
+ sub = os.path.join(self.common_test_dir, '40 options')
+ shutil.copytree(sub, s)
+
+ for opt, value in [('testoption', 'some other val'), ('other_one', True),
+ ('combo_opt', 'one'), ('array_opt', ['two']),
+ ('integer_opt', 0)]:
+ config = self.helper_create_native_file({'sub:project options': {opt: value}})
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ self.init(testcase, extra_args=['--native-file', config])
+ self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option')
+
+ def test_option_bool(self):
+ # Bools are allowed to be unquoted
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({'built-in options': {'werror': True}})
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ # Test that no-per subproject options are inherited from the parent
+ if 'werror' in each['name']:
+ self.assertEqual(each['value'], True)
+ break
+ else:
+ self.fail('Did not find werror in build options?')
+
+ def test_option_integer(self):
+ # Bools are allowed to be unquoted
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({'built-in options': {'unity_size': 100}})
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ # Test that no-per subproject options are inherited from the parent
+ if 'unity_size' in each['name']:
+ self.assertEqual(each['value'], 100)
+ break
+ else:
+ self.fail('Did not find unity_size in build options?')
+
+ def test_builtin_options(self):
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ config = self.helper_create_native_file({'built-in options': {'cpp_std': 'c++14'}})
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'cpp_std':
+ self.assertEqual(each['value'], 'c++14')
+ break
+ else:
+ self.fail('Did not find werror in build options?')
+
+ def test_builtin_options_conf_overrides_env(self):
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ config = self.helper_create_native_file({'built-in options': {'pkg_config_path': '/foo'}})
+
+ self.init(testcase, extra_args=['--native-file', config], override_envvars={'PKG_CONFIG_PATH': '/bar'})
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'pkg_config_path':
+ self.assertEqual(each['value'], ['/foo'])
+ break
+ else:
+ self.fail('Did not find pkg_config_path in build options?')
+
+ def test_builtin_options_subprojects(self):
+ testcase = os.path.join(self.common_test_dir, '98 subproject subdir')
+ config = self.helper_create_native_file({'built-in options': {'default_library': 'both', 'c_args': ['-Dfoo']}, 'sub:built-in options': {'default_library': 'static'}})
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ found = 0
+ for each in configuration:
+ # Test that no-per subproject options are inherited from the parent
+ if 'c_args' in each['name']:
+ # This path will be hit twice, once for build and once for host,
+ self.assertEqual(each['value'], ['-Dfoo'])
+ found += 1
+ elif each['name'] == 'default_library':
+ self.assertEqual(each['value'], 'both')
+ found += 1
+ elif each['name'] == 'sub:default_library':
+ self.assertEqual(each['value'], 'static')
+ found += 1
+ self.assertEqual(found, 4, 'Did not find all three sections')
+
+ def test_builtin_options_subprojects_overrides_buildfiles(self):
+ # If the buildfile says subproject(... default_library: shared), ensure that's overwritten
+ testcase = os.path.join(self.common_test_dir, '223 persubproject options')
+ config = self.helper_create_native_file({'sub2:built-in options': {'default_library': 'shared'}})
+
+ with self.assertRaises((RuntimeError, subprocess.CalledProcessError)) as cm:
+ self.init(testcase, extra_args=['--native-file', config])
+ if isinstance(cm, RuntimeError):
+ check = str(cm.exception)
+ else:
+ check = cm.exception.stdout
+ self.assertIn(check, 'Parent should override default_library')
+
+ def test_builtin_options_subprojects_dont_inherits_parent_override(self):
+ # If the buildfile says subproject(... default_library: shared), ensure that's overwritten
+ testcase = os.path.join(self.common_test_dir, '223 persubproject options')
+ config = self.helper_create_native_file({'built-in options': {'default_library': 'both'}})
+ self.init(testcase, extra_args=['--native-file', config])
+
+ def test_builtin_options_compiler_properties(self):
+ # the properties section can have lang_args, and those need to be
+ # overwritten by the built-in options
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({
+ 'built-in options': {'c_args': ['-DFOO']},
+ 'properties': {'c_args': ['-DBAR']},
+ })
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'c_args':
+ self.assertEqual(each['value'], ['-DFOO'])
+ break
+ else:
+ self.fail('Did not find c_args in build options?')
+
+ def test_builtin_options_compiler_properties_legacy(self):
+ # The legacy placement in properties is still valid if a 'built-in
+ # options' setting is present, but doesn't have the lang_args
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({
+ 'built-in options': {'default_library': 'static'},
+ 'properties': {'c_args': ['-DBAR']},
+ })
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'c_args':
+ self.assertEqual(each['value'], ['-DBAR'])
+ break
+ else:
+ self.fail('Did not find c_args in build options?')
+
+ def test_builtin_options_paths(self):
+ # the properties section can have lang_args, and those need to be
+ # overwritten by the built-in options
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({
+ 'built-in options': {'bindir': 'foo'},
+ 'paths': {'bindir': 'bar'},
+ })
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'bindir':
+ self.assertEqual(each['value'], 'foo')
+ break
+ else:
+ self.fail('Did not find bindir in build options?')
+
+ def test_builtin_options_paths_legacy(self):
+ testcase = os.path.join(self.common_test_dir, '1 trivial')
+ config = self.helper_create_native_file({
+ 'built-in options': {'default_library': 'static'},
+ 'paths': {'bindir': 'bar'},
+ })
+
+ self.init(testcase, extra_args=['--native-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'bindir':
+ self.assertEqual(each['value'], 'bar')
+ break
+ else:
+ self.fail('Did not find bindir in build options?')
+
+
+class CrossFileTests(BasePlatformTests):
+
+ """Tests for cross file functionality not directly related to
+ cross compiling.
+
+ This is mainly aimed to testing overrides from cross files.
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.current_config = 0
+ self.current_wrapper = 0
+
+ def _cross_file_generator(self, *, needs_exe_wrapper: bool = False,
+ exe_wrapper: T.Optional[T.List[str]] = None) -> str:
+ if is_windows():
+ raise SkipTest('Cannot run this test on non-mingw/non-cygwin windows')
+
+ return textwrap.dedent(f"""\
+ [binaries]
+ c = '{shutil.which('gcc' if is_sunos() else 'cc')}'
+ ar = '{shutil.which('ar')}'
+ strip = '{shutil.which('strip')}'
+ exe_wrapper = {str(exe_wrapper) if exe_wrapper is not None else '[]'}
+
+ [properties]
+ needs_exe_wrapper = {needs_exe_wrapper}
+
+ [host_machine]
+ system = 'linux'
+ cpu_family = 'x86'
+ cpu = 'i686'
+ endian = 'little'
+ """)
+
+ def _stub_exe_wrapper(self) -> str:
+ return textwrap.dedent('''\
+ #!/usr/bin/env python3
+ import subprocess
+ import sys
+
+ sys.exit(subprocess.run(sys.argv[1:]).returncode)
+ ''')
+
+ def test_needs_exe_wrapper_true(self):
+ testdir = os.path.join(self.unit_test_dir, '70 cross test passed')
+ with tempfile.TemporaryDirectory() as d:
+ p = Path(d) / 'crossfile'
+ with p.open('wt', encoding='utf-8') as f:
+ f.write(self._cross_file_generator(needs_exe_wrapper=True))
+ self.init(testdir, extra_args=['--cross-file=' + str(p)])
+ out = self.run_target('test')
+ self.assertRegex(out, r'Skipped:\s*1\s*\n')
+
+ def test_needs_exe_wrapper_false(self):
+ testdir = os.path.join(self.unit_test_dir, '70 cross test passed')
+ with tempfile.TemporaryDirectory() as d:
+ p = Path(d) / 'crossfile'
+ with p.open('wt', encoding='utf-8') as f:
+ f.write(self._cross_file_generator(needs_exe_wrapper=False))
+ self.init(testdir, extra_args=['--cross-file=' + str(p)])
+ out = self.run_target('test')
+ self.assertNotRegex(out, r'Skipped:\s*1\n')
+
+ def test_needs_exe_wrapper_true_wrapper(self):
+ testdir = os.path.join(self.unit_test_dir, '70 cross test passed')
+ with tempfile.TemporaryDirectory() as d:
+ s = Path(d) / 'wrapper.py'
+ with s.open('wt', encoding='utf-8') as f:
+ f.write(self._stub_exe_wrapper())
+ s.chmod(0o774)
+ p = Path(d) / 'crossfile'
+ with p.open('wt', encoding='utf-8') as f:
+ f.write(self._cross_file_generator(
+ needs_exe_wrapper=True,
+ exe_wrapper=[str(s)]))
+
+ self.init(testdir, extra_args=['--cross-file=' + str(p), '-Dexpect=true'])
+ out = self.run_target('test')
+ self.assertRegex(out, r'Ok:\s*3\s*\n')
+
+ def test_cross_exe_passed_no_wrapper(self):
+ testdir = os.path.join(self.unit_test_dir, '70 cross test passed')
+ with tempfile.TemporaryDirectory() as d:
+ p = Path(d) / 'crossfile'
+ with p.open('wt', encoding='utf-8') as f:
+ f.write(self._cross_file_generator(needs_exe_wrapper=True))
+
+ self.init(testdir, extra_args=['--cross-file=' + str(p)])
+ self.build()
+ out = self.run_target('test')
+ self.assertRegex(out, r'Skipped:\s*1\s*\n')
+
+ # The test uses mocking and thus requires that the current process is the
+ # one to run the Meson steps. If we are using an external test executable
+ # (most commonly in Debian autopkgtests) then the mocking won't work.
+ @skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.')
+ def test_cross_file_system_paths(self):
+ if is_windows():
+ raise SkipTest('system crossfile paths not defined for Windows (yet)')
+
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+ cross_content = self._cross_file_generator()
+ with tempfile.TemporaryDirectory() as d:
+ dir_ = os.path.join(d, 'meson', 'cross')
+ os.makedirs(dir_)
+ with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
+ f.write(cross_content)
+ name = os.path.basename(f.name)
+
+ with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}):
+ self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+ self.wipe()
+
+ with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}):
+ os.environ.pop('XDG_DATA_HOME', None)
+ self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+ self.wipe()
+
+ with tempfile.TemporaryDirectory() as d:
+ dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross')
+ os.makedirs(dir_)
+ with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
+ f.write(cross_content)
+ name = os.path.basename(f.name)
+
+ # If XDG_DATA_HOME is set in the environment running the
+ # tests this test will fail, os mock the environment, pop
+ # it, then test
+ with mock.patch.dict(os.environ):
+ os.environ.pop('XDG_DATA_HOME', None)
+ with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)):
+ self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+ self.wipe()
+
+ def helper_create_cross_file(self, values):
+ """Create a config file as a temporary file.
+
+ values should be a nested dictionary structure of {section: {key:
+ value}}
+ """
+ filename = os.path.join(self.builddir, f'generated{self.current_config}.config')
+ self.current_config += 1
+ with open(filename, 'wt', encoding='utf-8') as f:
+ for section, entries in values.items():
+ f.write(f'[{section}]\n')
+ for k, v in entries.items():
+ f.write(f"{k}={v!r}\n")
+ return filename
+
+ def test_cross_file_dirs(self):
+ testcase = os.path.join(self.unit_test_dir, '59 native file override')
+ self.init(testcase, default_args=False,
+ extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
+ '--cross-file', os.path.join(testcase, 'crossfile'),
+ '-Ddef_bindir=binbar',
+ '-Ddef_datadir=databar',
+ '-Ddef_includedir=includebar',
+ '-Ddef_infodir=infobar',
+ '-Ddef_libdir=libbar',
+ '-Ddef_libexecdir=libexecbar',
+ '-Ddef_localedir=localebar',
+ '-Ddef_localstatedir=localstatebar',
+ '-Ddef_mandir=manbar',
+ '-Ddef_sbindir=sbinbar',
+ '-Ddef_sharedstatedir=sharedstatebar',
+ '-Ddef_sysconfdir=sysconfbar'])
+
+ def test_cross_file_dirs_overridden(self):
+ testcase = os.path.join(self.unit_test_dir, '59 native file override')
+ self.init(testcase, default_args=False,
+ extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
+ '--cross-file', os.path.join(testcase, 'crossfile'),
+ '-Ddef_libdir=liblib', '-Dlibdir=liblib',
+ '-Ddef_bindir=binbar',
+ '-Ddef_datadir=databar',
+ '-Ddef_includedir=includebar',
+ '-Ddef_infodir=infobar',
+ '-Ddef_libexecdir=libexecbar',
+ '-Ddef_localedir=localebar',
+ '-Ddef_localstatedir=localstatebar',
+ '-Ddef_mandir=manbar',
+ '-Ddef_sbindir=sbinbar',
+ '-Ddef_sharedstatedir=sharedstatebar',
+ '-Ddef_sysconfdir=sysconfbar'])
+
+ def test_cross_file_dirs_chain(self):
+ # crossfile2 overrides crossfile overrides nativefile
+ testcase = os.path.join(self.unit_test_dir, '59 native file override')
+ self.init(testcase, default_args=False,
+ extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
+ '--cross-file', os.path.join(testcase, 'crossfile'),
+ '--cross-file', os.path.join(testcase, 'crossfile2'),
+ '-Ddef_bindir=binbar2',
+ '-Ddef_datadir=databar',
+ '-Ddef_includedir=includebar',
+ '-Ddef_infodir=infobar',
+ '-Ddef_libdir=libbar',
+ '-Ddef_libexecdir=libexecbar',
+ '-Ddef_localedir=localebar',
+ '-Ddef_localstatedir=localstatebar',
+ '-Ddef_mandir=manbar',
+ '-Ddef_sbindir=sbinbar',
+ '-Ddef_sharedstatedir=sharedstatebar',
+ '-Ddef_sysconfdir=sysconfbar'])
+
+ def test_user_options(self):
+ # This is just a touch test for cross file, since the implementation
+ # shares code after loading from the files
+ testcase = os.path.join(self.common_test_dir, '40 options')
+ config = self.helper_create_cross_file({'project options': {'testoption': 'some other value'}})
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ self.init(testcase, extra_args=['--cross-file', config])
+ self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option')
+
+ def test_builtin_options(self):
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ config = self.helper_create_cross_file({'built-in options': {'cpp_std': 'c++14'}})
+
+ self.init(testcase, extra_args=['--cross-file', config])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'cpp_std':
+ self.assertEqual(each['value'], 'c++14')
+ break
+ else:
+ self.fail('No c++ standard set?')
+
+ def test_builtin_options_per_machine(self):
+ """Test options that are allowed to be set on a per-machine basis.
+
+ Such options could be passed twice, once for the build machine, and
+ once for the host machine. I've picked pkg-config path, but any would
+ do that can be set for both.
+ """
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross/path', 'cpp_std': 'c++17'}})
+ native = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native/path', 'cpp_std': 'c++14'}})
+
+ # Ensure that PKG_CONFIG_PATH is not set in the environment
+ with mock.patch.dict('os.environ'):
+ for k in ['PKG_CONFIG_PATH', 'PKG_CONFIG_PATH_FOR_BUILD']:
+ try:
+ del os.environ[k]
+ except KeyError:
+ pass
+ self.init(testcase, extra_args=['--cross-file', cross, '--native-file', native])
+
+ configuration = self.introspect('--buildoptions')
+ found = 0
+ for each in configuration:
+ if each['name'] == 'pkg_config_path':
+ self.assertEqual(each['value'], ['/cross/path'])
+ found += 1
+ elif each['name'] == 'cpp_std':
+ self.assertEqual(each['value'], 'c++17')
+ found += 1
+ elif each['name'] == 'build.pkg_config_path':
+ self.assertEqual(each['value'], ['/native/path'])
+ found += 1
+ elif each['name'] == 'build.cpp_std':
+ self.assertEqual(each['value'], 'c++14')
+ found += 1
+
+ if found == 4:
+ break
+ self.assertEqual(found, 4, 'Did not find all sections.')
+
+ def test_builtin_options_conf_overrides_env(self):
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ config = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native', 'cpp_args': '-DFILE'}})
+ cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross', 'cpp_args': '-DFILE'}})
+
+ self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross],
+ override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir',
+ 'CXXFLAGS': '-DENV', 'CXXFLAGS_FOR_BUILD': '-DENV'})
+ configuration = self.introspect('--buildoptions')
+ found = 0
+ expected = 4
+ for each in configuration:
+ if each['name'] == 'pkg_config_path':
+ self.assertEqual(each['value'], ['/cross'])
+ found += 1
+ elif each['name'] == 'build.pkg_config_path':
+ self.assertEqual(each['value'], ['/native'])
+ found += 1
+ elif each['name'].endswith('cpp_args'):
+ self.assertEqual(each['value'], ['-DFILE'])
+ found += 1
+ if found == expected:
+ break
+ self.assertEqual(found, expected, 'Did not find all sections.')
+
+ def test_for_build_env_vars(self) -> None:
+ testcase = os.path.join(self.common_test_dir, '2 cpp')
+ config = self.helper_create_cross_file({'built-in options': {}})
+ cross = self.helper_create_cross_file({'built-in options': {}})
+
+ self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross],
+ override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir'})
+ configuration = self.introspect('--buildoptions')
+ found = 0
+ for each in configuration:
+ if each['name'] == 'pkg_config_path':
+ self.assertEqual(each['value'], ['/bar'])
+ found += 1
+ elif each['name'] == 'build.pkg_config_path':
+ self.assertEqual(each['value'], ['/dir'])
+ found += 1
+ if found == 2:
+ break
+ self.assertEqual(found, 2, 'Did not find all sections.')
+
+ def test_project_options_native_only(self) -> None:
+ # Do not load project options from a native file when doing a cross
+ # build
+ testcase = os.path.join(self.unit_test_dir, '19 array option')
+ config = self.helper_create_cross_file({'project options': {'list': ['bar', 'foo']}})
+ cross = self.helper_create_cross_file({'binaries': {}})
+
+ self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross])
+ configuration = self.introspect('--buildoptions')
+ for each in configuration:
+ if each['name'] == 'list':
+ self.assertEqual(each['value'], ['foo', 'bar'])
+ break
+ else:
+ self.fail('Did not find expected option.')
diff --git a/unittests/platformagnostictests.py b/unittests/platformagnostictests.py
new file mode 100644
index 0000000..39965c6
--- /dev/null
+++ b/unittests/platformagnostictests.py
@@ -0,0 +1,123 @@
+# Copyright 2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import tempfile
+import subprocess
+import textwrap
+from unittest import skipIf
+from pathlib import Path
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import is_ci
+from mesonbuild.mesonlib import is_linux
+from mesonbuild.optinterpreter import OptionInterpreter, OptionException
+
+@skipIf(is_ci() and not is_linux(), "Run only on fast platforms")
+class PlatformAgnosticTests(BasePlatformTests):
+ '''
+ Tests that does not need to run on all platforms during CI
+ '''
+
+ def test_relative_find_program(self):
+ '''
+ Tests that find_program() with a relative path does not find the program
+ in current workdir.
+ '''
+ testdir = os.path.join(self.unit_test_dir, '100 relative find program')
+ self.init(testdir, workdir=testdir)
+
+ def test_invalid_option_names(self):
+ interp = OptionInterpreter('')
+
+ def write_file(code: str):
+ with tempfile.NamedTemporaryFile('w', dir=self.builddir, encoding='utf-8', delete=False) as f:
+ f.write(code)
+ return f.name
+
+ fname = write_file("option('default_library', type: 'string')")
+ self.assertRaisesRegex(OptionException, 'Option name default_library is reserved.',
+ interp.process, fname)
+
+ fname = write_file("option('c_anything', type: 'string')")
+ self.assertRaisesRegex(OptionException, 'Option name c_anything is reserved.',
+ interp.process, fname)
+
+ fname = write_file("option('b_anything', type: 'string')")
+ self.assertRaisesRegex(OptionException, 'Option name b_anything is reserved.',
+ interp.process, fname)
+
+ fname = write_file("option('backend_anything', type: 'string')")
+ self.assertRaisesRegex(OptionException, 'Option name backend_anything is reserved.',
+ interp.process, fname)
+
+ fname = write_file("option('foo.bar', type: 'string')")
+ self.assertRaisesRegex(OptionException, 'Option names can only contain letters, numbers or dashes.',
+ interp.process, fname)
+
+ # platlib is allowed, only python.platlib is reserved.
+ fname = write_file("option('platlib', type: 'string')")
+ interp.process(fname)
+
+ def test_python_dependency_without_pkgconfig(self):
+ testdir = os.path.join(self.unit_test_dir, '102 python without pkgconfig')
+ self.init(testdir, override_envvars={'PKG_CONFIG': 'notfound'})
+
+ def test_debug_function_outputs_to_meson_log(self):
+ testdir = os.path.join(self.unit_test_dir, '104 debug function')
+ log_msg = 'This is an example debug output, should only end up in debug log'
+ output = self.init(testdir)
+
+ # Check if message is not printed to stdout while configuring
+ self.assertNotIn(log_msg, output)
+
+ # Check if message is written to the meson log
+ mesonlog = self.get_meson_log_raw()
+ self.assertIn(log_msg, mesonlog)
+
+ def test_new_subproject_reconfigure(self):
+ testdir = os.path.join(self.unit_test_dir, '107 new subproject on reconfigure')
+ self.init(testdir)
+ self.build()
+
+ # Enable the subproject "foo" and reconfigure, this is used to fail
+ # because per-subproject builtin options were not initialized:
+ # https://github.com/mesonbuild/meson/issues/10225.
+ self.setconf('-Dfoo=enabled')
+ self.build('reconfigure')
+
+ def check_connectivity(self):
+ import urllib
+ try:
+ with urllib.request.urlopen('https://wrapdb.mesonbuild.com') as p:
+ pass
+ except urllib.error.URLError as e:
+ self.skipTest('No internet connectivity: ' + str(e))
+
+ def test_update_wrapdb(self):
+ self.check_connectivity()
+ # Write the project into a temporary directory because it will add files
+ # into subprojects/ and we don't want to pollute meson source tree.
+ with tempfile.TemporaryDirectory() as testdir:
+ with Path(testdir, 'meson.build').open('w', encoding='utf-8') as f:
+ f.write(textwrap.dedent(
+ '''
+ project('wrap update-db',
+ default_options: ['wrap_mode=forcefallback'])
+
+ zlib_dep = dependency('zlib')
+ assert(zlib_dep.type_name() == 'internal')
+ '''))
+ subprocess.check_call(self.wrap_command + ['update-db'], cwd=testdir)
+ self.init(testdir, workdir=testdir)
diff --git a/unittests/pythontests.py b/unittests/pythontests.py
new file mode 100644
index 0000000..d49107f
--- /dev/null
+++ b/unittests/pythontests.py
@@ -0,0 +1,62 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import unittest
+import pathlib
+import subprocess
+
+from run_tests import (
+ Backend
+)
+
+from .allplatformstests import git_init
+
+from .baseplatformtests import BasePlatformTests
+from mesonbuild.mesonlib import TemporaryDirectoryWinProof
+
+class PythonTests(BasePlatformTests):
+ '''
+ Tests that verify compilation of python extension modules
+ '''
+
+ def test_bad_versions(self):
+ if self.backend is not Backend.ninja:
+ raise unittest.SkipTest(f'Skipping python tests with {self.backend.name} backend')
+
+ testdir = os.path.join(self.src_root, 'test cases', 'python', '8 different python versions')
+
+ # The test is configured to error out with MESON_SKIP_TEST
+ # in case it could not find python
+ with self.assertRaises(unittest.SkipTest):
+ self.init(testdir, extra_args=['-Dpython=not-python'])
+ self.wipe()
+
+ # While dir is an external command on both Windows and Linux,
+ # it certainly isn't python
+ with self.assertRaises(unittest.SkipTest):
+ self.init(testdir, extra_args=['-Dpython=dir'])
+ self.wipe()
+
+ def test_dist(self):
+ with TemporaryDirectoryWinProof() as dirstr:
+ dirobj = pathlib.Path(dirstr)
+ mesonfile = dirobj / 'meson.build'
+ mesonfile.write_text('''project('test', 'c', version: '1')
+pymod = import('python')
+python = pymod.find_installation('python3', required: true)
+''', encoding='utf-8')
+ git_init(dirstr)
+ self.init(dirstr)
+ subprocess.check_call(self.meson_command + ['dist', '-C', self.builddir], stdout=subprocess.DEVNULL)
diff --git a/unittests/rewritetests.py b/unittests/rewritetests.py
new file mode 100644
index 0000000..4979c51
--- /dev/null
+++ b/unittests/rewritetests.py
@@ -0,0 +1,398 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import json
+import os
+import shutil
+import unittest
+
+from mesonbuild.mesonlib import windows_proof_rmtree
+from .baseplatformtests import BasePlatformTests
+
+class RewriterTests(BasePlatformTests):
+ def setUp(self):
+ super().setUp()
+ self.maxDiff = None
+
+ def prime(self, dirname):
+ if os.path.exists(self.builddir):
+ windows_proof_rmtree(self.builddir)
+ shutil.copytree(os.path.join(self.rewrite_test_dir, dirname), self.builddir)
+
+ def rewrite_raw(self, directory, args):
+ if isinstance(args, str):
+ args = [args]
+ command = self.rewrite_command + ['--verbose', '--skip', '--sourcedir', directory] + args
+ p = subprocess.run(command, capture_output=True, text=True, timeout=60)
+ print('STDOUT:')
+ print(p.stdout)
+ print('STDERR:')
+ print(p.stderr)
+ if p.returncode != 0:
+ if 'MESON_SKIP_TEST' in p.stdout:
+ raise unittest.SkipTest('Project requested skipping.')
+ raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout)
+ if not p.stderr:
+ return {}
+ return json.loads(p.stderr)
+
+ def rewrite(self, directory, args):
+ if isinstance(args, str):
+ args = [args]
+ return self.rewrite_raw(directory, ['command'] + args)
+
+ def test_target_source_list(self):
+ self.prime('1 basic')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_target_add_sources(self):
+ self.prime('1 basic')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['a7.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['a5.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['a5.cpp', 'main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['a3.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp', 'a4.cpp'], 'extra_files': []},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(out, expected)
+
+ def test_target_add_sources_abs(self):
+ self.prime('1 basic')
+ abs_src = [os.path.join(self.builddir, x) for x in ['a1.cpp', 'a2.cpp', 'a6.cpp']]
+ add = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "src_add", "sources": abs_src}])
+ inf = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "info"}])
+ self.rewrite(self.builddir, add)
+ out = self.rewrite(self.builddir, inf)
+ expected = {'target': {'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp'], 'extra_files': []}}}
+ self.assertDictEqual(out, expected)
+
+ def test_target_remove_sources(self):
+ self.prime('1 basic')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'rmSrc.json'))
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileC.cpp'], 'extra_files': []},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileC.cpp', 'main.cpp'], 'extra_files': []},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(out, expected)
+
+ def test_target_subdir(self):
+ self.prime('2 subdirs')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
+ expected = {'name': 'something', 'sources': ['first.c', 'second.c', 'third.c'], 'extra_files': []}
+ self.assertDictEqual(list(out['target'].values())[0], expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(list(out['target'].values())[0], expected)
+
+ def test_target_remove(self):
+ self.prime('1 basic')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+
+ expected = {
+ 'target': {
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_tatrget_add(self):
+ self.prime('1 basic')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp'], 'extra_files': []},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp'], 'extra_files': []},
+ 'trivialprog10@sha': {'name': 'trivialprog10', 'sources': ['new1.cpp', 'new2.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_target_remove_subdir(self):
+ self.prime('2 subdirs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(out, {})
+
+ def test_target_add_subdir(self):
+ self.prime('2 subdirs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {'name': 'something', 'sources': ['first.c', 'second.c'], 'extra_files': []}
+ self.assertDictEqual(out['target']['94b671c@@something@exe'], expected)
+
+ def test_target_source_sorting(self):
+ self.prime('5 sorting')
+ add_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'src_add', 'sources': ['a666.c']}])
+ inf_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'info'}])
+ out = self.rewrite(self.builddir, add_json)
+ out = self.rewrite(self.builddir, inf_json)
+ expected = {
+ 'target': {
+ 'exe1@exe': {
+ 'name': 'exe1',
+ 'sources': [
+ 'aaa/a/a1.c',
+ 'aaa/b/b1.c',
+ 'aaa/b/b2.c',
+ 'aaa/f1.c',
+ 'aaa/f2.c',
+ 'aaa/f3.c',
+ 'bbb/a/b1.c',
+ 'bbb/b/b2.c',
+ 'bbb/c1/b5.c',
+ 'bbb/c2/b7.c',
+ 'bbb/c10/b6.c',
+ 'bbb/a4.c',
+ 'bbb/b3.c',
+ 'bbb/b4.c',
+ 'bbb/b5.c',
+ 'a1.c',
+ 'a2.c',
+ 'a3.c',
+ 'a10.c',
+ 'a20.c',
+ 'a30.c',
+ 'a100.c',
+ 'a101.c',
+ 'a110.c',
+ 'a210.c',
+ 'a666.c',
+ 'b1.c',
+ 'c2.c'
+ ],
+ 'extra_files': []
+ }
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_target_same_name_skip(self):
+ self.prime('4 same name targets')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {'name': 'myExe', 'sources': ['main.cpp'], 'extra_files': []}
+ self.assertEqual(len(out['target']), 2)
+ for val in out['target'].values():
+ self.assertDictEqual(expected, val)
+
+ def test_kwargs_info(self):
+ self.prime('3 kwargs')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1'},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_kwargs_set(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'set.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.2', 'meson_version': '0.50.0', 'license': ['GPL', 'MIT']},
+ 'target#tgt1': {'build_by_default': False, 'build_rpath': '/usr/local', 'dependencies': 'dep1'},
+ 'dependency#dep1': {'required': True, 'method': 'cmake'}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_kwargs_add(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'add.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1', 'license': ['GPL', 'MIT', 'BSD', 'Boost']},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_kwargs_remove(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'remove.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1', 'license': 'GPL'},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_kwargs_remove_regex(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'remove_regex.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1', 'default_options': 'debug=true'},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_kwargs_delete(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'delete.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {},
+ 'target#tgt1': {},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_default_options_set(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_set.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1', 'default_options': ['buildtype=release', 'debug=True', 'cpp_std=c++11']},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_default_options_delete(self):
+ self.prime('3 kwargs')
+ self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_delete.json'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ expected = {
+ 'kwargs': {
+ 'project#/': {'version': '0.0.1', 'default_options': ['cpp_std=c++14', 'debug=true']},
+ 'target#tgt1': {'build_by_default': True},
+ 'dependency#dep1': {'required': False}
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ def test_target_add_extra_files(self):
+ self.prime('6 extra_files')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addExtraFiles.json'))
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp'], 'extra_files': ['a1.hpp', 'a2.hpp', 'a6.hpp', 'fileA.hpp', 'main.hpp', 'a7.hpp', 'fileB.hpp', 'fileC.hpp']},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp'], 'extra_files': ['a1.hpp', 'a2.hpp', 'a6.hpp', 'fileA.hpp', 'main.hpp']},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['main.cpp'], 'extra_files': ['a7.hpp', 'fileB.hpp', 'fileC.hpp']},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp'], 'extra_files': ['a5.hpp', 'fileA.hpp', 'main.hpp']},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp'], 'extra_files': ['a5.hpp', 'main.hpp', 'fileA.hpp']},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp'], 'extra_files': ['a3.hpp', 'main.hpp', 'a7.hpp', 'fileB.hpp', 'fileC.hpp']},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp'], 'extra_files': ['a1.hpp', 'a2.hpp', 'a6.hpp', 'fileA.hpp', 'main.hpp']},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['main.cpp'], 'extra_files': ['a1.hpp', 'a2.hpp', 'a6.hpp', 'fileA.hpp', 'main.hpp']},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp'], 'extra_files': ['a2.hpp', 'a7.hpp']},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp'], 'extra_files': ['a8.hpp', 'a9.hpp']},
+ 'trivialprog10@exe': {'name': 'trivialprog10', 'sources': ['main.cpp'], 'extra_files': ['a1.hpp', 'a4.hpp']},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(out, expected)
+
+ def test_target_remove_extra_files(self):
+ self.prime('6 extra_files')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'rmExtraFiles.json'))
+ expected = {
+ 'target': {
+ 'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp'], 'extra_files': ['main.hpp', 'fileC.hpp']},
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp'], 'extra_files': ['main.hpp']},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['main.cpp'], 'extra_files': ['fileC.hpp']},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp'], 'extra_files': ['main.hpp']},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp'], 'extra_files': ['main.hpp']},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp'], 'extra_files': ['main.hpp', 'fileC.hpp']},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp'], 'extra_files': ['main.hpp']},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['main.cpp'], 'extra_files': ['main.hpp']},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp'], 'extra_files': []},
+ 'trivialprog10@exe': {'name': 'trivialprog10', 'sources': ['main.cpp'], 'extra_files': []},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ self.assertDictEqual(out, expected)
diff --git a/unittests/subprojectscommandtests.py b/unittests/subprojectscommandtests.py
new file mode 100644
index 0000000..bca124d
--- /dev/null
+++ b/unittests/subprojectscommandtests.py
@@ -0,0 +1,300 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import tempfile
+import textwrap
+import os
+from pathlib import Path
+import typing as T
+
+from mesonbuild.mesonlib import (
+ version_compare, git, search_version
+)
+
+
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+class SubprojectsCommandTests(BasePlatformTests):
+ def setUp(self):
+ super().setUp()
+ self.root_dir = Path(self.builddir)
+
+ self.project_dir = self.root_dir / 'src'
+ self._create_project(self.project_dir)
+
+ self.subprojects_dir = self.project_dir / 'subprojects'
+ os.makedirs(str(self.subprojects_dir))
+ self.packagecache_dir = self.subprojects_dir / 'packagecache'
+ os.makedirs(str(self.packagecache_dir))
+
+ def _create_project(self, path, project_name='dummy'):
+ os.makedirs(str(path), exist_ok=True)
+ with open(str(path / 'meson.build'), 'w', encoding='utf-8') as f:
+ f.write(f"project('{project_name}')")
+
+ def _git(self, cmd, workdir):
+ return git(cmd, str(workdir), check=True)[1].strip()
+
+ def _git_config(self, workdir):
+ self._git(['config', 'user.name', 'Meson Test'], workdir)
+ self._git(['config', 'user.email', 'meson.test@example.com'], workdir)
+
+ def _git_remote(self, cmd, name):
+ return self._git(cmd, self.root_dir / name)
+
+ def _git_local(self, cmd, name):
+ return self._git(cmd, self.subprojects_dir / name)
+
+ def _git_local_branch(self, name):
+ # Same as `git branch --show-current` but compatible with older git version
+ branch = self._git_local(['rev-parse', '--abbrev-ref', 'HEAD'], name)
+ return branch if branch != 'HEAD' else ''
+
+ def _git_local_commit(self, name, ref='HEAD'):
+ return self._git_local(['rev-parse', ref], name)
+
+ def _git_remote_commit(self, name, ref='HEAD'):
+ return self._git_remote(['rev-parse', ref], name)
+
+ def _git_create_repo(self, path):
+ # If a user has git configuration init.defaultBranch set we want to override that
+ with tempfile.TemporaryDirectory() as d:
+ out = git(['--version'], str(d))[1]
+ if version_compare(search_version(out), '>= 2.28'):
+ extra_cmd = ['--initial-branch', 'master']
+ else:
+ extra_cmd = []
+
+ self._create_project(path)
+ self._git(['init'] + extra_cmd, path)
+ self._git_config(path)
+ self._git(['add', '.'], path)
+ self._git(['commit', '--no-gpg-sign', '-m', 'Initial commit'], path)
+
+ def _git_create_remote_repo(self, name):
+ self._git_create_repo(self.root_dir / name)
+
+ def _git_create_local_repo(self, name):
+ self._git_create_repo(self.subprojects_dir / name)
+
+ def _git_create_remote_commit(self, name, branch):
+ self._git_remote(['checkout', branch], name)
+ self._git_remote(['commit', '--no-gpg-sign', '--allow-empty', '-m', f'initial {branch} commit'], name)
+
+ def _git_create_remote_branch(self, name, branch):
+ self._git_remote(['checkout', '-b', branch], name)
+ self._git_remote(['commit', '--no-gpg-sign', '--allow-empty', '-m', f'initial {branch} commit'], name)
+
+ def _git_create_remote_tag(self, name, tag):
+ self._git_remote(['commit', '--no-gpg-sign', '--allow-empty', '-m', f'tag {tag} commit'], name)
+ self._git_remote(['tag', '--no-sign', tag], name)
+
+ def _wrap_create_git(self, name, revision='master', depth=None):
+ path = self.root_dir / name
+ with open(str((self.subprojects_dir / name).with_suffix('.wrap')), 'w', encoding='utf-8') as f:
+ if depth is None:
+ depth_line = ''
+ else:
+ depth_line = 'depth = {}'.format(depth)
+ f.write(textwrap.dedent(
+ '''
+ [wrap-git]
+ url={}
+ revision={}
+ {}
+ '''.format(os.path.abspath(str(path)), revision, depth_line)))
+
+ def _wrap_create_file(self, name, tarball='dummy.tar.gz'):
+ path = self.root_dir / tarball
+ with open(str((self.subprojects_dir / name).with_suffix('.wrap')), 'w', encoding='utf-8') as f:
+ f.write(textwrap.dedent(
+ f'''
+ [wrap-file]
+ source_url={os.path.abspath(str(path))}
+ source_filename={tarball}
+ '''))
+ Path(self.packagecache_dir / tarball).touch()
+
+ def _subprojects_cmd(self, args):
+ return self._run(self.meson_command + ['subprojects'] + args, workdir=str(self.project_dir))
+
+ def test_git_update(self):
+ subp_name = 'sub1'
+
+ # Create a fake remote git repository and a wrap file. Checks that
+ # "meson subprojects download" works.
+ self._git_create_remote_repo(subp_name)
+ self._wrap_create_git(subp_name)
+ self._subprojects_cmd(['download'])
+ self.assertPathExists(str(self.subprojects_dir / subp_name))
+ self._git_config(self.subprojects_dir / subp_name)
+
+ # Create a new remote branch and update the wrap file. Checks that
+ # "meson subprojects update --reset" checkout the new branch.
+ self._git_create_remote_branch(subp_name, 'newbranch')
+ self._wrap_create_git(subp_name, 'newbranch')
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_branch(subp_name), 'newbranch')
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch'))
+
+ # Update remote newbranch. Checks the new commit is pulled into existing
+ # local newbranch. Make sure it does not print spurious 'git stash' message.
+ self._git_create_remote_commit(subp_name, 'newbranch')
+ out = self._subprojects_cmd(['update', '--reset'])
+ self.assertNotIn('No local changes to save', out)
+ self.assertEqual(self._git_local_branch(subp_name), 'newbranch')
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch'))
+
+ # Update remote newbranch and switch to another branch. Checks that it
+ # switch current branch to newbranch and pull latest commit.
+ self._git_local(['checkout', 'master'], subp_name)
+ self._git_create_remote_commit(subp_name, 'newbranch')
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_branch(subp_name), 'newbranch')
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch'))
+
+ # Stage some local changes then update. Checks that local changes got
+ # stashed.
+ self._create_project(self.subprojects_dir / subp_name, 'new_project_name')
+ self._git_local(['add', '.'], subp_name)
+ self._git_create_remote_commit(subp_name, 'newbranch')
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_branch(subp_name), 'newbranch')
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch'))
+ self.assertTrue(self._git_local(['stash', 'list'], subp_name))
+
+ # Create a new remote tag and update the wrap file. Checks that
+ # "meson subprojects update --reset" checkout the new tag in detached mode.
+ self._git_create_remote_tag(subp_name, 'newtag')
+ self._wrap_create_git(subp_name, 'newtag')
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_branch(subp_name), '')
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newtag'))
+
+ # Create a new remote commit and update the wrap file with the commit id.
+ # Checks that "meson subprojects update --reset" checkout the new commit
+ # in detached mode.
+ self._git_local(['checkout', 'master'], subp_name)
+ self._git_create_remote_commit(subp_name, 'newbranch')
+ new_commit = self._git_remote(['rev-parse', 'HEAD'], subp_name)
+ self._wrap_create_git(subp_name, new_commit)
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_branch(subp_name), '')
+ self.assertEqual(self._git_local_commit(subp_name), new_commit)
+
+ # Create a local project not in a git repository, then update it with
+ # a git wrap. Without --reset it should print error message and return
+ # failure. With --reset it should delete existing project and clone the
+ # new project.
+ subp_name = 'sub2'
+ self._create_project(self.subprojects_dir / subp_name)
+ self._git_create_remote_repo(subp_name)
+ self._wrap_create_git(subp_name)
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ self._subprojects_cmd(['update'])
+ self.assertIn('Not a git repository', cm.exception.output)
+ self._subprojects_cmd(['update', '--reset'])
+ self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name))
+
+ # Create a fake remote git repository and a wrap file targeting
+ # HEAD and depth = 1. Checks that "meson subprojects download" works.
+ subp_name = 'sub3'
+ self._git_create_remote_repo(subp_name)
+ self._wrap_create_git(subp_name, revision='head', depth='1')
+ self._subprojects_cmd(['download'])
+ self.assertPathExists(str(self.subprojects_dir / subp_name))
+ self._git_config(self.subprojects_dir / subp_name)
+
+ @skipIfNoExecutable('true')
+ def test_foreach(self):
+ self._create_project(self.subprojects_dir / 'sub_file')
+ self._wrap_create_file('sub_file')
+ self._git_create_local_repo('sub_git')
+ self._wrap_create_git('sub_git')
+ self._git_create_local_repo('sub_git_no_wrap')
+
+ def ran_in(s):
+ ret = []
+ prefix = 'Executing command in '
+ for l in s.splitlines():
+ if l.startswith(prefix):
+ ret.append(l[len(prefix):])
+ return sorted(ret)
+
+ dummy_cmd = ['true']
+ out = self._subprojects_cmd(['foreach'] + dummy_cmd)
+ self.assertEqual(ran_in(out), sorted(['subprojects/sub_file', 'subprojects/sub_git', 'subprojects/sub_git_no_wrap']))
+ out = self._subprojects_cmd(['foreach', '--types', 'git,file'] + dummy_cmd)
+ self.assertEqual(ran_in(out), sorted(['subprojects/sub_file', 'subprojects/sub_git']))
+ out = self._subprojects_cmd(['foreach', '--types', 'file'] + dummy_cmd)
+ self.assertEqual(ran_in(out), ['subprojects/sub_file'])
+ out = self._subprojects_cmd(['foreach', '--types', 'git'] + dummy_cmd)
+ self.assertEqual(ran_in(out), ['subprojects/sub_git'])
+
+ def test_purge(self):
+ self._create_project(self.subprojects_dir / 'sub_file')
+ self._wrap_create_file('sub_file')
+ self._git_create_local_repo('sub_git')
+ self._wrap_create_git('sub_git')
+
+ sub_file_subprojects_dir = self.subprojects_dir / 'sub_file' / 'subprojects'
+ sub_file_subprojects_dir.mkdir(exist_ok=True, parents=True)
+ real_dir = Path('sub_file') / 'subprojects' / 'real'
+
+ self._wrap_create_file(real_dir, tarball='dummy2.tar.gz')
+
+ with open(str((self.subprojects_dir / 'redirect').with_suffix('.wrap')), 'w', encoding='utf-8') as f:
+ f.write(textwrap.dedent(
+ f'''
+ [wrap-redirect]
+ filename = {real_dir}.wrap
+ '''))
+
+ def deleting(s: str) -> T.List[str]:
+ ret = []
+ prefix = 'Deleting '
+ for l in s.splitlines():
+ if l.startswith(prefix):
+ ret.append(l[len(prefix):])
+ return sorted(ret)
+
+ out = self._subprojects_cmd(['purge'])
+ self.assertEqual(deleting(out), sorted([
+ str(self.subprojects_dir / 'redirect.wrap'),
+ str(self.subprojects_dir / 'sub_file'),
+ str(self.subprojects_dir / 'sub_git'),
+ ]))
+ out = self._subprojects_cmd(['purge', '--include-cache'])
+ self.assertEqual(deleting(out), sorted([
+ str(self.subprojects_dir / 'sub_git'),
+ str(self.subprojects_dir / 'redirect.wrap'),
+ str(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz'),
+ str(self.subprojects_dir / 'packagecache' / 'dummy2.tar.gz'),
+ str(self.subprojects_dir / 'sub_file'),
+ ]))
+ out = self._subprojects_cmd(['purge', '--include-cache', '--confirm'])
+ self.assertEqual(deleting(out), sorted([
+ str(self.subprojects_dir / 'sub_git'),
+ str(self.subprojects_dir / 'redirect.wrap'),
+ str(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz'),
+ str(self.subprojects_dir / 'packagecache' / 'dummy2.tar.gz'),
+ str(self.subprojects_dir / 'sub_file'),
+ ]))
+ self.assertFalse(Path(self.subprojects_dir / 'packagecache' / 'dummy.tar.gz').exists())
+ self.assertFalse(Path(self.subprojects_dir / 'sub_file').exists())
+ self.assertFalse(Path(self.subprojects_dir / 'sub_git').exists())
+ self.assertFalse(Path(self.subprojects_dir / 'redirect.wrap').exists())
diff --git a/unittests/taptests.py b/unittests/taptests.py
new file mode 100644
index 0000000..6c2ccb0
--- /dev/null
+++ b/unittests/taptests.py
@@ -0,0 +1,294 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import io
+
+from mesonbuild.mtest import TAPParser, TestResult
+
+
+class TAPParserTests(unittest.TestCase):
+ def assert_test(self, events, **kwargs):
+ if 'explanation' not in kwargs:
+ kwargs['explanation'] = None
+ self.assertEqual(next(events), TAPParser.Test(**kwargs))
+
+ def assert_plan(self, events, **kwargs):
+ if 'skipped' not in kwargs:
+ kwargs['skipped'] = False
+ if 'explanation' not in kwargs:
+ kwargs['explanation'] = None
+ self.assertEqual(next(events), TAPParser.Plan(**kwargs))
+
+ def assert_version(self, events, **kwargs):
+ self.assertEqual(next(events), TAPParser.Version(**kwargs))
+
+ def assert_error(self, events):
+ self.assertEqual(type(next(events)), TAPParser.Error)
+
+ def assert_unexpected(self, events, **kwargs):
+ self.assertEqual(next(events), TAPParser.UnknownLine(**kwargs))
+
+ def assert_bailout(self, events, **kwargs):
+ self.assertEqual(next(events), TAPParser.Bailout(**kwargs))
+
+ def assert_last(self, events):
+ with self.assertRaises(StopIteration):
+ next(events)
+
+ def parse_tap(self, s):
+ parser = TAPParser()
+ return iter(parser.parse(io.StringIO(s)))
+
+ def parse_tap_v13(self, s):
+ events = self.parse_tap('TAP version 13\n' + s)
+ self.assert_version(events, version=13)
+ return events
+
+ def test_empty(self):
+ events = self.parse_tap('')
+ self.assert_last(events)
+
+ def test_empty_plan(self):
+ events = self.parse_tap('1..0')
+ self.assert_plan(events, num_tests=0, late=False, skipped=True)
+ self.assert_last(events)
+
+ def test_plan_directive(self):
+ events = self.parse_tap('1..0 # skipped for some reason')
+ self.assert_plan(events, num_tests=0, late=False, skipped=True,
+ explanation='for some reason')
+ self.assert_last(events)
+
+ events = self.parse_tap('1..1 # skipped for some reason\nok 1')
+ self.assert_error(events)
+ self.assert_plan(events, num_tests=1, late=False, skipped=True,
+ explanation='for some reason')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ events = self.parse_tap('1..1 # todo not supported here\nok 1')
+ self.assert_error(events)
+ self.assert_plan(events, num_tests=1, late=False, skipped=False,
+ explanation='not supported here')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_one_test_ok(self):
+ events = self.parse_tap('ok')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_one_test_with_number(self):
+ events = self.parse_tap('ok 1')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_one_test_with_name(self):
+ events = self.parse_tap('ok 1 abc')
+ self.assert_test(events, number=1, name='abc', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_one_test_not_ok(self):
+ events = self.parse_tap('not ok')
+ self.assert_test(events, number=1, name='', result=TestResult.FAIL)
+ self.assert_last(events)
+
+ def test_one_test_todo(self):
+ events = self.parse_tap('not ok 1 abc # TODO')
+ self.assert_test(events, number=1, name='abc', result=TestResult.EXPECTEDFAIL)
+ self.assert_last(events)
+
+ events = self.parse_tap('ok 1 abc # TODO')
+ self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS)
+ self.assert_last(events)
+
+ def test_one_test_skip(self):
+ events = self.parse_tap('ok 1 abc # SKIP')
+ self.assert_test(events, number=1, name='abc', result=TestResult.SKIP)
+ self.assert_last(events)
+
+ def test_one_test_skip_failure(self):
+ events = self.parse_tap('not ok 1 abc # SKIP')
+ self.assert_test(events, number=1, name='abc', result=TestResult.FAIL)
+ self.assert_last(events)
+
+ def test_many_early_plan(self):
+ events = self.parse_tap('1..4\nok 1\nnot ok 2\nok 3\nnot ok 4')
+ self.assert_plan(events, num_tests=4, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_test(events, number=3, name='', result=TestResult.OK)
+ self.assert_test(events, number=4, name='', result=TestResult.FAIL)
+ self.assert_last(events)
+
+ def test_many_late_plan(self):
+ events = self.parse_tap('ok 1\nnot ok 2\nok 3\nnot ok 4\n1..4')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_test(events, number=3, name='', result=TestResult.OK)
+ self.assert_test(events, number=4, name='', result=TestResult.FAIL)
+ self.assert_plan(events, num_tests=4, late=True)
+ self.assert_last(events)
+
+ def test_directive_case(self):
+ events = self.parse_tap('ok 1 abc # skip')
+ self.assert_test(events, number=1, name='abc', result=TestResult.SKIP)
+ self.assert_last(events)
+
+ events = self.parse_tap('ok 1 abc # ToDo')
+ self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS)
+ self.assert_last(events)
+
+ def test_directive_explanation(self):
+ events = self.parse_tap('ok 1 abc # skip why')
+ self.assert_test(events, number=1, name='abc', result=TestResult.SKIP,
+ explanation='why')
+ self.assert_last(events)
+
+ events = self.parse_tap('ok 1 abc # ToDo Because')
+ self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS,
+ explanation='Because')
+ self.assert_last(events)
+
+ def test_one_test_early_plan(self):
+ events = self.parse_tap('1..1\nok')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_one_test_late_plan(self):
+ events = self.parse_tap('ok\n1..1')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_plan(events, num_tests=1, late=True)
+ self.assert_last(events)
+
+ def test_out_of_order(self):
+ events = self.parse_tap('ok 2')
+ self.assert_error(events)
+ self.assert_test(events, number=2, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_middle_plan(self):
+ events = self.parse_tap('ok 1\n1..2\nok 2')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_plan(events, num_tests=2, late=True)
+ self.assert_error(events)
+ self.assert_test(events, number=2, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_too_many_plans(self):
+ events = self.parse_tap('1..1\n1..2\nok 1')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_error(events)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_too_many(self):
+ events = self.parse_tap('ok 1\nnot ok 2\n1..1')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_plan(events, num_tests=1, late=True)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ events = self.parse_tap('1..1\nok 1\nnot ok 2')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ def test_too_few(self):
+ events = self.parse_tap('ok 1\nnot ok 2\n1..3')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_plan(events, num_tests=3, late=True)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ events = self.parse_tap('1..3\nok 1\nnot ok 2')
+ self.assert_plan(events, num_tests=3, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ def test_too_few_bailout(self):
+ events = self.parse_tap('1..3\nok 1\nnot ok 2\nBail out! no third test')
+ self.assert_plan(events, num_tests=3, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_bailout(events, message='no third test')
+ self.assert_last(events)
+
+ def test_diagnostics(self):
+ events = self.parse_tap('1..1\n# ignored\nok 1')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ events = self.parse_tap('# ignored\n1..1\nok 1\n# ignored too')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ events = self.parse_tap('# ignored\nok 1\n1..1\n# ignored too')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_plan(events, num_tests=1, late=True)
+ self.assert_last(events)
+
+ def test_empty_line(self):
+ events = self.parse_tap('1..1\n\nok 1')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_unexpected(self):
+ events = self.parse_tap('1..1\ninvalid\nok 1')
+ self.assert_plan(events, num_tests=1, late=False)
+ self.assert_unexpected(events, message='invalid', lineno=2)
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ def test_version(self):
+ events = self.parse_tap('TAP version 13\n')
+ self.assert_version(events, version=13)
+ self.assert_last(events)
+
+ events = self.parse_tap('TAP version 12\n')
+ self.assert_error(events)
+ self.assert_last(events)
+
+ events = self.parse_tap('1..0\nTAP version 13\n')
+ self.assert_plan(events, num_tests=0, late=False, skipped=True)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ def test_yaml(self):
+ events = self.parse_tap_v13('ok\n ---\n foo: abc\n bar: def\n ...\nok 2')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_test(events, number=2, name='', result=TestResult.OK)
+ self.assert_last(events)
+
+ events = self.parse_tap_v13('ok\n ---\n foo: abc\n bar: def')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_error(events)
+ self.assert_last(events)
+
+ events = self.parse_tap_v13('ok 1\n ---\n foo: abc\n bar: def\nnot ok 2')
+ self.assert_test(events, number=1, name='', result=TestResult.OK)
+ self.assert_error(events)
+ self.assert_test(events, number=2, name='', result=TestResult.FAIL)
+ self.assert_last(events)
diff --git a/unittests/windowstests.py b/unittests/windowstests.py
new file mode 100644
index 0000000..c81d924
--- /dev/null
+++ b/unittests/windowstests.py
@@ -0,0 +1,400 @@
+# Copyright 2016-2021 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import re
+import os
+import shutil
+from unittest import mock, SkipTest, skipUnless, skipIf
+from glob import glob
+
+import mesonbuild.mlog
+import mesonbuild.depfile
+import mesonbuild.dependencies.factory
+import mesonbuild.envconfig
+import mesonbuild.environment
+import mesonbuild.coredata
+import mesonbuild.modules.gnome
+from mesonbuild.mesonlib import (
+ MachineChoice, is_windows, is_cygwin, python_command, version_compare,
+ EnvironmentException, OptionKey
+)
+from mesonbuild.compilers import (
+ detect_c_compiler, detect_d_compiler, compiler_from_language,
+)
+from mesonbuild.programs import ExternalProgram
+import mesonbuild.dependencies.base
+import mesonbuild.modules.pkgconfig
+
+
+from run_tests import (
+ Backend, get_fake_env
+)
+
+from .baseplatformtests import BasePlatformTests
+from .helpers import *
+
+@skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)")
+class WindowsTests(BasePlatformTests):
+ '''
+ Tests that should run on Cygwin, MinGW, and MSVC
+ '''
+
+ def setUp(self):
+ super().setUp()
+ self.platform_test_dir = os.path.join(self.src_root, 'test cases/windows')
+
+ @skipIf(is_cygwin(), 'Test only applicable to Windows')
+ @mock.patch.dict(os.environ)
+ def test_find_program(self):
+ '''
+ Test that Windows-specific edge-cases in find_program are functioning
+ correctly. Cannot be an ordinary test because it involves manipulating
+ PATH to point to a directory with Python scripts.
+ '''
+ testdir = os.path.join(self.platform_test_dir, '8 find program')
+ # Find `cmd` and `cmd.exe`
+ prog1 = ExternalProgram('cmd')
+ self.assertTrue(prog1.found(), msg='cmd not found')
+ prog2 = ExternalProgram('cmd.exe')
+ self.assertTrue(prog2.found(), msg='cmd.exe not found')
+ self.assertPathEqual(prog1.get_path(), prog2.get_path())
+ # Find cmd.exe with args without searching
+ prog = ExternalProgram('cmd', command=['cmd', '/C'])
+ self.assertTrue(prog.found(), msg='cmd not found with args')
+ self.assertPathEqual(prog.get_command()[0], 'cmd')
+ # Find cmd with an absolute path that's missing the extension
+ cmd_path = prog2.get_path()[:-4]
+ prog = ExternalProgram(cmd_path)
+ self.assertTrue(prog.found(), msg=f'{cmd_path!r} not found')
+ # Finding a script with no extension inside a directory works
+ prog = ExternalProgram(os.path.join(testdir, 'test-script'))
+ self.assertTrue(prog.found(), msg='test-script not found')
+ # Finding a script with an extension inside a directory works
+ prog = ExternalProgram(os.path.join(testdir, 'test-script-ext.py'))
+ self.assertTrue(prog.found(), msg='test-script-ext.py not found')
+ # Finding a script in PATH
+ os.environ['PATH'] += os.pathsep + testdir
+ # If `.PY` is in PATHEXT, scripts can be found as programs
+ if '.PY' in [ext.upper() for ext in os.environ['PATHEXT'].split(';')]:
+ # Finding a script in PATH w/o extension works and adds the interpreter
+ prog = ExternalProgram('test-script-ext')
+ self.assertTrue(prog.found(), msg='test-script-ext not found in PATH')
+ self.assertPathEqual(prog.get_command()[0], python_command[0])
+ self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
+ # Finding a script in PATH with extension works and adds the interpreter
+ prog = ExternalProgram('test-script-ext.py')
+ self.assertTrue(prog.found(), msg='test-script-ext.py not found in PATH')
+ self.assertPathEqual(prog.get_command()[0], python_command[0])
+ self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
+ # Using a script with an extension directly via command= works and adds the interpreter
+ prog = ExternalProgram('test-script-ext.py', command=[os.path.join(testdir, 'test-script-ext.py'), '--help'])
+ self.assertTrue(prog.found(), msg='test-script-ext.py with full path not picked up via command=')
+ self.assertPathEqual(prog.get_command()[0], python_command[0])
+ self.assertPathEqual(prog.get_command()[2], '--help')
+ self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
+ # Using a script without an extension directly via command= works and adds the interpreter
+ prog = ExternalProgram('test-script', command=[os.path.join(testdir, 'test-script'), '--help'])
+ self.assertTrue(prog.found(), msg='test-script with full path not picked up via command=')
+ self.assertPathEqual(prog.get_command()[0], python_command[0])
+ self.assertPathEqual(prog.get_command()[2], '--help')
+ self.assertPathBasenameEqual(prog.get_path(), 'test-script')
+ # Ensure that WindowsApps gets removed from PATH
+ path = os.environ['PATH']
+ if 'WindowsApps' not in path:
+ username = os.environ['USERNAME']
+ appstore_dir = fr'C:\Users\{username}\AppData\Local\Microsoft\WindowsApps'
+ path = os.pathsep + appstore_dir
+ path = ExternalProgram._windows_sanitize_path(path)
+ self.assertNotIn('WindowsApps', path)
+
+ def test_ignore_libs(self):
+ '''
+ Test that find_library on libs that are to be ignored returns an empty
+ array of arguments. Must be a unit test because we cannot inspect
+ ExternalLibraryHolder from build files.
+ '''
+ testdir = os.path.join(self.platform_test_dir, '1 basic')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() != 'msvc':
+ raise SkipTest('Not using MSVC')
+ # To force people to update this test, and also test
+ self.assertEqual(set(cc.ignore_libs), {'c', 'm', 'pthread', 'dl', 'rt', 'execinfo'})
+ for l in cc.ignore_libs:
+ self.assertEqual(cc.find_library(l, env, []), [])
+
+ def test_rc_depends_files(self):
+ testdir = os.path.join(self.platform_test_dir, '5 resources')
+
+ # resource compiler depfile generation is not yet implemented for msvc
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ depfile_works = detect_c_compiler(env, MachineChoice.HOST).get_id() not in {'msvc', 'clang-cl', 'intel-cl'}
+
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Test compile_resources(depend_file:)
+ # Changing mtime of sample.ico should rebuild prog
+ self.utime(os.path.join(testdir, 'res', 'sample.ico'))
+ self.assertRebuiltTarget('prog')
+ # Test depfile generation by compile_resources
+ # Changing mtime of resource.h should rebuild myres.rc and then prog
+ if depfile_works:
+ self.utime(os.path.join(testdir, 'inc', 'resource', 'resource.h'))
+ self.assertRebuiltTarget('prog')
+ self.wipe()
+
+ if depfile_works:
+ testdir = os.path.join(self.platform_test_dir, '12 resources with custom targets')
+ self.init(testdir)
+ self.build()
+ # Immediately rebuilding should not do anything
+ self.assertBuildIsNoop()
+ # Changing mtime of resource.h should rebuild myres_1.rc and then prog_1
+ self.utime(os.path.join(testdir, 'res', 'resource.h'))
+ self.assertRebuiltTarget('prog_1')
+
+ def test_msvc_cpp17(self):
+ testdir = os.path.join(self.unit_test_dir, '44 vscpp17')
+
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() != 'msvc':
+ raise SkipTest('Test only applies to MSVC-like compilers')
+
+ try:
+ self.init(testdir)
+ except subprocess.CalledProcessError:
+ # According to Python docs, output is only stored when
+ # using check_output. We don't use it, so we can't check
+ # that the output is correct (i.e. that it failed due
+ # to the right reason).
+ return
+ self.build()
+
+ def test_install_pdb_introspection(self):
+ testdir = os.path.join(self.platform_test_dir, '1 basic')
+
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() != 'msvc':
+ raise SkipTest('Test only applies to MSVC-like compilers')
+
+ self.init(testdir)
+ installed = self.introspect('--installed')
+ files = [os.path.basename(path) for path in installed.values()]
+
+ self.assertIn('prog.pdb', files)
+
+ def _check_ld(self, name: str, lang: str, expected: str) -> None:
+ if not shutil.which(name):
+ raise SkipTest(f'Could not find {name}.')
+ envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']]
+
+ # Also test a deprecated variable if there is one.
+ if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP:
+ envvars.append(
+ mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld'])
+
+ for envvar in envvars:
+ with mock.patch.dict(os.environ, {envvar: name}):
+ env = get_fake_env()
+ try:
+ comp = compiler_from_language(env, lang, MachineChoice.HOST)
+ except EnvironmentException:
+ raise SkipTest(f'Could not find a compiler for {lang}')
+ self.assertEqual(comp.linker.id, expected)
+
+ def test_link_environment_variable_lld_link(self):
+ env = get_fake_env()
+ comp = detect_c_compiler(env, MachineChoice.HOST)
+ if comp.get_argument_syntax() == 'gcc':
+ raise SkipTest('GCC cannot be used with link compatible linkers.')
+ self._check_ld('lld-link', 'c', 'lld-link')
+
+ def test_link_environment_variable_link(self):
+ env = get_fake_env()
+ comp = detect_c_compiler(env, MachineChoice.HOST)
+ if comp.get_argument_syntax() == 'gcc':
+ raise SkipTest('GCC cannot be used with link compatible linkers.')
+ self._check_ld('link', 'c', 'link')
+
+ def test_link_environment_variable_optlink(self):
+ env = get_fake_env()
+ comp = detect_c_compiler(env, MachineChoice.HOST)
+ if comp.get_argument_syntax() == 'gcc':
+ raise SkipTest('GCC cannot be used with link compatible linkers.')
+ self._check_ld('optlink', 'c', 'optlink')
+
+ @skip_if_not_language('rust')
+ def test_link_environment_variable_rust(self):
+ self._check_ld('link', 'rust', 'link')
+
+ @skip_if_not_language('d')
+ def test_link_environment_variable_d(self):
+ env = get_fake_env()
+ comp = detect_d_compiler(env, MachineChoice.HOST)
+ if comp.id == 'dmd':
+ raise SkipTest('meson cannot reliably make DMD use a different linker.')
+ self._check_ld('lld-link', 'd', 'lld-link')
+
+ def test_pefile_checksum(self):
+ try:
+ import pefile
+ except ImportError:
+ if is_ci():
+ raise
+ raise SkipTest('pefile module not found')
+ testdir = os.path.join(self.common_test_dir, '6 linkshared')
+ self.init(testdir, extra_args=['--buildtype=release'])
+ self.build()
+ # Test that binaries have a non-zero checksum
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ cc_id = cc.get_id()
+ ld_id = cc.get_linker_id()
+ dll = glob(os.path.join(self.builddir, '*mycpplib.dll'))[0]
+ exe = os.path.join(self.builddir, 'cppprog.exe')
+ for f in (dll, exe):
+ pe = pefile.PE(f)
+ msg = f'PE file: {f!r}, compiler: {cc_id!r}, linker: {ld_id!r}'
+ if cc_id == 'clang-cl':
+ # Latest clang-cl tested (7.0) does not write checksums out
+ self.assertFalse(pe.verify_checksum(), msg=msg)
+ else:
+ # Verify that a valid checksum was written by all other compilers
+ self.assertTrue(pe.verify_checksum(), msg=msg)
+
+ def test_qt5dependency_vscrt(self):
+ '''
+ Test that qt5 dependencies use the debug module suffix when b_vscrt is
+ set to 'mdd'
+ '''
+ # Verify that the `b_vscrt` option is available
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if OptionKey('b_vscrt') not in cc.base_options:
+ raise SkipTest('Compiler does not support setting the VS CRT')
+ # Verify that qmake is for Qt5
+ if not shutil.which('qmake-qt5'):
+ if not shutil.which('qmake') and not is_ci():
+ raise SkipTest('QMake not found')
+ output = subprocess.getoutput('qmake --version')
+ if 'Qt version 5' not in output and not is_ci():
+ raise SkipTest('Qmake found, but it is not for Qt 5.')
+ # Setup with /MDd
+ testdir = os.path.join(self.framework_test_dir, '4 qt')
+ self.init(testdir, extra_args=['-Db_vscrt=mdd'])
+ # Verify that we're linking to the debug versions of Qt DLLs
+ build_ninja = os.path.join(self.builddir, 'build.ninja')
+ with open(build_ninja, encoding='utf-8') as f:
+ contents = f.read()
+ m = re.search('build qt5core.exe: cpp_LINKER.*Qt5Cored.lib', contents)
+ self.assertIsNotNone(m, msg=contents)
+
+ def test_compiler_checks_vscrt(self):
+ '''
+ Test that the correct VS CRT is used when running compiler checks
+ '''
+ # Verify that the `b_vscrt` option is available
+ env = get_fake_env()
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if OptionKey('b_vscrt') not in cc.base_options:
+ raise SkipTest('Compiler does not support setting the VS CRT')
+
+ def sanitycheck_vscrt(vscrt):
+ checks = self.get_meson_log_sanitychecks()
+ self.assertGreater(len(checks), 0)
+ for check in checks:
+ self.assertIn(vscrt, check)
+
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+ self.init(testdir)
+ sanitycheck_vscrt('/MDd')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Dbuildtype=debugoptimized'])
+ sanitycheck_vscrt('/MD')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Dbuildtype=release'])
+ sanitycheck_vscrt('/MD')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Db_vscrt=md'])
+ sanitycheck_vscrt('/MD')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Db_vscrt=mdd'])
+ sanitycheck_vscrt('/MDd')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Db_vscrt=mt'])
+ sanitycheck_vscrt('/MT')
+
+ self.new_builddir()
+ self.init(testdir, extra_args=['-Db_vscrt=mtd'])
+ sanitycheck_vscrt('/MTd')
+
+ def test_modules(self):
+ if self.backend is not Backend.ninja:
+ raise SkipTest(f'C++ modules only work with the Ninja backend (not {self.backend.name}).')
+ if 'VSCMD_VER' not in os.environ:
+ raise SkipTest('C++ modules is only supported with Visual Studio.')
+ if version_compare(os.environ['VSCMD_VER'], '<16.10.0'):
+ raise SkipTest('C++ modules are only supported with VS 2019 Preview or newer.')
+ self.init(os.path.join(self.unit_test_dir, '85 cpp modules'))
+ self.build()
+
+ def test_non_utf8_fails(self):
+ # FIXME: VS backend does not use flags from compiler.get_always_args()
+ # and thus it's missing /utf-8 argument. Was that intentional? This needs
+ # to be revisited.
+ if self.backend is not Backend.ninja:
+ raise SkipTest(f'This test only pass with ninja backend (not {self.backend.name}).')
+ testdir = os.path.join(self.platform_test_dir, '18 msvc charset')
+ env = get_fake_env(testdir, self.builddir, self.prefix)
+ cc = detect_c_compiler(env, MachineChoice.HOST)
+ if cc.get_argument_syntax() != 'msvc':
+ raise SkipTest('Not using MSVC')
+ self.init(testdir, extra_args=['-Dtest-failure=true'])
+ self.assertRaises(subprocess.CalledProcessError, self.build)
+
+ @unittest.skipIf(is_cygwin(), "Needs visual studio")
+ def test_vsenv_option(self):
+ if self.backend is not Backend.ninja:
+ raise SkipTest('Only ninja backend is valid for test')
+ env = os.environ.copy()
+ env['MESON_FORCE_VSENV_FOR_UNITTEST'] = '1'
+ # Remove ninja from PATH to ensure that the one provided by Visual
+ # Studio is picked, as a regression test for
+ # https://github.com/mesonbuild/meson/issues/9774
+ env['PATH'] = get_path_without_cmd('ninja', env['PATH'])
+ testdir = os.path.join(self.common_test_dir, '1 trivial')
+ out = self.init(testdir, extra_args=['--vsenv'], override_envvars=env)
+ self.assertIn('Activating VS', out)
+ self.assertRegex(out, 'Visual Studio environment is needed to run Ninja')
+ # All these directly call ninja with the full path, so we need to patch
+ # it out to use meson subcommands
+ with mock.patch.object(self, 'build_command', self.meson_command + ['compile']):
+ out = self.build(override_envvars=env)
+ self.assertIn('Activating VS', out)
+ with mock.patch.object(self, 'test_command', self.meson_command + ['test']):
+ out = self.run_tests(override_envvars=env)
+ self.assertIn('Activating VS', out)
+ with mock.patch.object(self, 'install_command', self.meson_command + ['install']):
+ out = self.install(override_envvars=env)
+ self.assertIn('Activating VS', out)