1# -*- coding: utf-8 -*-
2"""Easy install Tests
3"""
4from __future__ import absolute_import
5
6import sys
7import os
8import tempfile
9import site
10import contextlib
11import tarfile
12import logging
13import itertools
14import distutils.errors
15import io
16import zipfile
17import mock
18
19import time
20from setuptools.extern.six.moves import urllib
21
22import pytest
23
24from setuptools import sandbox
25from setuptools.sandbox import run_setup
26import setuptools.command.easy_install as ei
27from setuptools.command.easy_install import PthDistributions
28from setuptools.command import easy_install as easy_install_pkg
29from setuptools.dist import Distribution
30from pkg_resources import normalize_path, working_set
31from pkg_resources import Distribution as PRDistribution
32import setuptools.tests.server
33from setuptools.tests import fail_on_ascii
34import pkg_resources
35
36from . import contexts
37from .textwrap import DALS
38
39
40class FakeDist(object):
41    def get_entry_map(self, group):
42        if group != 'console_scripts':
43            return {}
44        return {'name': 'ep'}
45
46    def as_requirement(self):
47        return 'spec'
48
49
50SETUP_PY = DALS("""
51    from setuptools import setup
52
53    setup(name='foo')
54    """)
55
56
57class TestEasyInstallTest:
58    def test_install_site_py(self, tmpdir):
59        dist = Distribution()
60        cmd = ei.easy_install(dist)
61        cmd.sitepy_installed = False
62        cmd.install_dir = str(tmpdir)
63        cmd.install_site_py()
64        assert (tmpdir / 'site.py').exists()
65
66    def test_get_script_args(self):
67        header = ei.CommandSpec.best().from_environment().as_header()
68        expected = header + DALS(r"""
69            # EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name'
70            __requires__ = 'spec'
71            import re
72            import sys
73            from pkg_resources import load_entry_point
74
75            if __name__ == '__main__':
76                sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
77                sys.exit(
78                    load_entry_point('spec', 'console_scripts', 'name')()
79                )
80            """)
81        dist = FakeDist()
82
83        args = next(ei.ScriptWriter.get_args(dist))
84        name, script = itertools.islice(args, 2)
85
86        assert script == expected
87
88    def test_no_find_links(self):
89        # new option '--no-find-links', that blocks find-links added at
90        # the project level
91        dist = Distribution()
92        cmd = ei.easy_install(dist)
93        cmd.check_pth_processing = lambda: True
94        cmd.no_find_links = True
95        cmd.find_links = ['link1', 'link2']
96        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
97        cmd.args = ['ok']
98        cmd.ensure_finalized()
99        assert cmd.package_index.scanned_urls == {}
100
101        # let's try without it (default behavior)
102        cmd = ei.easy_install(dist)
103        cmd.check_pth_processing = lambda: True
104        cmd.find_links = ['link1', 'link2']
105        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
106        cmd.args = ['ok']
107        cmd.ensure_finalized()
108        keys = sorted(cmd.package_index.scanned_urls.keys())
109        assert keys == ['link1', 'link2']
110
111    def test_write_exception(self):
112        """
113        Test that `cant_write_to_target` is rendered as a DistutilsError.
114        """
115        dist = Distribution()
116        cmd = ei.easy_install(dist)
117        cmd.install_dir = os.getcwd()
118        with pytest.raises(distutils.errors.DistutilsError):
119            cmd.cant_write_to_target()
120
121    def test_all_site_dirs(self, monkeypatch):
122        """
123        get_site_dirs should always return site dirs reported by
124        site.getsitepackages.
125        """
126        path = normalize_path('/setuptools/test/site-packages')
127        mock_gsp = lambda: [path]
128        monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
129        assert path in ei.get_site_dirs()
130
131    def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
132        monkeypatch.delattr(site, 'getsitepackages', raising=False)
133        assert ei.get_site_dirs()
134
135    @pytest.fixture
136    def sdist_unicode(self, tmpdir):
137        files = [
138            (
139                'setup.py',
140                DALS("""
141                    import setuptools
142                    setuptools.setup(
143                        name="setuptools-test-unicode",
144                        version="1.0",
145                        packages=["mypkg"],
146                        include_package_data=True,
147                    )
148                    """),
149            ),
150            (
151                'mypkg/__init__.py',
152                "",
153            ),
154            (
155                u'mypkg/\u2603.txt',
156                "",
157            ),
158        ]
159        sdist_name = 'setuptools-test-unicode-1.0.zip'
160        sdist = tmpdir / sdist_name
161        # can't use make_sdist, because the issue only occurs
162        #  with zip sdists.
163        sdist_zip = zipfile.ZipFile(str(sdist), 'w')
164        for filename, content in files:
165            sdist_zip.writestr(filename, content)
166        sdist_zip.close()
167        return str(sdist)
168
169    @fail_on_ascii
170    def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch):
171        """
172        The install command should execute correctly even if
173        the package has unicode filenames.
174        """
175        dist = Distribution({'script_args': ['easy_install']})
176        target = (tmpdir / 'target').ensure_dir()
177        cmd = ei.easy_install(
178            dist,
179            install_dir=str(target),
180            args=['x'],
181        )
182        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
183        cmd.ensure_finalized()
184        cmd.easy_install(sdist_unicode)
185
186    @pytest.fixture
187    def sdist_script(self, tmpdir):
188        files = [
189            (
190                'setup.py',
191                DALS("""
192                    import setuptools
193                    setuptools.setup(
194                        name="setuptools-test-script",
195                        version="1.0",
196                        scripts=["mypkg_script"],
197                    )
198                    """),
199            ),
200            (
201                u'mypkg_script',
202                DALS("""
203                     #/usr/bin/python
204                     print('mypkg_script')
205                     """),
206            ),
207        ]
208        sdist_name = 'setuptools-test-script-1.0.zip'
209        sdist = str(tmpdir / sdist_name)
210        make_sdist(sdist, files)
211        return sdist
212
213    @pytest.mark.skipif(not sys.platform.startswith('linux'),
214                        reason="Test can only be run on Linux")
215    def test_script_install(self, sdist_script, tmpdir, monkeypatch):
216        """
217        Check scripts are installed.
218        """
219        dist = Distribution({'script_args': ['easy_install']})
220        target = (tmpdir / 'target').ensure_dir()
221        cmd = ei.easy_install(
222            dist,
223            install_dir=str(target),
224            args=['x'],
225        )
226        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
227        cmd.ensure_finalized()
228        cmd.easy_install(sdist_script)
229        assert (target / 'mypkg_script').exists()
230
231
232class TestPTHFileWriter:
233    def test_add_from_cwd_site_sets_dirty(self):
234        '''a pth file manager should set dirty
235        if a distribution is in site but also the cwd
236        '''
237        pth = PthDistributions('does-not_exist', [os.getcwd()])
238        assert not pth.dirty
239        pth.add(PRDistribution(os.getcwd()))
240        assert pth.dirty
241
242    def test_add_from_site_is_ignored(self):
243        location = '/test/location/does-not-have-to-exist'
244        # PthDistributions expects all locations to be normalized
245        location = pkg_resources.normalize_path(location)
246        pth = PthDistributions('does-not_exist', [location, ])
247        assert not pth.dirty
248        pth.add(PRDistribution(location))
249        assert not pth.dirty
250
251
252@pytest.yield_fixture
253def setup_context(tmpdir):
254    with (tmpdir / 'setup.py').open('w') as f:
255        f.write(SETUP_PY)
256    with tmpdir.as_cwd():
257        yield tmpdir
258
259
260@pytest.mark.usefixtures("user_override")
261@pytest.mark.usefixtures("setup_context")
262class TestUserInstallTest:
263
264    # prevent check that site-packages is writable. easy_install
265    # shouldn't be writing to system site-packages during finalize
266    # options, but while it does, bypass the behavior.
267    prev_sp_write = mock.patch(
268        'setuptools.command.easy_install.easy_install.check_site_dir',
269        mock.Mock(),
270    )
271
272    # simulate setuptools installed in user site packages
273    @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
274    @mock.patch('site.ENABLE_USER_SITE', True)
275    @prev_sp_write
276    def test_user_install_not_implied_user_site_enabled(self):
277        self.assert_not_user_site()
278
279    @mock.patch('site.ENABLE_USER_SITE', False)
280    @prev_sp_write
281    def test_user_install_not_implied_user_site_disabled(self):
282        self.assert_not_user_site()
283
284    @staticmethod
285    def assert_not_user_site():
286        # create a finalized easy_install command
287        dist = Distribution()
288        dist.script_name = 'setup.py'
289        cmd = ei.easy_install(dist)
290        cmd.args = ['py']
291        cmd.ensure_finalized()
292        assert not cmd.user, 'user should not be implied'
293
294    def test_multiproc_atexit(self):
295        pytest.importorskip('multiprocessing')
296
297        log = logging.getLogger('test_easy_install')
298        logging.basicConfig(level=logging.INFO, stream=sys.stderr)
299        log.info('this should not break')
300
301    @pytest.fixture()
302    def foo_package(self, tmpdir):
303        egg_file = tmpdir / 'foo-1.0.egg-info'
304        with egg_file.open('w') as f:
305            f.write('Name: foo\n')
306        return str(tmpdir)
307
308    @pytest.yield_fixture()
309    def install_target(self, tmpdir):
310        target = str(tmpdir)
311        with mock.patch('sys.path', sys.path + [target]):
312            python_path = os.path.pathsep.join(sys.path)
313            with mock.patch.dict(os.environ, PYTHONPATH=python_path):
314                yield target
315
316    def test_local_index(self, foo_package, install_target):
317        """
318        The local index must be used when easy_install locates installed
319        packages.
320        """
321        dist = Distribution()
322        dist.script_name = 'setup.py'
323        cmd = ei.easy_install(dist)
324        cmd.install_dir = install_target
325        cmd.args = ['foo']
326        cmd.ensure_finalized()
327        cmd.local_index.scan([foo_package])
328        res = cmd.easy_install('foo')
329        actual = os.path.normcase(os.path.realpath(res.location))
330        expected = os.path.normcase(os.path.realpath(foo_package))
331        assert actual == expected
332
333    @contextlib.contextmanager
334    def user_install_setup_context(self, *args, **kwargs):
335        """
336        Wrap sandbox.setup_context to patch easy_install in that context to
337        appear as user-installed.
338        """
339        with self.orig_context(*args, **kwargs):
340            import setuptools.command.easy_install as ei
341            ei.__file__ = site.USER_SITE
342            yield
343
344    def patched_setup_context(self):
345        self.orig_context = sandbox.setup_context
346
347        return mock.patch(
348            'setuptools.sandbox.setup_context',
349            self.user_install_setup_context,
350        )
351
352
353@pytest.yield_fixture
354def distutils_package():
355    distutils_setup_py = SETUP_PY.replace(
356        'from setuptools import setup',
357        'from distutils.core import setup',
358    )
359    with contexts.tempdir(cd=os.chdir):
360        with open('setup.py', 'w') as f:
361            f.write(distutils_setup_py)
362        yield
363
364
365class TestDistutilsPackage:
366    def test_bdist_egg_available_on_distutils_pkg(self, distutils_package):
367        run_setup('setup.py', ['bdist_egg'])
368
369
370class TestSetupRequires:
371    def test_setup_requires_honors_fetch_params(self):
372        """
373        When easy_install installs a source distribution which specifies
374        setup_requires, it should honor the fetch parameters (such as
375        allow-hosts, index-url, and find-links).
376        """
377        # set up a server which will simulate an alternate package index.
378        p_index = setuptools.tests.server.MockServer()
379        p_index.start()
380        netloc = 1
381        p_index_loc = urllib.parse.urlparse(p_index.url)[netloc]
382        if p_index_loc.endswith(':0'):
383            # Some platforms (Jython) don't find a port to which to bind,
384            #  so skip this test for them.
385            return
386        with contexts.quiet():
387            # create an sdist that has a build-time dependency.
388            with TestSetupRequires.create_sdist() as dist_file:
389                with contexts.tempdir() as temp_install_dir:
390                    with contexts.environment(PYTHONPATH=temp_install_dir):
391                        ei_params = [
392                            '--index-url', p_index.url,
393                            '--allow-hosts', p_index_loc,
394                            '--exclude-scripts',
395                            '--install-dir', temp_install_dir,
396                            dist_file,
397                        ]
398                        with sandbox.save_argv(['easy_install']):
399                            # attempt to install the dist. It should fail because
400                            #  it doesn't exist.
401                            with pytest.raises(SystemExit):
402                                easy_install_pkg.main(ei_params)
403        # there should have been two or three requests to the server
404        #  (three happens on Python 3.3a)
405        assert 2 <= len(p_index.requests) <= 3
406        assert p_index.requests[0].path == '/does-not-exist/'
407
408    @staticmethod
409    @contextlib.contextmanager
410    def create_sdist():
411        """
412        Return an sdist with a setup_requires dependency (of something that
413        doesn't exist)
414        """
415        with contexts.tempdir() as dir:
416            dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
417            make_sdist(dist_path, [
418                ('setup.py', DALS("""
419                    import setuptools
420                    setuptools.setup(
421                        name="setuptools-test-fetcher",
422                        version="1.0",
423                        setup_requires = ['does-not-exist'],
424                    )
425                """))])
426            yield dist_path
427
428    use_setup_cfg = (
429        (),
430        ('dependency_links',),
431        ('setup_requires',),
432        ('dependency_links', 'setup_requires'),
433    )
434
435    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
436    def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
437        """
438        Regression test for distribution issue 323:
439        https://bitbucket.org/tarek/distribute/issues/323
440
441        Ensures that a distribution's setup_requires requirements can still be
442        installed and used locally even if a conflicting version of that
443        requirement is already on the path.
444        """
445
446        fake_dist = PRDistribution('does-not-matter', project_name='foobar',
447                                   version='0.0')
448        working_set.add(fake_dist)
449
450        with contexts.save_pkg_resources_state():
451            with contexts.tempdir() as temp_dir:
452                test_pkg = create_setup_requires_package(temp_dir, use_setup_cfg=use_setup_cfg)
453                test_setup_py = os.path.join(test_pkg, 'setup.py')
454                with contexts.quiet() as (stdout, stderr):
455                    # Don't even need to install the package, just
456                    # running the setup.py at all is sufficient
457                    run_setup(test_setup_py, ['--name'])
458
459                lines = stdout.readlines()
460                assert len(lines) > 0
461                assert lines[-1].strip() == 'test_pkg'
462
463    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
464    def test_setup_requires_override_nspkg(self, use_setup_cfg):
465        """
466        Like ``test_setup_requires_overrides_version_conflict`` but where the
467        ``setup_requires`` package is part of a namespace package that has
468        *already* been imported.
469        """
470
471        with contexts.save_pkg_resources_state():
472            with contexts.tempdir() as temp_dir:
473                foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz')
474                make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
475                # Now actually go ahead an extract to the temp dir and add the
476                # extracted path to sys.path so foo.bar v0.1 is importable
477                foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1')
478                os.mkdir(foobar_1_dir)
479                with tarfile.open(foobar_1_archive) as tf:
480                    tf.extractall(foobar_1_dir)
481                sys.path.insert(1, foobar_1_dir)
482
483                dist = PRDistribution(foobar_1_dir, project_name='foo.bar',
484                                      version='0.1')
485                working_set.add(dist)
486
487                template = DALS("""\
488                    import foo  # Even with foo imported first the
489                                # setup_requires package should override
490                    import setuptools
491                    setuptools.setup(**%r)
492
493                    if not (hasattr(foo, '__path__') and
494                            len(foo.__path__) == 2):
495                        print('FAIL')
496
497                    if 'foo.bar-0.2' not in foo.__path__[0]:
498                        print('FAIL')
499                """)
500
501                test_pkg = create_setup_requires_package(
502                    temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template,
503                    use_setup_cfg=use_setup_cfg)
504
505                test_setup_py = os.path.join(test_pkg, 'setup.py')
506
507                with contexts.quiet() as (stdout, stderr):
508                    try:
509                        # Don't even need to install the package, just
510                        # running the setup.py at all is sufficient
511                        run_setup(test_setup_py, ['--name'])
512                    except pkg_resources.VersionConflict:
513                        self.fail('Installing setup.py requirements '
514                            'caused a VersionConflict')
515
516                assert 'FAIL' not in stdout.getvalue()
517                lines = stdout.readlines()
518                assert len(lines) > 0
519                assert lines[-1].strip() == 'test_pkg'
520
521    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
522    def test_setup_requires_with_attr_version(self, use_setup_cfg):
523        def make_dependency_sdist(dist_path, distname, version):
524            make_sdist(dist_path, [
525                ('setup.py',
526                 DALS("""
527                      import setuptools
528                      setuptools.setup(
529                          name={name!r},
530                          version={version!r},
531                          py_modules=[{name!r}],
532                      )
533                      """.format(name=distname, version=version))),
534                (distname + '.py',
535                 DALS("""
536                      version = 42
537                      """
538                     ))])
539        with contexts.save_pkg_resources_state():
540            with contexts.tempdir() as temp_dir:
541                test_pkg = create_setup_requires_package(
542                    temp_dir, setup_attrs=dict(version='attr: foobar.version'),
543                    make_package=make_dependency_sdist,
544                    use_setup_cfg=use_setup_cfg+('version',),
545                )
546                test_setup_py = os.path.join(test_pkg, 'setup.py')
547                with contexts.quiet() as (stdout, stderr):
548                    run_setup(test_setup_py, ['--version'])
549                lines = stdout.readlines()
550                assert len(lines) > 0
551                assert lines[-1].strip() == '42'
552
553
554def make_trivial_sdist(dist_path, distname, version):
555    """
556    Create a simple sdist tarball at dist_path, containing just a simple
557    setup.py.
558    """
559
560    make_sdist(dist_path, [
561        ('setup.py',
562         DALS("""\
563             import setuptools
564             setuptools.setup(
565                 name=%r,
566                 version=%r
567             )
568         """ % (distname, version)))])
569
570
571def make_nspkg_sdist(dist_path, distname, version):
572    """
573    Make an sdist tarball with distname and version which also contains one
574    package with the same name as distname.  The top-level package is
575    designated a namespace package).
576    """
577
578    parts = distname.split('.')
579    nspackage = parts[0]
580
581    packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
582
583    setup_py = DALS("""\
584        import setuptools
585        setuptools.setup(
586            name=%r,
587            version=%r,
588            packages=%r,
589            namespace_packages=[%r]
590        )
591    """ % (distname, version, packages, nspackage))
592
593    init = "__import__('pkg_resources').declare_namespace(__name__)"
594
595    files = [('setup.py', setup_py),
596             (os.path.join(nspackage, '__init__.py'), init)]
597    for package in packages[1:]:
598        filename = os.path.join(*(package.split('.') + ['__init__.py']))
599        files.append((filename, ''))
600
601    make_sdist(dist_path, files)
602
603
604def make_sdist(dist_path, files):
605    """
606    Create a simple sdist tarball at dist_path, containing the files
607    listed in ``files`` as ``(filename, content)`` tuples.
608    """
609
610    with tarfile.open(dist_path, 'w:gz') as dist:
611        for filename, content in files:
612            file_bytes = io.BytesIO(content.encode('utf-8'))
613            file_info = tarfile.TarInfo(name=filename)
614            file_info.size = len(file_bytes.getvalue())
615            file_info.mtime = int(time.time())
616            dist.addfile(file_info, fileobj=file_bytes)
617
618
619def create_setup_requires_package(path, distname='foobar', version='0.1',
620                                  make_package=make_trivial_sdist,
621                                  setup_py_template=None, setup_attrs={},
622                                  use_setup_cfg=()):
623    """Creates a source tree under path for a trivial test package that has a
624    single requirement in setup_requires--a tarball for that requirement is
625    also created and added to the dependency_links argument.
626
627    ``distname`` and ``version`` refer to the name/version of the package that
628    the test package requires via ``setup_requires``.  The name of the test
629    package itself is just 'test_pkg'.
630    """
631
632    test_setup_attrs = {
633        'name': 'test_pkg', 'version': '0.0',
634        'setup_requires': ['%s==%s' % (distname, version)],
635        'dependency_links': [os.path.abspath(path)]
636    }
637    test_setup_attrs.update(setup_attrs)
638
639    test_pkg = os.path.join(path, 'test_pkg')
640    os.mkdir(test_pkg)
641
642    if use_setup_cfg:
643        test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
644        options = []
645        metadata = []
646        for name in use_setup_cfg:
647            value = test_setup_attrs.pop(name)
648            if name in 'name version'.split():
649                section = metadata
650            else:
651                section = options
652            if isinstance(value, (tuple, list)):
653                value = ';'.join(value)
654            section.append('%s: %s' % (name, value))
655        with open(test_setup_cfg, 'w') as f:
656            f.write(DALS(
657                """
658                [metadata]
659                {metadata}
660                [options]
661                {options}
662                """
663            ).format(
664                options='\n'.join(options),
665                metadata='\n'.join(metadata),
666            ))
667
668    test_setup_py = os.path.join(test_pkg, 'setup.py')
669
670    if setup_py_template is None:
671        setup_py_template = DALS("""\
672            import setuptools
673            setuptools.setup(**%r)
674        """)
675
676    with open(test_setup_py, 'w') as f:
677        f.write(setup_py_template % test_setup_attrs)
678
679    foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version))
680    make_package(foobar_path, distname, version)
681
682    return test_pkg
683
684
685@pytest.mark.skipif(
686    sys.platform.startswith('java') and ei.is_sh(sys.executable),
687    reason="Test cannot run under java when executable is sh"
688)
689class TestScriptHeader:
690    non_ascii_exe = '/Users/José/bin/python'
691    exe_with_spaces = r'C:\Program Files\Python33\python.exe'
692
693    def test_get_script_header(self):
694        expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
695        actual = ei.ScriptWriter.get_script_header('#!/usr/local/bin/python')
696        assert actual == expected
697
698    def test_get_script_header_args(self):
699        expected = '#!%s -x\n' % ei.nt_quote_arg(os.path.normpath
700            (sys.executable))
701        actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python -x')
702        assert actual == expected
703
704    def test_get_script_header_non_ascii_exe(self):
705        actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python',
706            executable=self.non_ascii_exe)
707        expected = '#!%s -x\n' % self.non_ascii_exe
708        assert actual == expected
709
710    def test_get_script_header_exe_with_spaces(self):
711        actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python',
712            executable='"' + self.exe_with_spaces + '"')
713        expected = '#!"%s"\n' % self.exe_with_spaces
714        assert actual == expected
715
716
717class TestCommandSpec:
718    def test_custom_launch_command(self):
719        """
720        Show how a custom CommandSpec could be used to specify a #! executable
721        which takes parameters.
722        """
723        cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
724        assert cmd.as_header() == '#!/usr/bin/env python3\n'
725
726    def test_from_param_for_CommandSpec_is_passthrough(self):
727        """
728        from_param should return an instance of a CommandSpec
729        """
730        cmd = ei.CommandSpec(['python'])
731        cmd_new = ei.CommandSpec.from_param(cmd)
732        assert cmd is cmd_new
733
734    @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
735    @mock.patch.dict(os.environ)
736    def test_from_environment_with_spaces_in_executable(self):
737        os.environ.pop('__PYVENV_LAUNCHER__', None)
738        cmd = ei.CommandSpec.from_environment()
739        assert len(cmd) == 1
740        assert cmd.as_header().startswith('#!"')
741
742    def test_from_simple_string_uses_shlex(self):
743        """
744        In order to support `executable = /usr/bin/env my-python`, make sure
745        from_param invokes shlex on that input.
746        """
747        cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
748        assert len(cmd) == 2
749        assert '"' not in cmd.as_header()
750
751
752class TestWindowsScriptWriter:
753    def test_header(self):
754        hdr = ei.WindowsScriptWriter.get_script_header('')
755        assert hdr.startswith('#!')
756        assert hdr.endswith('\n')
757        hdr = hdr.lstrip('#!')
758        hdr = hdr.rstrip('\n')
759        # header should not start with an escaped quote
760        assert not hdr.startswith('\\"')
761