| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2016-2021 The Meson development team |
| |
| 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') |
| if not os.path.exists(self.moptions): |
| 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, ".*is not a pkgconfig dependency") |
| code = '''zlib_dep = dependency('zlib', required : false) |
| dep = declare_dependency(dependencies : zlib_dep) |
| dep.get_configtool_variable('foo') |
| ''' |
| self.assertMesonRaises(code, ".* is not a config-tool dependency") |
| |
| 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") |