"""Easy install Tests
"""

import sys
import os
import tempfile
import site
import contextlib
import tarfile
import logging
import itertools
import distutils.errors
import io
import zipfile
import mock
import time
import re

import pytest

from setuptools import sandbox
from setuptools.sandbox import run_setup
import setuptools.command.easy_install as ei
from setuptools.command.easy_install import (
    EasyInstallDeprecationWarning, ScriptWriter, PthDistributions,
    WindowsScriptWriter,
)
from setuptools.command import easy_install as easy_install_pkg
from setuptools.dist import Distribution
from pkg_resources import normalize_path, working_set
from pkg_resources import Distribution as PRDistribution
from setuptools.tests.server import MockServer, path_to_url
from setuptools.tests import fail_on_ascii
import pkg_resources

from . import contexts
from .files import build_files
from .textwrap import DALS


class FakeDist:
    def get_entry_map(self, group):
        if group != 'console_scripts':
            return {}
        return {str('name'): 'ep'}

    def as_requirement(self):
        return 'spec'


SETUP_PY = DALS("""
    from setuptools import setup

    setup(name='foo')
    """)


class TestEasyInstallTest:
    def test_get_script_args(self):
        header = ei.CommandSpec.best().from_environment().as_header()
        dist = FakeDist()
        args = next(ei.ScriptWriter.get_args(dist))
        name, script = itertools.islice(args, 2)
        assert script.startswith(header)
        assert "'spec'" in script
        assert "'console_scripts'" in script
        assert "'name'" in script
        assert re.search(
            '^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE)

    def test_no_find_links(self):
        # new option '--no-find-links', that blocks find-links added at
        # the project level
        dist = Distribution()
        cmd = ei.easy_install(dist)
        cmd.check_pth_processing = lambda: True
        cmd.no_find_links = True
        cmd.find_links = ['link1', 'link2']
        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
        cmd.args = ['ok']
        cmd.ensure_finalized()
        assert cmd.package_index.scanned_urls == {}

        # let's try without it (default behavior)
        cmd = ei.easy_install(dist)
        cmd.check_pth_processing = lambda: True
        cmd.find_links = ['link1', 'link2']
        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
        cmd.args = ['ok']
        cmd.ensure_finalized()
        keys = sorted(cmd.package_index.scanned_urls.keys())
        assert keys == ['link1', 'link2']

    def test_write_exception(self):
        """
        Test that `cant_write_to_target` is rendered as a DistutilsError.
        """
        dist = Distribution()
        cmd = ei.easy_install(dist)
        cmd.install_dir = os.getcwd()
        with pytest.raises(distutils.errors.DistutilsError):
            cmd.cant_write_to_target()

    def test_all_site_dirs(self, monkeypatch):
        """
        get_site_dirs should always return site dirs reported by
        site.getsitepackages.
        """
        path = normalize_path('/setuptools/test/site-packages')

        def mock_gsp():
            return [path]
        monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
        assert path in ei.get_site_dirs()

    def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
        monkeypatch.delattr(site, 'getsitepackages', raising=False)
        assert ei.get_site_dirs()

    @pytest.fixture
    def sdist_unicode(self, tmpdir):
        files = [
            (
                'setup.py',
                DALS("""
                    import setuptools
                    setuptools.setup(
                        name="setuptools-test-unicode",
                        version="1.0",
                        packages=["mypkg"],
                        include_package_data=True,
                    )
                    """),
            ),
            (
                'mypkg/__init__.py',
                "",
            ),
            (
                'mypkg/☃.txt',
                "",
            ),
        ]
        sdist_name = 'setuptools-test-unicode-1.0.zip'
        sdist = tmpdir / sdist_name
        # can't use make_sdist, because the issue only occurs
        #  with zip sdists.
        sdist_zip = zipfile.ZipFile(str(sdist), 'w')
        for filename, content in files:
            sdist_zip.writestr(filename, content)
        sdist_zip.close()
        return str(sdist)

    @fail_on_ascii
    def test_unicode_filename_in_sdist(
            self, sdist_unicode, tmpdir, monkeypatch):
        """
        The install command should execute correctly even if
        the package has unicode filenames.
        """
        dist = Distribution({'script_args': ['easy_install']})
        target = (tmpdir / 'target').ensure_dir()
        cmd = ei.easy_install(
            dist,
            install_dir=str(target),
            args=['x'],
        )
        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
        cmd.ensure_finalized()
        cmd.easy_install(sdist_unicode)

    @pytest.fixture
    def sdist_unicode_in_script(self, tmpdir):
        files = [
            (
                "setup.py",
                DALS("""
                    import setuptools
                    setuptools.setup(
                        name="setuptools-test-unicode",
                        version="1.0",
                        packages=["mypkg"],
                        include_package_data=True,
                        scripts=['mypkg/unicode_in_script'],
                    )
                    """),
            ),
            ("mypkg/__init__.py", ""),
            (
                "mypkg/unicode_in_script",
                DALS(
                    """
                    #!/bin/sh
                    # á

                    non_python_fn() {
                    }
                """),
            ),
        ]
        sdist_name = "setuptools-test-unicode-script-1.0.zip"
        sdist = tmpdir / sdist_name
        # can't use make_sdist, because the issue only occurs
        #  with zip sdists.
        sdist_zip = zipfile.ZipFile(str(sdist), "w")
        for filename, content in files:
            sdist_zip.writestr(filename, content.encode('utf-8'))
        sdist_zip.close()
        return str(sdist)

    @fail_on_ascii
    def test_unicode_content_in_sdist(
            self, sdist_unicode_in_script, tmpdir, monkeypatch):
        """
        The install command should execute correctly even if
        the package has unicode in scripts.
        """
        dist = Distribution({"script_args": ["easy_install"]})
        target = (tmpdir / "target").ensure_dir()
        cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
        monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
        cmd.ensure_finalized()
        cmd.easy_install(sdist_unicode_in_script)

    @pytest.fixture
    def sdist_script(self, tmpdir):
        files = [
            (
                'setup.py',
                DALS("""
                    import setuptools
                    setuptools.setup(
                        name="setuptools-test-script",
                        version="1.0",
                        scripts=["mypkg_script"],
                    )
                    """),
            ),
            (
                'mypkg_script',
                DALS("""
                     #/usr/bin/python
                     print('mypkg_script')
                     """),
            ),
        ]
        sdist_name = 'setuptools-test-script-1.0.zip'
        sdist = str(tmpdir / sdist_name)
        make_sdist(sdist, files)
        return sdist

    @pytest.mark.skipif(not sys.platform.startswith('linux'),
                        reason="Test can only be run on Linux")
    def test_script_install(self, sdist_script, tmpdir, monkeypatch):
        """
        Check scripts are installed.
        """
        dist = Distribution({'script_args': ['easy_install']})
        target = (tmpdir / 'target').ensure_dir()
        cmd = ei.easy_install(
            dist,
            install_dir=str(target),
            args=['x'],
        )
        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
        cmd.ensure_finalized()
        cmd.easy_install(sdist_script)
        assert (target / 'mypkg_script').exists()

    def test_dist_get_script_args_deprecated(self):
        with pytest.warns(EasyInstallDeprecationWarning):
            ScriptWriter.get_script_args(None, None)

    def test_dist_get_script_header_deprecated(self):
        with pytest.warns(EasyInstallDeprecationWarning):
            ScriptWriter.get_script_header("")

    def test_dist_get_writer_deprecated(self):
        with pytest.warns(EasyInstallDeprecationWarning):
            ScriptWriter.get_writer(None)

    def test_dist_WindowsScriptWriter_get_writer_deprecated(self):
        with pytest.warns(EasyInstallDeprecationWarning):
            WindowsScriptWriter.get_writer()


@pytest.mark.filterwarnings('ignore:Unbuilt egg')
class TestPTHFileWriter:
    def test_add_from_cwd_site_sets_dirty(self):
        '''a pth file manager should set dirty
        if a distribution is in site but also the cwd
        '''
        pth = PthDistributions('does-not_exist', [os.getcwd()])
        assert not pth.dirty
        pth.add(PRDistribution(os.getcwd()))
        assert pth.dirty

    def test_add_from_site_is_ignored(self):
        location = '/test/location/does-not-have-to-exist'
        # PthDistributions expects all locations to be normalized
        location = pkg_resources.normalize_path(location)
        pth = PthDistributions('does-not_exist', [location, ])
        assert not pth.dirty
        pth.add(PRDistribution(location))
        assert not pth.dirty


@pytest.yield_fixture
def setup_context(tmpdir):
    with (tmpdir / 'setup.py').open('w') as f:
        f.write(SETUP_PY)
    with tmpdir.as_cwd():
        yield tmpdir


@pytest.mark.usefixtures("user_override")
@pytest.mark.usefixtures("setup_context")
class TestUserInstallTest:

    # prevent check that site-packages is writable. easy_install
    # shouldn't be writing to system site-packages during finalize
    # options, but while it does, bypass the behavior.
    prev_sp_write = mock.patch(
        'setuptools.command.easy_install.easy_install.check_site_dir',
        mock.Mock(),
    )

    # simulate setuptools installed in user site packages
    @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
    @mock.patch('site.ENABLE_USER_SITE', True)
    @prev_sp_write
    def test_user_install_not_implied_user_site_enabled(self):
        self.assert_not_user_site()

    @mock.patch('site.ENABLE_USER_SITE', False)
    @prev_sp_write
    def test_user_install_not_implied_user_site_disabled(self):
        self.assert_not_user_site()

    @staticmethod
    def assert_not_user_site():
        # create a finalized easy_install command
        dist = Distribution()
        dist.script_name = 'setup.py'
        cmd = ei.easy_install(dist)
        cmd.args = ['py']
        cmd.ensure_finalized()
        assert not cmd.user, 'user should not be implied'

    def test_multiproc_atexit(self):
        pytest.importorskip('multiprocessing')

        log = logging.getLogger('test_easy_install')
        logging.basicConfig(level=logging.INFO, stream=sys.stderr)
        log.info('this should not break')

    @pytest.fixture()
    def foo_package(self, tmpdir):
        egg_file = tmpdir / 'foo-1.0.egg-info'
        with egg_file.open('w') as f:
            f.write('Name: foo\n')
        return str(tmpdir)

    @pytest.yield_fixture()
    def install_target(self, tmpdir):
        target = str(tmpdir)
        with mock.patch('sys.path', sys.path + [target]):
            python_path = os.path.pathsep.join(sys.path)
            with mock.patch.dict(os.environ, PYTHONPATH=python_path):
                yield target

    def test_local_index(self, foo_package, install_target):
        """
        The local index must be used when easy_install locates installed
        packages.
        """
        dist = Distribution()
        dist.script_name = 'setup.py'
        cmd = ei.easy_install(dist)
        cmd.install_dir = install_target
        cmd.args = ['foo']
        cmd.ensure_finalized()
        cmd.local_index.scan([foo_package])
        res = cmd.easy_install('foo')
        actual = os.path.normcase(os.path.realpath(res.location))
        expected = os.path.normcase(os.path.realpath(foo_package))
        assert actual == expected

    @contextlib.contextmanager
    def user_install_setup_context(self, *args, **kwargs):
        """
        Wrap sandbox.setup_context to patch easy_install in that context to
        appear as user-installed.
        """
        with self.orig_context(*args, **kwargs):
            import setuptools.command.easy_install as ei
            ei.__file__ = site.USER_SITE
            yield

    def patched_setup_context(self):
        self.orig_context = sandbox.setup_context

        return mock.patch(
            'setuptools.sandbox.setup_context',
            self.user_install_setup_context,
        )


@pytest.yield_fixture
def distutils_package():
    distutils_setup_py = SETUP_PY.replace(
        'from setuptools import setup',
        'from distutils.core import setup',
    )
    with contexts.tempdir(cd=os.chdir):
        with open('setup.py', 'w') as f:
            f.write(distutils_setup_py)
        yield


@pytest.fixture
def mock_index():
    # set up a server which will simulate an alternate package index.
    p_index = MockServer()
    if p_index.server_port == 0:
        # Some platforms (Jython) don't find a port to which to bind,
        # so skip test for them.
        pytest.skip("could not find a valid port")
    p_index.start()
    return p_index


class TestDistutilsPackage:
    def test_bdist_egg_available_on_distutils_pkg(self, distutils_package):
        run_setup('setup.py', ['bdist_egg'])


class TestSetupRequires:

    def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
        """
        When easy_install installs a source distribution which specifies
        setup_requires, it should honor the fetch parameters (such as
        index-url, and find-links).
        """
        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
        with contexts.quiet():
            # create an sdist that has a build-time dependency.
            with TestSetupRequires.create_sdist() as dist_file:
                with contexts.tempdir() as temp_install_dir:
                    with contexts.environment(PYTHONPATH=temp_install_dir):
                        ei_params = [
                            '--index-url', mock_index.url,
                            '--exclude-scripts',
                            '--install-dir', temp_install_dir,
                            dist_file,
                        ]
                        with sandbox.save_argv(['easy_install']):
                            # attempt to install the dist. It should
                            # fail because it doesn't exist.
                            with pytest.raises(SystemExit):
                                easy_install_pkg.main(ei_params)
        # there should have been one requests to the server
        assert [r.path for r in mock_index.requests] == ['/does-not-exist/']

    @staticmethod
    @contextlib.contextmanager
    def create_sdist():
        """
        Return an sdist with a setup_requires dependency (of something that
        doesn't exist)
        """
        with contexts.tempdir() as dir:
            dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
            make_sdist(dist_path, [
                ('setup.py', DALS("""
                    import setuptools
                    setuptools.setup(
                        name="setuptools-test-fetcher",
                        version="1.0",
                        setup_requires = ['does-not-exist'],
                    )
                """)),
                ('setup.cfg', ''),
            ])
            yield dist_path

    use_setup_cfg = (
        (),
        ('dependency_links',),
        ('setup_requires',),
        ('dependency_links', 'setup_requires'),
    )

    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
    def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
        """
        Regression test for distribution issue 323:
        https://bitbucket.org/tarek/distribute/issues/323

        Ensures that a distribution's setup_requires requirements can still be
        installed and used locally even if a conflicting version of that
        requirement is already on the path.
        """

        fake_dist = PRDistribution('does-not-matter', project_name='foobar',
                                   version='0.0')
        working_set.add(fake_dist)

        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                test_pkg = create_setup_requires_package(
                    temp_dir, use_setup_cfg=use_setup_cfg)
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                with contexts.quiet() as (stdout, stderr):
                    # Don't even need to install the package, just
                    # running the setup.py at all is sufficient
                    run_setup(test_setup_py, [str('--name')])

                lines = stdout.readlines()
                assert len(lines) > 0
                assert lines[-1].strip() == 'test_pkg'

    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
    def test_setup_requires_override_nspkg(self, use_setup_cfg):
        """
        Like ``test_setup_requires_overrides_version_conflict`` but where the
        ``setup_requires`` package is part of a namespace package that has
        *already* been imported.
        """

        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz')
                make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
                # Now actually go ahead an extract to the temp dir and add the
                # extracted path to sys.path so foo.bar v0.1 is importable
                foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1')
                os.mkdir(foobar_1_dir)
                with tarfile.open(foobar_1_archive) as tf:
                    tf.extractall(foobar_1_dir)
                sys.path.insert(1, foobar_1_dir)

                dist = PRDistribution(foobar_1_dir, project_name='foo.bar',
                                      version='0.1')
                working_set.add(dist)

                template = DALS("""\
                    import foo  # Even with foo imported first the
                                # setup_requires package should override
                    import setuptools
                    setuptools.setup(**%r)

                    if not (hasattr(foo, '__path__') and
                            len(foo.__path__) == 2):
                        print('FAIL')

                    if 'foo.bar-0.2' not in foo.__path__[0]:
                        print('FAIL')
                """)

                test_pkg = create_setup_requires_package(
                    temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template,
                    use_setup_cfg=use_setup_cfg)

                test_setup_py = os.path.join(test_pkg, 'setup.py')

                with contexts.quiet() as (stdout, stderr):
                    try:
                        # Don't even need to install the package, just
                        # running the setup.py at all is sufficient
                        run_setup(test_setup_py, [str('--name')])
                    except pkg_resources.VersionConflict:
                        self.fail(
                            'Installing setup.py requirements '
                            'caused a VersionConflict')

                assert 'FAIL' not in stdout.getvalue()
                lines = stdout.readlines()
                assert len(lines) > 0
                assert lines[-1].strip() == 'test_pkg'

    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
    def test_setup_requires_with_attr_version(self, use_setup_cfg):
        def make_dependency_sdist(dist_path, distname, version):
            files = [(
                'setup.py',
                DALS("""
                    import setuptools
                    setuptools.setup(
                        name={name!r},
                        version={version!r},
                        py_modules=[{name!r}],
                    )
                    """.format(name=distname, version=version)),
            ), (
                distname + '.py',
                DALS("""
                    version = 42
                    """),
            )]
            make_sdist(dist_path, files)
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                test_pkg = create_setup_requires_package(
                    temp_dir, setup_attrs=dict(version='attr: foobar.version'),
                    make_package=make_dependency_sdist,
                    use_setup_cfg=use_setup_cfg + ('version',),
                )
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                with contexts.quiet() as (stdout, stderr):
                    run_setup(test_setup_py, [str('--version')])
                lines = stdout.readlines()
                assert len(lines) > 0
                assert lines[-1].strip() == '42'

    def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch):
        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
        monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url)
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                test_pkg = create_setup_requires_package(
                    temp_dir, 'python-xlib', '0.19',
                    setup_attrs=dict(dependency_links=[]))
                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
                with open(test_setup_cfg, 'w') as fp:
                    fp.write(DALS(
                        '''
                        [easy_install]
                        index_url = https://pypi.org/legacy/
                        '''))
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                with pytest.raises(distutils.errors.DistutilsError):
                    run_setup(test_setup_py, [str('--version')])
        assert len(mock_index.requests) == 1
        assert mock_index.requests[0].path == '/python-xlib/'

    def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch):
        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
        monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url)
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                dep_sdist = os.path.join(temp_dir, 'dep.tar.gz')
                make_trivial_sdist(dep_sdist, 'dependency', '42')
                dep_url = path_to_url(dep_sdist, authority='localhost')
                test_pkg = create_setup_requires_package(
                    temp_dir,
                    # Ignored (overriden by setup_attrs)
                    'python-xlib', '0.19',
                    setup_attrs=dict(
                        setup_requires='dependency @ %s' % dep_url))
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                run_setup(test_setup_py, [str('--version')])
        assert len(mock_index.requests) == 0

    def test_setup_requires_with_allow_hosts(self, mock_index):
        ''' The `allow-hosts` option in not supported anymore. '''
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                test_pkg = os.path.join(temp_dir, 'test_pkg')
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
                os.mkdir(test_pkg)
                with open(test_setup_py, 'w') as fp:
                    fp.write(DALS(
                        '''
                        from setuptools import setup
                        setup(setup_requires='python-xlib')
                        '''))
                with open(test_setup_cfg, 'w') as fp:
                    fp.write(DALS(
                        '''
                        [easy_install]
                        allow_hosts = *
                        '''))
                with pytest.raises(distutils.errors.DistutilsError):
                    run_setup(test_setup_py, [str('--version')])
        assert len(mock_index.requests) == 0

    def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir):
        ''' Check `python_requires` is honored. '''
        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
        monkeypatch.setenv(str('PIP_NO_INDEX'), str('1'))
        monkeypatch.setenv(str('PIP_VERBOSE'), str('1'))
        dep_1_0_sdist = 'dep-1.0.tar.gz'
        dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist))
        dep_1_0_python_requires = '>=2.7'
        make_python_requires_sdist(
            str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires)
        dep_2_0_sdist = 'dep-2.0.tar.gz'
        dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist))
        dep_2_0_python_requires = '!=' + '.'.join(
            map(str, sys.version_info[:2])) + '.*'
        make_python_requires_sdist(
            str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires)
        index = tmpdir / 'index.html'
        index.write_text(DALS(
            '''
            <!DOCTYPE html>
            <html><head><title>Links for dep</title></head>
            <body>
                <h1>Links for dep</h1>
                <a href="{dep_1_0_url}" data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/>
                <a href="{dep_2_0_url}" data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/>
            </body>
            </html>
            ''').format(  # noqa
                dep_1_0_url=dep_1_0_url,
                dep_1_0_sdist=dep_1_0_sdist,
                dep_1_0_python_requires=dep_1_0_python_requires,
                dep_2_0_url=dep_2_0_url,
                dep_2_0_sdist=dep_2_0_sdist,
                dep_2_0_python_requires=dep_2_0_python_requires,
        ), 'utf-8')
        index_url = path_to_url(str(index))
        with contexts.save_pkg_resources_state():
            test_pkg = create_setup_requires_package(
                str(tmpdir),
                'python-xlib', '0.19',  # Ignored (overriden by setup_attrs).
                setup_attrs=dict(
                    setup_requires='dep', dependency_links=[index_url]))
            test_setup_py = os.path.join(test_pkg, 'setup.py')
            run_setup(test_setup_py, [str('--version')])
        eggs = list(map(str, pkg_resources.find_distributions(
            os.path.join(test_pkg, '.eggs'))))
        assert eggs == ['dep 1.0']

    @pytest.mark.parametrize(
        'use_legacy_installer,with_dependency_links_in_setup_py',
        itertools.product((False, True), (False, True)))
    def test_setup_requires_with_find_links_in_setup_cfg(
            self, monkeypatch, use_legacy_installer,
            with_dependency_links_in_setup_py):
        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                make_trivial_sdist(
                    os.path.join(temp_dir, 'python-xlib-42.tar.gz'),
                    'python-xlib',
                    '42')
                test_pkg = os.path.join(temp_dir, 'test_pkg')
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
                os.mkdir(test_pkg)
                with open(test_setup_py, 'w') as fp:
                    if with_dependency_links_in_setup_py:
                        dependency_links = [os.path.join(temp_dir, 'links')]
                    else:
                        dependency_links = []
                    fp.write(DALS(
                        '''
                        from setuptools import installer, setup
                        if {use_legacy_installer}:
                            installer.fetch_build_egg = installer._legacy_fetch_build_egg
                        setup(setup_requires='python-xlib==42',
                        dependency_links={dependency_links!r})
                        ''').format(use_legacy_installer=use_legacy_installer,  # noqa
                                    dependency_links=dependency_links))
                with open(test_setup_cfg, 'w') as fp:
                    fp.write(DALS(
                        '''
                        [easy_install]
                        index_url = {index_url}
                        find_links = {find_links}
                        ''').format(index_url=os.path.join(temp_dir, 'index'),
                                    find_links=temp_dir))
                run_setup(test_setup_py, [str('--version')])

    def test_setup_requires_with_transitive_extra_dependency(
            self, monkeypatch):
        # Use case: installing a package with a build dependency on
        # an already installed `dep[extra]`, which in turn depends
        # on `extra_dep` (whose is not already installed).
        with contexts.save_pkg_resources_state():
            with contexts.tempdir() as temp_dir:
                # Create source distribution for `extra_dep`.
                make_trivial_sdist(
                    os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'),
                    'extra_dep', '1.0')
                # Create source tree for `dep`.
                dep_pkg = os.path.join(temp_dir, 'dep')
                os.mkdir(dep_pkg)
                build_files({
                    'setup.py':
                    DALS("""
                          import setuptools
                          setuptools.setup(
                              name='dep', version='2.0',
                              extras_require={'extra': ['extra_dep']},
                          )
                         """),
                    'setup.cfg': '',
                }, prefix=dep_pkg)
                # "Install" dep.
                run_setup(
                    os.path.join(dep_pkg, 'setup.py'), [str('dist_info')])
                working_set.add_entry(dep_pkg)
                # Create source tree for test package.
                test_pkg = os.path.join(temp_dir, 'test_pkg')
                test_setup_py = os.path.join(test_pkg, 'setup.py')
                os.mkdir(test_pkg)
                with open(test_setup_py, 'w') as fp:
                    fp.write(DALS(
                        '''
                        from setuptools import installer, setup
                        setup(setup_requires='dep[extra]')
                        '''))
                # Check...
                monkeypatch.setenv(str('PIP_FIND_LINKS'), str(temp_dir))
                monkeypatch.setenv(str('PIP_NO_INDEX'), str('1'))
                monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
                monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
                run_setup(test_setup_py, [str('--version')])


