diff options
Diffstat (limited to '')
-rw-r--r-- | unittests/baseplatformtests.py | 483 |
1 files changed, 483 insertions, 0 deletions
diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py new file mode 100644 index 0000000..d83ef3f --- /dev/null +++ b/unittests/baseplatformtests.py @@ -0,0 +1,483 @@ +# 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. + +from pathlib import PurePath +from unittest import mock, TestCase, SkipTest +import json +import io +import os +import re +import subprocess +import sys +import tempfile +import typing as T + +import mesonbuild.mlog +import mesonbuild.depfile +import mesonbuild.dependencies.base +import mesonbuild.dependencies.factory +import mesonbuild.compilers +import mesonbuild.envconfig +import mesonbuild.environment +import mesonbuild.coredata +import mesonbuild.modules.gnome +from mesonbuild.mesonlib import ( + is_cygwin, join_args, windows_proof_rmtree, python_command +) +import mesonbuild.modules.pkgconfig + + +from run_tests import ( + Backend, ensure_backend_detects_changes, get_backend_commands, + get_builddir_target_args, get_meson_script, run_configure_inprocess, + run_mtest_inprocess +) + + +class BasePlatformTests(TestCase): + prefix = '/usr' + libdir = 'lib' + + def setUp(self): + super().setUp() + self.maxDiff = None + src_root = str(PurePath(__file__).parents[1]) + self.src_root = src_root + # Get the backend + self.backend = getattr(Backend, os.environ['MESON_UNIT_TEST_BACKEND']) + self.meson_args = ['--backend=' + self.backend.name] + self.meson_native_files = [] + self.meson_cross_files = [] + self.meson_command = python_command + [get_meson_script()] + self.setup_command = self.meson_command + ['setup'] + self.meson_args + self.mconf_command = self.meson_command + ['configure'] + self.mintro_command = self.meson_command + ['introspect'] + self.wrap_command = self.meson_command + ['wrap'] + self.rewrite_command = self.meson_command + ['rewrite'] + # Backend-specific build commands + self.build_command, self.clean_command, self.test_command, self.install_command, \ + self.uninstall_command = get_backend_commands(self.backend) + # Test directories + self.common_test_dir = os.path.join(src_root, 'test cases/common') + self.rust_test_dir = os.path.join(src_root, 'test cases/rust') + self.vala_test_dir = os.path.join(src_root, 'test cases/vala') + self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') + self.unit_test_dir = os.path.join(src_root, 'test cases/unit') + self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite') + self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike') + self.objc_test_dir = os.path.join(src_root, 'test cases/objc') + self.objcpp_test_dir = os.path.join(src_root, 'test cases/objcpp') + + # Misc stuff + self.orig_env = os.environ.copy() + if self.backend is Backend.ninja: + self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do'] + else: + # VS doesn't have a stable output when no changes are done + # XCode backend is untested with unit tests, help welcome! + self.no_rebuild_stdout = [f'UNKNOWN BACKEND {self.backend.name!r}'] + + self.builddirs = [] + self.new_builddir() + + def change_builddir(self, newdir): + self.builddir = newdir + self.privatedir = os.path.join(self.builddir, 'meson-private') + self.logdir = os.path.join(self.builddir, 'meson-logs') + self.installdir = os.path.join(self.builddir, 'install') + self.distdir = os.path.join(self.builddir, 'meson-dist') + self.mtest_command = self.meson_command + ['test', '-C', self.builddir] + self.builddirs.append(self.builddir) + + def new_builddir(self): + # Keep builddirs inside the source tree so that virus scanners + # don't complain + newdir = tempfile.mkdtemp(dir=os.getcwd()) + # In case the directory is inside a symlinked directory, find the real + # path otherwise we might not find the srcdir from inside the builddir. + newdir = os.path.realpath(newdir) + self.change_builddir(newdir) + + def new_builddir_in_tempdir(self): + # Can't keep the builddir inside the source tree for the umask tests: + # https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523 + # And we can't do this for all tests because it causes the path to be + # a short-path which breaks other tests: + # https://github.com/mesonbuild/meson/pull/9497 + newdir = tempfile.mkdtemp() + # In case the directory is inside a symlinked directory, find the real + # path otherwise we might not find the srcdir from inside the builddir. + newdir = os.path.realpath(newdir) + self.change_builddir(newdir) + + def _open_meson_log(self) -> io.TextIOWrapper: + log = os.path.join(self.logdir, 'meson-log.txt') + return open(log, encoding='utf-8') + + def _get_meson_log(self) -> T.Optional[str]: + try: + with self._open_meson_log() as f: + return f.read() + except FileNotFoundError as e: + print(f"{e.filename!r} doesn't exist", file=sys.stderr) + return None + + def _print_meson_log(self) -> None: + log = self._get_meson_log() + if log: + print(log) + + def tearDown(self): + for path in self.builddirs: + try: + windows_proof_rmtree(path) + except FileNotFoundError: + pass + os.environ.clear() + os.environ.update(self.orig_env) + super().tearDown() + + def _run(self, command, *, workdir=None, override_envvars: T.Optional[T.Mapping[str, str]] = None, stderr=True): + ''' + Run a command while printing the stdout and stderr to stdout, + and also return a copy of it + ''' + # If this call hangs CI will just abort. It is very hard to distinguish + # between CI issue and test bug in that case. Set timeout and fail loud + # instead. + if override_envvars is None: + env = None + else: + env = os.environ.copy() + env.update(override_envvars) + + p = subprocess.run(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT if stderr else subprocess.PIPE, + env=env, + encoding='utf-8', + text=True, cwd=workdir, timeout=60 * 5) + print('$', join_args(command)) + print('stdout:') + print(p.stdout) + if not stderr: + print('stderr:') + print(p.stderr) + if p.returncode != 0: + if 'MESON_SKIP_TEST' in p.stdout: + raise SkipTest('Project requested skipping.') + raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout) + return p.stdout + + def init(self, srcdir, *, + extra_args=None, + default_args=True, + inprocess=False, + override_envvars: T.Optional[T.Mapping[str, str]] = None, + workdir=None, + allow_fail: bool = False) -> str: + """Call `meson setup` + + :param allow_fail: If set to true initialization is allowed to fail. + When it does the log will be returned instead of stdout. + :return: the value of stdout on success, or the meson log on failure + when :param allow_fail: is true + """ + self.assertPathExists(srcdir) + if extra_args is None: + extra_args = [] + if not isinstance(extra_args, list): + extra_args = [extra_args] + args = [srcdir, self.builddir] + if default_args: + args += ['--prefix', self.prefix] + if self.libdir: + args += ['--libdir', self.libdir] + for f in self.meson_native_files: + args += ['--native-file', f] + for f in self.meson_cross_files: + args += ['--cross-file', f] + self.privatedir = os.path.join(self.builddir, 'meson-private') + if inprocess: + try: + returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args, override_envvars) + except Exception as e: + if not allow_fail: + self._print_meson_log() + raise + out = self._get_meson_log() # Best we can do here + err = '' # type checkers can't figure out that on this path returncode will always be 0 + returncode = 0 + finally: + # Close log file to satisfy Windows file locking + mesonbuild.mlog.shutdown() + mesonbuild.mlog.log_dir = None + mesonbuild.mlog.log_file = None + + if 'MESON_SKIP_TEST' in out: + raise SkipTest('Project requested skipping.') + if returncode != 0: + self._print_meson_log() + print('Stdout:\n') + print(out) + print('Stderr:\n') + print(err) + if not allow_fail: + raise RuntimeError('Configure failed') + else: + try: + out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir) + except SkipTest: + raise SkipTest('Project requested skipping: ' + srcdir) + except Exception: + if not allow_fail: + self._print_meson_log() + raise + out = self._get_meson_log() # best we can do here + return out + + def build(self, target=None, *, extra_args=None, override_envvars=None, stderr=True): + if extra_args is None: + extra_args = [] + # Add arguments for building the target (if specified), + # and using the build dir (if required, with VS) + args = get_builddir_target_args(self.backend, self.builddir, target) + return self._run(self.build_command + args + extra_args, workdir=self.builddir, override_envvars=override_envvars, stderr=stderr) + + def clean(self, *, override_envvars=None): + dir_args = get_builddir_target_args(self.backend, self.builddir, None) + self._run(self.clean_command + dir_args, workdir=self.builddir, override_envvars=override_envvars) + + def run_tests(self, *, inprocess=False, override_envvars=None): + if not inprocess: + return self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars) + else: + with mock.patch.dict(os.environ, override_envvars): + return run_mtest_inprocess(['-C', self.builddir])[1] + + def install(self, *, use_destdir=True, override_envvars=None): + if self.backend is not Backend.ninja: + raise SkipTest(f'{self.backend.name!r} backend can\'t install files') + if use_destdir: + destdir = {'DESTDIR': self.installdir} + if override_envvars is None: + override_envvars = destdir + else: + override_envvars.update(destdir) + return self._run(self.install_command, workdir=self.builddir, override_envvars=override_envvars) + + def uninstall(self, *, override_envvars=None): + self._run(self.uninstall_command, workdir=self.builddir, override_envvars=override_envvars) + + def run_target(self, target, *, override_envvars=None): + ''' + Run a Ninja target while printing the stdout and stderr to stdout, + and also return a copy of it + ''' + return self.build(target=target, override_envvars=override_envvars) + + def setconf(self, arg, will_build=True): + if not isinstance(arg, list): + arg = [arg] + if will_build: + ensure_backend_detects_changes(self.backend) + self._run(self.mconf_command + arg + [self.builddir]) + + def wipe(self): + windows_proof_rmtree(self.builddir) + + def utime(self, f): + ensure_backend_detects_changes(self.backend) + os.utime(f) + + def get_compdb(self): + if self.backend is not Backend.ninja: + raise SkipTest(f'Compiler db not available with {self.backend.name} backend') + try: + with open(os.path.join(self.builddir, 'compile_commands.json'), encoding='utf-8') as ifile: + contents = json.load(ifile) + except FileNotFoundError: + raise SkipTest('Compiler db not found') + # If Ninja is using .rsp files, generate them, read their contents, and + # replace it as the command for all compile commands in the parsed json. + if len(contents) > 0 and contents[0]['command'].endswith('.rsp'): + # Pretend to build so that the rsp files are generated + self.build(extra_args=['-d', 'keeprsp', '-n']) + for each in contents: + # Extract the actual command from the rsp file + compiler, rsp = each['command'].split(' @') + rsp = os.path.join(self.builddir, rsp) + # Replace the command with its contents + with open(rsp, encoding='utf-8') as f: + each['command'] = compiler + ' ' + f.read() + return contents + + def get_meson_log_raw(self): + with self._open_meson_log() as f: + return f.read() + + def get_meson_log(self): + with self._open_meson_log() as f: + return f.readlines() + + def get_meson_log_compiler_checks(self): + ''' + Fetch a list command-lines run by meson for compiler checks. + Each command-line is returned as a list of arguments. + ''' + prefix = 'Command line:' + with self._open_meson_log() as log: + cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] + return cmds + + def get_meson_log_sanitychecks(self): + ''' + Same as above, but for the sanity checks that were run + ''' + prefix = 'Sanity check compiler command line:' + with self._open_meson_log() as log: + cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] + return cmds + + def introspect(self, args): + if isinstance(args, str): + args = [args] + out = subprocess.check_output(self.mintro_command + args + [self.builddir], + universal_newlines=True) + return json.loads(out) + + def introspect_directory(self, directory, args): + if isinstance(args, str): + args = [args] + out = subprocess.check_output(self.mintro_command + args + [directory], + universal_newlines=True) + try: + obj = json.loads(out) + except Exception as e: + print(out) + raise e + return obj + + def assertPathEqual(self, path1, path2): + ''' + Handles a lot of platform-specific quirks related to paths such as + separator, case-sensitivity, etc. + ''' + self.assertEqual(PurePath(path1), PurePath(path2)) + + def assertPathListEqual(self, pathlist1, pathlist2): + self.assertEqual(len(pathlist1), len(pathlist2)) + worklist = list(zip(pathlist1, pathlist2)) + for i in worklist: + if i[0] is None: + self.assertEqual(i[0], i[1]) + else: + self.assertPathEqual(i[0], i[1]) + + def assertPathBasenameEqual(self, path, basename): + msg = f'{path!r} does not end with {basename!r}' + # We cannot use os.path.basename because it returns '' when the path + # ends with '/' for some silly reason. This is not how the UNIX utility + # `basename` works. + path_basename = PurePath(path).parts[-1] + self.assertEqual(PurePath(path_basename), PurePath(basename), msg) + + def assertReconfiguredBuildIsNoop(self): + 'Assert that we reconfigured and then there was nothing to do' + ret = self.build(stderr=False) + self.assertIn('The Meson build system', ret) + if self.backend is Backend.ninja: + for line in ret.split('\n'): + if line in self.no_rebuild_stdout: + break + else: + raise AssertionError('build was reconfigured, but was not no-op') + elif self.backend is Backend.vs: + # Ensure that some target said that no rebuild was done + # XXX: Note CustomBuild did indeed rebuild, because of the regen checker! + self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) + self.assertIn('Link:\n All outputs are up-to-date.', ret) + # Ensure that no targets were built + self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) + self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) + elif self.backend is Backend.xcode: + raise SkipTest('Please help us fix this test on the xcode backend') + else: + raise RuntimeError(f'Invalid backend: {self.backend.name!r}') + + def assertBuildIsNoop(self): + ret = self.build(stderr=False) + if self.backend is Backend.ninja: + self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout) + elif self.backend is Backend.vs: + # Ensure that some target of each type said that no rebuild was done + # We always have at least one CustomBuild target for the regen checker + self.assertIn('CustomBuild:\n All outputs are up-to-date.', ret) + self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) + self.assertIn('Link:\n All outputs are up-to-date.', ret) + # Ensure that no targets were built + self.assertNotRegex(ret, re.compile('CustomBuild:\n [^\n]*cl', flags=re.IGNORECASE)) + self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) + self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) + elif self.backend is Backend.xcode: + raise SkipTest('Please help us fix this test on the xcode backend') + else: + raise RuntimeError(f'Invalid backend: {self.backend.name!r}') + + def assertRebuiltTarget(self, target): + ret = self.build() + if self.backend is Backend.ninja: + self.assertIn(f'Linking target {target}', ret) + elif self.backend is Backend.vs: + # Ensure that this target was rebuilt + linkre = re.compile('Link:\n [^\n]*link[^\n]*' + target, flags=re.IGNORECASE) + self.assertRegex(ret, linkre) + elif self.backend is Backend.xcode: + raise SkipTest('Please help us fix this test on the xcode backend') + else: + raise RuntimeError(f'Invalid backend: {self.backend.name!r}') + + @staticmethod + def get_target_from_filename(filename): + base = os.path.splitext(filename)[0] + if base.startswith(('lib', 'cyg')): + return base[3:] + return base + + def assertBuildRelinkedOnlyTarget(self, target): + ret = self.build() + if self.backend is Backend.ninja: + linked_targets = [] + for line in ret.split('\n'): + if 'Linking target' in line: + fname = line.rsplit('target ')[-1] + linked_targets.append(self.get_target_from_filename(fname)) + self.assertEqual(linked_targets, [target]) + elif self.backend is Backend.vs: + # Ensure that this target was rebuilt + linkre = re.compile(r'Link:\n [^\n]*link.exe[^\n]*/OUT:".\\([^"]*)"', flags=re.IGNORECASE) + matches = linkre.findall(ret) + self.assertEqual(len(matches), 1, msg=matches) + self.assertEqual(self.get_target_from_filename(matches[0]), target) + elif self.backend is Backend.xcode: + raise SkipTest('Please help us fix this test on the xcode backend') + else: + raise RuntimeError(f'Invalid backend: {self.backend.name!r}') + + def assertPathExists(self, path): + m = f'Path {path!r} should exist' + self.assertTrue(os.path.exists(path), msg=m) + + def assertPathDoesNotExist(self, path): + m = f'Path {path!r} should not exist' + self.assertFalse(os.path.exists(path), msg=m) |