1from __future__ import absolute_import, unicode_literals 2 3import os 4import pipes 5import re 6import subprocess 7import sys 8from os.path import dirname, join, normcase, realpath 9 10import pytest 11import six 12 13import virtualenv 14 15IS_INSIDE_CI = "CI_RUN" in os.environ 16 17 18def need_executable(name, check_cmd): 19 """skip running this locally if executable not found, unless we're inside the CI""" 20 21 def wrapper(fn): 22 fn = getattr(pytest.mark, name)(fn) 23 if not IS_INSIDE_CI: 24 # locally we disable, so that contributors don't need to have everything setup 25 # noinspection PyBroadException 26 try: 27 fn.version = subprocess.check_output(check_cmd, env=get_env()) 28 except Exception as exception: 29 return pytest.mark.skip(reason="{} is not available due {}".format(name, exception))(fn) 30 return fn 31 32 return wrapper 33 34 35def requires(on): 36 def wrapper(fn): 37 return need_executable(on.cmd.replace(".exe", ""), on.check)(fn) 38 39 return wrapper 40 41 42def norm_path(path): 43 # python may return Windows short paths, normalize 44 path = realpath(path) 45 if virtualenv.IS_WIN: 46 from ctypes import create_unicode_buffer, windll 47 48 buffer_cont = create_unicode_buffer(256) 49 get_long_path_name = windll.kernel32.GetLongPathNameW 50 get_long_path_name(six.text_type(path), buffer_cont, 256) # noqa: F821 51 result = buffer_cont.value 52 else: 53 result = path 54 return normcase(result) 55 56 57class Activation(object): 58 cmd = "" 59 extension = "test" 60 invoke_script = [] 61 command_separator = os.linesep 62 activate_cmd = "source" 63 activate_script = "" 64 check_has_exe = [] 65 check = [] 66 env = {} 67 also_test_error_if_not_sourced = False 68 69 def __init__(self, activation_env, tmp_path): 70 self.home_dir = activation_env[0] 71 self.bin_dir = activation_env[1] 72 self.path = tmp_path 73 74 def quote(self, s): 75 return pipes.quote(s) 76 77 def python_cmd(self, cmd): 78 return "{} -c {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(cmd)) 79 80 def python_script(self, script): 81 return "{} {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(script)) 82 83 def print_python_exe(self): 84 return self.python_cmd("import sys; print(sys.executable)") 85 86 def print_os_env_var(self, var): 87 val = '"{}"'.format(var) 88 return self.python_cmd("import os; print(os.environ.get({}, None))".format(val)) 89 90 def __call__(self, monkeypatch): 91 absolute_activate_script = norm_path(join(self.bin_dir, self.activate_script)) 92 93 commands = [ 94 self.print_python_exe(), 95 self.print_os_env_var("VIRTUAL_ENV"), 96 self.activate_call(absolute_activate_script), 97 self.print_python_exe(), 98 self.print_os_env_var("VIRTUAL_ENV"), 99 # pydoc loads documentation from the virtualenv site packages 100 "pydoc -w pydoc_test", 101 "deactivate", 102 self.print_python_exe(), 103 self.print_os_env_var("VIRTUAL_ENV"), 104 "", # just finish with an empty new line 105 ] 106 script = self.command_separator.join(commands) 107 test_script = self.path / "script.{}".format(self.extension) 108 test_script.write_text(script) 109 assert test_script.exists() 110 111 monkeypatch.chdir(str(self.path)) 112 invoke_shell = self.invoke_script + [str(test_script)] 113 114 monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False) 115 116 # in case the tool is provided by the dev environment (e.g. xonosh) 117 env = get_env() 118 env.update(self.env) 119 120 try: 121 raw = subprocess.check_output(invoke_shell, universal_newlines=True, stderr=subprocess.STDOUT, env=env) 122 except subprocess.CalledProcessError as exception: 123 assert not exception.returncode, exception.output 124 out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n") 125 126 # pre-activation 127 assert out[0], raw 128 assert out[1] == "None", raw 129 130 # post-activation 131 exe = "{}.exe".format(virtualenv.EXPECTED_EXE) if virtualenv.IS_WIN else virtualenv.EXPECTED_EXE 132 assert norm_path(out[2]) == norm_path(join(self.bin_dir, exe)), raw 133 assert norm_path(out[3]) == norm_path(str(self.home_dir)).replace("\\\\", "\\"), raw 134 135 assert out[4] == "wrote pydoc_test.html" 136 content = self.path / "pydoc_test.html" 137 assert content.exists(), raw 138 139 # post deactivation, same as before 140 assert out[-2] == out[0], raw 141 assert out[-1] == "None", raw 142 143 if self.also_test_error_if_not_sourced: 144 invoke_shell = self.invoke_script + [absolute_activate_script] 145 146 with pytest.raises(subprocess.CalledProcessError) as c: 147 subprocess.check_output(invoke_shell, stderr=subprocess.STDOUT, env=env) 148 assert c.value.returncode, c 149 150 def activate_call(self, script): 151 return "{} {}".format(pipes.quote(self.activate_cmd), pipes.quote(script)).strip() 152 153 154def get_env(): 155 env = os.environ.copy() 156 env[str("PATH")] = os.pathsep.join([dirname(sys.executable)] + env.get(str("PATH"), str("")).split(os.pathsep)) 157 return env 158 159 160class BashActivation(Activation): 161 cmd = "bash.exe" if virtualenv.IS_WIN else "bash" 162 invoke_script = [cmd] 163 extension = "sh" 164 activate_script = "activate" 165 check = [cmd, "--version"] 166 also_test_error_if_not_sourced = True 167 168 169@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision bash on Windows yet") 170@requires(BashActivation) 171def test_bash(clean_python, monkeypatch, tmp_path): 172 BashActivation(clean_python, tmp_path)(monkeypatch) 173 174 175class CshActivation(Activation): 176 cmd = "csh.exe" if virtualenv.IS_WIN else "csh" 177 invoke_script = [cmd] 178 extension = "csh" 179 activate_script = "activate.csh" 180 check = [cmd, "--version"] 181 182 183@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision csh on Windows yet") 184@requires(CshActivation) 185def test_csh(clean_python, monkeypatch, tmp_path): 186 CshActivation(clean_python, tmp_path)(monkeypatch) 187 188 189class FishActivation(Activation): 190 cmd = "fish.exe" if virtualenv.IS_WIN else "fish" 191 invoke_script = [cmd] 192 extension = "fish" 193 activate_script = "activate.fish" 194 check = [cmd, "--version"] 195 196 197@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision fish on Windows yet") 198@requires(FishActivation) 199def test_fish(clean_python, monkeypatch, tmp_path): 200 FishActivation(clean_python, tmp_path)(monkeypatch) 201 202 203class PowershellActivation(Activation): 204 cmd = "powershell.exe" if virtualenv.IS_WIN else "pwsh" 205 extension = "ps1" 206 invoke_script = [cmd, "-File"] 207 activate_script = "activate.ps1" 208 activate_cmd = "." 209 check = [cmd, "-c", "$PSVersionTable"] 210 211 def quote(self, s): 212 """powershell double double quote needed for quotes within single quotes""" 213 return pipes.quote(s).replace('"', '""') 214 215 216@requires(PowershellActivation) 217def test_powershell(clean_python, monkeypatch, tmp_path): 218 PowershellActivation(clean_python, tmp_path)(monkeypatch) 219 220 221class XonoshActivation(Activation): 222 cmd = "xonsh" 223 extension = "xsh" 224 invoke_script = [sys.executable, "-m", "xonsh"] 225 activate_script = "activate.xsh" 226 check = [sys.executable, "-m", "xonsh", "--version"] 227 env = {"XONSH_DEBUG": "1", "XONSH_SHOW_TRACEBACK": "True"} 228 229 def activate_call(self, script): 230 return "{} {}".format(self.activate_cmd, repr(script)).strip() 231 232 233@pytest.mark.skipif(sys.version_info < (3, 5), reason="xonosh requires Python 3.5 at least") 234@requires(XonoshActivation) 235def test_xonosh(clean_python, monkeypatch, tmp_path): 236 XonoshActivation(clean_python, tmp_path)(monkeypatch) 237