diff options
Diffstat (limited to '')
-rw-r--r-- | unittests/windowstests.py | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/unittests/windowstests.py b/unittests/windowstests.py new file mode 100644 index 0000000..c81d924 --- /dev/null +++ b/unittests/windowstests.py @@ -0,0 +1,400 @@ +# 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 re +import os +import shutil +from unittest import mock, SkipTest, skipUnless, skipIf +from glob import glob + +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_cygwin, python_command, version_compare, + EnvironmentException, OptionKey +) +from mesonbuild.compilers import ( + detect_c_compiler, detect_d_compiler, compiler_from_language, +) +from mesonbuild.programs import ExternalProgram +import mesonbuild.dependencies.base +import mesonbuild.modules.pkgconfig + + +from run_tests import ( + Backend, get_fake_env +) + +from .baseplatformtests import BasePlatformTests +from .helpers import * + +@skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)") +class WindowsTests(BasePlatformTests): + ''' + Tests that should run on Cygwin, MinGW, and MSVC + ''' + + def setUp(self): + super().setUp() + self.platform_test_dir = os.path.join(self.src_root, 'test cases/windows') + + @skipIf(is_cygwin(), 'Test only applicable to Windows') + @mock.patch.dict(os.environ) + def test_find_program(self): + ''' + Test that Windows-specific edge-cases in find_program are functioning + correctly. Cannot be an ordinary test because it involves manipulating + PATH to point to a directory with Python scripts. + ''' + testdir = os.path.join(self.platform_test_dir, '8 find program') + # Find `cmd` and `cmd.exe` + prog1 = ExternalProgram('cmd') + self.assertTrue(prog1.found(), msg='cmd not found') + prog2 = ExternalProgram('cmd.exe') + self.assertTrue(prog2.found(), msg='cmd.exe not found') + self.assertPathEqual(prog1.get_path(), prog2.get_path()) + # Find cmd.exe with args without searching + prog = ExternalProgram('cmd', command=['cmd', '/C']) + self.assertTrue(prog.found(), msg='cmd not found with args') + self.assertPathEqual(prog.get_command()[0], 'cmd') + # Find cmd with an absolute path that's missing the extension + cmd_path = prog2.get_path()[:-4] + prog = ExternalProgram(cmd_path) + self.assertTrue(prog.found(), msg=f'{cmd_path!r} not found') + # Finding a script with no extension inside a directory works + prog = ExternalProgram(os.path.join(testdir, 'test-script')) + self.assertTrue(prog.found(), msg='test-script not found') + # Finding a script with an extension inside a directory works + prog = ExternalProgram(os.path.join(testdir, 'test-script-ext.py')) + self.assertTrue(prog.found(), msg='test-script-ext.py not found') + # Finding a script in PATH + os.environ['PATH'] += os.pathsep + testdir + # If `.PY` is in PATHEXT, scripts can be found as programs + if '.PY' in [ext.upper() for ext in os.environ['PATHEXT'].split(';')]: + # Finding a script in PATH w/o extension works and adds the interpreter + prog = ExternalProgram('test-script-ext') + self.assertTrue(prog.found(), msg='test-script-ext not found in PATH') + self.assertPathEqual(prog.get_command()[0], python_command[0]) + self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') + # Finding a script in PATH with extension works and adds the interpreter + prog = ExternalProgram('test-script-ext.py') + self.assertTrue(prog.found(), msg='test-script-ext.py not found in PATH') + self.assertPathEqual(prog.get_command()[0], python_command[0]) + self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') + # Using a script with an extension directly via command= works and adds the interpreter + prog = ExternalProgram('test-script-ext.py', command=[os.path.join(testdir, 'test-script-ext.py'), '--help']) + self.assertTrue(prog.found(), msg='test-script-ext.py with full path not picked up via command=') + self.assertPathEqual(prog.get_command()[0], python_command[0]) + self.assertPathEqual(prog.get_command()[2], '--help') + self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') + # Using a script without an extension directly via command= works and adds the interpreter + prog = ExternalProgram('test-script', command=[os.path.join(testdir, 'test-script'), '--help']) + self.assertTrue(prog.found(), msg='test-script with full path not picked up via command=') + self.assertPathEqual(prog.get_command()[0], python_command[0]) + self.assertPathEqual(prog.get_command()[2], '--help') + self.assertPathBasenameEqual(prog.get_path(), 'test-script') + # Ensure that WindowsApps gets removed from PATH + path = os.environ['PATH'] + if 'WindowsApps' not in path: + username = os.environ['USERNAME'] + appstore_dir = fr'C:\Users\{username}\AppData\Local\Microsoft\WindowsApps' + path = os.pathsep + appstore_dir + path = ExternalProgram._windows_sanitize_path(path) + self.assertNotIn('WindowsApps', path) + + def test_ignore_libs(self): + ''' + Test that find_library on libs that are to be ignored returns an empty + array of arguments. Must be a unit test because we cannot inspect + ExternalLibraryHolder from build files. + ''' + testdir = os.path.join(self.platform_test_dir, '1 basic') + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Not using MSVC') + # To force people to update this test, and also test + self.assertEqual(set(cc.ignore_libs), {'c', 'm', 'pthread', 'dl', 'rt', 'execinfo'}) + for l in cc.ignore_libs: + self.assertEqual(cc.find_library(l, env, []), []) + + def test_rc_depends_files(self): + testdir = os.path.join(self.platform_test_dir, '5 resources') + + # resource compiler depfile generation is not yet implemented for msvc + env = get_fake_env(testdir, self.builddir, self.prefix) + depfile_works = detect_c_compiler(env, MachineChoice.HOST).get_id() not in {'msvc', 'clang-cl', 'intel-cl'} + + self.init(testdir) + self.build() + # Immediately rebuilding should not do anything + self.assertBuildIsNoop() + # Test compile_resources(depend_file:) + # Changing mtime of sample.ico should rebuild prog + self.utime(os.path.join(testdir, 'res', 'sample.ico')) + self.assertRebuiltTarget('prog') + # Test depfile generation by compile_resources + # Changing mtime of resource.h should rebuild myres.rc and then prog + if depfile_works: + self.utime(os.path.join(testdir, 'inc', 'resource', 'resource.h')) + self.assertRebuiltTarget('prog') + self.wipe() + + if depfile_works: + testdir = os.path.join(self.platform_test_dir, '12 resources with custom targets') + self.init(testdir) + self.build() + # Immediately rebuilding should not do anything + self.assertBuildIsNoop() + # Changing mtime of resource.h should rebuild myres_1.rc and then prog_1 + self.utime(os.path.join(testdir, 'res', 'resource.h')) + self.assertRebuiltTarget('prog_1') + + def test_msvc_cpp17(self): + testdir = os.path.join(self.unit_test_dir, '44 vscpp17') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Test only applies to MSVC-like compilers') + + try: + self.init(testdir) + except subprocess.CalledProcessError: + # According to Python docs, output is only stored when + # using check_output. We don't use it, so we can't check + # that the output is correct (i.e. that it failed due + # to the right reason). + return + self.build() + + def test_install_pdb_introspection(self): + testdir = os.path.join(self.platform_test_dir, '1 basic') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Test only applies to MSVC-like compilers') + + self.init(testdir) + installed = self.introspect('--installed') + files = [os.path.basename(path) for path in installed.values()] + + self.assertIn('prog.pdb', files) + + def _check_ld(self, name: str, lang: str, expected: str) -> None: + if not shutil.which(name): + raise SkipTest(f'Could not find {name}.') + envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']] + + # Also test a deprecated variable if there is one. + if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP: + envvars.append( + mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld']) + + for envvar in envvars: + with mock.patch.dict(os.environ, {envvar: name}): + env = get_fake_env() + try: + comp = compiler_from_language(env, lang, MachineChoice.HOST) + except EnvironmentException: + raise SkipTest(f'Could not find a compiler for {lang}') + self.assertEqual(comp.linker.id, expected) + + def test_link_environment_variable_lld_link(self): + env = get_fake_env() + comp = detect_c_compiler(env, MachineChoice.HOST) + if comp.get_argument_syntax() == 'gcc': + raise SkipTest('GCC cannot be used with link compatible linkers.') + self._check_ld('lld-link', 'c', 'lld-link') + + def test_link_environment_variable_link(self): + env = get_fake_env() + comp = detect_c_compiler(env, MachineChoice.HOST) + if comp.get_argument_syntax() == 'gcc': + raise SkipTest('GCC cannot be used with link compatible linkers.') + self._check_ld('link', 'c', 'link') + + def test_link_environment_variable_optlink(self): + env = get_fake_env() + comp = detect_c_compiler(env, MachineChoice.HOST) + if comp.get_argument_syntax() == 'gcc': + raise SkipTest('GCC cannot be used with link compatible linkers.') + self._check_ld('optlink', 'c', 'optlink') + + @skip_if_not_language('rust') + def test_link_environment_variable_rust(self): + self._check_ld('link', 'rust', 'link') + + @skip_if_not_language('d') + def test_link_environment_variable_d(self): + env = get_fake_env() + comp = detect_d_compiler(env, MachineChoice.HOST) + if comp.id == 'dmd': + raise SkipTest('meson cannot reliably make DMD use a different linker.') + self._check_ld('lld-link', 'd', 'lld-link') + + def test_pefile_checksum(self): + try: + import pefile + except ImportError: + if is_ci(): + raise + raise SkipTest('pefile module not found') + testdir = os.path.join(self.common_test_dir, '6 linkshared') + self.init(testdir, extra_args=['--buildtype=release']) + self.build() + # Test that binaries have a non-zero checksum + env = get_fake_env() + cc = detect_c_compiler(env, MachineChoice.HOST) + cc_id = cc.get_id() + ld_id = cc.get_linker_id() + dll = glob(os.path.join(self.builddir, '*mycpplib.dll'))[0] + exe = os.path.join(self.builddir, 'cppprog.exe') + for f in (dll, exe): + pe = pefile.PE(f) + msg = f'PE file: {f!r}, compiler: {cc_id!r}, linker: {ld_id!r}' + if cc_id == 'clang-cl': + # Latest clang-cl tested (7.0) does not write checksums out + self.assertFalse(pe.verify_checksum(), msg=msg) + else: + # Verify that a valid checksum was written by all other compilers + self.assertTrue(pe.verify_checksum(), msg=msg) + + def test_qt5dependency_vscrt(self): + ''' + Test that qt5 dependencies use the debug module suffix when b_vscrt is + set to 'mdd' + ''' + # Verify that the `b_vscrt` option is available + env = get_fake_env() + cc = detect_c_compiler(env, MachineChoice.HOST) + if OptionKey('b_vscrt') not in cc.base_options: + raise SkipTest('Compiler does not support setting the VS CRT') + # Verify that qmake is for Qt5 + if not shutil.which('qmake-qt5'): + if not shutil.which('qmake') and not is_ci(): + raise SkipTest('QMake not found') + output = subprocess.getoutput('qmake --version') + if 'Qt version 5' not in output and not is_ci(): + raise SkipTest('Qmake found, but it is not for Qt 5.') + # Setup with /MDd + testdir = os.path.join(self.framework_test_dir, '4 qt') + self.init(testdir, extra_args=['-Db_vscrt=mdd']) + # Verify that we're linking to the debug versions of Qt DLLs + build_ninja = os.path.join(self.builddir, 'build.ninja') + with open(build_ninja, encoding='utf-8') as f: + contents = f.read() + m = re.search('build qt5core.exe: cpp_LINKER.*Qt5Cored.lib', contents) + self.assertIsNotNone(m, msg=contents) + + def test_compiler_checks_vscrt(self): + ''' + Test that the correct VS CRT is used when running compiler checks + ''' + # Verify that the `b_vscrt` option is available + env = get_fake_env() + cc = detect_c_compiler(env, MachineChoice.HOST) + if OptionKey('b_vscrt') not in cc.base_options: + raise SkipTest('Compiler does not support setting the VS CRT') + + def sanitycheck_vscrt(vscrt): + checks = self.get_meson_log_sanitychecks() + self.assertGreater(len(checks), 0) + for check in checks: + self.assertIn(vscrt, check) + + testdir = os.path.join(self.common_test_dir, '1 trivial') + self.init(testdir) + sanitycheck_vscrt('/MDd') + + self.new_builddir() + self.init(testdir, extra_args=['-Dbuildtype=debugoptimized']) + sanitycheck_vscrt('/MD') + + self.new_builddir() + self.init(testdir, extra_args=['-Dbuildtype=release']) + sanitycheck_vscrt('/MD') + + self.new_builddir() + self.init(testdir, extra_args=['-Db_vscrt=md']) + sanitycheck_vscrt('/MD') + + self.new_builddir() + self.init(testdir, extra_args=['-Db_vscrt=mdd']) + sanitycheck_vscrt('/MDd') + + self.new_builddir() + self.init(testdir, extra_args=['-Db_vscrt=mt']) + sanitycheck_vscrt('/MT') + + self.new_builddir() + self.init(testdir, extra_args=['-Db_vscrt=mtd']) + sanitycheck_vscrt('/MTd') + + def test_modules(self): + if self.backend is not Backend.ninja: + raise SkipTest(f'C++ modules only work with the Ninja backend (not {self.backend.name}).') + if 'VSCMD_VER' not in os.environ: + raise SkipTest('C++ modules is only supported with Visual Studio.') + if version_compare(os.environ['VSCMD_VER'], '<16.10.0'): + raise SkipTest('C++ modules are only supported with VS 2019 Preview or newer.') + self.init(os.path.join(self.unit_test_dir, '85 cpp modules')) + self.build() + + def test_non_utf8_fails(self): + # FIXME: VS backend does not use flags from compiler.get_always_args() + # and thus it's missing /utf-8 argument. Was that intentional? This needs + # to be revisited. + if self.backend is not Backend.ninja: + raise SkipTest(f'This test only pass with ninja backend (not {self.backend.name}).') + testdir = os.path.join(self.platform_test_dir, '18 msvc charset') + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Not using MSVC') + self.init(testdir, extra_args=['-Dtest-failure=true']) + self.assertRaises(subprocess.CalledProcessError, self.build) + + @unittest.skipIf(is_cygwin(), "Needs visual studio") + def test_vsenv_option(self): + if self.backend is not Backend.ninja: + raise SkipTest('Only ninja backend is valid for test') + env = os.environ.copy() + env['MESON_FORCE_VSENV_FOR_UNITTEST'] = '1' + # Remove ninja from PATH to ensure that the one provided by Visual + # Studio is picked, as a regression test for + # https://github.com/mesonbuild/meson/issues/9774 + env['PATH'] = get_path_without_cmd('ninja', env['PATH']) + testdir = os.path.join(self.common_test_dir, '1 trivial') + out = self.init(testdir, extra_args=['--vsenv'], override_envvars=env) + self.assertIn('Activating VS', out) + self.assertRegex(out, 'Visual Studio environment is needed to run Ninja') + # All these directly call ninja with the full path, so we need to patch + # it out to use meson subcommands + with mock.patch.object(self, 'build_command', self.meson_command + ['compile']): + out = self.build(override_envvars=env) + self.assertIn('Activating VS', out) + with mock.patch.object(self, 'test_command', self.meson_command + ['test']): + out = self.run_tests(override_envvars=env) + self.assertIn('Activating VS', out) + with mock.patch.object(self, 'install_command', self.meson_command + ['install']): + out = self.install(override_envvars=env) + self.assertIn('Activating VS', out) |