From 7b6e527f440cd7e6f8be2b07cee320ee6ca18786 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 29 Apr 2024 06:41:38 +0200 Subject: Adding upstream version 1.0.1. Signed-off-by: Daniel Baumann --- unittests/allplatformstests.py | 4499 ++++++++++++++++++++++++++++++++++ unittests/baseplatformtests.py | 483 ++++ unittests/darwintests.py | 150 ++ unittests/datatests.py | 242 ++ unittests/failuretests.py | 392 +++ unittests/helpers.py | 206 ++ unittests/internaltests.py | 1648 +++++++++++++ unittests/linuxcrosstests.py | 192 ++ unittests/linuxliketests.py | 1830 ++++++++++++++ unittests/machinefiletests.py | 953 +++++++ unittests/platformagnostictests.py | 123 + unittests/pythontests.py | 62 + unittests/rewritetests.py | 398 +++ unittests/subprojectscommandtests.py | 300 +++ unittests/taptests.py | 294 +++ unittests/windowstests.py | 400 +++ 16 files changed, 12172 insertions(+) create mode 100644 unittests/allplatformstests.py create mode 100644 unittests/baseplatformtests.py create mode 100644 unittests/darwintests.py create mode 100644 unittests/datatests.py create mode 100644 unittests/failuretests.py create mode 100644 unittests/helpers.py create mode 100644 unittests/internaltests.py create mode 100644 unittests/linuxcrosstests.py create mode 100644 unittests/linuxliketests.py create mode 100644 unittests/machinefiletests.py create mode 100644 unittests/platformagnostictests.py create mode 100644 unittests/pythontests.py create mode 100644 unittests/rewritetests.py create mode 100644 unittests/subprojectscommandtests.py create mode 100644 unittests/taptests.py create mode 100644 unittests/windowstests.py (limited to 'unittests') 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 ', 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 + + 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 "), + ("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) -- cgit v1.2.3