summaryrefslogtreecommitdiffstats
path: root/tests/test_env.py
blob: f6f381ad44ca8e77928bfae420794ecb1494d8fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# SPDX-License-Identifier: MIT
import collections
import inspect
import logging
import platform
import subprocess
import sys
import sysconfig

import pytest

from packaging.version import Version

import build.env


IS_PYPY3 = platform.python_implementation() == 'PyPy'


@pytest.mark.isolated
def test_isolation():
    subprocess.check_call([sys.executable, '-c', 'import build.env'])
    with build.env.IsolatedEnvBuilder() as env:
        with pytest.raises(subprocess.CalledProcessError):
            debug = 'import sys; import os; print(os.linesep.join(sys.path));'
            subprocess.check_call([env.executable, '-c', f'{debug} import build.env'])


@pytest.mark.isolated
def test_isolated_environment_install(mocker):
    with build.env.IsolatedEnvBuilder() as env:
        mocker.patch('build.env._subprocess')

        env.install([])
        build.env._subprocess.assert_not_called()

        env.install(['some', 'requirements'])
        build.env._subprocess.assert_called()
        args = build.env._subprocess.call_args[0][0][:-1]
        assert args == [
            env.executable,
            '-Im',
            'pip',
            'install',
            '--use-pep517',
            '--no-warn-script-location',
            '-r',
        ]


@pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv')
@pytest.mark.skipif(sys.platform != 'darwin', reason='workaround for Apple Python')
def test_can_get_venv_paths_with_conflicting_default_scheme(mocker):
    get_scheme_names = mocker.patch('sysconfig.get_scheme_names', return_value=('osx_framework_library',))
    with build.env.IsolatedEnvBuilder():
        pass
    assert get_scheme_names.call_count == 1


@pytest.mark.skipif('posix_local' not in sysconfig.get_scheme_names(), reason='workaround for Debian/Ubuntu Python')
def test_can_get_venv_paths_with_posix_local_default_scheme(mocker):
    get_paths = mocker.spy(sysconfig, 'get_paths')
    # We should never call this, but we patch it to ensure failure if we do
    get_default_scheme = mocker.patch('sysconfig.get_default_scheme', return_value='posix_local')
    with build.env.IsolatedEnvBuilder():
        pass
    get_paths.assert_called_once_with(scheme='posix_prefix', vars=mocker.ANY)
    assert get_default_scheme.call_count == 0


def test_executable_missing_post_creation(mocker):
    venv_create = mocker.patch('venv.EnvBuilder.create')
    with pytest.raises(RuntimeError, match='Virtual environment creation failed, executable .* missing'):
        with build.env.IsolatedEnvBuilder():
            pass
    assert venv_create.call_count == 1


def test_isolated_env_abstract():
    with pytest.raises(TypeError):
        build.env.IsolatedEnv()


def test_isolated_env_has_executable_still_abstract():
    class Env(build.env.IsolatedEnv):
        @property
        def executable(self):
            raise NotImplementedError

    with pytest.raises(TypeError):
        Env()


def test_isolated_env_has_install_still_abstract():
    class Env(build.env.IsolatedEnv):
        def install(self, requirements):
            raise NotImplementedError

    with pytest.raises(TypeError):
        Env()


@pytest.mark.pypy3323bug
def test_isolated_env_log(mocker, caplog, package_test_flit):
    mocker.patch('build.env._subprocess')
    caplog.set_level(logging.DEBUG)

    builder = build.env.IsolatedEnvBuilder()
    frameinfo = inspect.getframeinfo(inspect.currentframe())
    builder.log('something')  # line number 106
    with builder as env:
        env.install(['something'])

    assert [(record.levelname, record.message) for record in caplog.records] == [
        ('INFO', 'something'),
        ('INFO', 'Creating venv isolated environment...'),
        ('INFO', 'Installing packages in isolated environment... (something)'),
    ]
    if sys.version_info >= (3, 8):  # stacklevel
        assert [(record.lineno) for record in caplog.records] == [
            frameinfo.lineno + 1,
            frameinfo.lineno - 6,
            frameinfo.lineno + 85,
        ]


@pytest.mark.isolated
def test_default_pip_is_never_too_old():
    with build.env.IsolatedEnvBuilder() as env:
        version = subprocess.check_output(
            [env.executable, '-c', 'import pip; print(pip.__version__)'], universal_newlines=True
        ).strip()
        assert Version(version) >= Version('19.1')


@pytest.mark.isolated
@pytest.mark.parametrize('pip_version', ['20.2.0', '20.3.0', '21.0.0', '21.0.1'])
@pytest.mark.parametrize('arch', ['x86_64', 'arm64'])
def test_pip_needs_upgrade_mac_os_11(mocker, pip_version, arch):
    SimpleNamespace = collections.namedtuple('SimpleNamespace', 'version')

    _subprocess = mocker.patch('build.env._subprocess')
    mocker.patch('platform.system', return_value='Darwin')
    mocker.patch('platform.machine', return_value=arch)
    mocker.patch('platform.mac_ver', return_value=('11.0', ('', '', ''), ''))
    metadata_name = 'importlib_metadata' if sys.version_info < (3, 8) else 'importlib.metadata'
    mocker.patch(metadata_name + '.distributions', return_value=(SimpleNamespace(version=pip_version),))

    min_version = Version('20.3' if arch == 'x86_64' else '21.0.1')
    with build.env.IsolatedEnvBuilder():
        if Version(pip_version) < min_version:
            print(_subprocess.call_args_list)
            upgrade_call, uninstall_call = _subprocess.call_args_list
            answer = 'pip>=20.3.0' if arch == 'x86_64' else 'pip>=21.0.1'
            assert upgrade_call[0][0][1:] == ['-m', 'pip', 'install', answer]
            assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y']
        else:
            (uninstall_call,) = _subprocess.call_args_list
            assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y']


@pytest.mark.isolated
@pytest.mark.skipif(IS_PYPY3 and sys.platform.startswith('win'), reason='Isolated tests not supported on PyPy3 + Windows')
@pytest.mark.parametrize('has_symlink', [True, False] if sys.platform.startswith('win') else [True])
def test_venv_symlink(mocker, has_symlink):
    if has_symlink:
        mocker.patch('os.symlink')
        mocker.patch('os.unlink')
    else:
        mocker.patch('os.symlink', side_effect=OSError())

    # Cache must be cleared to rerun
    build.env._fs_supports_symlink.cache_clear()
    supports_symlink = build.env._fs_supports_symlink()
    build.env._fs_supports_symlink.cache_clear()

    assert supports_symlink is has_symlink