summaryrefslogtreecommitdiff
path: root/tests/activation/test_activation.py
blob: d2ad1c13e73cd915d0c864953b0517f63b1afb1d (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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
from __future__ import absolute_import, unicode_literals

import os
import pipes
import re
import subprocess
import sys
from os.path import dirname, join, normcase, realpath

import pytest
import six

import virtualenv

IS_INSIDE_CI = "CI_RUN" in os.environ


def need_executable(name, check_cmd):
    """skip running this locally if executable not found, unless we're inside the CI"""

    def wrapper(fn):
        fn = getattr(pytest.mark, name)(fn)
        if not IS_INSIDE_CI:
            # locally we disable, so that contributors don't need to have everything setup
            # noinspection PyBroadException
            try:
                fn.version = subprocess.check_output(check_cmd, env=get_env())
            except Exception as exception:
                return pytest.mark.skip(reason="{} is not available due {}".format(name, exception))(fn)
        return fn

    return wrapper


def requires(on):
    def wrapper(fn):
        return need_executable(on.cmd.replace(".exe", ""), on.check)(fn)

    return wrapper


def norm_path(path):
    # python may return Windows short paths, normalize
    path = realpath(path)
    if virtualenv.IS_WIN:
        from ctypes import create_unicode_buffer, windll

        buffer_cont = create_unicode_buffer(256)
        get_long_path_name = windll.kernel32.GetLongPathNameW
        get_long_path_name(six.text_type(path), buffer_cont, 256)  # noqa: F821
        result = buffer_cont.value
    else:
        result = path
    return normcase(result)


class Activation(object):
    cmd = ""
    extension = "test"
    invoke_script = []
    command_separator = os.linesep
    activate_cmd = "source"
    activate_script = ""
    check_has_exe = []
    check = []
    env = {}

    def __init__(self, activation_env, tmp_path):
        self.home_dir = activation_env[0]
        self.bin_dir = activation_env[1]
        self.path = tmp_path

    def quote(self, s):
        return pipes.quote(s)

    def python_cmd(self, cmd):
        return "{} -c {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(cmd))

    def python_script(self, script):
        return "{} {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(script))

    def print_python_exe(self):
        return self.python_cmd("import sys; print(sys.executable)")

    def print_os_env_var(self, var):
        val = '"{}"'.format(var)
        return self.python_cmd("import os; print(os.environ.get({}, None))".format(val))

    def __call__(self, monkeypatch):
        absolute_activate_script = norm_path(join(self.bin_dir, self.activate_script))

        commands = [
            self.print_python_exe(),
            self.print_os_env_var("VIRTUAL_ENV"),
            self.activate_call(absolute_activate_script),
            self.print_python_exe(),
            self.print_os_env_var("VIRTUAL_ENV"),
            # pydoc loads documentation from the virtualenv site packages
            "pydoc -w pydoc_test",
            "deactivate",
            self.print_python_exe(),
            self.print_os_env_var("VIRTUAL_ENV"),
            "",  # just finish with an empty new line
        ]
        script = self.command_separator.join(commands)
        test_script = self.path / "script.{}".format(self.extension)
        test_script.write_text(script)
        assert test_script.exists()

        monkeypatch.chdir(str(self.path))
        invoke_shell = self.invoke_script + [str(test_script)]

        monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False)

        # in case the tool is provided by the dev environment (e.g. xonosh)
        env = get_env()
        env.update(self.env)

        try:
            raw = subprocess.check_output(invoke_shell, universal_newlines=True, stderr=subprocess.STDOUT, env=env)
        except subprocess.CalledProcessError as exception:
            assert not exception.returncode, exception.output
        out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n")

        # pre-activation
        assert out[0], raw
        assert out[1] == "None", raw

        # post-activation
        exe = "{}.exe".format(virtualenv.EXPECTED_EXE) if virtualenv.IS_WIN else virtualenv.EXPECTED_EXE
        assert norm_path(out[2]) == norm_path(join(self.bin_dir, exe)), raw
        assert norm_path(out[3]) == norm_path(str(self.home_dir)).replace("\\\\", "\\"), raw

        assert out[4] == "wrote pydoc_test.html"
        content = self.path / "pydoc_test.html"
        assert content.exists(), raw

        # post deactivation, same as before
        assert out[-2] == out[0], raw
        assert out[-1] == "None", raw

    def activate_call(self, script):
        return "{} {}".format(pipes.quote(self.activate_cmd), pipes.quote(script)).strip()


def get_env():
    env = os.environ.copy()
    env[str("PATH")] = os.pathsep.join([dirname(sys.executable)] + env.get(str("PATH"), str("")).split(os.pathsep))
    return env


class BashActivation(Activation):
    cmd = "bash.exe" if virtualenv.IS_WIN else "bash"
    invoke_script = [cmd]
    extension = "sh"
    activate_script = "activate"
    check = [cmd, "--version"]


@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision bash on Windows yet")
@requires(BashActivation)
def test_bash(clean_python, monkeypatch, tmp_path):
    BashActivation(clean_python, tmp_path)(monkeypatch)


class CshActivation(Activation):
    cmd = "csh.exe" if virtualenv.IS_WIN else "csh"
    invoke_script = [cmd]
    extension = "csh"
    activate_script = "activate.csh"
    check = [cmd, "--version"]


@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision csh on Windows yet")
@requires(CshActivation)
def test_csh(clean_python, monkeypatch, tmp_path):
    CshActivation(clean_python, tmp_path)(monkeypatch)


class FishActivation(Activation):
    cmd = "fish.exe" if virtualenv.IS_WIN else "fish"
    invoke_script = [cmd]
    extension = "fish"
    activate_script = "activate.fish"
    check = [cmd, "--version"]


@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision fish on Windows yet")
@requires(FishActivation)
def test_fish(clean_python, monkeypatch, tmp_path):
    FishActivation(clean_python, tmp_path)(monkeypatch)


class PowershellActivation(Activation):
    cmd = "powershell.exe" if virtualenv.IS_WIN else "pwsh"
    extension = "ps1"
    invoke_script = [cmd, "-File"]
    activate_script = "activate.ps1"
    activate_cmd = "."
    check = [cmd, "-c", "$PSVersionTable"]

    def quote(self, s):
        """powershell double double quote needed for quotes within single quotes"""
        return pipes.quote(s).replace('"', '""')


@requires(PowershellActivation)
def test_powershell(clean_python, monkeypatch, tmp_path):
    PowershellActivation(clean_python, tmp_path)(monkeypatch)


class XonoshActivation(Activation):
    cmd = "xonsh"
    extension = "xsh"
    invoke_script = [sys.executable, "-m", "xonsh"]
    activate_script = "activate.xsh"
    check = [sys.executable, "-m", "xonsh", "--version"]
    env = {"XONSH_DEBUG": "1", "XONSH_SHOW_TRACEBACK": "True"}

    def activate_call(self, script):
        return "{} {}".format(self.activate_cmd, repr(script)).strip()


@pytest.mark.skipif(sys.version_info < (3, 4), reason="xonosh requires Python 3.4 at least")
@requires(XonoshActivation)
def test_xonosh(clean_python, monkeypatch, tmp_path):
    XonoshActivation(clean_python, tmp_path)(monkeypatch)