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
20from test.support import (captured_stdout, captured_stderr, requires_zlib,
21                          skip_if_broken_multiprocessing_synchronize, verbose,
22                          requires_subprocess, is_emscripten, is_wasi,
23                          requires_venv_with_pip, TEST_HOME_DIR)
24from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
25import unittest
26import venv
27from unittest.mock import patch, Mock
28
29try:
30    import ctypes
31except ImportError:
32    ctypes = None
33
34# Platforms that set sys._base_executable can create venvs from within
35# another venv, so no need to skip tests that require venv.create().
36requireVenvCreate = unittest.skipUnless(
37    sys.prefix == sys.base_prefix
38    or sys._base_executable != sys.executable,
39    'cannot run venv.create from within a venv on this platform')
40
41if is_emscripten or is_wasi:
42    raise unittest.SkipTest("venv is not available on Emscripten/WASI.")
43
44@requires_subprocess()
45def check_output(cmd, encoding=None):
46    p = subprocess.Popen(cmd,
47        stdout=subprocess.PIPE,
48        stderr=subprocess.PIPE,
49        encoding=encoding)
50    out, err = p.communicate()
51    if p.returncode:
52        if verbose and err:
53            print(err.decode('utf-8', 'backslashreplace'))
54        raise subprocess.CalledProcessError(
55            p.returncode, cmd, out, err)
56    return out, err
57
58class BaseTest(unittest.TestCase):
59    """Base class for venv tests."""
60    maxDiff = 80 * 50
61
62    def setUp(self):
63        self.env_dir = os.path.realpath(tempfile.mkdtemp())
64        if os.name == 'nt':
65            self.bindir = 'Scripts'
66            self.lib = ('Lib',)
67            self.include = 'Include'
68        else:
69            self.bindir = 'bin'
70            self.lib = ('lib', 'python%d.%d' % sys.version_info[:2])
71            self.include = 'include'
72        executable = sys._base_executable
73        self.exe = os.path.split(executable)[-1]
74        if (sys.platform == 'win32'
75            and os.path.lexists(executable)
76            and not os.path.exists(executable)):
77            self.cannot_link_exe = True
78        else:
79            self.cannot_link_exe = False
80
81    def tearDown(self):
82        rmtree(self.env_dir)
83
84    def run_with_capture(self, func, *args, **kwargs):
85        with captured_stdout() as output:
86            with captured_stderr() as error:
87                func(*args, **kwargs)
88        return output.getvalue(), error.getvalue()
89
90    def get_env_file(self, *args):
91        return os.path.join(self.env_dir, *args)
92
93    def get_text_file_contents(self, *args, encoding='utf-8'):
94        with open(self.get_env_file(*args), 'r', encoding=encoding) as f:
95            result = f.read()
96        return result
97
98class BasicTest(BaseTest):
99    """Test venv module functionality."""
100
101    def isdir(self, *args):
102        fn = self.get_env_file(*args)
103        self.assertTrue(os.path.isdir(fn))
104
105    def test_defaults_with_str_path(self):
106        """
107        Test the create function with default arguments and a str path.
108        """
109        rmtree(self.env_dir)
110        self.run_with_capture(venv.create, self.env_dir)
111        self._check_output_of_default_create()
112
113    def test_defaults_with_pathlib_path(self):
114        """
115        Test the create function with default arguments and a pathlib.Path path.
116        """
117        rmtree(self.env_dir)
118        self.run_with_capture(venv.create, pathlib.Path(self.env_dir))
119        self._check_output_of_default_create()
120
121    def _check_output_of_default_create(self):
122        self.isdir(self.bindir)
123        self.isdir(self.include)
124        self.isdir(*self.lib)
125        # Issue 21197
126        p = self.get_env_file('lib64')
127        conditions = ((struct.calcsize('P') == 8) and (os.name == 'posix') and
128                      (sys.platform != 'darwin'))
129        if conditions:
130            self.assertTrue(os.path.islink(p))
131        else:
132            self.assertFalse(os.path.exists(p))
133        data = self.get_text_file_contents('pyvenv.cfg')
134        executable = sys._base_executable
135        path = os.path.dirname(executable)
136        self.assertIn('home = %s' % path, data)
137        self.assertIn('executable = %s' %
138                      os.path.realpath(sys.executable), data)
139        copies = '' if os.name=='nt' else ' --copies'
140        cmd = f'command = {sys.executable} -m venv{copies} --without-pip {self.env_dir}'
141        self.assertIn(cmd, data)
142        fn = self.get_env_file(self.bindir, self.exe)
143        if not os.path.exists(fn):  # diagnostics for Windows buildbot failures
144            bd = self.get_env_file(self.bindir)
145            print('Contents of %r:' % bd)
146            print('    %r' % os.listdir(bd))
147        self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
148
149    def test_config_file_command_key(self):
150        attrs = [
151            (None, None),
152            ('symlinks', '--copies'),
153            ('with_pip', '--without-pip'),
154            ('system_site_packages', '--system-site-packages'),
155            ('clear', '--clear'),
156            ('upgrade', '--upgrade'),
157            ('upgrade_deps', '--upgrade-deps'),
158            ('prompt', '--prompt'),
159        ]
160        for attr, opt in attrs:
161            rmtree(self.env_dir)
162            if not attr:
163                b = venv.EnvBuilder()
164            else:
165                b = venv.EnvBuilder(
166                    **{attr: False if attr in ('with_pip', 'symlinks') else True})
167            b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
168            b._setup_pip = Mock() # avoid pip setup
169            self.run_with_capture(b.create, self.env_dir)
170            data = self.get_text_file_contents('pyvenv.cfg')
171            if not attr:
172                for opt in ('--system-site-packages', '--clear', '--upgrade',
173                        '--upgrade-deps', '--prompt'):
174                    self.assertNotRegex(data, rf'command = .* {opt}')
175            elif os.name=='nt' and attr=='symlinks':
176                pass
177            else:
178                self.assertRegex(data, rf'command = .* {opt}')
179
180    def test_prompt(self):
181        env_name = os.path.split(self.env_dir)[1]
182
183        rmtree(self.env_dir)
184        builder = venv.EnvBuilder()
185        self.run_with_capture(builder.create, self.env_dir)
186        context = builder.ensure_directories(self.env_dir)
187        data = self.get_text_file_contents('pyvenv.cfg')
188        self.assertEqual(context.prompt, '(%s) ' % env_name)
189        self.assertNotIn("prompt = ", data)
190
191        rmtree(self.env_dir)
192        builder = venv.EnvBuilder(prompt='My prompt')
193        self.run_with_capture(builder.create, self.env_dir)
194        context = builder.ensure_directories(self.env_dir)
195        data = self.get_text_file_contents('pyvenv.cfg')
196        self.assertEqual(context.prompt, '(My prompt) ')
197        self.assertIn("prompt = 'My prompt'\n", data)
198
199        rmtree(self.env_dir)
200        builder = venv.EnvBuilder(prompt='.')
201        cwd = os.path.basename(os.getcwd())
202        self.run_with_capture(builder.create, self.env_dir)
203        context = builder.ensure_directories(self.env_dir)
204        data = self.get_text_file_contents('pyvenv.cfg')
205        self.assertEqual(context.prompt, '(%s) ' % cwd)
206        self.assertIn("prompt = '%s'\n" % cwd, data)
207
208    def test_upgrade_dependencies(self):
209        builder = venv.EnvBuilder()
210        bin_path = 'Scripts' if sys.platform == 'win32' else 'bin'
211        python_exe = os.path.split(sys.executable)[1]
212        with tempfile.TemporaryDirectory() as fake_env_dir:
213            expect_exe = os.path.normcase(
214                os.path.join(fake_env_dir, bin_path, python_exe)
215            )
216            if sys.platform == 'win32':
217                expect_exe = os.path.normcase(os.path.realpath(expect_exe))
218
219            def pip_cmd_checker(cmd, **kwargs):
220                cmd[0] = os.path.normcase(cmd[0])
221                self.assertEqual(
222                    cmd,
223                    [
224                        expect_exe,
225                        '-m',
226                        'pip',
227                        'install',
228                        '--upgrade',
229                        'pip',
230                        'setuptools'
231                    ]
232                )
233
234            fake_context = builder.ensure_directories(fake_env_dir)
235            with patch('venv.subprocess.check_output', pip_cmd_checker):
236                builder.upgrade_dependencies(fake_context)
237
238    @requireVenvCreate
239    def test_prefixes(self):
240        """
241        Test that the prefix values are as expected.
242        """
243        # check a venv's prefixes
244        rmtree(self.env_dir)
245        self.run_with_capture(venv.create, self.env_dir)
246        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
247        cmd = [envpy, '-c', None]
248        for prefix, expected in (
249            ('prefix', self.env_dir),
250            ('exec_prefix', self.env_dir),
251            ('base_prefix', sys.base_prefix),
252            ('base_exec_prefix', sys.base_exec_prefix)):
253            cmd[2] = 'import sys; print(sys.%s)' % prefix
254            out, err = check_output(cmd)
255            self.assertEqual(out.strip(), expected.encode(), prefix)
256
257    @requireVenvCreate
258    def test_sysconfig(self):
259        """
260        Test that the sysconfig functions work in a virtual environment.
261        """
262        rmtree(self.env_dir)
263        self.run_with_capture(venv.create, self.env_dir, symlinks=False)
264        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
265        cmd = [envpy, '-c', None]
266        for call, expected in (
267            # installation scheme
268            ('get_preferred_scheme("prefix")', 'venv'),
269            ('get_default_scheme()', 'venv'),
270            # build environment
271            ('is_python_build()', str(sysconfig.is_python_build())),
272            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
273            ('get_config_h_filename()', sysconfig.get_config_h_filename())):
274            with self.subTest(call):
275                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
276                out, err = check_output(cmd)
277                self.assertEqual(out.strip(), expected.encode(), err)
278
279    @requireVenvCreate
280    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
281    def test_sysconfig_symlinks(self):
282        """
283        Test that the sysconfig functions work in a virtual environment.
284        """
285        rmtree(self.env_dir)
286        self.run_with_capture(venv.create, self.env_dir, symlinks=True)
287        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
288        cmd = [envpy, '-c', None]
289        for call, expected in (
290            # installation scheme
291            ('get_preferred_scheme("prefix")', 'venv'),
292            ('get_default_scheme()', 'venv'),
293            # build environment
294            ('is_python_build()', str(sysconfig.is_python_build())),
295            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
296            ('get_config_h_filename()', sysconfig.get_config_h_filename())):
297            with self.subTest(call):
298                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
299                out, err = check_output(cmd)
300                self.assertEqual(out.strip(), expected.encode(), err)
301
302    if sys.platform == 'win32':
303        ENV_SUBDIRS = (
304            ('Scripts',),
305            ('Include',),
306            ('Lib',),
307            ('Lib', 'site-packages'),
308        )
309    else:
310        ENV_SUBDIRS = (
311            ('bin',),
312            ('include',),
313            ('lib',),
314            ('lib', 'python%d.%d' % sys.version_info[:2]),
315            ('lib', 'python%d.%d' % sys.version_info[:2], 'site-packages'),
316        )
317
318    def create_contents(self, paths, filename):
319        """
320        Create some files in the environment which are unrelated
321        to the virtual environment.
322        """
323        for subdirs in paths:
324            d = os.path.join(self.env_dir, *subdirs)
325            os.mkdir(d)
326            fn = os.path.join(d, filename)
327            with open(fn, 'wb') as f:
328                f.write(b'Still here?')
329
330    def test_overwrite_existing(self):
331        """
332        Test creating environment in an existing directory.
333        """
334        self.create_contents(self.ENV_SUBDIRS, 'foo')
335        venv.create(self.env_dir)
336        for subdirs in self.ENV_SUBDIRS:
337            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
338            self.assertTrue(os.path.exists(fn))
339            with open(fn, 'rb') as f:
340                self.assertEqual(f.read(), b'Still here?')
341
342        builder = venv.EnvBuilder(clear=True)
343        builder.create(self.env_dir)
344        for subdirs in self.ENV_SUBDIRS:
345            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
346            self.assertFalse(os.path.exists(fn))
347
348    def clear_directory(self, path):
349        for fn in os.listdir(path):
350            fn = os.path.join(path, fn)
351            if os.path.islink(fn) or os.path.isfile(fn):
352                os.remove(fn)
353            elif os.path.isdir(fn):
354                rmtree(fn)
355
356    def test_unoverwritable_fails(self):
357        #create a file clashing with directories in the env dir
358        for paths in self.ENV_SUBDIRS[:3]:
359            fn = os.path.join(self.env_dir, *paths)
360            with open(fn, 'wb') as f:
361                f.write(b'')
362            self.assertRaises((ValueError, OSError), venv.create, self.env_dir)
363            self.clear_directory(self.env_dir)
364
365    def test_upgrade(self):
366        """
367        Test upgrading an existing environment directory.
368        """
369        # See Issue #21643: the loop needs to run twice to ensure
370        # that everything works on the upgrade (the first run just creates
371        # the venv).
372        for upgrade in (False, True):
373            builder = venv.EnvBuilder(upgrade=upgrade)
374            self.run_with_capture(builder.create, self.env_dir)
375            self.isdir(self.bindir)
376            self.isdir(self.include)
377            self.isdir(*self.lib)
378            fn = self.get_env_file(self.bindir, self.exe)
379            if not os.path.exists(fn):
380                # diagnostics for Windows buildbot failures
381                bd = self.get_env_file(self.bindir)
382                print('Contents of %r:' % bd)
383                print('    %r' % os.listdir(bd))
384            self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
385
386    def test_isolation(self):
387        """
388        Test isolation from system site-packages
389        """
390        for ssp, s in ((True, 'true'), (False, 'false')):
391            builder = venv.EnvBuilder(clear=True, system_site_packages=ssp)
392            builder.create(self.env_dir)
393            data = self.get_text_file_contents('pyvenv.cfg')
394            self.assertIn('include-system-site-packages = %s\n' % s, data)
395
396    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
397    def test_symlinking(self):
398        """
399        Test symlinking works as expected
400        """
401        for usl in (False, True):
402            builder = venv.EnvBuilder(clear=True, symlinks=usl)
403            builder.create(self.env_dir)
404            fn = self.get_env_file(self.bindir, self.exe)
405            # Don't test when False, because e.g. 'python' is always
406            # symlinked to 'python3.3' in the env, even when symlinking in
407            # general isn't wanted.
408            if usl:
409                if self.cannot_link_exe:
410                    # Symlinking is skipped when our executable is already a
411                    # special app symlink
412                    self.assertFalse(os.path.islink(fn))
413                else:
414                    self.assertTrue(os.path.islink(fn))
415
416    # If a venv is created from a source build and that venv is used to
417    # run the test, the pyvenv.cfg in the venv created in the test will
418    # point to the venv being used to run the test, and we lose the link
419    # to the source build - so Python can't initialise properly.
420    @requireVenvCreate
421    def test_executable(self):
422        """
423        Test that the sys.executable value is as expected.
424        """
425        rmtree(self.env_dir)
426        self.run_with_capture(venv.create, self.env_dir)
427        envpy = os.path.join(os.path.realpath(self.env_dir),
428                             self.bindir, self.exe)
429        out, err = check_output([envpy, '-c',
430            'import sys; print(sys.executable)'])
431        self.assertEqual(out.strip(), envpy.encode())
432
433    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
434    def test_executable_symlinks(self):
435        """
436        Test that the sys.executable value is as expected.
437        """
438        rmtree(self.env_dir)
439        builder = venv.EnvBuilder(clear=True, symlinks=True)
440        builder.create(self.env_dir)
441        envpy = os.path.join(os.path.realpath(self.env_dir),
442                             self.bindir, self.exe)
443        out, err = check_output([envpy, '-c',
444            'import sys; print(sys.executable)'])
445        self.assertEqual(out.strip(), envpy.encode())
446
447    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
448    def test_unicode_in_batch_file(self):
449        """
450        Test handling of Unicode paths
451        """
452        rmtree(self.env_dir)
453        env_dir = os.path.join(os.path.realpath(self.env_dir), 'ϼўТλФЙ')
454        builder = venv.EnvBuilder(clear=True)
455        builder.create(env_dir)
456        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
457        envpy = os.path.join(env_dir, self.bindir, self.exe)
458        out, err = check_output(
459            [activate, '&', self.exe, '-c', 'print(0)'],
460            encoding='oem',
461        )
462        self.assertEqual(out.strip(), '0')
463
464    @requireVenvCreate
465    def test_multiprocessing(self):
466        """
467        Test that the multiprocessing is able to spawn.
468        """
469        # bpo-36342: Instantiation of a Pool object imports the
470        # multiprocessing.synchronize module. Skip the test if this module
471        # cannot be imported.
472        skip_if_broken_multiprocessing_synchronize()
473
474        rmtree(self.env_dir)
475        self.run_with_capture(venv.create, self.env_dir)
476        envpy = os.path.join(os.path.realpath(self.env_dir),
477                             self.bindir, self.exe)
478        out, err = check_output([envpy, '-c',
479            'from multiprocessing import Pool; '
480            'pool = Pool(1); '
481            'print(pool.apply_async("Python".lower).get(3)); '
482            'pool.terminate()'])
483        self.assertEqual(out.strip(), "python".encode())
484
485    @requireVenvCreate
486    def test_multiprocessing_recursion(self):
487        """
488        Test that the multiprocessing is able to spawn itself
489        """
490        skip_if_broken_multiprocessing_synchronize()
491
492        rmtree(self.env_dir)
493        self.run_with_capture(venv.create, self.env_dir)
494        envpy = os.path.join(os.path.realpath(self.env_dir),
495                             self.bindir, self.exe)
496        script = os.path.join(TEST_HOME_DIR, '_test_venv_multiprocessing.py')
497        subprocess.check_call([envpy, script])
498
499    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
500    def test_deactivate_with_strict_bash_opts(self):
501        bash = shutil.which("bash")
502        if bash is None:
503            self.skipTest("bash required for this test")
504        rmtree(self.env_dir)
505        builder = venv.EnvBuilder(clear=True)
506        builder.create(self.env_dir)
507        activate = os.path.join(self.env_dir, self.bindir, "activate")
508        test_script = os.path.join(self.env_dir, "test_strict.sh")
509        with open(test_script, "w") as f:
510            f.write("set -euo pipefail\n"
511                    f"source {activate}\n"
512                    "deactivate\n")
513        out, err = check_output([bash, test_script])
514        self.assertEqual(out, "".encode())
515        self.assertEqual(err, "".encode())
516
517
518    @unittest.skipUnless(sys.platform == 'darwin', 'only relevant on macOS')
519    def test_macos_env(self):
520        rmtree(self.env_dir)
521        builder = venv.EnvBuilder()
522        builder.create(self.env_dir)
523
524        envpy = os.path.join(os.path.realpath(self.env_dir),
525                             self.bindir, self.exe)
526        out, err = check_output([envpy, '-c',
527            'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
528        self.assertEqual(out.strip(), 'False'.encode())
529
530    def test_pathsep_error(self):
531        """
532        Test that venv creation fails when the target directory contains
533        the path separator.
534        """
535        rmtree(self.env_dir)
536        bad_itempath = self.env_dir + os.pathsep
537        self.assertRaises(ValueError, venv.create, bad_itempath)
538        self.assertRaises(ValueError, venv.create, pathlib.Path(bad_itempath))
539
540    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
541    @requireVenvCreate
542    def test_zippath_from_non_installed_posix(self):
543        """
544        Test that when create venv from non-installed python, the zip path
545        value is as expected.
546        """
547        rmtree(self.env_dir)
548        # First try to create a non-installed python. It's not a real full
549        # functional non-installed python, but enough for this test.
550        platlibdir = sys.platlibdir
551        non_installed_dir = os.path.realpath(tempfile.mkdtemp())
552        self.addCleanup(rmtree, non_installed_dir)
553        bindir = os.path.join(non_installed_dir, self.bindir)
554        os.mkdir(bindir)
555        shutil.copy2(sys.executable, bindir)
556        libdir = os.path.join(non_installed_dir, platlibdir, self.lib[1])
557        os.makedirs(libdir)
558        landmark = os.path.join(libdir, "os.py")
559        stdlib_zip = "python%d%d.zip" % sys.version_info[:2]
560        zip_landmark = os.path.join(non_installed_dir,
561                                    platlibdir,
562                                    stdlib_zip)
563        additional_pythonpath_for_non_installed = []
564        # Copy stdlib files to the non-installed python so venv can
565        # correctly calculate the prefix.
566        for eachpath in sys.path:
567            if eachpath.endswith(".zip"):
568                if os.path.isfile(eachpath):
569                    shutil.copyfile(
570                        eachpath,
571                        os.path.join(non_installed_dir, platlibdir))
572            elif os.path.isfile(os.path.join(eachpath, "os.py")):
573                for name in os.listdir(eachpath):
574                    if name == "site-packages":
575                        continue
576                    fn = os.path.join(eachpath, name)
577                    if os.path.isfile(fn):
578                        shutil.copy(fn, libdir)
579                    elif os.path.isdir(fn):
580                        shutil.copytree(fn, os.path.join(libdir, name))
581            else:
582                additional_pythonpath_for_non_installed.append(
583                    eachpath)
584        cmd = [os.path.join(non_installed_dir, self.bindir, self.exe),
585               "-m",
586               "venv",
587               "--without-pip",
588               self.env_dir]
589        # Our fake non-installed python is not fully functional because
590        # it cannot find the extensions. Set PYTHONPATH so it can run the
591        # venv module correctly.
592        pythonpath = os.pathsep.join(
593            additional_pythonpath_for_non_installed)
594        # For python built with shared enabled. We need to set
595        # LD_LIBRARY_PATH so the non-installed python can find and link
596        # libpython.so
597        ld_library_path = sysconfig.get_config_var("LIBDIR")
598        if not ld_library_path or sysconfig.is_python_build():
599            ld_library_path = os.path.abspath(os.path.dirname(sys.executable))
600        if sys.platform == 'darwin':
601            ld_library_path_env = "DYLD_LIBRARY_PATH"
602        else:
603            ld_library_path_env = "LD_LIBRARY_PATH"
604        subprocess.check_call(cmd,
605                              env={"PYTHONPATH": pythonpath,
606                                   ld_library_path_env: ld_library_path})
607        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
608        # Now check the venv created from the non-installed python has
609        # correct zip path in pythonpath.
610        cmd = [envpy, '-S', '-c', 'import sys; print(sys.path)']
611        out, err = check_output(cmd)
612        self.assertTrue(zip_landmark.encode() in out)
613
614@requireVenvCreate
615class EnsurePipTest(BaseTest):
616    """Test venv module installation of pip."""
617    def assert_pip_not_installed(self):
618        envpy = os.path.join(os.path.realpath(self.env_dir),
619                             self.bindir, self.exe)
620        out, err = check_output([envpy, '-c',
621            'try:\n import pip\nexcept ImportError:\n print("OK")'])
622        # We force everything to text, so unittest gives the detailed diff
623        # if we get unexpected results
624        err = err.decode("latin-1") # Force to text, prevent decoding errors
625        self.assertEqual(err, "")
626        out = out.decode("latin-1") # Force to text, prevent decoding errors
627        self.assertEqual(out.strip(), "OK")
628
629
630    def test_no_pip_by_default(self):
631        rmtree(self.env_dir)
632        self.run_with_capture(venv.create, self.env_dir)
633        self.assert_pip_not_installed()
634
635    def test_explicit_no_pip(self):
636        rmtree(self.env_dir)
637        self.run_with_capture(venv.create, self.env_dir, with_pip=False)
638        self.assert_pip_not_installed()
639
640    def test_devnull(self):
641        # Fix for issue #20053 uses os.devnull to force a config file to
642        # appear empty. However http://bugs.python.org/issue20541 means
643        # that doesn't currently work properly on Windows. Once that is
644        # fixed, the "win_location" part of test_with_pip should be restored
645        with open(os.devnull, "rb") as f:
646            self.assertEqual(f.read(), b"")
647
648        self.assertTrue(os.path.exists(os.devnull))
649
650    def do_test_with_pip(self, system_site_packages):
651        rmtree(self.env_dir)
652        with EnvironmentVarGuard() as envvars:
653            # pip's cross-version compatibility may trigger deprecation
654            # warnings in current versions of Python. Ensure related
655            # environment settings don't cause venv to fail.
656            envvars["PYTHONWARNINGS"] = "ignore"
657            # ensurepip is different enough from a normal pip invocation
658            # that we want to ensure it ignores the normal pip environment
659            # variable settings. We set PIP_NO_INSTALL here specifically
660            # to check that ensurepip (and hence venv) ignores it.
661            # See http://bugs.python.org/issue19734
662            envvars["PIP_NO_INSTALL"] = "1"
663            # Also check that we ignore the pip configuration file
664            # See http://bugs.python.org/issue20053
665            with tempfile.TemporaryDirectory() as home_dir:
666                envvars["HOME"] = home_dir
667                bad_config = "[global]\nno-install=1"
668                # Write to both config file names on all platforms to reduce
669                # cross-platform variation in test code behaviour
670                win_location = ("pip", "pip.ini")
671                posix_location = (".pip", "pip.conf")
672                # Skips win_location due to http://bugs.python.org/issue20541
673                for dirname, fname in (posix_location,):
674                    dirpath = os.path.join(home_dir, dirname)
675                    os.mkdir(dirpath)
676                    fpath = os.path.join(dirpath, fname)
677                    with open(fpath, 'w') as f:
678                        f.write(bad_config)
679
680                # Actually run the create command with all that unhelpful
681                # config in place to ensure we ignore it
682                with self.nicer_error():
683                    self.run_with_capture(venv.create, self.env_dir,
684                                          system_site_packages=system_site_packages,
685                                          with_pip=True)
686        # Ensure pip is available in the virtual environment
687        envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
688        # Ignore DeprecationWarning since pip code is not part of Python
689        out, err = check_output([envpy, '-W', 'ignore::DeprecationWarning',
690               '-W', 'ignore::ImportWarning', '-I',
691               '-m', 'pip', '--version'])
692        # We force everything to text, so unittest gives the detailed diff
693        # if we get unexpected results
694        err = err.decode("latin-1") # Force to text, prevent decoding errors
695        self.assertEqual(err, "")
696        out = out.decode("latin-1") # Force to text, prevent decoding errors
697        expected_version = "pip {}".format(ensurepip.version())
698        self.assertEqual(out[:len(expected_version)], expected_version)
699        env_dir = os.fsencode(self.env_dir).decode("latin-1")
700        self.assertIn(env_dir, out)
701
702        # http://bugs.python.org/issue19728
703        # Check the private uninstall command provided for the Windows
704        # installers works (at least in a virtual environment)
705        with EnvironmentVarGuard() as envvars:
706            with self.nicer_error():
707                # It seems ensurepip._uninstall calls subprocesses which do not
708                # inherit the interpreter settings.
709                envvars["PYTHONWARNINGS"] = "ignore"
710                out, err = check_output([envpy,
711                    '-W', 'ignore::DeprecationWarning',
712                    '-W', 'ignore::ImportWarning', '-I',
713                    '-m', 'ensurepip._uninstall'])
714        # We force everything to text, so unittest gives the detailed diff
715        # if we get unexpected results
716        err = err.decode("latin-1") # Force to text, prevent decoding errors
717        # Ignore the warning:
718        #   "The directory '$HOME/.cache/pip/http' or its parent directory
719        #    is not owned by the current user and the cache has been disabled.
720        #    Please check the permissions and owner of that directory. If
721        #    executing pip with sudo, you may want sudo's -H flag."
722        # where $HOME is replaced by the HOME environment variable.
723        err = re.sub("^(WARNING: )?The directory .* or its parent directory "
724                     "is not owned or is not writable by the current user.*$", "",
725                     err, flags=re.MULTILINE)
726        self.assertEqual(err.rstrip(), "")
727        # Being fairly specific regarding the expected behaviour for the
728        # initial bundling phase in Python 3.4. If the output changes in
729        # future pip versions, this test can likely be relaxed further.
730        out = out.decode("latin-1") # Force to text, prevent decoding errors
731        self.assertIn("Successfully uninstalled pip", out)
732        self.assertIn("Successfully uninstalled setuptools", out)
733        # Check pip is now gone from the virtual environment. This only
734        # applies in the system_site_packages=False case, because in the
735        # other case, pip may still be available in the system site-packages
736        if not system_site_packages:
737            self.assert_pip_not_installed()
738
739    @contextlib.contextmanager
740    def nicer_error(self):
741        """
742        Capture output from a failed subprocess for easier debugging.
743
744        The output this handler produces can be a little hard to read,
745        but at least it has all the details.
746        """
747        try:
748            yield
749        except subprocess.CalledProcessError as exc:
750            out = (exc.output or b'').decode(errors="replace")
751            err = (exc.stderr or b'').decode(errors="replace")
752            self.fail(
753                f"{exc}\n\n"
754                f"**Subprocess Output**\n{out}\n\n"
755                f"**Subprocess Error**\n{err}"
756            )
757
758    @requires_venv_with_pip()
759    def test_with_pip(self):
760        self.do_test_with_pip(False)
761        self.do_test_with_pip(True)
762
763
764if __name__ == "__main__":
765    unittest.main()
766