diff options
Diffstat (limited to '')
-rw-r--r-- | unittests/machinefiletests.py | 953 |
1 files changed, 953 insertions, 0 deletions
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.') |