|
|
|
# Copyright 2016-2021 The Meson development team
|
|
|
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
import tempfile
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import unittest
|
|
|
|
from contextlib import contextmanager
|
|
|
|
|
|
|
|
from mesonbuild.mesonlib import (
|
|
|
|
MachineChoice, is_windows, is_osx, windows_proof_rmtree, windows_proof_rm
|
|
|
|
)
|
|
|
|
from mesonbuild.compilers import (
|
|
|
|
detect_objc_compiler, detect_objcpp_compiler
|
|
|
|
)
|
|
|
|
from mesonbuild.mesonlib import EnvironmentException, MesonException
|
|
|
|
from mesonbuild.programs import ExternalProgram
|
|
|
|
|
|
|
|
|
|
|
|
from run_tests import (
|
|
|
|
get_fake_env
|
|
|
|
)
|
|
|
|
|
|
|
|
from .baseplatformtests import BasePlatformTests
|
|
|
|
from .helpers import *
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def no_pkgconfig():
|
|
|
|
'''
|
|
|
|
A context manager that overrides shutil.which and ExternalProgram to force
|
|
|
|
them to return None for pkg-config to simulate it not existing.
|
|
|
|
'''
|
|
|
|
old_which = shutil.which
|
|
|
|
old_search = ExternalProgram._search
|
|
|
|
|
|
|
|
def new_search(self, name, search_dir):
|
|
|
|
if name == 'pkg-config':
|
|
|
|
return [None]
|
|
|
|
return old_search(self, name, search_dir)
|
|
|
|
|
|
|
|
def new_which(cmd, *kwargs):
|
|
|
|
if cmd == 'pkg-config':
|
|
|
|
return None
|
|
|
|
return old_which(cmd, *kwargs)
|
|
|
|
|
|
|
|
shutil.which = new_which
|
|
|
|
ExternalProgram._search = new_search
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
shutil.which = old_which
|
|
|
|
ExternalProgram._search = old_search
|
|
|
|
|
|
|
|
class FailureTests(BasePlatformTests):
|
|
|
|
'''
|
|
|
|
Tests that test failure conditions. Build files here should be dynamically
|
|
|
|
generated and static tests should go into `test cases/failing*`.
|
|
|
|
This is useful because there can be many ways in which a particular
|
|
|
|
function can fail, and creating failing tests for all of them is tedious
|
|
|
|
and slows down testing.
|
|
|
|
'''
|
|
|
|
dnf = "[Dd]ependency.*not found(:.*)?"
|
|
|
|
nopkg = '[Pp]kg-config.*not found'
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
|
|
|
self.srcdir = os.path.realpath(tempfile.mkdtemp())
|
|
|
|
self.mbuild = os.path.join(self.srcdir, 'meson.build')
|
|
|
|
self.moptions = os.path.join(self.srcdir, 'meson_options.txt')
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
super().tearDown()
|
|
|
|
windows_proof_rmtree(self.srcdir)
|
|
|
|
|
|
|
|
def assertMesonRaises(self, contents, match, *,
|
|
|
|
extra_args=None,
|
|
|
|
langs=None,
|
|
|
|
meson_version=None,
|
|
|
|
options=None,
|
|
|
|
override_envvars=None):
|
|
|
|
'''
|
|
|
|
Assert that running meson configure on the specified @contents raises
|
|
|
|
a error message matching regex @match.
|
|
|
|
'''
|
|
|
|
if langs is None:
|
|
|
|
langs = []
|
|
|
|
with open(self.mbuild, 'w', encoding='utf-8') as f:
|
|
|
|
f.write("project('failure test', 'c', 'cpp'")
|
|
|
|
if meson_version:
|
|
|
|
f.write(f", meson_version: '{meson_version}'")
|
|
|
|
f.write(")\n")
|
|
|
|
for lang in langs:
|
|
|
|
f.write(f"add_languages('{lang}', required : false)\n")
|
|
|
|
f.write(contents)
|
|
|
|
if options is not None:
|
|
|
|
with open(self.moptions, 'w', encoding='utf-8') as f:
|
|
|
|
f.write(options)
|
|
|
|
o = {'MESON_FORCE_BACKTRACE': '1'}
|
|
|
|
if override_envvars is None:
|
|
|
|
override_envvars = o
|
|
|
|
else:
|
|
|
|
override_envvars.update(o)
|
|
|
|
# Force tracebacks so we can detect them properly
|
|
|
|
with self.assertRaisesRegex(MesonException, match, msg=contents):
|
|
|
|
# Must run in-process or we'll get a generic CalledProcessError
|
|
|
|
self.init(self.srcdir, extra_args=extra_args,
|
|
|
|
inprocess=True,
|
|
|
|
override_envvars = override_envvars)
|
|
|
|
|
|
|
|
def obtainMesonOutput(self, contents, match, extra_args, langs, meson_version=None):
|
|
|
|
if langs is None:
|
|
|
|
langs = []
|
|
|
|
with open(self.mbuild, 'w', encoding='utf-8') as f:
|
|
|
|
f.write("project('output test', 'c', 'cpp'")
|
|
|
|
if meson_version:
|
|
|
|
f.write(f", meson_version: '{meson_version}'")
|
|
|
|
f.write(")\n")
|
|
|
|
for lang in langs:
|
|
|
|
f.write(f"add_languages('{lang}', required : false)\n")
|
|
|
|
f.write(contents)
|
|
|
|
# Run in-process for speed and consistency with assertMesonRaises
|
|
|
|
return self.init(self.srcdir, extra_args=extra_args, inprocess=True)
|
|
|
|
|
|
|
|
def assertMesonOutputs(self, contents, match, extra_args=None, langs=None, meson_version=None):
|
|
|
|
'''
|
|
|
|
Assert that running meson configure on the specified @contents outputs
|
|
|
|
something that matches regex @match.
|
|
|
|
'''
|
|
|
|
out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
|
|
|
|
self.assertRegex(out, match)
|
|
|
|
|
|
|
|
def assertMesonDoesNotOutput(self, contents, match, extra_args=None, langs=None, meson_version=None):
|
|
|
|
'''
|
|
|
|
Assert that running meson configure on the specified @contents does not output
|
|
|
|
something that matches regex @match.
|
|
|
|
'''
|
|
|
|
out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
|
|
|
|
self.assertNotRegex(out, match)
|
|
|
|
|
|
|
|
@skipIfNoPkgconfig
|
|
|
|
def test_dependency(self):
|
|
|
|
if subprocess.call(['pkg-config', '--exists', 'zlib']) != 0:
|
|
|
|
raise unittest.SkipTest('zlib not found with pkg-config')
|
|
|
|
a = (("dependency('zlib', method : 'fail')", "'fail' is invalid"),
|
|
|
|
("dependency('zlib', static : '1')", "[Ss]tatic.*boolean"),
|
|
|
|
("dependency('zlib', version : 1)", "Item must be a list or one of <class 'str'>"),
|
|
|
|
("dependency('zlib', required : 1)", "[Rr]equired.*boolean"),
|
|
|
|
("dependency('zlib', method : 1)", "[Mm]ethod.*string"),
|
|
|
|
("dependency('zlibfail')", self.dnf),)
|
|
|
|
for contents, match in a:
|
|
|
|
self.assertMesonRaises(contents, match)
|
|
|
|
|
|
|
|
def test_apple_frameworks_dependency(self):
|
|
|
|
if not is_osx():
|
|
|
|
raise unittest.SkipTest('only run on macOS')
|
|
|
|
self.assertMesonRaises("dependency('appleframeworks')",
|
|
|
|
"requires at least one module")
|
|
|
|
|
|
|
|
def test_extraframework_dependency_method(self):
|
|
|
|
code = "dependency('metal', method : 'extraframework')"
|
|
|
|
if not is_osx():
|
|
|
|
self.assertMesonRaises(code, self.dnf)
|
|
|
|
else:
|
|
|
|
# metal framework is always available on macOS
|
|
|
|
self.assertMesonOutputs(code, '[Dd]ependency.*metal.*found.*YES')
|
|
|
|
|
|
|
|
def test_sdl2_notfound_dependency(self):
|
|
|
|
# Want to test failure, so skip if available
|
|
|
|
if shutil.which('sdl2-config'):
|
|
|
|
raise unittest.SkipTest('sdl2-config found')
|
|
|
|
self.assertMesonRaises("dependency('sdl2', method : 'sdlconfig')", self.dnf)
|
|
|
|
if shutil.which('pkg-config'):
|
|
|
|
self.assertMesonRaises("dependency('sdl2', method : 'pkg-config')", self.dnf)
|
|
|
|
with no_pkgconfig():
|
|
|
|
# Look for pkg-config, cache it, then
|
|
|
|
# Use cached pkg-config without erroring out, then
|
|
|
|
# Use cached pkg-config to error out
|
|
|
|
code = "dependency('foobarrr', method : 'pkg-config', required : false)\n" \
|
|
|
|
"dependency('foobarrr2', method : 'pkg-config', required : false)\n" \
|
|
|
|
"dependency('sdl2', method : 'pkg-config')"
|
|
|
|
self.assertMesonRaises(code, self.nopkg)
|
|
|
|
|
|
|
|
def test_gnustep_notfound_dependency(self):
|
|
|
|
# Want to test failure, so skip if available
|
|
|
|
if shutil.which('gnustep-config'):
|
|
|
|
raise unittest.SkipTest('gnustep-config found')
|
|
|
|
self.assertMesonRaises("dependency('gnustep')",
|
|
|
|
f"(requires a Objc compiler|{self.dnf})",
|
|
|
|
langs = ['objc'])
|
|
|
|
|
|
|
|
def test_wx_notfound_dependency(self):
|
|
|
|
# Want to test failure, so skip if available
|
|
|
|
if shutil.which('wx-config-3.0') or shutil.which('wx-config') or shutil.which('wx-config-gtk3'):
|
|
|
|
raise unittest.SkipTest('wx-config, wx-config-3.0 or wx-config-gtk3 found')
|
|
|
|
self.assertMesonRaises("dependency('wxwidgets')", self.dnf)
|
|
|
|
self.assertMesonOutputs("dependency('wxwidgets', required : false)",
|
|
|
|
"Run-time dependency .*WxWidgets.* found: .*NO.*")
|
|
|
|
|
|
|
|
def test_wx_dependency(self):
|
|
|
|
if not shutil.which('wx-config-3.0') and not shutil.which('wx-config') and not shutil.which('wx-config-gtk3'):
|
|
|
|
raise unittest.SkipTest('Neither wx-config, wx-config-3.0 nor wx-config-gtk3 found')
|
|
|
|
self.assertMesonRaises("dependency('wxwidgets', modules : 1)",
|
|
|
|
"module argument is not a string")
|
|
|
|
|
|
|
|
def test_llvm_dependency(self):
|
|
|
|
self.assertMesonRaises("dependency('llvm', modules : 'fail')",
|
|
|
|
f"(required.*fail|{self.dnf})")
|
|
|
|
|
|
|
|
def test_boost_notfound_dependency(self):
|
|
|
|
# Can be run even if Boost is found or not
|
|
|
|
self.assertMesonRaises("dependency('boost', modules : 1)",
|
|
|
|
"module.*not a string")
|
|
|
|
self.assertMesonRaises("dependency('boost', modules : 'fail')",
|
|
|
|
f"(fail.*not found|{self.dnf})")
|
|
|
|
|
|
|
|
def test_boost_BOOST_ROOT_dependency(self):
|
|
|
|
# Test BOOST_ROOT; can be run even if Boost is found or not
|
|
|
|
self.assertMesonRaises("dependency('boost')",
|
|
|
|
f"(boost_root.*absolute|{self.dnf})",
|
|
|
|
override_envvars = {'BOOST_ROOT': 'relative/path'})
|
|
|
|
|
|
|
|
def test_dependency_invalid_method(self):
|
|
|
|
code = '''zlib_dep = dependency('zlib', required : false)
|
|
|
|
zlib_dep.get_configtool_variable('foo')
|
|
|
|
'''
|
|
|
|
self.assertMesonRaises(code, ".* is not a config-tool dependency")
|
|
|
|
code = '''zlib_dep = dependency('zlib', required : false)
|
|
|
|
dep = declare_dependency(dependencies : zlib_dep)
|
|
|
|
dep.get_pkgconfig_variable('foo')
|
|
|
|
'''
|
|
|
|
self.assertMesonRaises(code, "Method.*pkgconfig.*is invalid.*internal")
|
|
|
|
code = '''zlib_dep = dependency('zlib', required : false)
|
|
|
|
dep = declare_dependency(dependencies : zlib_dep)
|
|
|
|
dep.get_configtool_variable('foo')
|
|
|
|
'''
|
|
|
|
self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal")
|
|
|
|
|
|
|
|
def test_objc_cpp_detection(self):
|
|
|
|
'''
|
|
|
|
Test that when we can't detect objc or objcpp, we fail gracefully.
|
|
|
|
'''
|
|
|
|
env = get_fake_env()
|
|
|
|
try:
|
|
|
|
detect_objc_compiler(env, MachineChoice.HOST)
|
|
|
|
detect_objcpp_compiler(env, MachineChoice.HOST)
|
|
|
|
except EnvironmentException:
|
|
|
|
code = "add_languages('objc')\nadd_languages('objcpp')"
|
|
|
|
self.assertMesonRaises(code, "Unknown compiler")
|
|
|
|
return
|
|
|
|
raise unittest.SkipTest("objc and objcpp found, can't test detection failure")
|
|
|
|
|
|
|
|
def test_subproject_variables(self):
|
|
|
|
'''
|
|
|
|
Test that:
|
|
|
|
1. The correct message is outputted when a not-required dep is not
|
|
|
|
found and the fallback subproject is also not found.
|
|
|
|
2. A not-required fallback dependency is not found because the
|
|
|
|
subproject failed to parse.
|
|
|
|
3. A not-found not-required dep with a fallback subproject outputs the
|
|
|
|
correct message when the fallback subproject is found but the
|
|
|
|
variable inside it is not.
|
|
|
|
4. A fallback dependency is found from the subproject parsed in (3)
|
|
|
|
5. A wrap file from a subproject is used but fails because it does not
|
|
|
|
contain required keys.
|
|
|
|
'''
|
|
|
|
tdir = os.path.join(self.unit_test_dir, '20 subproj dep variables')
|
|
|
|
stray_file = os.path.join(tdir, 'subprojects/subsubproject.wrap')
|
|
|
|
if os.path.exists(stray_file):
|
|
|
|
windows_proof_rm(stray_file)
|
|
|
|
out = self.init(tdir, inprocess=True)
|
|
|
|
self.assertRegex(out, r"Neither a subproject directory nor a .*nosubproj.wrap.* file was found")
|
|
|
|
self.assertRegex(out, r'Function does not take positional arguments.')
|
|
|
|
self.assertRegex(out, r'Dependency .*somenotfounddep.* from subproject .*subprojects/somesubproj.* found: .*NO.*')
|
|
|
|
self.assertRegex(out, r'Dependency .*zlibproxy.* from subproject .*subprojects.*somesubproj.* found: .*YES.*')
|
|
|
|
self.assertRegex(out, r'Missing key .*source_filename.* in subsubproject.wrap')
|
|
|
|
windows_proof_rm(stray_file)
|
|
|
|
|
|
|
|
def test_exception_exit_status(self):
|
|
|
|
'''
|
|
|
|
Test exit status on python exception
|
|
|
|
'''
|
|
|
|
tdir = os.path.join(self.unit_test_dir, '21 exit status')
|
|
|
|
with self.assertRaises(subprocess.CalledProcessError) as cm:
|
|
|
|
self.init(tdir, inprocess=False, override_envvars = {'MESON_UNIT_TEST': '1', 'MESON_FORCE_BACKTRACE': ''})
|
|
|
|
self.assertEqual(cm.exception.returncode, 2)
|
|
|
|
self.wipe()
|
|
|
|
|
|
|
|
def test_dict_requires_key_value_pairs(self):
|
|
|
|
self.assertMesonRaises("dict = {3, 'foo': 'bar'}",
|
|
|
|
'Only key:value pairs are valid in dict construction.')
|
|
|
|
self.assertMesonRaises("{'foo': 'bar', 3}",
|
|
|
|
'Only key:value pairs are valid in dict construction.')
|
|
|
|
|
|
|
|
def test_dict_forbids_duplicate_keys(self):
|
|
|
|
self.assertMesonRaises("dict = {'a': 41, 'a': 42}",
|
|
|
|
'Duplicate dictionary key: a.*')
|
|
|
|
|
|
|
|
def test_dict_forbids_integer_key(self):
|
|
|
|
self.assertMesonRaises("dict = {3: 'foo'}",
|
|
|
|
'Key must be a string.*')
|
|
|
|
|
|
|
|
def test_using_too_recent_feature(self):
|
|
|
|
# Here we use a dict, which was introduced in 0.47.0
|
|
|
|
self.assertMesonOutputs("dict = {}",
|
|
|
|
".*WARNING.*Project targeting.*but.*",
|
|
|
|
meson_version='>= 0.46.0')
|
|
|
|
|
|
|
|
def test_using_recent_feature(self):
|
|
|
|
# Same as above, except the meson version is now appropriate
|
|
|
|
self.assertMesonDoesNotOutput("dict = {}",
|
|
|
|
".*WARNING.*Project targeting.*but.*",
|
|
|
|
meson_version='>= 0.47')
|
|
|
|
|
|
|
|
def test_using_too_recent_feature_dependency(self):
|
|
|
|
self.assertMesonOutputs("dependency('pcap', required: false)",
|
|
|
|
".*WARNING.*Project targeting.*but.*",
|
|
|
|
meson_version='>= 0.41.0')
|
|
|
|
|
|
|
|
def test_vcs_tag_featurenew_build_always_stale(self):
|
|
|
|
'https://github.com/mesonbuild/meson/issues/3904'
|
|
|
|
vcs_tag = '''version_data = configuration_data()
|
|
|
|
version_data.set('PROJVER', '@VCS_TAG@')
|
|
|
|
vf = configure_file(output : 'version.h.in', configuration: version_data)
|
|
|
|
f = vcs_tag(input : vf, output : 'version.h')
|
|
|
|
'''
|
|
|
|
msg = '.*WARNING:.*feature.*build_always_stale.*custom_target.*'
|
|
|
|
self.assertMesonDoesNotOutput(vcs_tag, msg, meson_version='>=0.43')
|
|
|
|
|
|
|
|
def test_missing_subproject_not_required_and_required(self):
|
|
|
|
self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
|
|
|
|
"sub2 = subproject('not-found-subproject', required: true)",
|
|
|
|
""".*Subproject "subprojects/not-found-subproject" required but not found.*""")
|
|
|
|
|
|
|
|
def test_get_variable_on_not_found_project(self):
|
|
|
|
self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
|
|
|
|
"sub1.get_variable('naaa')",
|
|
|
|
"""Subproject "subprojects/not-found-subproject" disabled can't get_variable on it.""")
|
|
|
|
|
|
|
|
def test_version_checked_before_parsing_options(self):
|
|
|
|
'''
|
|
|
|
https://github.com/mesonbuild/meson/issues/5281
|
|
|
|
'''
|
|
|
|
options = "option('some-option', type: 'foo', value: '')"
|
|
|
|
match = 'Meson version is.*but project requires >=2000'
|
|
|
|
self.assertMesonRaises("", match, meson_version='>=2000', options=options)
|
|
|
|
|
|
|
|
def test_assert_default_message(self):
|
|
|
|
self.assertMesonRaises("k1 = 'a'\n" +
|
|
|
|
"assert({\n" +
|
|
|
|
" k1: 1,\n" +
|
|
|
|
"}['a'] == 2)\n",
|
|
|
|
r"Assert failed: {k1 : 1}\['a'\] == 2")
|
|
|
|
|
|
|
|
def test_wrap_nofallback(self):
|
|
|
|
self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])",
|
|
|
|
r"Dependency 'notfound' is required but not found.",
|
|
|
|
extra_args=['--wrap-mode=nofallback'])
|
|
|
|
|
|
|
|
def test_message(self):
|
|
|
|
self.assertMesonOutputs("message('Array:', ['a', 'b'])",
|
|
|
|
r"Message:.* Array: \['a', 'b'\]")
|
|
|
|
|
|
|
|
def test_warning(self):
|
|
|
|
self.assertMesonOutputs("warning('Array:', ['a', 'b'])",
|
|
|
|
r"WARNING:.* Array: \['a', 'b'\]")
|
|
|
|
|
|
|
|
def test_override_dependency_twice(self):
|
|
|
|
self.assertMesonRaises("meson.override_dependency('foo', declare_dependency())\n" +
|
|
|
|
"meson.override_dependency('foo', declare_dependency())",
|
|
|
|
"""Tried to override dependency 'foo' which has already been resolved or overridden""")
|
|
|
|
|
|
|
|
@unittest.skipIf(is_windows(), 'zlib is not available on Windows')
|
|
|
|
def test_override_resolved_dependency(self):
|
|
|
|
self.assertMesonRaises("dependency('zlib')\n" +
|
|
|
|
"meson.override_dependency('zlib', declare_dependency())",
|
|
|
|
"""Tried to override dependency 'zlib' which has already been resolved or overridden""")
|
|
|
|
|
|
|
|
def test_error_func(self):
|
|
|
|
self.assertMesonRaises("error('a', 'b', ['c', ['d', {'e': 'f'}]], 'g')",
|
|
|
|
r"Problem encountered: a b \['c', \['d', {'e' : 'f'}\]\] g")
|