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