def make_trivial_sdist(dist_path, distname, version):
    """
    Create a simple sdist tarball at dist_path, containing just a simple
    setup.py.
    """

    make_sdist(dist_path, [
        ('setup.py',
         DALS("""\
             import setuptools
             setuptools.setup(
                 name=%r,
                 version=%r
             )
         """ % (distname, version))),
        ('setup.cfg', ''),
    ])


def make_nspkg_sdist(dist_path, distname, version):
    """
    Make an sdist tarball with distname and version which also contains one
    package with the same name as distname.  The top-level package is
    designated a namespace package).
    """

    parts = distname.split('.')
    nspackage = parts[0]

    packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]

    setup_py = DALS("""\
        import setuptools
        setuptools.setup(
            name=%r,
            version=%r,
            packages=%r,
            namespace_packages=[%r]
        )
    """ % (distname, version, packages, nspackage))

    init = "__import__('pkg_resources').declare_namespace(__name__)"

    files = [('setup.py', setup_py),
             (os.path.join(nspackage, '__init__.py'), init)]
    for package in packages[1:]:
        filename = os.path.join(*(package.split('.') + ['__init__.py']))
        files.append((filename, ''))

    make_sdist(dist_path, files)


def make_python_requires_sdist(dist_path, distname, version, python_requires):
    make_sdist(dist_path, [
        (
            'setup.py',
            DALS("""\
                import setuptools
                setuptools.setup(
                  name={name!r},
                  version={version!r},
                  python_requires={python_requires!r},
                )
                """).format(
                name=distname, version=version,
                python_requires=python_requires)),
        ('setup.cfg', ''),
    ])


