|
|
|
# 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 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, 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_file = None
|
|
|
|
self.meson_cross_file = None
|
|
|
|
self.meson_command = python_command + [get_meson_script()]
|
|
|
|
self.setup_command = self.meson_command + 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.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):
|
|
|
|
if not is_cygwin():
|
|
|
|
# Keep builddirs inside the source tree so that virus scanners
|
|
|
|
# don't complain
|
|
|
|
newdir = tempfile.mkdtemp(dir=os.getcwd())
|
|
|
|
else:
|
|
|
|
# But not on Cygwin because that breaks the umask tests. See:
|
|
|
|
# https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523
|
|
|
|
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 _get_meson_log(self) -> T.Optional[str]:
|
|
|
|
log = os.path.join(self.logdir, 'meson-log.txt')
|
|
|
|
if not os.path.isfile(log):
|
|
|
|
print(f"{log!r} doesn't exist", file=sys.stderr)
|
|
|
|
return None
|
|
|
|
with open(log, encoding='utf-8') as f:
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
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=None):
|
|
|
|
'''
|
|
|
|
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, env=env,
|
|
|
|
encoding='utf-8',
|
|
|
|
universal_newlines=True, cwd=workdir, timeout=60 * 5)
|
|
|
|
print(p.stdout)
|
|
|
|
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=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]
|
|
|
|
if self.meson_native_file:
|
|
|
|
args += ['--native-file', self.meson_native_file]
|
|
|
|
if self.meson_cross_file:
|
|
|
|
args += ['--cross-file', self.meson_cross_file]
|
|
|
|
self.privatedir = os.path.join(self.builddir, 'meson-private')
|
|
|
|
if inprocess:
|
|
|
|
try:
|
|
|
|
returncode, out, err = run_configure_inprocess(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):
|
|
|
|
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)
|
|
|
|
|
|
|
|
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:
|
|
|
|
self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars)
|
|
|
|
else:
|
|
|
|
with mock.patch.dict(os.environ, override_envvars):
|
|
|
|
run_mtest_inprocess(['-C', self.builddir])
|
|
|
|
|
|
|
|
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)
|
|
|
|
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(self):
|
|
|
|
with open(os.path.join(self.builddir, 'meson-logs', 'meson-log.txt'), encoding='utf-8') 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.
|
|
|
|
'''
|
|
|
|
log = self.get_meson_log()
|
|
|
|
prefix = 'Command line:'
|
|
|
|
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
|
|
|
|
'''
|
|
|
|
log = self.get_meson_log()
|
|
|
|
prefix = 'Sanity check compiler command line:'
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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)
|