• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Test harness for the venv module.
3
4Copyright (C) 2011-2012 Vinay Sajip.
5Licensed to the PSF under a contributor agreement.
6"""
7
8import contextlib
9import ensurepip
10import os
11import os.path
12import pathlib
13import re
14import shutil
15import struct
16import subprocess
17import sys
18import sysconfig
19import tempfile
20import shlex
21from test.support import (captured_stdout, captured_stderr,
22                          skip_if_broken_multiprocessing_synchronize, verbose,
23                          requires_subprocess, is_android, is_apple_mobile,
24                          is_emscripten, is_wasi,
25                          requires_venv_with_pip, TEST_HOME_DIR,
26                          requires_resource, copy_python_src_ignore)
27from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
28                                    TESTFN, FakePath)
29import unittest
30import venv
31from unittest.mock import patch, Mock
32
33try:
34    import ctypes
35except ImportError:
36    ctypes = None
37
38# Platforms that set sys._base_executable can create venvs from within
39# another venv, so no need to skip tests that require venv.create().
40requireVenvCreate = unittest.skipUnless(
41    sys.prefix == sys.base_prefix
42    or sys._base_executable != sys.executable,
43    'cannot run venv.create from within a venv on this platform')
44
45if is_android or is_apple_mobile or is_emscripten or is_wasi:
46    raise unittest.SkipTest("venv is not available on this platform")
47
48@requires_subprocess()
49def check_output(cmd, encoding=None):
50    p = subprocess.Popen(cmd,
51        stdout=subprocess.PIPE,
52        stderr=subprocess.PIPE,
53        env={**os.environ, "PYTHONHOME": ""})
54    out, err = p.communicate()
55    if p.returncode:
56        if verbose and err:
57            print(err.decode(encoding or 'utf-8', 'backslashreplace'))
58        raise subprocess.CalledProcessError(
59            p.returncode, cmd, out, err)
60    if encoding:
61        return (
62            out.decode(encoding, 'backslashreplace'),
63            err.decode(encoding, 'backslashreplace'),
64        )
65    return out, err
66
67class BaseTest(unittest.TestCase):
68    """Base class for venv tests."""
69    maxDiff = 80 * 50
70
71    def setUp(self):
72        self.env_dir = os.path.realpath(tempfile.mkdtemp())
73        if os.name == 'nt':
74            self.bindir = 'Scripts'
75            self.lib = ('Lib',)
76            self.include = 'Include'
77        else:
78            self.bindir = 'bin'
79            self.lib = ('lib', f'python{sysconfig._get_python_version_abi()}')
80            self.include = 'include'
81        executable = sys._base_executable
82        self.exe = os.path.split(executable)[-1]
83        if (sys.platform == 'win32'
84            and os.path.lexists(executable)
85            and not os.path.exists(executable)):
86            self.cannot_link_exe = True
87        else:
88            self.cannot_link_exe = False
89
90    def tearDown(self):
91        rmtree(self.env_dir)
92
93    def envpy(self, *, real_env_dir=False):
94        if real_env_dir:
95            env_dir = os.path.realpath(self.env_dir)
96        else:
97            env_dir = self.env_dir
98        return os.path.join(env_dir, self.bindir, self.exe)
99
100    def run_with_capture(self, func, *args, **kwargs):
101        with captured_stdout() as output:
102            with captured_stderr() as error:
103                func(*args, **kwargs)
104        return output.getvalue(), error.getvalue()
105
106    def get_env_file(self, *args):
107        return os.path.join(self.env_dir, *args)
108
109    def get_text_file_contents(self, *args, encoding='utf-8'):
110        with open(self.get_env_file(*args), 'r', encoding=encoding) as f:
111            result = f.read()
112        return result
113
114    def assertEndsWith(self, string, tail):
115        if not string.endswith(tail):
116            self.fail(f"String {string!r} does not end with {tail!r}")
117
118class BasicTest(BaseTest):
119    """Test venv module functionality."""
120
121    def isdir(self, *args):
122        fn = self.get_env_file(*args)
123        self.assertTrue(os.path.isdir(fn))
124
125    def test_defaults_with_str_path(self):
126        """
127        Test the create function with default arguments and a str path.
128        """
129        rmtree(self.env_dir)
130        self.run_with_capture(venv.create, self.env_dir)
131        self._check_output_of_default_create()
132
133    def test_defaults_with_pathlike(self):
134        """
135        Test the create function with default arguments and a path-like path.
136        """
137        rmtree(self.env_dir)
138        self.run_with_capture(venv.create, FakePath(self.env_dir))
139        self._check_output_of_default_create()
140
141    def _check_output_of_default_create(self):
142        self.isdir(self.bindir)
143        self.isdir(self.include)
144        self.isdir(*self.lib)
145        # Issue 21197
146        p = self.get_env_file('lib64')
147        conditions = ((struct.calcsize('P') == 8) and (os.name == 'posix') and
148                      (sys.platform != 'darwin'))
149        if conditions:
150            self.assertTrue(os.path.islink(p))
151        else:
152            self.assertFalse(os.path.exists(p))
153        data = self.get_text_file_contents('pyvenv.cfg')
154        executable = sys._base_executable
155        path = os.path.dirname(executable)
156        self.assertIn('home = %s' % path, data)
157        self.assertIn('executable = %s' %
158                      os.path.realpath(sys.executable), data)
159        copies = '' if os.name=='nt' else ' --copies'
160        cmd = (f'command = {sys.executable} -m venv{copies} --without-pip '
161               f'--without-scm-ignore-files {self.env_dir}')
162        self.assertIn(cmd, data)
163        fn = self.get_env_file(self.bindir, self.exe)
164        if not os.path.exists(fn):  # diagnostics for Windows buildbot failures
165            bd = self.get_env_file(self.bindir)
166            print('Contents of %r:' % bd)
167            print('    %r' % os.listdir(bd))
168        self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
169
170    def test_config_file_command_key(self):
171        options = [
172            (None, None, None),  # Default case.
173            ('--copies', 'symlinks', False),
174            ('--without-pip', 'with_pip', False),
175            ('--system-site-packages', 'system_site_packages', True),
176            ('--clear', 'clear', True),
177            ('--upgrade', 'upgrade', True),
178            ('--upgrade-deps', 'upgrade_deps', True),
179            ('--prompt="foobar"', 'prompt', 'foobar'),
180            ('--without-scm-ignore-files', 'scm_ignore_files', frozenset()),
181        ]
182        for opt, attr, value in options:
183            with self.subTest(opt=opt, attr=attr, value=value):
184                rmtree(self.env_dir)
185                if not attr:
186                    kwargs = {}
187                else:
188                    kwargs = {attr: value}
189                b = venv.EnvBuilder(**kwargs)
190                b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
191                b._setup_pip = Mock() # avoid pip setup
192                self.run_with_capture(b.create, self.env_dir)
193                data = self.get_text_file_contents('pyvenv.cfg')
194                if not attr or opt.endswith('git'):
195                    for opt in ('--system-site-packages', '--clear', '--upgrade',
196                                '--upgrade-deps', '--prompt'):
197                        self.assertNotRegex(data, rf'command = .* {opt}')
198                elif os.name=='nt' and attr=='symlinks':
199                    pass
200                else:
201                    self.assertRegex(data, rf'command = .* {opt}')
202
203    def test_prompt(self):
204        env_name = os.path.split(self.env_dir)[1]
205
206        rmtree(self.env_dir)
207        builder = venv.EnvBuilder()
208        self.run_with_capture(builder.create, self.env_dir)
209        context = builder.ensure_directories(self.env_dir)
210        data = self.get_text_file_contents('pyvenv.cfg')
211        self.assertEqual(context.prompt, env_name)
212        self.assertNotIn("prompt = ", data)
213
214        rmtree(self.env_dir)
215        builder = venv.EnvBuilder(prompt='My prompt')
216        self.run_with_capture(builder.create, self.env_dir)
217        context = builder.ensure_directories(self.env_dir)
218        data = self.get_text_file_contents('pyvenv.cfg')
219        self.assertEqual(context.prompt, 'My prompt')
220        self.assertIn("prompt = 'My prompt'\n", data)
221
222        rmtree(self.env_dir)
223        builder = venv.EnvBuilder(prompt='.')
224        cwd = os.path.basename(os.getcwd())
225        self.run_with_capture(builder.create, self.env_dir)
226        context = builder.ensure_directories(self.env_dir)
227        data = self.get_text_file_contents('pyvenv.cfg')
228        self.assertEqual(context.prompt, cwd)
229        self.assertIn("prompt = '%s'\n" % cwd, data)
230
231    def test_upgrade_dependencies(self):
232        builder = venv.EnvBuilder()
233        bin_path = 'bin'
234        python_exe = os.path.split(sys.executable)[1]
235        if sys.platform == 'win32':
236            bin_path = 'Scripts'
237            if os.path.normcase(os.path.splitext(python_exe)[0]).endswith('_d'):
238                python_exe = 'python_d.exe'
239            else:
240                python_exe = 'python.exe'
241        with tempfile.TemporaryDirectory() as fake_env_dir:
242            expect_exe = os.path.normcase(
243                os.path.join(fake_env_dir, bin_path, python_exe)
244            )
245            if sys.platform == 'win32':
246                expect_exe = os.path.normcase(os.path.realpath(expect_exe))
247
248            def pip_cmd_checker(cmd, **kwargs):
249                cmd[0] = os.path.normcase(cmd[0])
250                self.assertEqual(
251                    cmd,
252                    [
253                        expect_exe,
254                        '-m',
255                        'pip',
256                        'install',
257                        '--upgrade',
258                        'pip',
259                    ]
260                )
261
262            fake_context = builder.ensure_directories(fake_env_dir)
263            with patch('venv.subprocess.check_output', pip_cmd_checker):
264                builder.upgrade_dependencies(fake_context)
265
266    @requireVenvCreate
267    def test_prefixes(self):
268        """
269        Test that the prefix values are as expected.
270        """
271        # check a venv's prefixes
272        rmtree(self.env_dir)
273        self.run_with_capture(venv.create, self.env_dir)
274        cmd = [self.envpy(), '-c', None]
275        for prefix, expected in (
276            ('prefix', self.env_dir),
277            ('exec_prefix', self.env_dir),
278            ('base_prefix', sys.base_prefix),
279            ('base_exec_prefix', sys.base_exec_prefix)):
280            cmd[2] = 'import sys; print(sys.%s)' % prefix
281            out, err = check_output(cmd)
282            self.assertEqual(pathlib.Path(out.strip().decode()),
283                             pathlib.Path(expected), prefix)
284
285    @requireVenvCreate
286    def test_sysconfig(self):
287        """
288        Test that the sysconfig functions work in a virtual environment.
289        """
290        rmtree(self.env_dir)
291        self.run_with_capture(venv.create, self.env_dir, symlinks=False)
292        cmd = [self.envpy(), '-c', None]
293        for call, expected in (
294            # installation scheme
295            ('get_preferred_scheme("prefix")', 'venv'),
296            ('get_default_scheme()', 'venv'),
297            # build environment
298            ('is_python_build()', str(sysconfig.is_python_build())),
299            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
300            ('get_config_h_filename()', sysconfig.get_config_h_filename()),
301            ('get_config_var("Py_GIL_DISABLED")',
302             str(sysconfig.get_config_var("Py_GIL_DISABLED")))):
303            with self.subTest(call):
304                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
305                out, err = check_output(cmd, encoding='utf-8')
306                self.assertEqual(out.strip(), expected, err)
307        for attr, expected in (
308            ('executable', self.envpy()),
309            # Usually compare to sys.executable, but if we're running in our own
310            # venv then we really need to compare to our base executable
311            ('_base_executable', sys._base_executable),
312        ):
313            with self.subTest(attr):
314                cmd[2] = f'import sys; print(sys.{attr})'
315                out, err = check_output(cmd, encoding='utf-8')
316                self.assertEqual(out.strip(), expected, err)
317
318    @requireVenvCreate
319    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
320    def test_sysconfig_symlinks(self):
321        """
322        Test that the sysconfig functions work in a virtual environment.
323        """
324        rmtree(self.env_dir)
325        self.run_with_capture(venv.create, self.env_dir, symlinks=True)
326        cmd = [self.envpy(), '-c', None]
327        for call, expected in (
328            # installation scheme
329            ('get_preferred_scheme("prefix")', 'venv'),
330            ('get_default_scheme()', 'venv'),
331            # build environment
332            ('is_python_build()', str(sysconfig.is_python_build())),
333            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
334            ('get_config_h_filename()', sysconfig.get_config_h_filename()),
335            ('get_config_var("Py_GIL_DISABLED")',
336             str(sysconfig.get_config_var("Py_GIL_DISABLED")))):
337            with self.subTest(call):
338                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
339                out, err = check_output(cmd, encoding='utf-8')
340                self.assertEqual(out.strip(), expected, err)
341        for attr, expected in (
342            ('executable', self.envpy()),
343            # Usually compare to sys.executable, but if we're running in our own
344            # venv then we really need to compare to our base executable
345            # HACK: Test fails on POSIX with unversioned binary (PR gh-113033)
346            #('_base_executable', sys._base_executable),
347        ):
348            with self.subTest(attr):
349                cmd[2] = f'import sys; print(sys.{attr})'
350                out, err = check_output(cmd, encoding='utf-8')
351                self.assertEqual(out.strip(), expected, err)
352
353    if sys.platform == 'win32':
354        ENV_SUBDIRS = (
355            ('Scripts',),
356            ('Include',),
357            ('Lib',),
358            ('Lib', 'site-packages'),
359        )
360    else:
361        ENV_SUBDIRS = (
362            ('bin',),
363            ('include',),
364            ('lib',),
365            ('lib', 'python%d.%d' % sys.version_info[:2]),
366            ('lib', 'python%d.%d' % sys.version_info[:2], 'site-packages'),
367        )
368
369    def create_contents(self, paths, filename):
370        """
371        Create some files in the environment which are unrelated
372        to the virtual environment.
373        """
374        for subdirs in paths:
375            d = os.path.join(self.env_dir, *subdirs)
376            os.mkdir(d)
377            fn = os.path.join(d, filename)
378            with open(fn, 'wb') as f:
379                f.write(b'Still here?')
380
381    def test_overwrite_existing(self):
382        """
383        Test creating environment in an existing directory.
384        """
385        self.create_contents(self.ENV_SUBDIRS, 'foo')
386        venv.create(self.env_dir)
387        for subdirs in self.ENV_SUBDIRS:
388            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
389            self.assertTrue(os.path.exists(fn))
390            with open(fn, 'rb') as f:
391                self.assertEqual(f.read(), b'Still here?')
392
393        builder = venv.EnvBuilder(clear=True)
394        builder.create(self.env_dir)
395        for subdirs in self.ENV_SUBDIRS:
396            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
397            self.assertFalse(os.path.exists(fn))
398
399    def clear_directory(self, path):
400        for fn in os.listdir(path):
401            fn = os.path.join(path, fn)
402            if os.path.islink(fn) or os.path.isfile(fn):
403                os.remove(fn)
404            elif os.path.isdir(fn):
405                rmtree(fn)
406
407    def test_unoverwritable_fails(self):
408        #create a file clashing with directories in the env dir
409        for paths in self.ENV_SUBDIRS[:3]:
410            fn = os.path.join(self.env_dir, *paths)
411            with open(fn, 'wb') as f:
412                f.write(b'')
413            self.assertRaises((ValueError, OSError), venv.create, self.env_dir)
414            self.clear_directory(self.env_dir)
415
416    def test_upgrade(self):
417        """
418        Test upgrading an existing environment directory.
419        """
420        # See Issue #21643: the loop needs to run twice to ensure
421        # that everything works on the upgrade (the first run just creates
422        # the venv).
423        for upgrade in (False, True):
424            builder = venv.EnvBuilder(upgrade=upgrade)
425            self.run_with_capture(builder.create, self.env_dir)
426            self.isdir(self.bindir)
427            self.isdir(self.include)
428            self.isdir(*self.lib)
429            fn = self.get_env_file(self.bindir, self.exe)
430            if not os.path.exists(fn):
431                # diagnostics for Windows buildbot failures
432                bd = self.get_env_file(self.bindir)
433                print('Contents of %r:' % bd)
434                print('    %r' % os.listdir(bd))
435            self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
436
437    def test_isolation(self):
438        """
439        Test isolation from system site-packages
440        """
441        for ssp, s in ((True, 'true'), (False, 'false')):
442            builder = venv.EnvBuilder(clear=True, system_site_packages=ssp)
443            builder.create(self.env_dir)
444            data = self.get_text_file_contents('pyvenv.cfg')
445            self.assertIn('include-system-site-packages = %s\n' % s, data)
446
447    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
448    def test_symlinking(self):
449        """
450        Test symlinking works as expected
451        """
452        for usl in (False, True):
453            builder = venv.EnvBuilder(clear=True, symlinks=usl)
454            builder.create(self.env_dir)
455            fn = self.get_env_file(self.bindir, self.exe)
456            # Don't test when False, because e.g. 'python' is always
457            # symlinked to 'python3.3' in the env, even when symlinking in
458            # general isn't wanted.
459            if usl:
460                if self.cannot_link_exe:
461                    # Symlinking is skipped when our executable is already a
462                    # special app symlink
463                    self.assertFalse(os.path.islink(fn))
464                else:
465                    self.assertTrue(os.path.islink(fn))
466
467    # If a venv is created from a source build and that venv is used to
468    # run the test, the pyvenv.cfg in the venv created in the test will
469    # point to the venv being used to run the test, and we lose the link
470    # to the source build - so Python can't initialise properly.
471    @requireVenvCreate
472    def test_executable(self):
473        """
474        Test that the sys.executable value is as expected.
475        """
476        rmtree(self.env_dir)
477        self.run_with_capture(venv.create, self.env_dir)
478        envpy = self.envpy(real_env_dir=True)
479        out, err = check_output([envpy, '-c',
480            'import sys; print(sys.executable)'])
481        self.assertEqual(out.strip(), envpy.encode())
482
483    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
484    def test_executable_symlinks(self):
485        """
486        Test that the sys.executable value is as expected.
487        """
488        rmtree(self.env_dir)
489        builder = venv.EnvBuilder(clear=True, symlinks=True)
490        builder.create(self.env_dir)
491        envpy = self.envpy(real_env_dir=True)
492        out, err = check_output([envpy, '-c',
493            'import sys; print(sys.executable)'])
494        self.assertEqual(out.strip(), envpy.encode())
495
496    # gh-124651: test quoted strings
497    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
498    def test_special_chars_bash(self):
499        """
500        Test that the template strings are quoted properly (bash)
501        """
502        rmtree(self.env_dir)
503        bash = shutil.which('bash')
504        if bash is None:
505            self.skipTest('bash required for this test')
506        env_name = '"\';&&$e|\'"'
507        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
508        builder = venv.EnvBuilder(clear=True)
509        builder.create(env_dir)
510        activate = os.path.join(env_dir, self.bindir, 'activate')
511        test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
512        with open(test_script, "w") as f:
513            f.write(f'source {shlex.quote(activate)}\n'
514                    'python -c \'import sys; print(sys.executable)\'\n'
515                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
516                    'deactivate\n')
517        out, err = check_output([bash, test_script])
518        lines = out.splitlines()
519        self.assertTrue(env_name.encode() in lines[0])
520        self.assertEndsWith(lines[1], env_name.encode())
521
522    # gh-124651: test quoted strings
523    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
524    def test_special_chars_csh(self):
525        """
526        Test that the template strings are quoted properly (csh)
527        """
528        rmtree(self.env_dir)
529        csh = shutil.which('tcsh') or shutil.which('csh')
530        if csh is None:
531            self.skipTest('csh required for this test')
532        env_name = '"\';&&$e|\'"'
533        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
534        builder = venv.EnvBuilder(clear=True)
535        builder.create(env_dir)
536        activate = os.path.join(env_dir, self.bindir, 'activate.csh')
537        test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
538        with open(test_script, "w") as f:
539            f.write(f'source {shlex.quote(activate)}\n'
540                    'python -c \'import sys; print(sys.executable)\'\n'
541                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
542                    'deactivate\n')
543        out, err = check_output([csh, test_script])
544        lines = out.splitlines()
545        self.assertTrue(env_name.encode() in lines[0])
546        self.assertEndsWith(lines[1], env_name.encode())
547
548    # gh-124651: test quoted strings on Windows
549    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
550    def test_special_chars_windows(self):
551        """
552        Test that the template strings are quoted properly on Windows
553        """
554        rmtree(self.env_dir)
555        env_name = "'&&^$e"
556        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
557        builder = venv.EnvBuilder(clear=True)
558        builder.create(env_dir)
559        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
560        test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
561        with open(test_batch, "w") as f:
562            f.write('@echo off\n'
563                    f'"{activate}" & '
564                    f'{self.exe} -c "import sys; print(sys.executable)" & '
565                    f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
566                    'deactivate')
567        out, err = check_output([test_batch])
568        lines = out.splitlines()
569        self.assertTrue(env_name.encode() in lines[0])
570        self.assertEndsWith(lines[1], env_name.encode())
571
572    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
573    def test_unicode_in_batch_file(self):
574        """
575        Test handling of Unicode paths
576        """
577        rmtree(self.env_dir)
578        env_dir = os.path.join(os.path.realpath(self.env_dir), 'ϼўТλФЙ')
579        builder = venv.EnvBuilder(clear=True)
580        builder.create(env_dir)
581        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
582        out, err = check_output(
583            [activate, '&', self.exe, '-c', 'print(0)'],
584            encoding='oem',
585        )
586        self.assertEqual(out.strip(), '0')
587
588    @unittest.skipUnless(os.name == 'nt' and can_symlink(),
589                         'symlinks on Windows')
590    def test_failed_symlink(self):
591        """
592        Test handling of failed symlinks on Windows.
593        """
594        rmtree(self.env_dir)
595        env_dir = os.path.join(os.path.realpath(self.env_dir), 'venv')
596        with patch('os.symlink') as mock_symlink:
597            mock_symlink.side_effect = OSError()
598            builder = venv.EnvBuilder(clear=True, symlinks=True)
599            _, err = self.run_with_capture(builder.create, env_dir)
600            filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'"
601            self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}")
602
603    @requireVenvCreate
604    def test_multiprocessing(self):
605        """
606        Test that the multiprocessing is able to spawn.
607        """
608        # bpo-36342: Instantiation of a Pool object imports the
609        # multiprocessing.synchronize module. Skip the test if this module
610        # cannot be imported.
611        skip_if_broken_multiprocessing_synchronize()
612
613        rmtree(self.env_dir)
614        self.run_with_capture(venv.create, self.env_dir)
615        out, err = check_output([self.envpy(real_env_dir=True), '-c',
616            'from multiprocessing import Pool; '
617            'pool = Pool(1); '
618            'print(pool.apply_async("Python".lower).get(3)); '
619            'pool.terminate()'])
620        self.assertEqual(out.strip(), "python".encode())
621
622    @requireVenvCreate
623    def test_multiprocessing_recursion(self):
624        """
625        Test that the multiprocessing is able to spawn itself
626        """
627        skip_if_broken_multiprocessing_synchronize()
628
629        rmtree(self.env_dir)
630        self.run_with_capture(venv.create, self.env_dir)
631        script = os.path.join(TEST_HOME_DIR, '_test_venv_multiprocessing.py')
632        subprocess.check_call([self.envpy(real_env_dir=True), "-I", script])
633
634    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
635    def test_deactivate_with_strict_bash_opts(self):
636        bash = shutil.which("bash")
637        if bash is None:
638            self.skipTest("bash required for this test")
639        rmtree(self.env_dir)
640        builder = venv.EnvBuilder(clear=True)
641        builder.create(self.env_dir)
642        activate = os.path.join(self.env_dir, self.bindir, "activate")
643        test_script = os.path.join(self.env_dir, "test_strict.sh")
644        with open(test_script, "w") as f:
645            f.write("set -euo pipefail\n"
646                    f"source {activate}\n"
647                    "deactivate\n")
648        out, err = check_output([bash, test_script])
649        self.assertEqual(out, "".encode())
650        self.assertEqual(err, "".encode())
651
652
653    @unittest.skipUnless(sys.platform == 'darwin', 'only relevant on macOS')
654    def test_macos_env(self):
655        rmtree(self.env_dir)
656        builder = venv.EnvBuilder()
657        builder.create(self.env_dir)
658
659        out, err = check_output([self.envpy(real_env_dir=True), '-c',
660            'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
661        self.assertEqual(out.strip(), 'False'.encode())
662
663    def test_pathsep_error(self):
664        """
665        Test that venv creation fails when the target directory contains
666        the path separator.
667        """
668        rmtree(self.env_dir)
669        bad_itempath = self.env_dir + os.pathsep
670        self.assertRaises(ValueError, venv.create, bad_itempath)
671        self.assertRaises(ValueError, venv.create, FakePath(bad_itempath))
672
673    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
674    @requireVenvCreate
675    def test_zippath_from_non_installed_posix(self):
676        """
677        Test that when create venv from non-installed python, the zip path
678        value is as expected.
679        """
680        rmtree(self.env_dir)
681        # First try to create a non-installed python. It's not a real full
682        # functional non-installed python, but enough for this test.
683        platlibdir = sys.platlibdir
684        non_installed_dir = os.path.realpath(tempfile.mkdtemp())
685        self.addCleanup(rmtree, non_installed_dir)
686        bindir = os.path.join(non_installed_dir, self.bindir)
687        os.mkdir(bindir)
688        shutil.copy2(sys.executable, bindir)
689        libdir = os.path.join(non_installed_dir, platlibdir, self.lib[1])
690        os.makedirs(libdir)
691        landmark = os.path.join(libdir, "os.py")
692        abi_thread = "t" if sysconfig.get_config_var("Py_GIL_DISABLED") else ""
693        stdlib_zip = f"python{sys.version_info.major}{sys.version_info.minor}{abi_thread}"
694        zip_landmark = os.path.join(non_installed_dir,
695                                    platlibdir,
696                                    stdlib_zip)
697        additional_pythonpath_for_non_installed = []
698
699        # Copy stdlib files to the non-installed python so venv can
700        # correctly calculate the prefix.
701        for eachpath in sys.path:
702            if eachpath.endswith(".zip"):
703                if os.path.isfile(eachpath):
704                    shutil.copyfile(
705                        eachpath,
706                        os.path.join(non_installed_dir, platlibdir))
707            elif os.path.isfile(os.path.join(eachpath, "os.py")):
708                names = os.listdir(eachpath)
709                ignored_names = copy_python_src_ignore(eachpath, names)
710                for name in names:
711                    if name in ignored_names:
712                        continue
713                    if name == "site-packages":
714                        continue
715                    fn = os.path.join(eachpath, name)
716                    if os.path.isfile(fn):
717                        shutil.copy(fn, libdir)
718                    elif os.path.isdir(fn):
719                        shutil.copytree(fn, os.path.join(libdir, name),
720                                        ignore=copy_python_src_ignore)
721            else:
722                additional_pythonpath_for_non_installed.append(
723                    eachpath)
724        cmd = [os.path.join(non_installed_dir, self.bindir, self.exe),
725               "-m",
726               "venv",
727               "--without-pip",
728               "--without-scm-ignore-files",
729               self.env_dir]
730        # Our fake non-installed python is not fully functional because
731        # it cannot find the extensions. Set PYTHONPATH so it can run the
732        # venv module correctly.
733        pythonpath = os.pathsep.join(
734            additional_pythonpath_for_non_installed)
735        # For python built with shared enabled. We need to set
736        # LD_LIBRARY_PATH so the non-installed python can find and link
737        # libpython.so
738        ld_library_path = sysconfig.get_config_var("LIBDIR")
739        if not ld_library_path or sysconfig.is_python_build():
740            ld_library_path = os.path.abspath(os.path.dirname(sys.executable))
741        if sys.platform == 'darwin':
742            ld_library_path_env = "DYLD_LIBRARY_PATH"
743        else:
744            ld_library_path_env = "LD_LIBRARY_PATH"
745        child_env = {
746                "PYTHONPATH": pythonpath,
747                ld_library_path_env: ld_library_path,
748        }
749        if asan_options := os.environ.get("ASAN_OPTIONS"):
750            # prevent https://github.com/python/cpython/issues/104839
751            child_env["ASAN_OPTIONS"] = asan_options
752        subprocess.check_call(cmd, env=child_env)
753        # Now check the venv created from the non-installed python has
754        # correct zip path in pythonpath.
755        cmd = [self.envpy(), '-S', '-c', 'import sys; print(sys.path)']
756        out, err = check_output(cmd)
757        self.assertTrue(zip_landmark.encode() in out)
758
759    @requireVenvCreate
760    def test_activate_shell_script_has_no_dos_newlines(self):
761        """
762        Test that the `activate` shell script contains no CR LF.
763        This is relevant for Cygwin, as the Windows build might have
764        converted line endings accidentally.
765        """
766        venv_dir = pathlib.Path(self.env_dir)
767        rmtree(venv_dir)
768        [[scripts_dir], *_] = self.ENV_SUBDIRS
769        script_path = venv_dir / scripts_dir / "activate"
770        venv.create(venv_dir)
771        with open(script_path, 'rb') as script:
772            for i, line in enumerate(script, 1):
773                error_message = f"CR LF found in line {i}"
774                self.assertFalse(line.endswith(b'\r\n'), error_message)
775
776    @requireVenvCreate
777    def test_scm_ignore_files_git(self):
778        """
779        Test that a .gitignore file is created when "git" is specified.
780        The file should contain a `*\n` line.
781        """
782        self.run_with_capture(venv.create, self.env_dir,
783                              scm_ignore_files={'git'})
784        file_lines = self.get_text_file_contents('.gitignore').splitlines()
785        self.assertIn('*', file_lines)
786
787    @requireVenvCreate
788    def test_create_scm_ignore_files_multiple(self):
789        """
790        Test that ``scm_ignore_files`` can work with multiple SCMs.
791        """
792        bzrignore_name = ".bzrignore"
793        contents = "# For Bazaar.\n*\n"
794
795        class BzrEnvBuilder(venv.EnvBuilder):
796            def create_bzr_ignore_file(self, context):
797                gitignore_path = os.path.join(context.env_dir, bzrignore_name)
798                with open(gitignore_path, 'w', encoding='utf-8') as file:
799                    file.write(contents)
800
801        builder = BzrEnvBuilder(scm_ignore_files={'git', 'bzr'})
802        self.run_with_capture(builder.create, self.env_dir)
803
804        gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
805        self.assertIn('*', gitignore_lines)
806
807        bzrignore = self.get_text_file_contents(bzrignore_name)
808        self.assertEqual(bzrignore, contents)
809
810    @requireVenvCreate
811    def test_create_scm_ignore_files_empty(self):
812        """
813        Test that no default ignore files are created when ``scm_ignore_files``
814        is empty.
815        """
816        # scm_ignore_files is set to frozenset() by default.
817        self.run_with_capture(venv.create, self.env_dir)
818        with self.assertRaises(FileNotFoundError):
819            self.get_text_file_contents('.gitignore')
820
821        self.assertIn("--without-scm-ignore-files",
822                      self.get_text_file_contents('pyvenv.cfg'))
823
824    @requireVenvCreate
825    def test_cli_with_scm_ignore_files(self):
826        """
827        Test that default SCM ignore files are created by default via the CLI.
828        """
829        self.run_with_capture(venv.main, ['--without-pip', self.env_dir])
830
831        gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
832        self.assertIn('*', gitignore_lines)
833
834    @requireVenvCreate
835    def test_cli_without_scm_ignore_files(self):
836        """
837        Test that ``--without-scm-ignore-files`` doesn't create SCM ignore files.
838        """
839        args = ['--without-pip', '--without-scm-ignore-files', self.env_dir]
840        self.run_with_capture(venv.main, args)
841
842        with self.assertRaises(FileNotFoundError):
843            self.get_text_file_contents('.gitignore')
844
845    def test_venv_same_path(self):
846        same_path = venv.EnvBuilder._same_path
847        if sys.platform == 'win32':
848            # Case-insensitive, and handles short/long names
849            tests = [
850                (True, TESTFN, TESTFN),
851                (True, TESTFN.lower(), TESTFN.upper()),
852            ]
853            import _winapi
854            # ProgramFiles is the most reliable path that will have short/long
855            progfiles = os.getenv('ProgramFiles')
856            if progfiles:
857                tests = [
858                    *tests,
859                    (True, progfiles, progfiles),
860                    (True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)),
861                ]
862        else:
863            # Just a simple case-sensitive comparison
864            tests = [
865                (True, TESTFN, TESTFN),
866                (False, TESTFN.lower(), TESTFN.upper()),
867            ]
868        for r, path1, path2 in tests:
869            with self.subTest(f"{path1}-{path2}"):
870                if r:
871                    self.assertTrue(same_path(path1, path2))
872                else:
873                    self.assertFalse(same_path(path1, path2))
874
875    # gh-126084: venvwlauncher should run pythonw, not python
876    @requireVenvCreate
877    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
878    def test_venvwlauncher(self):
879        """
880        Test that the GUI launcher runs the GUI python.
881        """
882        rmtree(self.env_dir)
883        venv.create(self.env_dir)
884        exename = self.exe
885        # Retain the debug suffix if present
886        if "python" in exename and not "pythonw" in exename:
887            exename = exename.replace("python", "pythonw")
888        envpyw = os.path.join(self.env_dir, self.bindir, exename)
889        try:
890            subprocess.check_call([envpyw, "-c", "import sys; "
891                "assert sys._base_executable.endswith('%s')" % exename])
892        except subprocess.CalledProcessError:
893            self.fail("venvwlauncher.exe did not run %s" % exename)
894
895
896@requireVenvCreate
897class EnsurePipTest(BaseTest):
898    """Test venv module installation of pip."""
899    def assert_pip_not_installed(self):
900        out, err = check_output([self.envpy(real_env_dir=True), '-c',
901            'try:\n import pip\nexcept ImportError:\n print("OK")'])
902        # We force everything to text, so unittest gives the detailed diff
903        # if we get unexpected results
904        err = err.decode("latin-1") # Force to text, prevent decoding errors
905        self.assertEqual(err, "")
906        out = out.decode("latin-1") # Force to text, prevent decoding errors
907        self.assertEqual(out.strip(), "OK")
908
909
910    def test_no_pip_by_default(self):
911        rmtree(self.env_dir)
912        self.run_with_capture(venv.create, self.env_dir)
913        self.assert_pip_not_installed()
914
915    def test_explicit_no_pip(self):
916        rmtree(self.env_dir)
917        self.run_with_capture(venv.create, self.env_dir, with_pip=False)
918        self.assert_pip_not_installed()
919
920    def test_devnull(self):
921        # Fix for issue #20053 uses os.devnull to force a config file to
922        # appear empty. However http://bugs.python.org/issue20541 means
923        # that doesn't currently work properly on Windows. Once that is
924        # fixed, the "win_location" part of test_with_pip should be restored
925        with open(os.devnull, "rb") as f:
926            self.assertEqual(f.read(), b"")
927
928        self.assertTrue(os.path.exists(os.devnull))
929
930    def do_test_with_pip(self, system_site_packages):
931        rmtree(self.env_dir)
932        with EnvironmentVarGuard() as envvars:
933            # pip's cross-version compatibility may trigger deprecation
934            # warnings in current versions of Python. Ensure related
935            # environment settings don't cause venv to fail.
936            envvars["PYTHONWARNINGS"] = "ignore"
937            # ensurepip is different enough from a normal pip invocation
938            # that we want to ensure it ignores the normal pip environment
939            # variable settings. We set PIP_NO_INSTALL here specifically
940            # to check that ensurepip (and hence venv) ignores it.
941            # See http://bugs.python.org/issue19734
942            envvars["PIP_NO_INSTALL"] = "1"
943            # Also check that we ignore the pip configuration file
944            # See http://bugs.python.org/issue20053
945            with tempfile.TemporaryDirectory() as home_dir:
946                envvars["HOME"] = home_dir
947                bad_config = "[global]\nno-install=1"
948                # Write to both config file names on all platforms to reduce
949                # cross-platform variation in test code behaviour
950                win_location = ("pip", "pip.ini")
951                posix_location = (".pip", "pip.conf")
952                # Skips win_location due to http://bugs.python.org/issue20541
953                for dirname, fname in (posix_location,):
954                    dirpath = os.path.join(home_dir, dirname)
955                    os.mkdir(dirpath)
956                    fpath = os.path.join(dirpath, fname)
957                    with open(fpath, 'w') as f:
958                        f.write(bad_config)
959
960                # Actually run the create command with all that unhelpful
961                # config in place to ensure we ignore it
962                with self.nicer_error():
963                    self.run_with_capture(venv.create, self.env_dir,
964                                          system_site_packages=system_site_packages,
965                                          with_pip=True)
966        # Ensure pip is available in the virtual environment
967        # Ignore DeprecationWarning since pip code is not part of Python
968        out, err = check_output([self.envpy(real_env_dir=True),
969               '-W', 'ignore::DeprecationWarning',
970               '-W', 'ignore::ImportWarning', '-I',
971               '-m', 'pip', '--version'])
972        # We force everything to text, so unittest gives the detailed diff
973        # if we get unexpected results
974        err = err.decode("latin-1") # Force to text, prevent decoding errors
975        self.assertEqual(err, "")
976        out = out.decode("latin-1") # Force to text, prevent decoding errors
977        expected_version = "pip {}".format(ensurepip.version())
978        self.assertEqual(out[:len(expected_version)], expected_version)
979        env_dir = os.fsencode(self.env_dir).decode("latin-1")
980        self.assertIn(env_dir, out)
981
982        # http://bugs.python.org/issue19728
983        # Check the private uninstall command provided for the Windows
984        # installers works (at least in a virtual environment)
985        with EnvironmentVarGuard() as envvars:
986            with self.nicer_error():
987                # It seems ensurepip._uninstall calls subprocesses which do not
988                # inherit the interpreter settings.
989                envvars["PYTHONWARNINGS"] = "ignore"
990                out, err = check_output([self.envpy(real_env_dir=True),
991                    '-W', 'ignore::DeprecationWarning',
992                    '-W', 'ignore::ImportWarning', '-I',
993                    '-m', 'ensurepip._uninstall'])
994        # We force everything to text, so unittest gives the detailed diff
995        # if we get unexpected results
996        err = err.decode("latin-1") # Force to text, prevent decoding errors
997        # Ignore the warning:
998        #   "The directory '$HOME/.cache/pip/http' or its parent directory
999        #    is not owned by the current user and the cache has been disabled.
1000        #    Please check the permissions and owner of that directory. If
1001        #    executing pip with sudo, you may want sudo's -H flag."
1002        # where $HOME is replaced by the HOME environment variable.
1003        err = re.sub("^(WARNING: )?The directory .* or its parent directory "
1004                     "is not owned or is not writable by the current user.*$", "",
1005                     err, flags=re.MULTILINE)
1006        # Ignore warning about missing optional module:
1007        try:
1008            import ssl
1009        except ImportError:
1010            err = re.sub(
1011                "^WARNING: Disabling truststore since ssl support is missing$",
1012                "",
1013                err, flags=re.MULTILINE)
1014        self.assertEqual(err.rstrip(), "")
1015        # Being fairly specific regarding the expected behaviour for the
1016        # initial bundling phase in Python 3.4. If the output changes in
1017        # future pip versions, this test can likely be relaxed further.
1018        out = out.decode("latin-1") # Force to text, prevent decoding errors
1019        self.assertIn("Successfully uninstalled pip", out)
1020        # Check pip is now gone from the virtual environment. This only
1021        # applies in the system_site_packages=False case, because in the
1022        # other case, pip may still be available in the system site-packages
1023        if not system_site_packages:
1024            self.assert_pip_not_installed()
1025
1026    @contextlib.contextmanager
1027    def nicer_error(self):
1028        """
1029        Capture output from a failed subprocess for easier debugging.
1030
1031        The output this handler produces can be a little hard to read,
1032        but at least it has all the details.
1033        """
1034        try:
1035            yield
1036        except subprocess.CalledProcessError as exc:
1037            out = (exc.output or b'').decode(errors="replace")
1038            err = (exc.stderr or b'').decode(errors="replace")
1039            self.fail(
1040                f"{exc}\n\n"
1041                f"**Subprocess Output**\n{out}\n\n"
1042                f"**Subprocess Error**\n{err}"
1043            )
1044
1045    @requires_venv_with_pip()
1046    @requires_resource('cpu')
1047    def test_with_pip(self):
1048        self.do_test_with_pip(False)
1049        self.do_test_with_pip(True)
1050
1051
1052if __name__ == "__main__":
1053    unittest.main()
1054