|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | # Copyright 2016-2021 The Meson development team | 
|  | # Copyright © 2024 Intel Corporation | 
|  |  | 
|  | from __future__ import annotations | 
|  | from pathlib import PurePath | 
|  | from unittest import mock, TestCase, SkipTest | 
|  | import json | 
|  | import io | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | import shutil | 
|  | 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, split_args, windows_proof_rmtree, python_command | 
|  | ) | 
|  | import mesonbuild.modules.pkgconfig | 
|  |  | 
|  |  | 
|  | from run_tests import ( | 
|  | Backend, get_backend_commands, | 
|  | get_builddir_target_args, get_meson_script, run_configure_inprocess, | 
|  | run_mtest_inprocess, handle_meson_skip_test, | 
|  | ) | 
|  |  | 
|  |  | 
|  | # magic attribute used by unittest.result.TestResult._is_relevant_tb_level | 
|  | # This causes tracebacks to hide these internal implementation details, | 
|  | # e.g. for assertXXX helpers. | 
|  | __unittest = True | 
|  |  | 
|  | class BasePlatformTests(TestCase): | 
|  | prefix = '/usr' | 
|  | libdir = 'lib' | 
|  |  | 
|  | @classmethod | 
|  | def setUpClass(cls) -> None: | 
|  | super().setUpClass() | 
|  | cls.maxDiff = None | 
|  | src_root = str(PurePath(__file__).parents[1]) | 
|  | cls.src_root = src_root | 
|  | # Get the backend | 
|  | cls.backend_name = os.environ.get('MESON_UNIT_TEST_BACKEND', 'ninja') | 
|  | backend_type = 'vs' if cls.backend_name.startswith('vs') else cls.backend_name | 
|  | cls.backend = getattr(Backend, backend_type) | 
|  | cls.meson_args = ['--backend=' + cls.backend_name] | 
|  | cls.meson_command = python_command + [get_meson_script()] | 
|  | cls.setup_command = cls.meson_command + ['setup'] + cls.meson_args | 
|  | cls.mconf_command = cls.meson_command + ['configure'] | 
|  | cls.mintro_command = cls.meson_command + ['introspect'] | 
|  | cls.wrap_command = cls.meson_command + ['wrap'] | 
|  | cls.rewrite_command = cls.meson_command + ['rewrite'] | 
|  | # Backend-specific build commands | 
|  | cls.build_command, cls.clean_command, cls.test_command, cls.install_command, \ | 
|  | cls.uninstall_command = get_backend_commands(cls.backend) | 
|  | # Test directories | 
|  | cls.common_test_dir = os.path.join(src_root, 'test cases/common') | 
|  | cls.python_test_dir = os.path.join(src_root, 'test cases/python') | 
|  | cls.rust_test_dir = os.path.join(src_root, 'test cases/rust') | 
|  | cls.vala_test_dir = os.path.join(src_root, 'test cases/vala') | 
|  | cls.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') | 
|  | cls.unit_test_dir = os.path.join(src_root, 'test cases/unit') | 
|  | cls.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite') | 
|  | cls.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike') | 
|  | cls.objc_test_dir = os.path.join(src_root, 'test cases/objc') | 
|  | cls.objcpp_test_dir = os.path.join(src_root, 'test cases/objcpp') | 
|  | cls.darwin_test_dir = os.path.join(src_root, 'test cases/darwin') | 
|  |  | 
|  | # Misc stuff | 
|  | if cls.backend is Backend.ninja: | 
|  | cls.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! | 
|  | cls.no_rebuild_stdout = [f'UNKNOWN BACKEND {cls.backend.name!r}'] | 
|  |  | 
|  | cls.env_patch = mock.patch.dict(os.environ) | 
|  | cls.env_patch.start() | 
|  |  | 
|  | os.environ['COLUMNS'] = '80' | 
|  | os.environ['PYTHONIOENCODING'] = 'utf8' | 
|  |  | 
|  | @classmethod | 
|  | def tearDownClass(cls) -> None: | 
|  | super().tearDownClass() | 
|  | cls.env_patch.stop() | 
|  |  | 
|  | def setUp(self): | 
|  | super().setUp() | 
|  | self.meson_native_files = [] | 
|  | self.meson_cross_files = [] | 
|  | 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] | 
|  | if os.path.islink(newdir): | 
|  | self.addCleanup(os.unlink, self.builddir) | 
|  | else: | 
|  | self.addCleanup(windows_proof_rmtree, 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 _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) | 
|  |  | 
|  | proc = 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(proc.stdout) | 
|  | if not stderr: | 
|  | print('stderr:') | 
|  | print(proc.stderr) | 
|  | if proc.returncode != 0: | 
|  | skipped, reason = handle_meson_skip_test(proc.stdout) | 
|  | if skipped: | 
|  | raise SkipTest(f'Project requested skipping: {reason}') | 
|  | raise subprocess.CalledProcessError(proc.returncode, command, output=proc.stdout) | 
|  | return proc.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] | 
|  | build_and_src_dir_args = [self.builddir, srcdir] | 
|  | args = [] | 
|  | 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 + build_and_src_dir_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._logger.log_dir = None | 
|  | mesonbuild.mlog._logger.log_file = None | 
|  |  | 
|  | skipped, reason = handle_meson_skip_test(out) | 
|  | if skipped: | 
|  | raise SkipTest(f'Project requested skipping: {reason}') | 
|  | 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 + build_and_src_dir_args, override_envvars=override_envvars, workdir=workdir) | 
|  | 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: T.Sequence[str], will_build: bool = True) -> None: | 
|  | if isinstance(arg, str): | 
|  | arg = [arg] | 
|  | else: | 
|  | arg = list(arg) | 
|  | self._run(self.mconf_command + arg + [self.builddir]) | 
|  | if will_build: | 
|  | self.build() | 
|  |  | 
|  | def getconf(self, optname: str): | 
|  | opts = self.introspect('--buildoptions') | 
|  | for x in opts: | 
|  | if x.get('name') == optname: | 
|  | return x.get('value') | 
|  | self.fail(f'Option {optname} not found') | 
|  |  | 
|  | def wipe(self): | 
|  | windows_proof_rmtree(self.builddir) | 
|  |  | 
|  | def utime(self, f): | 
|  | 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: `' | 
|  | suffix = '` -> 0\n' | 
|  | with self._open_meson_log() as log: | 
|  | cmds = [split_args(l[len(prefix):-len(suffix)]) 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], | 
|  | encoding='utf-8', 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], | 
|  | encoding='utf-8', 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) | 
|  |  | 
|  | def assertLength(self, val, length): | 
|  | assert len(val) == length, f'{val} is not length {length}' | 
|  |  | 
|  | def copy_srcdir(self, srcdir: str) -> str: | 
|  | """Copies a source tree and returns that copy. | 
|  |  | 
|  | ensures that the copied tree is deleted after running. | 
|  |  | 
|  | :param srcdir: The location of the source tree to copy | 
|  | :return: The location of the copy | 
|  | """ | 
|  | dest = tempfile.mkdtemp() | 
|  | self.addCleanup(windows_proof_rmtree, dest) | 
|  |  | 
|  | # shutil.copytree expects the destination directory to not exist, Once | 
|  | # python 3.8 is required the `dirs_exist_ok` parameter negates the need | 
|  | # for this | 
|  | dest = os.path.join(dest, 'subdir') | 
|  |  | 
|  | shutil.copytree(srcdir, dest) | 
|  |  | 
|  | return dest |