|  | # 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') | 
|  | 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, "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") |