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 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
|
import errno
import functools
import json
import logging
import os
import shutil
from shutil import rmtree as _rmtree
import sys
import warnings
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import pytest
from pipenv.patched.pip._vendor import requests
from pipenv.vendor import tomlkit
from pipenv.utils.processes import subprocess_run
from pipenv.utils.funktools import handle_remove_readonly
from pipenv.utils.shell import temp_environ
import contextlib
log = logging.getLogger(__name__)
warnings.simplefilter("default", category=ResourceWarning)
HAS_WARNED_GITHUB = False
DEFAULT_PRIVATE_PYPI_SERVER = os.environ.get("PIPENV_PYPI_SERVER", "http://localhost:8080/simple")
def try_internet(url="http://httpbin.org/ip", timeout=1.5):
resp = requests.get(url, timeout=timeout)
resp.raise_for_status()
def check_internet():
has_internet = False
for url in ("http://httpbin.org/ip", "http://clients3.google.com/generate_204"):
try:
try_internet(url)
except KeyboardInterrupt: # noqa: PERF203
warnings.warn(
f"Skipped connecting to internet: {url}", RuntimeWarning, stacklevel=1
)
except Exception:
warnings.warn(
f"Failed connecting to internet: {url}", RuntimeWarning, stacklevel=1
)
else:
has_internet = True
break
return has_internet
def check_github_ssh():
res = False
try:
# `ssh -T git@github.com` will return successfully with return_code==1
# and message 'Hi <username>! You've successfully authenticated, but
# GitHub does not provide shell access.' if ssh keys are available and
# registered with GitHub. Otherwise, the command will fail with
# return_code=255 and say 'Permission denied (publickey).'
c = subprocess_run('ssh -o StrictHostKeyChecking=no -o CheckHostIP=no -T git@github.com', timeout=30, shell=True)
res = c.returncode == 1
except KeyboardInterrupt:
warnings.warn(
"KeyboardInterrupt while checking GitHub ssh access", RuntimeWarning, stacklevel=1
)
except Exception:
pass
global HAS_WARNED_GITHUB
if not res and not HAS_WARNED_GITHUB:
warnings.warn(
'Cannot connect to GitHub via SSH', RuntimeWarning, stacklevel=1
)
warnings.warn(
'Will skip tests requiring SSH access to GitHub', RuntimeWarning, stacklevel=1
)
HAS_WARNED_GITHUB = True
return res
def check_for_mercurial():
c = subprocess_run("hg --help", shell=True)
return c.returncode == 0
TESTS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PYPI_VENDOR_DIR = os.path.join(TESTS_ROOT, 'pypi')
WE_HAVE_HG = check_for_mercurial()
def pytest_runtest_setup(item):
if item.get_closest_marker('needs_internet') is not None and not WE_HAVE_INTERNET:
pytest.skip('requires internet')
if item.get_closest_marker('needs_github_ssh') is not None and not WE_HAVE_GITHUB_SSH_KEYS:
pytest.skip('requires github ssh')
if item.get_closest_marker('needs_hg') is not None and not WE_HAVE_HG:
pytest.skip('requires mercurial')
if item.get_closest_marker('skip_py38') is not None and (
sys.version_info[:2] == (3, 8)
):
pytest.skip('test not applicable on python 3.8')
if item.get_closest_marker('skip_osx') is not None and sys.platform == 'darwin':
pytest.skip('test does not apply on OSX')
if item.get_closest_marker('skip_windows') is not None and (os.name == 'nt'):
pytest.skip('test does not run on windows')
WE_HAVE_INTERNET = check_internet()
WE_HAVE_GITHUB_SSH_KEYS = False
class _Pipfile:
def __init__(self, path, index):
self.path = path
self.index = index
if self.path.exists():
self.loads()
else:
self.document = tomlkit.document()
self.document["source"] = self.document.get("source", tomlkit.aot())
self.document["requires"] = self.document.get("requires", tomlkit.table())
self.document["packages"] = self.document.get("packages", tomlkit.table())
self.document["dev-packages"] = self.document.get("dev-packages", tomlkit.table())
self.write()
def install(self, package, value, dev=False):
section = "packages" if not dev else "dev-packages"
if isinstance(value, dict):
table = tomlkit.inline_table()
table.update(value)
self.document[section][package] = table
else:
self.document[section][package] = value
self.write()
def remove(self, package, dev=False):
section = "packages" if not dev else "dev-packages"
if not dev and package not in self.document[section] and package in self.document["dev-packages"]:
section = "dev-packages"
del self.document[section][package]
self.write()
def add(self, package, value, dev=False):
self.install(package, value, dev=dev)
def update(self, package, value, dev=False):
self.install(package, value, dev=dev)
def loads(self):
self.document = tomlkit.loads(self.path.read_text())
def dumps(self):
if not self.document.get("source"):
source_table = tomlkit.table()
source_table["url"] = self.index
source_table["verify_ssl"] = bool(self.index.startswith("https"))
source_table["name"] = "pipenv_test_index"
self.document["source"].append(source_table)
return tomlkit.dumps(self.document)
def write(self):
self.path.write_text(self.dumps())
@classmethod
def get_fixture_path(cls, path, fixtures="test_artifacts"):
return Path(Path(__file__).resolve().parent.parent / fixtures / path)
class _PipenvInstance:
"""An instance of a Pipenv Project..."""
def __init__(self, pipfile=True, capfd=None, index_url=None):
self.index_url = index_url
self.pypi = None
self.env = {}
self.capfd = capfd
if self.index_url is not None:
self.pypi, _, _ = self.index_url.rpartition("/") if self.index_url else ""
self.env["PYTHONWARNINGS"] = "ignore:DEPRECATION"
os.environ.pop("PIPENV_CUSTOM_VENV_NAME", None)
self.original_dir = Path(__file__).parent.parent.parent
self._path = TemporaryDirectory(prefix='pipenv-', suffix="-tests")
path = Path(self._path.name)
try:
self.path = str(path.resolve())
except OSError:
self.path = str(path.absolute())
os.chdir(self.path)
# set file creation perms
self.pipfile_path = None
p_path = os.sep.join([self.path, 'Pipfile'])
self.pipfile_path = p_path
if pipfile:
with contextlib.suppress(FileNotFoundError):
os.remove(p_path)
with open(p_path, 'a'):
os.utime(p_path, None)
self._pipfile = _Pipfile(Path(p_path), index=self.index_url)
else:
self._pipfile = None
def __enter__(self):
return self
def __exit__(self, *args):
warn_msg = 'Failed to remove resource: {!r}'
if self.pipfile_path:
with contextlib.suppress(OSError):
os.remove(self.pipfile_path)
os.chdir(self.original_dir)
if self._path:
try:
self._path.cleanup()
except OSError as e:
_warn_msg = warn_msg.format(e)
warnings.warn(_warn_msg, ResourceWarning, stacklevel=1)
self.path = None
self._path = None
def run_command(self, cmd):
result = subprocess.run(cmd, shell=True, capture_output=True, check=False)
try:
std_out_decoded = result.stdout.decode("utf-8")
except UnicodeDecodeError:
std_out_decoded = result.stdout
result.stdout = std_out_decoded
try:
std_err_decoded = result.stderr.decode("utf-8")
except UnicodeDecodeError:
std_err_decoded = result.stderr
result.stderr = std_err_decoded
return result
def pipenv(self, cmd, block=True):
self.capfd.readouterr()
r = self.run_command(f"pipenv {cmd}")
# Pretty output for failing tests.
out, err = self.capfd.readouterr()
if out:
r.stdout_bytes = r.stdout_bytes + out
if err:
r.stderr_bytes = r.stderr_bytes + err
if block:
print(f'$ pipenv {cmd}')
print(r.stdout)
print(r.stderr, file=sys.stderr)
if r.returncode != 0:
print("Command failed...")
# Where the action happens.
return r
@property
def pipfile(self):
p_path = os.sep.join([self.path, 'Pipfile'])
with open(p_path) as f:
return tomlkit.loads(f.read())
@property
def lockfile(self):
p_path = self.lockfile_path
with open(p_path) as f:
return json.loads(f.read())
@property
def lockfile_path(self):
return os.sep.join([self.path, 'Pipfile.lock'])
if sys.version_info[:2] <= (3, 8):
# Windows python3.8 fails without this patch. Additional details: https://bugs.python.org/issue42796
def _rmtree_func(path, ignore_errors=True, onerror=None):
shutil_rmtree = _rmtree
if onerror is None:
onerror = handle_remove_readonly
try:
shutil_rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
except (OSError, FileNotFoundError, PermissionError) as exc:
# Ignore removal failures where the file doesn't exist
if exc.errno != errno.ENOENT:
raise
else:
_rmtree_func = _rmtree
@pytest.fixture()
def pipenv_instance_pypi(capfdbinary, monkeypatch):
with temp_environ(), monkeypatch.context() as m:
m.setattr(shutil, "rmtree", _rmtree_func)
original_umask = os.umask(0o007)
os.environ["PIPENV_NOSPIN"] = "1"
os.environ["CI"] = "1"
os.environ["PIPENV_DONT_USE_PYENV"] = "1"
warnings.simplefilter("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*<ssl.SSLSocket.*>")
try:
yield functools.partial(_PipenvInstance, capfd=capfdbinary, index_url="https://pypi.org/simple")
finally:
os.umask(original_umask)
@pytest.fixture()
def pipenv_instance_private_pypi(capfdbinary, monkeypatch):
with temp_environ(), monkeypatch.context() as m:
m.setattr(shutil, "rmtree", _rmtree_func)
original_umask = os.umask(0o007)
os.environ["PIPENV_NOSPIN"] = "1"
os.environ["CI"] = "1"
os.environ["PIPENV_DONT_USE_PYENV"] = "1"
warnings.simplefilter("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*<ssl.SSLSocket.*>")
try:
yield functools.partial(_PipenvInstance, capfd=capfdbinary, index_url=DEFAULT_PRIVATE_PYPI_SERVER)
finally:
os.umask(original_umask)
@pytest.fixture()
def testsroot():
return TESTS_ROOT
|