def make_sdist(dist_path, files):
    """
    Create a simple sdist tarball at dist_path, containing the files
    listed in ``files`` as ``(filename, content)`` tuples.
    """

    # Distributions with only one file don't play well with pip.
    assert len(files) > 1
    with tarfile.open(dist_path, 'w:gz') as dist:
        for filename, content in files:
            file_bytes = io.BytesIO(content.encode('utf-8'))
            file_info = tarfile.TarInfo(name=filename)
            file_info.size = len(file_bytes.getvalue())
            file_info.mtime = int(time.time())
            dist.addfile(file_info, fileobj=file_bytes)


def create_setup_requires_package(path, distname='foobar', version='0.1',
                                  make_package=make_trivial_sdist,
                                  setup_py_template=None, setup_attrs={},
                                  use_setup_cfg=()):
    """Creates a source tree under path for a trivial test package that has a
    single requirement in setup_requires--a tarball for that requirement is
    also created and added to the dependency_links argument.

    ``distname`` and ``version`` refer to the name/version of the package that
    the test package requires via ``setup_requires``.  The name of the test
    package itself is just 'test_pkg'.
    """

    test_setup_attrs = {
        'name': 'test_pkg', 'version': '0.0',
        'setup_requires': ['%s==%s' % (distname, version)],
        'dependency_links': [os.path.abspath(path)]
    }
    test_setup_attrs.update(setup_attrs)

    test_pkg = os.path.join(path, 'test_pkg')
    os.mkdir(test_pkg)

    # setup.cfg
    if use_setup_cfg:
        options = []
        metadata = []
        for name in use_setup_cfg:
            value = test_setup_attrs.pop(name)
            if name in 'name version'.split():
                section = metadata
            else:
                section = options
            if isinstance(value, (tuple, list)):
                value = ';'.join(value)
            section.append('%s: %s' % (name, value))
        test_setup_cfg_contents = DALS(
            """
            [metadata]
            {metadata}
            [options]
            {options}
            """
        ).format(
            options='\n'.join(options),
            metadata='\n'.join(metadata),
        )
    else:
        test_setup_cfg_contents = ''
    with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f:
        f.write(test_setup_cfg_contents)

    # setup.py
    if setup_py_template is None:
        setup_py_template = DALS("""\
            import setuptools
            setuptools.setup(**%r)
        """)
    with open(os.path.join(test_pkg, 'setup.py'), 'w') as f:
        f.write(setup_py_template % test_setup_attrs)

    foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version))
    make_package(foobar_path, distname, version)

    return test_pkg


