1# Copyright 2016-2021 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from pathlib import PurePath 16from unittest import mock, TestCase, SkipTest 17import json 18import os 19import re 20import subprocess 21import sys 22import tempfile 23import typing as T 24 25import mesonbuild.mlog 26import mesonbuild.depfile 27import mesonbuild.dependencies.base 28import mesonbuild.dependencies.factory 29import mesonbuild.compilers 30import mesonbuild.envconfig 31import mesonbuild.environment 32import mesonbuild.coredata 33import mesonbuild.modules.gnome 34from mesonbuild.mesonlib import ( 35 is_cygwin, windows_proof_rmtree, python_command 36) 37import mesonbuild.modules.pkgconfig 38 39 40from run_tests import ( 41 Backend, ensure_backend_detects_changes, get_backend_commands, 42 get_builddir_target_args, get_meson_script, run_configure_inprocess, 43 run_mtest_inprocess 44) 45 46 47class BasePlatformTests(TestCase): 48 prefix = '/usr' 49 libdir = 'lib' 50 51 def setUp(self): 52 super().setUp() 53 self.maxDiff = None 54 src_root = str(PurePath(__file__).parents[1]) 55 self.src_root = src_root 56 # Get the backend 57 self.backend = getattr(Backend, os.environ['MESON_UNIT_TEST_BACKEND']) 58 self.meson_args = ['--backend=' + self.backend.name] 59 self.meson_native_file = None 60 self.meson_cross_file = None 61 self.meson_command = python_command + [get_meson_script()] 62 self.setup_command = self.meson_command + self.meson_args 63 self.mconf_command = self.meson_command + ['configure'] 64 self.mintro_command = self.meson_command + ['introspect'] 65 self.wrap_command = self.meson_command + ['wrap'] 66 self.rewrite_command = self.meson_command + ['rewrite'] 67 # Backend-specific build commands 68 self.build_command, self.clean_command, self.test_command, self.install_command, \ 69 self.uninstall_command = get_backend_commands(self.backend) 70 # Test directories 71 self.common_test_dir = os.path.join(src_root, 'test cases/common') 72 self.rust_test_dir = os.path.join(src_root, 'test cases/rust') 73 self.vala_test_dir = os.path.join(src_root, 'test cases/vala') 74 self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') 75 self.unit_test_dir = os.path.join(src_root, 'test cases/unit') 76 self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite') 77 self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike') 78 self.objc_test_dir = os.path.join(src_root, 'test cases/objc') 79 self.objcpp_test_dir = os.path.join(src_root, 'test cases/objcpp') 80 81 # Misc stuff 82 self.orig_env = os.environ.copy() 83 if self.backend is Backend.ninja: 84 self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do'] 85 else: 86 # VS doesn't have a stable output when no changes are done 87 # XCode backend is untested with unit tests, help welcome! 88 self.no_rebuild_stdout = [f'UNKNOWN BACKEND {self.backend.name!r}'] 89 90 self.builddirs = [] 91 self.new_builddir() 92 93 def change_builddir(self, newdir): 94 self.builddir = newdir 95 self.privatedir = os.path.join(self.builddir, 'meson-private') 96 self.logdir = os.path.join(self.builddir, 'meson-logs') 97 self.installdir = os.path.join(self.builddir, 'install') 98 self.distdir = os.path.join(self.builddir, 'meson-dist') 99 self.mtest_command = self.meson_command + ['test', '-C', self.builddir] 100 self.builddirs.append(self.builddir) 101 102 def new_builddir(self): 103 # Keep builddirs inside the source tree so that virus scanners 104 # don't complain 105 newdir = tempfile.mkdtemp(dir=os.getcwd()) 106 # In case the directory is inside a symlinked directory, find the real 107 # path otherwise we might not find the srcdir from inside the builddir. 108 newdir = os.path.realpath(newdir) 109 self.change_builddir(newdir) 110 111 def new_builddir_in_tempdir(self): 112 # Can't keep the builddir inside the source tree for the umask tests: 113 # https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523 114 # And we can't do this for all tests because it causes the path to be 115 # a short-path which breaks other tests: 116 # https://github.com/mesonbuild/meson/pull/9497 117 newdir = tempfile.mkdtemp() 118 # In case the directory is inside a symlinked directory, find the real 119 # path otherwise we might not find the srcdir from inside the builddir. 120 newdir = os.path.realpath(newdir) 121 self.change_builddir(newdir) 122 123 def _get_meson_log(self) -> T.Optional[str]: 124 log = os.path.join(self.logdir, 'meson-log.txt') 125 if not os.path.isfile(log): 126 print(f"{log!r} doesn't exist", file=sys.stderr) 127 return None 128 with open(log, encoding='utf-8') as f: 129 return f.read() 130 131 def _print_meson_log(self) -> None: 132 log = self._get_meson_log() 133 if log: 134 print(log) 135 136 def tearDown(self): 137 for path in self.builddirs: 138 try: 139 windows_proof_rmtree(path) 140 except FileNotFoundError: 141 pass 142 os.environ.clear() 143 os.environ.update(self.orig_env) 144 super().tearDown() 145 146 def _run(self, command, *, workdir=None, override_envvars: T.Optional[T.Mapping[str, str]] = None): 147 ''' 148 Run a command while printing the stdout and stderr to stdout, 149 and also return a copy of it 150 ''' 151 # If this call hangs CI will just abort. It is very hard to distinguish 152 # between CI issue and test bug in that case. Set timeout and fail loud 153 # instead. 154 if override_envvars is None: 155 env = None 156 else: 157 env = os.environ.copy() 158 env.update(override_envvars) 159 160 p = subprocess.run(command, stdout=subprocess.PIPE, 161 stderr=subprocess.STDOUT, env=env, 162 encoding='utf-8', 163 universal_newlines=True, cwd=workdir, timeout=60 * 5) 164 print(p.stdout) 165 if p.returncode != 0: 166 if 'MESON_SKIP_TEST' in p.stdout: 167 raise SkipTest('Project requested skipping.') 168 raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout) 169 return p.stdout 170 171 def init(self, srcdir, *, 172 extra_args=None, 173 default_args=True, 174 inprocess=False, 175 override_envvars: T.Optional[T.Mapping[str, str]] = None, 176 workdir=None, 177 allow_fail: bool = False) -> str: 178 """Call `meson setup` 179 180 :param allow_fail: If set to true initialization is allowed to fail. 181 When it does the log will be returned instead of stdout. 182 :return: the value of stdout on success, or the meson log on failure 183 when :param allow_fail: is true 184 """ 185 self.assertPathExists(srcdir) 186 if extra_args is None: 187 extra_args = [] 188 if not isinstance(extra_args, list): 189 extra_args = [extra_args] 190 args = [srcdir, self.builddir] 191 if default_args: 192 args += ['--prefix', self.prefix] 193 if self.libdir: 194 args += ['--libdir', self.libdir] 195 if self.meson_native_file: 196 args += ['--native-file', self.meson_native_file] 197 if self.meson_cross_file: 198 args += ['--cross-file', self.meson_cross_file] 199 self.privatedir = os.path.join(self.builddir, 'meson-private') 200 if inprocess: 201 try: 202 returncode, out, err = run_configure_inprocess(self.meson_args + args + extra_args, override_envvars) 203 except Exception as e: 204 if not allow_fail: 205 self._print_meson_log() 206 raise 207 out = self._get_meson_log() # Best we can do here 208 err = '' # type checkers can't figure out that on this path returncode will always be 0 209 returncode = 0 210 finally: 211 # Close log file to satisfy Windows file locking 212 mesonbuild.mlog.shutdown() 213 mesonbuild.mlog.log_dir = None 214 mesonbuild.mlog.log_file = None 215 216 if 'MESON_SKIP_TEST' in out: 217 raise SkipTest('Project requested skipping.') 218 if returncode != 0: 219 self._print_meson_log() 220 print('Stdout:\n') 221 print(out) 222 print('Stderr:\n') 223 print(err) 224 if not allow_fail: 225 raise RuntimeError('Configure failed') 226 else: 227 try: 228 out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir) 229 except SkipTest: 230 raise SkipTest('Project requested skipping: ' + srcdir) 231 except Exception: 232 if not allow_fail: 233 self._print_meson_log() 234 raise 235 out = self._get_meson_log() # best we can do here 236 return out 237 238 def build(self, target=None, *, extra_args=None, override_envvars=None): 239 if extra_args is None: 240 extra_args = [] 241 # Add arguments for building the target (if specified), 242 # and using the build dir (if required, with VS) 243 args = get_builddir_target_args(self.backend, self.builddir, target) 244 return self._run(self.build_command + args + extra_args, workdir=self.builddir, override_envvars=override_envvars) 245 246 def clean(self, *, override_envvars=None): 247 dir_args = get_builddir_target_args(self.backend, self.builddir, None) 248 self._run(self.clean_command + dir_args, workdir=self.builddir, override_envvars=override_envvars) 249 250 def run_tests(self, *, inprocess=False, override_envvars=None): 251 if not inprocess: 252 self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars) 253 else: 254 with mock.patch.dict(os.environ, override_envvars): 255 run_mtest_inprocess(['-C', self.builddir]) 256 257 def install(self, *, use_destdir=True, override_envvars=None): 258 if self.backend is not Backend.ninja: 259 raise SkipTest(f'{self.backend.name!r} backend can\'t install files') 260 if use_destdir: 261 destdir = {'DESTDIR': self.installdir} 262 if override_envvars is None: 263 override_envvars = destdir 264 else: 265 override_envvars.update(destdir) 266 self._run(self.install_command, workdir=self.builddir, override_envvars=override_envvars) 267 268 def uninstall(self, *, override_envvars=None): 269 self._run(self.uninstall_command, workdir=self.builddir, override_envvars=override_envvars) 270 271 def run_target(self, target, *, override_envvars=None): 272 ''' 273 Run a Ninja target while printing the stdout and stderr to stdout, 274 and also return a copy of it 275 ''' 276 return self.build(target=target, override_envvars=override_envvars) 277 278 def setconf(self, arg, will_build=True): 279 if not isinstance(arg, list): 280 arg = [arg] 281 if will_build: 282 ensure_backend_detects_changes(self.backend) 283 self._run(self.mconf_command + arg + [self.builddir]) 284 285 def wipe(self): 286 windows_proof_rmtree(self.builddir) 287 288 def utime(self, f): 289 ensure_backend_detects_changes(self.backend) 290 os.utime(f) 291 292 def get_compdb(self): 293 if self.backend is not Backend.ninja: 294 raise SkipTest(f'Compiler db not available with {self.backend.name} backend') 295 try: 296 with open(os.path.join(self.builddir, 'compile_commands.json'), encoding='utf-8') as ifile: 297 contents = json.load(ifile) 298 except FileNotFoundError: 299 raise SkipTest('Compiler db not found') 300 # If Ninja is using .rsp files, generate them, read their contents, and 301 # replace it as the command for all compile commands in the parsed json. 302 if len(contents) > 0 and contents[0]['command'].endswith('.rsp'): 303 # Pretend to build so that the rsp files are generated 304 self.build(extra_args=['-d', 'keeprsp', '-n']) 305 for each in contents: 306 # Extract the actual command from the rsp file 307 compiler, rsp = each['command'].split(' @') 308 rsp = os.path.join(self.builddir, rsp) 309 # Replace the command with its contents 310 with open(rsp, encoding='utf-8') as f: 311 each['command'] = compiler + ' ' + f.read() 312 return contents 313 314 def get_meson_log(self): 315 with open(os.path.join(self.builddir, 'meson-logs', 'meson-log.txt'), encoding='utf-8') as f: 316 return f.readlines() 317 318 def get_meson_log_compiler_checks(self): 319 ''' 320 Fetch a list command-lines run by meson for compiler checks. 321 Each command-line is returned as a list of arguments. 322 ''' 323 log = self.get_meson_log() 324 prefix = 'Command line:' 325 cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] 326 return cmds 327 328 def get_meson_log_sanitychecks(self): 329 ''' 330 Same as above, but for the sanity checks that were run 331 ''' 332 log = self.get_meson_log() 333 prefix = 'Sanity check compiler command line:' 334 cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] 335 return cmds 336 337 def introspect(self, args): 338 if isinstance(args, str): 339 args = [args] 340 out = subprocess.check_output(self.mintro_command + args + [self.builddir], 341 universal_newlines=True) 342 return json.loads(out) 343 344 def introspect_directory(self, directory, args): 345 if isinstance(args, str): 346 args = [args] 347 out = subprocess.check_output(self.mintro_command + args + [directory], 348 universal_newlines=True) 349 try: 350 obj = json.loads(out) 351 except Exception as e: 352 print(out) 353 raise e 354 return obj 355 356 def assertPathEqual(self, path1, path2): 357 ''' 358 Handles a lot of platform-specific quirks related to paths such as 359 separator, case-sensitivity, etc. 360 ''' 361 self.assertEqual(PurePath(path1), PurePath(path2)) 362 363 def assertPathListEqual(self, pathlist1, pathlist2): 364 self.assertEqual(len(pathlist1), len(pathlist2)) 365 worklist = list(zip(pathlist1, pathlist2)) 366 for i in worklist: 367 if i[0] is None: 368 self.assertEqual(i[0], i[1]) 369 else: 370 self.assertPathEqual(i[0], i[1]) 371 372 def assertPathBasenameEqual(self, path, basename): 373 msg = f'{path!r} does not end with {basename!r}' 374 # We cannot use os.path.basename because it returns '' when the path 375 # ends with '/' for some silly reason. This is not how the UNIX utility 376 # `basename` works. 377 path_basename = PurePath(path).parts[-1] 378 self.assertEqual(PurePath(path_basename), PurePath(basename), msg) 379 380 def assertReconfiguredBuildIsNoop(self): 381 'Assert that we reconfigured and then there was nothing to do' 382 ret = self.build() 383 self.assertIn('The Meson build system', ret) 384 if self.backend is Backend.ninja: 385 for line in ret.split('\n'): 386 if line in self.no_rebuild_stdout: 387 break 388 else: 389 raise AssertionError('build was reconfigured, but was not no-op') 390 elif self.backend is Backend.vs: 391 # Ensure that some target said that no rebuild was done 392 # XXX: Note CustomBuild did indeed rebuild, because of the regen checker! 393 self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) 394 self.assertIn('Link:\n All outputs are up-to-date.', ret) 395 # Ensure that no targets were built 396 self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) 397 self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) 398 elif self.backend is Backend.xcode: 399 raise SkipTest('Please help us fix this test on the xcode backend') 400 else: 401 raise RuntimeError(f'Invalid backend: {self.backend.name!r}') 402 403 def assertBuildIsNoop(self): 404 ret = self.build() 405 if self.backend is Backend.ninja: 406 self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout) 407 elif self.backend is Backend.vs: 408 # Ensure that some target of each type said that no rebuild was done 409 # We always have at least one CustomBuild target for the regen checker 410 self.assertIn('CustomBuild:\n All outputs are up-to-date.', ret) 411 self.assertIn('ClCompile:\n All outputs are up-to-date.', ret) 412 self.assertIn('Link:\n All outputs are up-to-date.', ret) 413 # Ensure that no targets were built 414 self.assertNotRegex(ret, re.compile('CustomBuild:\n [^\n]*cl', flags=re.IGNORECASE)) 415 self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE)) 416 self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE)) 417 elif self.backend is Backend.xcode: 418 raise SkipTest('Please help us fix this test on the xcode backend') 419 else: 420 raise RuntimeError(f'Invalid backend: {self.backend.name!r}') 421 422 def assertRebuiltTarget(self, target): 423 ret = self.build() 424 if self.backend is Backend.ninja: 425 self.assertIn(f'Linking target {target}', ret) 426 elif self.backend is Backend.vs: 427 # Ensure that this target was rebuilt 428 linkre = re.compile('Link:\n [^\n]*link[^\n]*' + target, flags=re.IGNORECASE) 429 self.assertRegex(ret, linkre) 430 elif self.backend is Backend.xcode: 431 raise SkipTest('Please help us fix this test on the xcode backend') 432 else: 433 raise RuntimeError(f'Invalid backend: {self.backend.name!r}') 434 435 @staticmethod 436 def get_target_from_filename(filename): 437 base = os.path.splitext(filename)[0] 438 if base.startswith(('lib', 'cyg')): 439 return base[3:] 440 return base 441 442 def assertBuildRelinkedOnlyTarget(self, target): 443 ret = self.build() 444 if self.backend is Backend.ninja: 445 linked_targets = [] 446 for line in ret.split('\n'): 447 if 'Linking target' in line: 448 fname = line.rsplit('target ')[-1] 449 linked_targets.append(self.get_target_from_filename(fname)) 450 self.assertEqual(linked_targets, [target]) 451 elif self.backend is Backend.vs: 452 # Ensure that this target was rebuilt 453 linkre = re.compile(r'Link:\n [^\n]*link.exe[^\n]*/OUT:".\\([^"]*)"', flags=re.IGNORECASE) 454 matches = linkre.findall(ret) 455 self.assertEqual(len(matches), 1, msg=matches) 456 self.assertEqual(self.get_target_from_filename(matches[0]), target) 457 elif self.backend is Backend.xcode: 458 raise SkipTest('Please help us fix this test on the xcode backend') 459 else: 460 raise RuntimeError(f'Invalid backend: {self.backend.name!r}') 461 462 def assertPathExists(self, path): 463 m = f'Path {path!r} should exist' 464 self.assertTrue(os.path.exists(path), msg=m) 465 466 def assertPathDoesNotExist(self, path): 467 m = f'Path {path!r} should not exist' 468 self.assertFalse(os.path.exists(path), msg=m) 469