@pytest.mark.skipif(
    sys.platform.startswith('java') and ei.is_sh(sys.executable),
    reason="Test cannot run under java when executable is sh"
)
class TestScriptHeader:
    non_ascii_exe = '/Users/José/bin/python'
    exe_with_spaces = r'C:\Program Files\Python36\python.exe'

    def test_get_script_header(self):
        expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
        actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python')
        assert actual == expected

    def test_get_script_header_args(self):
        expected = '#!%s -x\n' % ei.nt_quote_arg(
            os.path.normpath(sys.executable))
        actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x')
        assert actual == expected

    def test_get_script_header_non_ascii_exe(self):
        actual = ei.ScriptWriter.get_header(
            '#!/usr/bin/python',
            executable=self.non_ascii_exe)
        expected = str('#!%s -x\n') % self.non_ascii_exe
        assert actual == expected

    def test_get_script_header_exe_with_spaces(self):
        actual = ei.ScriptWriter.get_header(
            '#!/usr/bin/python',
            executable='"' + self.exe_with_spaces + '"')
        expected = '#!"%s"\n' % self.exe_with_spaces
        assert actual == expected


class TestCommandSpec:
    def test_custom_launch_command(self):
        """
        Show how a custom CommandSpec could be used to specify a #! executable
        which takes parameters.
        """
        cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
        assert cmd.as_header() == '#!/usr/bin/env python3\n'

    def test_from_param_for_CommandSpec_is_passthrough(self):
        """
        from_param should return an instance of a CommandSpec
        """
        cmd = ei.CommandSpec(['python'])
        cmd_new = ei.CommandSpec.from_param(cmd)
        assert cmd is cmd_new

    @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
    @mock.patch.dict(os.environ)
    def test_from_environment_with_spaces_in_executable(self):
        os.environ.pop('__PYVENV_LAUNCHER__', None)
        cmd = ei.CommandSpec.from_environment()
        assert len(cmd) == 1
        assert cmd.as_header().startswith('#!"')

    def test_from_simple_string_uses_shlex(self):
        """
        In order to support `executable = /usr/bin/env my-python`, make sure
        from_param invokes shlex on that input.
        """
        cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
        assert len(cmd) == 2
        assert '"' not in cmd.as_header()


class TestWindowsScriptWriter:
    def test_header(self):
        hdr = ei.WindowsScriptWriter.get_header('')
        assert hdr.startswith('#!')
        assert hdr.endswith('\n')
        hdr = hdr.lstrip('#!')
        hdr = hdr.rstrip('\n')
        # header should not start with an escaped quote
        assert not hdr.startswith('\\"')
