2018-10-08 18:09:53 +02:00
|
|
|
import compileall
|
2018-11-29 09:50:33 +01:00
|
|
|
import fnmatch
|
2023-08-02 04:11:07 +02:00
|
|
|
import http.server
|
2017-03-26 17:18:29 +02:00
|
|
|
import io
|
2014-05-07 16:20:14 +02:00
|
|
|
import os
|
2019-10-13 23:32:00 +02:00
|
|
|
import re
|
2013-08-22 13:49:53 +02:00
|
|
|
import shutil
|
2017-05-14 00:23:17 +02:00
|
|
|
import subprocess
|
2017-03-26 17:18:29 +02:00
|
|
|
import sys
|
2023-08-02 04:11:07 +02:00
|
|
|
import threading
|
2020-12-28 00:48:06 +01:00
|
|
|
from contextlib import ExitStack, contextmanager
|
2023-08-02 04:11:07 +02:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from enum import Enum
|
|
|
|
from hashlib import sha256
|
2022-06-07 11:52:38 +02:00
|
|
|
from pathlib import Path
|
2023-08-02 04:11:07 +02:00
|
|
|
from textwrap import dedent
|
2022-06-07 11:52:38 +02:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
2023-08-02 04:11:07 +02:00
|
|
|
Any,
|
2022-12-15 12:19:15 +01:00
|
|
|
AnyStr,
|
2022-06-07 11:52:38 +02:00
|
|
|
Callable,
|
2023-08-02 04:11:07 +02:00
|
|
|
ClassVar,
|
2022-06-07 11:52:38 +02:00
|
|
|
Dict,
|
|
|
|
Iterable,
|
|
|
|
Iterator,
|
|
|
|
List,
|
|
|
|
Optional,
|
2023-08-02 04:11:07 +02:00
|
|
|
Set,
|
|
|
|
Tuple,
|
2022-06-07 11:52:38 +02:00
|
|
|
Union,
|
|
|
|
)
|
2021-02-10 11:38:21 +01:00
|
|
|
from unittest.mock import patch
|
2022-07-12 00:52:44 +02:00
|
|
|
from zipfile import ZipFile
|
2013-08-22 13:49:53 +02:00
|
|
|
|
2017-06-13 14:17:00 +02:00
|
|
|
import pytest
|
2021-08-28 17:52:10 +02:00
|
|
|
|
|
|
|
# Config will be available from the public API in pytest >= 7.0.0:
|
|
|
|
# https://github.com/pytest-dev/pytest/commit/88d84a57916b592b070f4201dc84f0286d1f9fef
|
|
|
|
from _pytest.config import Config
|
|
|
|
|
|
|
|
# Parser will be available from the public API in pytest >= 7.0.0:
|
|
|
|
# https://github.com/pytest-dev/pytest/commit/538b5c24999e9ebb4fab43faabc8bcc28737bcdf
|
|
|
|
from _pytest.config.argparsing import Parser
|
2022-07-23 15:17:23 +02:00
|
|
|
from installer import install
|
|
|
|
from installer.destinations import SchemeDictionaryDestination
|
|
|
|
from installer.sources import WheelFile
|
2017-03-26 17:49:02 +02:00
|
|
|
|
2022-07-12 00:52:44 +02:00
|
|
|
from pip import __file__ as pip_location
|
2019-12-19 02:21:22 +01:00
|
|
|
from pip._internal.cli.main import main as pip_entry_point
|
2021-08-13 12:08:57 +02:00
|
|
|
from pip._internal.locations import _USE_SYSCONFIG
|
2019-12-08 03:28:38 +01:00
|
|
|
from pip._internal.utils.temp_dir import global_tempdir_manager
|
2020-03-31 08:42:40 +02:00
|
|
|
from tests.lib import DATA_DIR, SRC_DIR, PipTestEnvironment, TestData
|
2021-02-19 13:56:59 +01:00
|
|
|
from tests.lib.server import MockServer as _MockServer
|
2021-08-22 06:04:11 +02:00
|
|
|
from tests.lib.server import make_mock_server, server_running
|
2021-08-28 17:52:10 +02:00
|
|
|
from tests.lib.venv import VirtualEnvironment, VirtualEnvironmentType
|
2013-08-22 04:28:15 +02:00
|
|
|
|
2020-12-28 00:48:06 +01:00
|
|
|
from .lib.compat import nullcontext
|
|
|
|
|
2021-08-22 06:04:11 +02:00
|
|
|
if TYPE_CHECKING:
|
2021-09-13 00:31:12 +02:00
|
|
|
from typing import Protocol
|
|
|
|
|
2023-07-04 11:01:08 +02:00
|
|
|
from _typeshed.wsgi import WSGIApplication
|
2021-09-13 00:31:12 +02:00
|
|
|
else:
|
|
|
|
# TODO: Protocol was introduced in Python 3.8. Remove this branch when
|
|
|
|
# dropping support for Python 3.7.
|
|
|
|
Protocol = object
|
2021-08-22 06:04:11 +02:00
|
|
|
|
2013-08-22 04:28:15 +02:00
|
|
|
|
2021-08-28 17:52:10 +02:00
|
|
|
def pytest_addoption(parser: Parser) -> None:
|
2018-07-30 16:27:58 +02:00
|
|
|
parser.addoption(
|
2020-05-14 11:09:33 +02:00
|
|
|
"--keep-tmpdir",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="keep temporary test directories",
|
|
|
|
)
|
|
|
|
parser.addoption(
|
2020-10-30 01:36:18 +01:00
|
|
|
"--resolver",
|
|
|
|
action="store",
|
|
|
|
default="2020-resolver",
|
|
|
|
choices=["2020-resolver", "legacy"],
|
|
|
|
help="use given resolver in tests",
|
2020-05-20 16:37:28 +02:00
|
|
|
)
|
2020-05-14 11:09:33 +02:00
|
|
|
parser.addoption(
|
|
|
|
"--use-venv",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="use venv for virtual environment creation",
|
2018-07-30 16:27:58 +02:00
|
|
|
)
|
2020-12-15 10:18:12 +01:00
|
|
|
parser.addoption(
|
|
|
|
"--run-search",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="run 'pip search' tests",
|
|
|
|
)
|
2022-03-11 16:04:21 +01:00
|
|
|
parser.addoption(
|
|
|
|
"--proxy",
|
|
|
|
action="store",
|
|
|
|
default=None,
|
|
|
|
help="use given proxy in session network tests",
|
|
|
|
)
|
2022-07-11 17:26:24 +02:00
|
|
|
parser.addoption(
|
|
|
|
"--use-zipapp",
|
2022-07-11 21:01:26 +02:00
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="use a zipapp when running pip in tests",
|
2022-07-11 17:26:24 +02:00
|
|
|
)
|
2018-07-30 16:27:58 +02:00
|
|
|
|
|
|
|
|
2022-06-23 17:30:06 +02:00
|
|
|
def pytest_collection_modifyitems(config: Config, items: List[pytest.Function]) -> None:
|
2014-07-01 02:00:35 +02:00
|
|
|
for item in items:
|
2021-04-02 11:22:08 +02:00
|
|
|
if not hasattr(item, "module"): # e.g.: DoctestTextfile
|
2015-03-19 22:24:25 +01:00
|
|
|
continue
|
2017-05-19 05:18:45 +02:00
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
if item.get_closest_marker("search") and not config.getoption("--run-search"):
|
|
|
|
item.add_marker(pytest.mark.skip("pip search test skipped"))
|
2020-12-15 10:18:12 +01:00
|
|
|
|
2020-10-25 18:32:59 +01:00
|
|
|
if "CI" in os.environ:
|
|
|
|
# Mark network tests as flaky
|
2021-04-02 11:22:08 +02:00
|
|
|
if item.get_closest_marker("network") is not None:
|
2020-10-25 18:33:10 +01:00
|
|
|
item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2))
|
2017-05-19 05:18:45 +02:00
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
if (
|
|
|
|
item.get_closest_marker("incompatible_with_venv")
|
|
|
|
and sys.prefix != sys.base_prefix
|
|
|
|
):
|
|
|
|
item.add_marker(pytest.mark.skip("Incompatible with venv"))
|
2018-10-09 08:23:03 +02:00
|
|
|
|
2021-08-13 12:08:57 +02:00
|
|
|
if item.get_closest_marker("incompatible_with_sysconfig") and _USE_SYSCONFIG:
|
|
|
|
item.add_marker(pytest.mark.skip("Incompatible with sysconfig"))
|
|
|
|
|
2022-06-23 17:30:06 +02:00
|
|
|
module_file = item.module.__file__
|
2014-07-01 02:00:35 +02:00
|
|
|
module_path = os.path.relpath(
|
2021-08-28 17:52:10 +02:00
|
|
|
module_file, os.path.commonprefix([__file__, module_file])
|
2014-07-01 02:00:35 +02:00
|
|
|
)
|
|
|
|
|
2014-08-29 21:42:06 +02:00
|
|
|
module_root_dir = module_path.split(os.pathsep)[0]
|
2021-04-02 11:22:08 +02:00
|
|
|
if (
|
|
|
|
module_root_dir.startswith("functional")
|
|
|
|
or module_root_dir.startswith("integration")
|
|
|
|
or module_root_dir.startswith("lib")
|
|
|
|
):
|
2014-07-01 02:00:35 +02:00
|
|
|
item.add_marker(pytest.mark.integration)
|
2014-08-29 21:42:06 +02:00
|
|
|
elif module_root_dir.startswith("unit"):
|
2014-07-01 02:00:35 +02:00
|
|
|
item.add_marker(pytest.mark.unit)
|
2022-03-02 23:04:10 +01:00
|
|
|
|
|
|
|
# We don't want to allow using the script resource if this is a
|
|
|
|
# unit test, as unit tests should not need all that heavy lifting
|
2022-06-23 17:30:06 +02:00
|
|
|
if "script" in item.fixturenames:
|
2022-03-02 23:04:10 +01:00
|
|
|
raise RuntimeError(
|
|
|
|
"Cannot use the ``script`` funcarg in a unit test: "
|
|
|
|
"(filename = {}, item = {})".format(module_path, item)
|
|
|
|
)
|
2014-07-01 02:00:35 +02:00
|
|
|
else:
|
2021-04-02 11:22:08 +02:00
|
|
|
raise RuntimeError(f"Unknown test type (filename = {module_path})")
|
2014-07-01 02:00:35 +02:00
|
|
|
|
|
|
|
|
2020-05-14 11:09:33 +02:00
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
2021-08-28 17:52:10 +02:00
|
|
|
def resolver_variant(request: pytest.FixtureRequest) -> Iterator[str]:
|
2021-04-02 11:22:08 +02:00
|
|
|
"""Set environment variable to make pip default to the correct resolver."""
|
2020-10-30 01:36:18 +01:00
|
|
|
resolver = request.config.getoption("--resolver")
|
|
|
|
|
|
|
|
# Handle the environment variables for this test.
|
2020-07-23 10:01:37 +02:00
|
|
|
features = set(os.environ.get("PIP_USE_FEATURE", "").split())
|
2020-10-30 02:27:08 +01:00
|
|
|
deprecated_features = set(os.environ.get("PIP_USE_DEPRECATED", "").split())
|
|
|
|
|
2020-12-20 20:58:50 +01:00
|
|
|
if resolver == "legacy":
|
|
|
|
deprecated_features.add("legacy-resolver")
|
2020-05-14 11:09:33 +02:00
|
|
|
else:
|
2020-12-20 20:58:50 +01:00
|
|
|
deprecated_features.discard("legacy-resolver")
|
2020-10-30 01:28:02 +01:00
|
|
|
|
2020-10-30 02:27:08 +01:00
|
|
|
env = {
|
|
|
|
"PIP_USE_FEATURE": " ".join(features),
|
|
|
|
"PIP_USE_DEPRECATED": " ".join(deprecated_features),
|
|
|
|
}
|
|
|
|
with patch.dict(os.environ, env):
|
2020-10-30 01:36:18 +01:00
|
|
|
yield resolver
|
2020-05-14 11:09:33 +02:00
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2022-06-07 11:52:38 +02:00
|
|
|
def tmp_path_factory(
|
|
|
|
request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory
|
|
|
|
) -> Iterator[pytest.TempPathFactory]:
|
2021-04-02 11:22:08 +02:00
|
|
|
"""Modified `tmpdir_factory` session fixture
|
2018-10-17 18:59:12 +02:00
|
|
|
that will automatically cleanup after itself.
|
|
|
|
"""
|
2022-06-07 11:52:38 +02:00
|
|
|
yield tmp_path_factory
|
2018-10-17 18:59:12 +02:00
|
|
|
if not request.config.getoption("--keep-tmpdir"):
|
2020-01-09 08:30:49 +01:00
|
|
|
shutil.rmtree(
|
2022-06-07 11:52:38 +02:00
|
|
|
tmp_path_factory.getbasetemp(),
|
2020-01-09 08:30:49 +01:00
|
|
|
ignore_errors=True,
|
|
|
|
)
|
2018-10-17 18:59:12 +02:00
|
|
|
|
|
|
|
|
2022-06-07 11:52:38 +02:00
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def tmpdir_factory(tmp_path_factory: pytest.TempPathFactory) -> pytest.TempPathFactory:
|
|
|
|
"""Override Pytest's ``tmpdir_factory`` with our pathlib implementation.
|
|
|
|
|
|
|
|
This prevents mis-use of this fixture.
|
|
|
|
"""
|
|
|
|
return tmp_path_factory
|
|
|
|
|
|
|
|
|
2019-09-16 01:51:24 +02:00
|
|
|
@pytest.fixture
|
2022-06-07 11:52:38 +02:00
|
|
|
def tmp_path(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[Path]:
|
2013-08-22 04:28:15 +02:00
|
|
|
"""
|
|
|
|
Return a temporary directory path object which is unique to each test
|
|
|
|
function invocation, created as a sub directory of the base temporary
|
2022-06-07 11:52:38 +02:00
|
|
|
directory. The returned object is a ``Path`` object.
|
2013-08-22 04:28:15 +02:00
|
|
|
|
2022-06-07 11:52:38 +02:00
|
|
|
This uses the built-in tmp_path fixture from pytest itself, but deletes the
|
|
|
|
temporary directories at the end of each test case.
|
2013-08-22 04:28:15 +02:00
|
|
|
"""
|
2022-06-07 11:52:38 +02:00
|
|
|
assert tmp_path.is_dir()
|
|
|
|
yield tmp_path
|
2014-02-08 03:35:37 +01:00
|
|
|
# Clear out the temporary directory after the test has finished using it.
|
|
|
|
# This should prevent us from needing a multiple gigabyte temporary
|
|
|
|
# directory while running the tests.
|
2018-07-30 16:27:58 +02:00
|
|
|
if not request.config.getoption("--keep-tmpdir"):
|
2022-06-07 11:52:38 +02:00
|
|
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def tmpdir(tmp_path: Path) -> Path:
|
|
|
|
"""Override Pytest's ``tmpdir`` with our pathlib implementation.
|
|
|
|
|
|
|
|
This prevents mis-use of this fixture.
|
|
|
|
"""
|
|
|
|
return tmp_path
|
2013-08-22 06:39:07 +02:00
|
|
|
|
|
|
|
|
2014-05-07 16:20:14 +02:00
|
|
|
@pytest.fixture(autouse=True)
|
2021-08-28 17:52:10 +02:00
|
|
|
def isolate(tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
2014-05-07 16:20:14 +02:00
|
|
|
"""
|
|
|
|
Isolate our tests so that things like global configuration files and the
|
|
|
|
like do not affect our test results.
|
|
|
|
|
|
|
|
We use an autouse function scoped fixture because we want to ensure that
|
|
|
|
every test has it's own isolated home directory.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# TODO: Figure out how to isolate from *system* level configuration files
|
|
|
|
# as well as user level configuration files.
|
|
|
|
|
|
|
|
# Create a directory to use as our home location.
|
|
|
|
home_dir = os.path.join(str(tmpdir), "home")
|
|
|
|
os.makedirs(home_dir)
|
|
|
|
|
|
|
|
# Create a directory to use as a fake root
|
|
|
|
fake_root = os.path.join(str(tmpdir), "fake-root")
|
|
|
|
os.makedirs(fake_root)
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
if sys.platform == "win32":
|
2017-10-06 21:51:42 +02:00
|
|
|
# Note: this will only take effect in subprocesses...
|
|
|
|
home_drive, home_path = os.path.splitdrive(home_dir)
|
2021-04-02 11:22:08 +02:00
|
|
|
monkeypatch.setenv("USERPROFILE", home_dir)
|
|
|
|
monkeypatch.setenv("HOMEDRIVE", home_drive)
|
|
|
|
monkeypatch.setenv("HOMEPATH", home_path)
|
2017-10-06 21:51:42 +02:00
|
|
|
for env_var, sub_path in (
|
2021-04-02 11:22:08 +02:00
|
|
|
("APPDATA", "AppData/Roaming"),
|
|
|
|
("LOCALAPPDATA", "AppData/Local"),
|
2017-10-06 21:51:42 +02:00
|
|
|
):
|
2021-04-02 11:22:08 +02:00
|
|
|
path = os.path.join(home_dir, *sub_path.split("/"))
|
2020-07-23 10:01:37 +02:00
|
|
|
monkeypatch.setenv(env_var, path)
|
2017-10-06 21:51:42 +02:00
|
|
|
os.makedirs(path)
|
|
|
|
else:
|
|
|
|
# Set our home directory to our temporary directory, this should force
|
|
|
|
# all of our relative configuration files to be read from here instead
|
|
|
|
# of the user's actual $HOME directory.
|
2020-07-23 10:01:37 +02:00
|
|
|
monkeypatch.setenv("HOME", home_dir)
|
2017-10-06 21:51:42 +02:00
|
|
|
# Isolate ourselves from XDG directories
|
2021-04-02 11:22:08 +02:00
|
|
|
monkeypatch.setenv(
|
|
|
|
"XDG_DATA_HOME",
|
|
|
|
os.path.join(
|
|
|
|
home_dir,
|
|
|
|
".local",
|
|
|
|
"share",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
monkeypatch.setenv(
|
|
|
|
"XDG_CONFIG_HOME",
|
|
|
|
os.path.join(
|
|
|
|
home_dir,
|
|
|
|
".config",
|
|
|
|
),
|
|
|
|
)
|
2020-07-23 10:01:37 +02:00
|
|
|
monkeypatch.setenv("XDG_CACHE_HOME", os.path.join(home_dir, ".cache"))
|
2021-04-02 11:22:08 +02:00
|
|
|
monkeypatch.setenv(
|
|
|
|
"XDG_RUNTIME_DIR",
|
|
|
|
os.path.join(
|
|
|
|
home_dir,
|
|
|
|
".runtime",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
monkeypatch.setenv(
|
|
|
|
"XDG_DATA_DIRS",
|
|
|
|
os.pathsep.join(
|
|
|
|
[
|
|
|
|
os.path.join(fake_root, "usr", "local", "share"),
|
|
|
|
os.path.join(fake_root, "usr", "share"),
|
|
|
|
]
|
|
|
|
),
|
|
|
|
)
|
|
|
|
monkeypatch.setenv(
|
|
|
|
"XDG_CONFIG_DIRS",
|
|
|
|
os.path.join(
|
|
|
|
fake_root,
|
|
|
|
"etc",
|
|
|
|
"xdg",
|
|
|
|
),
|
|
|
|
)
|
2014-05-07 16:20:14 +02:00
|
|
|
|
|
|
|
# Configure git, because without an author name/email git will complain
|
|
|
|
# and cause test failures.
|
2020-07-23 10:01:37 +02:00
|
|
|
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
|
|
monkeypatch.setenv("GIT_AUTHOR_NAME", "pip")
|
|
|
|
monkeypatch.setenv("GIT_AUTHOR_EMAIL", "distutils-sig@python.org")
|
2014-05-07 16:20:14 +02:00
|
|
|
|
2014-12-24 01:37:00 +01:00
|
|
|
# We want to disable the version check from running in the tests
|
2020-07-23 10:01:37 +02:00
|
|
|
monkeypatch.setenv("PIP_DISABLE_PIP_VERSION_CHECK", "true")
|
2014-12-24 01:37:00 +01:00
|
|
|
|
2018-04-23 11:41:34 +02:00
|
|
|
# Make sure tests don't share a requirements tracker.
|
2022-03-26 11:04:54 +01:00
|
|
|
monkeypatch.delenv("PIP_BUILD_TRACKER", False)
|
2018-04-23 11:41:34 +02:00
|
|
|
|
2017-10-06 21:51:42 +02:00
|
|
|
# FIXME: Windows...
|
2014-05-07 16:20:14 +02:00
|
|
|
os.makedirs(os.path.join(home_dir, ".config", "git"))
|
|
|
|
with open(os.path.join(home_dir, ".config", "git", "config"), "wb") as fp:
|
2021-04-02 11:22:08 +02:00
|
|
|
fp.write(b"[user]\n\tname = pip\n\temail = distutils-sig@python.org\n")
|
2014-05-07 16:20:14 +02:00
|
|
|
|
|
|
|
|
2019-12-08 03:28:38 +01:00
|
|
|
@pytest.fixture(autouse=True)
|
2021-08-28 17:52:10 +02:00
|
|
|
def scoped_global_tempdir_manager(request: pytest.FixtureRequest) -> Iterator[None]:
|
2019-12-08 03:28:38 +01:00
|
|
|
"""Make unit tests with globally-managed tempdirs easier
|
|
|
|
|
|
|
|
Each test function gets its own individual scope for globally-managed
|
|
|
|
temporary directories in the application.
|
|
|
|
"""
|
2020-01-25 21:45:49 +01:00
|
|
|
if "no_auto_tempdir_manager" in request.keywords:
|
|
|
|
ctx = nullcontext
|
|
|
|
else:
|
|
|
|
ctx = global_tempdir_manager
|
|
|
|
|
|
|
|
with ctx():
|
2019-12-08 03:28:38 +01:00
|
|
|
yield
|
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2022-06-07 11:52:38 +02:00
|
|
|
def pip_src(tmpdir_factory: pytest.TempPathFactory) -> Path:
|
2021-08-28 17:52:10 +02:00
|
|
|
def not_code_files_and_folders(path: str, names: List[str]) -> Iterable[str]:
|
2019-08-08 04:59:10 +02:00
|
|
|
# In the root directory...
|
2022-06-12 06:19:14 +02:00
|
|
|
if os.path.samefile(path, SRC_DIR):
|
2019-08-08 04:59:10 +02:00
|
|
|
# ignore all folders except "src"
|
2021-08-28 17:52:10 +02:00
|
|
|
folders = {
|
|
|
|
name for name in names if os.path.isdir(os.path.join(path, name))
|
|
|
|
}
|
2019-08-08 04:59:10 +02:00
|
|
|
to_ignore = folders - {"src"}
|
|
|
|
# and ignore ".git" if present (which may be a file if in a linked
|
|
|
|
# worktree).
|
|
|
|
if ".git" in names:
|
|
|
|
to_ignore.add(".git")
|
|
|
|
return to_ignore
|
2018-11-29 09:50:33 +01:00
|
|
|
|
|
|
|
# Ignore all compiled files and egg-info.
|
2019-09-24 13:45:19 +02:00
|
|
|
ignored = set()
|
|
|
|
for pattern in ("__pycache__", "*.pyc", "pip.egg-info"):
|
|
|
|
ignored.update(fnmatch.filter(names, pattern))
|
|
|
|
return ignored
|
2018-11-29 09:50:33 +01:00
|
|
|
|
2022-06-07 11:52:38 +02:00
|
|
|
pip_src = tmpdir_factory.mktemp("pip_src").joinpath("pip_src")
|
2018-04-15 00:33:28 +02:00
|
|
|
# Copy over our source tree so that each use is self contained
|
2014-01-28 15:17:51 +01:00
|
|
|
shutil.copytree(
|
|
|
|
SRC_DIR,
|
2019-10-07 14:30:59 +02:00
|
|
|
pip_src.resolve(),
|
2018-11-29 09:50:33 +01:00
|
|
|
ignore=not_code_files_and_folders,
|
2013-08-24 12:43:01 +02:00
|
|
|
)
|
2018-04-15 00:33:28 +02:00
|
|
|
return pip_src
|
|
|
|
|
|
|
|
|
2021-08-28 17:52:10 +02:00
|
|
|
def _common_wheel_editable_install(
|
2022-06-07 11:52:38 +02:00
|
|
|
tmpdir_factory: pytest.TempPathFactory, common_wheels: Path, package: str
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> Path:
|
2021-04-02 11:22:08 +02:00
|
|
|
wheel_candidates = list(common_wheels.glob(f"{package}-*.whl"))
|
2018-10-08 18:09:53 +02:00
|
|
|
assert len(wheel_candidates) == 1, wheel_candidates
|
2022-06-07 11:52:38 +02:00
|
|
|
install_dir = tmpdir_factory.mktemp(package) / "install"
|
2022-07-23 15:17:23 +02:00
|
|
|
lib_install_dir = install_dir / "lib"
|
|
|
|
bin_install_dir = install_dir / "bin"
|
|
|
|
with WheelFile.open(wheel_candidates[0]) as source:
|
|
|
|
install(
|
|
|
|
source,
|
|
|
|
SchemeDictionaryDestination(
|
|
|
|
{
|
|
|
|
"purelib": os.fspath(lib_install_dir),
|
|
|
|
"platlib": os.fspath(lib_install_dir),
|
|
|
|
"scripts": os.fspath(bin_install_dir),
|
|
|
|
},
|
|
|
|
interpreter=sys.executable,
|
|
|
|
script_kind="posix",
|
|
|
|
),
|
|
|
|
additional_metadata={},
|
|
|
|
)
|
|
|
|
# The scripts are not necessary for our use cases, and they would be installed with
|
|
|
|
# the wrong interpreter, so remove them.
|
|
|
|
# TODO consider a refactoring by adding a install_from_wheel(path) method
|
|
|
|
# to the virtualenv fixture.
|
|
|
|
if bin_install_dir.exists():
|
|
|
|
shutil.rmtree(bin_install_dir)
|
|
|
|
return lib_install_dir
|
2018-10-08 18:09:53 +02:00
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def setuptools_install(
|
2022-06-07 11:52:38 +02:00
|
|
|
tmpdir_factory: pytest.TempPathFactory, common_wheels: Path
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> Path:
|
2021-04-02 11:22:08 +02:00
|
|
|
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "setuptools")
|
2018-10-08 18:09:53 +02:00
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2022-06-07 11:52:38 +02:00
|
|
|
def wheel_install(tmpdir_factory: pytest.TempPathFactory, common_wheels: Path) -> Path:
|
2021-04-02 11:22:08 +02:00
|
|
|
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "wheel")
|
2018-10-08 18:09:53 +02:00
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def coverage_install(
|
2022-06-07 11:52:38 +02:00
|
|
|
tmpdir_factory: pytest.TempPathFactory, common_wheels: Path
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> Path:
|
2021-04-02 11:22:08 +02:00
|
|
|
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "coverage")
|
2019-12-01 01:13:06 +01:00
|
|
|
|
|
|
|
|
2022-07-23 15:17:23 +02:00
|
|
|
def install_pth_link(
|
|
|
|
venv: VirtualEnvironment, project_name: str, lib_dir: Path
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> None:
|
2022-07-23 15:17:23 +02:00
|
|
|
venv.site.joinpath(f"_pip_testsuite_{project_name}.pth").write_text(
|
|
|
|
str(lib_dir.resolve()), encoding="utf-8"
|
|
|
|
)
|
2018-10-08 18:09:53 +02:00
|
|
|
|
|
|
|
|
2021-04-02 11:22:08 +02:00
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def virtualenv_template(
|
2021-08-28 17:52:10 +02:00
|
|
|
request: pytest.FixtureRequest,
|
2022-06-07 11:52:38 +02:00
|
|
|
tmpdir_factory: pytest.TempPathFactory,
|
2021-08-28 17:52:10 +02:00
|
|
|
pip_src: Path,
|
|
|
|
setuptools_install: Path,
|
2023-03-18 14:17:39 +01:00
|
|
|
wheel_install: Path,
|
2021-08-28 17:52:10 +02:00
|
|
|
coverage_install: Path,
|
|
|
|
) -> Iterator[VirtualEnvironment]:
|
|
|
|
venv_type: VirtualEnvironmentType
|
2021-04-02 11:22:08 +02:00
|
|
|
if request.config.getoption("--use-venv"):
|
|
|
|
venv_type = "venv"
|
2018-10-09 08:23:03 +02:00
|
|
|
else:
|
2021-04-02 11:22:08 +02:00
|
|
|
venv_type = "virtualenv"
|
2018-10-09 08:23:03 +02:00
|
|
|
|
2013-08-24 12:43:01 +02:00
|
|
|
# Create the virtual environment
|
2022-06-07 11:52:38 +02:00
|
|
|
tmpdir = tmpdir_factory.mktemp("virtualenv")
|
2021-04-02 11:22:08 +02:00
|
|
|
venv = VirtualEnvironment(tmpdir.joinpath("venv_orig"), venv_type=venv_type)
|
2018-10-08 18:09:53 +02:00
|
|
|
|
2023-03-18 14:17:39 +01:00
|
|
|
# Install setuptools, wheel and pip.
|
2022-07-23 15:17:23 +02:00
|
|
|
install_pth_link(venv, "setuptools", setuptools_install)
|
2023-03-18 14:17:39 +01:00
|
|
|
install_pth_link(venv, "wheel", wheel_install)
|
2022-06-07 11:52:38 +02:00
|
|
|
pip_editable = tmpdir_factory.mktemp("pip") / "pip"
|
2019-07-20 02:04:34 +02:00
|
|
|
shutil.copytree(pip_src, pip_editable, symlinks=True)
|
2019-10-13 23:32:00 +02:00
|
|
|
# noxfile.py is Python 3 only
|
|
|
|
assert compileall.compile_dir(
|
2021-04-02 11:22:08 +02:00
|
|
|
str(pip_editable),
|
|
|
|
quiet=1,
|
|
|
|
rx=re.compile("noxfile.py$"),
|
|
|
|
)
|
|
|
|
subprocess.check_call(
|
2022-06-07 11:52:38 +02:00
|
|
|
[os.fspath(venv.bin / "python"), "setup.py", "-q", "develop"], cwd=pip_editable
|
2019-10-13 23:32:00 +02:00
|
|
|
)
|
2018-10-08 18:09:53 +02:00
|
|
|
|
2019-12-01 01:13:06 +01:00
|
|
|
# Install coverage and pth file for executing it in any spawned processes
|
|
|
|
# in this virtual environment.
|
2022-07-23 15:17:23 +02:00
|
|
|
install_pth_link(venv, "coverage", coverage_install)
|
2019-12-01 01:13:06 +01:00
|
|
|
# zz prefix ensures the file is after easy-install.pth.
|
2021-04-02 11:22:08 +02:00
|
|
|
with open(venv.site / "zz-coverage-helper.pth", "a") as f:
|
|
|
|
f.write("import coverage; coverage.process_startup()")
|
2019-12-01 01:13:06 +01:00
|
|
|
|
2018-10-08 18:09:53 +02:00
|
|
|
# Drop (non-relocatable) launchers.
|
|
|
|
for exe in os.listdir(venv.bin):
|
|
|
|
if not (
|
2021-04-02 11:22:08 +02:00
|
|
|
exe.startswith("python")
|
|
|
|
or exe.startswith("libpy") # Don't remove libpypy-c.so...
|
2018-10-08 18:09:53 +02:00
|
|
|
):
|
2019-07-02 07:00:32 +02:00
|
|
|
(venv.bin / exe).unlink()
|
2018-10-08 18:09:53 +02:00
|
|
|
|
2017-09-01 23:31:03 +02:00
|
|
|
# Rename original virtualenv directory to make sure
|
|
|
|
# it's not reused by mistake from one of the copies.
|
|
|
|
venv_template = tmpdir / "venv_template"
|
2018-10-08 18:09:53 +02:00
|
|
|
venv.move(venv_template)
|
|
|
|
yield venv
|
2013-08-24 12:43:01 +02:00
|
|
|
|
2015-04-01 03:39:45 +02:00
|
|
|
|
2019-10-30 04:41:34 +01:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def virtualenv_factory(
|
|
|
|
virtualenv_template: VirtualEnvironment,
|
|
|
|
) -> Callable[[Path], VirtualEnvironment]:
|
|
|
|
def factory(tmpdir: Path) -> VirtualEnvironment:
|
2019-10-30 04:41:34 +01:00
|
|
|
return VirtualEnvironment(tmpdir, virtualenv_template)
|
|
|
|
|
|
|
|
return factory
|
|
|
|
|
|
|
|
|
2019-09-16 01:51:24 +02:00
|
|
|
@pytest.fixture
|
2021-08-28 17:52:10 +02:00
|
|
|
def virtualenv(
|
|
|
|
virtualenv_factory: Callable[[Path], VirtualEnvironment], tmpdir: Path
|
|
|
|
) -> Iterator[VirtualEnvironment]:
|
2017-09-01 23:31:03 +02:00
|
|
|
"""
|
|
|
|
Return a virtual environment which is unique to each test function
|
|
|
|
invocation created inside of a sub directory of the test function's
|
|
|
|
temporary directory. The returned object is a
|
|
|
|
``tests.lib.venv.VirtualEnvironment`` object.
|
|
|
|
"""
|
2019-10-30 04:41:34 +01:00
|
|
|
yield virtualenv_factory(tmpdir.joinpath("workspace", "venv"))
|
2013-08-22 06:39:07 +02:00
|
|
|
|
|
|
|
|
2021-09-13 00:31:12 +02:00
|
|
|
class ScriptFactory(Protocol):
|
|
|
|
def __call__(
|
2022-12-15 12:19:15 +01:00
|
|
|
self,
|
|
|
|
tmpdir: Path,
|
|
|
|
virtualenv: Optional[VirtualEnvironment] = None,
|
|
|
|
environ: Optional[Dict[AnyStr, AnyStr]] = None,
|
2021-09-13 00:31:12 +02:00
|
|
|
) -> PipTestEnvironment:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
2019-10-30 04:41:34 +01:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def script_factory(
|
2022-07-12 10:02:11 +02:00
|
|
|
virtualenv_factory: Callable[[Path], VirtualEnvironment],
|
|
|
|
deprecated_python: bool,
|
|
|
|
zipapp: Optional[str],
|
2021-09-13 00:31:12 +02:00
|
|
|
) -> ScriptFactory:
|
2021-08-28 17:52:10 +02:00
|
|
|
def factory(
|
2022-07-12 10:02:11 +02:00
|
|
|
tmpdir: Path,
|
|
|
|
virtualenv: Optional[VirtualEnvironment] = None,
|
2022-12-15 12:19:15 +01:00
|
|
|
environ: Optional[Dict[AnyStr, AnyStr]] = None,
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> PipTestEnvironment:
|
2022-12-15 12:19:15 +01:00
|
|
|
kwargs = {}
|
|
|
|
if environ:
|
|
|
|
kwargs["environ"] = environ
|
2019-10-30 04:41:34 +01:00
|
|
|
if virtualenv is None:
|
|
|
|
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
|
|
|
|
return PipTestEnvironment(
|
|
|
|
# The base location for our test environment
|
|
|
|
tmpdir,
|
|
|
|
# Tell the Test Environment where our virtualenv is located
|
|
|
|
virtualenv=virtualenv,
|
|
|
|
# Do not ignore hidden files, they need to be checked as well
|
|
|
|
ignore_hidden=False,
|
|
|
|
# We are starting with an already empty directory
|
|
|
|
start_clear=False,
|
|
|
|
# We want to ensure no temporary files are left behind, so the
|
|
|
|
# PipTestEnvironment needs to capture and assert against temp
|
|
|
|
capture_temp=True,
|
|
|
|
assert_no_temp=True,
|
|
|
|
# Deprecated python versions produce an extra deprecation warning
|
|
|
|
pip_expect_warning=deprecated_python,
|
2022-07-11 17:26:24 +02:00
|
|
|
# Tell the Test Environment if we want to run pip via a zipapp
|
|
|
|
zipapp=zipapp,
|
2022-12-15 12:19:15 +01:00
|
|
|
**kwargs,
|
2019-10-30 04:41:34 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
return factory
|
|
|
|
|
|
|
|
|
2022-07-12 00:52:44 +02:00
|
|
|
ZIPAPP_MAIN = """\
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
import os
|
|
|
|
import runpy
|
|
|
|
import sys
|
|
|
|
|
|
|
|
lib = os.path.join(os.path.dirname(__file__), "lib")
|
|
|
|
sys.path.insert(0, lib)
|
|
|
|
|
|
|
|
runpy.run_module("pip", run_name="__main__")
|
|
|
|
"""
|
|
|
|
|
2022-07-12 10:02:11 +02:00
|
|
|
|
2022-07-12 00:52:44 +02:00
|
|
|
def make_zipapp_from_pip(zipapp_name: Path) -> None:
|
|
|
|
pip_dir = Path(pip_location).parent
|
|
|
|
with zipapp_name.open("wb") as zipapp_file:
|
|
|
|
zipapp_file.write(b"#!/usr/bin/env python\n")
|
|
|
|
with ZipFile(zipapp_file, "w") as zipapp:
|
|
|
|
for pip_file in pip_dir.rglob("*"):
|
|
|
|
if pip_file.suffix == ".pyc":
|
|
|
|
continue
|
|
|
|
if pip_file.name == "__pycache__":
|
|
|
|
continue
|
|
|
|
rel_name = pip_file.relative_to(pip_dir.parent)
|
|
|
|
zipapp.write(pip_file, arcname=f"lib/{rel_name}")
|
|
|
|
zipapp.writestr("__main__.py", ZIPAPP_MAIN)
|
|
|
|
|
|
|
|
|
2022-07-11 21:01:26 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2022-07-12 10:02:11 +02:00
|
|
|
def zipapp(
|
|
|
|
request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory
|
|
|
|
) -> Optional[str]:
|
2022-07-11 21:01:26 +02:00
|
|
|
"""
|
|
|
|
If the user requested for pip to be run from a zipapp, build that zipapp
|
|
|
|
and return its location. If the user didn't request a zipapp, return None.
|
|
|
|
|
|
|
|
This fixture is session scoped, so the zipapp will only be created once.
|
|
|
|
"""
|
|
|
|
if not request.config.getoption("--use-zipapp"):
|
|
|
|
return None
|
|
|
|
|
|
|
|
temp_location = tmpdir_factory.mktemp("zipapp")
|
|
|
|
pyz_file = temp_location / "pip.pyz"
|
2022-07-12 00:52:44 +02:00
|
|
|
make_zipapp_from_pip(pyz_file)
|
2022-07-11 21:01:26 +02:00
|
|
|
return str(pyz_file)
|
|
|
|
|
|
|
|
|
2013-08-22 06:39:07 +02:00
|
|
|
@pytest.fixture
|
2021-08-28 17:52:10 +02:00
|
|
|
def script(
|
2022-07-11 17:26:24 +02:00
|
|
|
request: pytest.FixtureRequest,
|
2021-08-28 17:52:10 +02:00
|
|
|
tmpdir: Path,
|
|
|
|
virtualenv: VirtualEnvironment,
|
2022-06-07 11:52:38 +02:00
|
|
|
script_factory: ScriptFactory,
|
2021-08-28 17:52:10 +02:00
|
|
|
) -> PipTestEnvironment:
|
2013-08-22 06:39:07 +02:00
|
|
|
"""
|
|
|
|
Return a PipTestEnvironment which is unique to each test function and
|
|
|
|
will execute all commands inside of the unique virtual environment for this
|
|
|
|
test function. The returned object is a
|
2020-03-31 08:25:45 +02:00
|
|
|
``tests.lib.PipTestEnvironment``.
|
2013-08-22 06:39:07 +02:00
|
|
|
"""
|
2022-07-11 21:01:26 +02:00
|
|
|
return script_factory(tmpdir.joinpath("workspace"), virtualenv)
|
2013-08-23 13:09:53 +02:00
|
|
|
|
|
|
|
|
2017-05-14 00:23:17 +02:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def common_wheels() -> Path:
|
2017-05-14 00:23:17 +02:00
|
|
|
"""Provide a directory with latest setuptools and wheel wheels"""
|
2021-04-02 11:22:08 +02:00
|
|
|
return DATA_DIR.joinpath("common_wheels")
|
2017-05-14 00:23:17 +02:00
|
|
|
|
|
|
|
|
2019-10-30 06:38:13 +01:00
|
|
|
@pytest.fixture(scope="session")
|
2022-06-07 11:52:38 +02:00
|
|
|
def shared_data(tmpdir_factory: pytest.TempPathFactory) -> TestData:
|
|
|
|
return TestData.copy(tmpdir_factory.mktemp("data"))
|
2019-10-30 06:38:13 +01:00
|
|
|
|
|
|
|
|
2013-08-23 13:09:53 +02:00
|
|
|
@pytest.fixture
|
2021-08-28 17:52:10 +02:00
|
|
|
def data(tmpdir: Path) -> TestData:
|
2019-07-02 07:00:32 +02:00
|
|
|
return TestData.copy(tmpdir.joinpath("data"))
|
2017-03-26 17:17:02 +02:00
|
|
|
|
|
|
|
|
2020-12-24 22:23:07 +01:00
|
|
|
class InMemoryPipResult:
|
2021-08-28 17:52:10 +02:00
|
|
|
def __init__(self, returncode: int, stdout: str) -> None:
|
2017-03-26 17:17:02 +02:00
|
|
|
self.returncode = returncode
|
|
|
|
self.stdout = stdout
|
|
|
|
|
|
|
|
|
2020-12-24 22:23:07 +01:00
|
|
|
class InMemoryPip:
|
2022-06-07 11:52:38 +02:00
|
|
|
def pip(self, *args: Union[str, Path]) -> InMemoryPipResult:
|
2017-03-26 17:17:02 +02:00
|
|
|
orig_stdout = sys.stdout
|
2020-12-20 20:58:50 +01:00
|
|
|
stdout = io.StringIO()
|
2017-03-26 17:49:02 +02:00
|
|
|
sys.stdout = stdout
|
2017-03-26 17:17:02 +02:00
|
|
|
try:
|
2022-06-07 11:52:38 +02:00
|
|
|
returncode = pip_entry_point([os.fspath(a) for a in args])
|
2017-03-26 17:17:02 +02:00
|
|
|
except SystemExit as e:
|
2023-07-04 11:01:08 +02:00
|
|
|
if isinstance(e.code, int):
|
|
|
|
returncode = e.code
|
|
|
|
elif e.code:
|
|
|
|
returncode = 1
|
|
|
|
else:
|
|
|
|
returncode = 0
|
2017-03-26 17:17:02 +02:00
|
|
|
finally:
|
|
|
|
sys.stdout = orig_stdout
|
|
|
|
return InMemoryPipResult(returncode, stdout.getvalue())
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2021-08-28 17:52:10 +02:00
|
|
|
def in_memory_pip() -> InMemoryPip:
|
2017-03-26 17:17:02 +02:00
|
|
|
return InMemoryPip()
|
2019-01-11 11:12:44 +01:00
|
|
|
|
|
|
|
|
2019-10-30 04:41:34 +01:00
|
|
|
@pytest.fixture(scope="session")
|
2021-08-28 17:52:10 +02:00
|
|
|
def deprecated_python() -> bool:
|
2020-08-18 14:22:16 +02:00
|
|
|
"""Used to indicate whether pip deprecated this Python version"""
|
2020-11-30 21:57:40 +01:00
|
|
|
return sys.version_info[:2] in []
|
2019-11-03 19:09:18 +01:00
|
|
|
|
|
|
|
|
2021-09-13 00:31:12 +02:00
|
|
|
CertFactory = Callable[[], str]
|
|
|
|
|
|
|
|
|
2019-11-03 19:09:18 +01:00
|
|
|
@pytest.fixture(scope="session")
|
2022-06-07 11:52:38 +02:00
|
|
|
def cert_factory(tmpdir_factory: pytest.TempPathFactory) -> CertFactory:
|
2021-11-26 16:12:16 +01:00
|
|
|
# Delay the import requiring cryptography in order to make it possible
|
|
|
|
# to deselect relevant tests on systems where cryptography cannot
|
|
|
|
# be installed.
|
|
|
|
from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key
|
|
|
|
|
2021-08-08 02:14:17 +02:00
|
|
|
def factory() -> str:
|
2021-04-02 11:22:08 +02:00
|
|
|
"""Returns path to cert/key file."""
|
2022-06-07 11:52:38 +02:00
|
|
|
output_path = tmpdir_factory.mktemp("certs") / "cert.pem"
|
2019-11-03 19:09:18 +01:00
|
|
|
# Must be Text on PY2.
|
2020-12-24 17:53:38 +01:00
|
|
|
cert, key = make_tls_cert("localhost")
|
2019-11-03 19:09:18 +01:00
|
|
|
with open(str(output_path), "wb") as f:
|
|
|
|
f.write(serialize_cert(cert))
|
|
|
|
f.write(serialize_key(key))
|
|
|
|
|
|
|
|
return str(output_path)
|
|
|
|
|
|
|
|
return factory
|
2019-11-17 23:36:36 +01:00
|
|
|
|
|
|
|
|
2020-12-24 22:23:07 +01:00
|
|
|
class MockServer:
|
2021-08-08 02:14:17 +02:00
|
|
|
def __init__(self, server: _MockServer) -> None:
|
2019-11-17 23:36:36 +01:00
|
|
|
self._server = server
|
|
|
|
self._running = False
|
|
|
|
self.context = ExitStack()
|
|
|
|
|
|
|
|
@property
|
2021-08-28 17:52:10 +02:00
|
|
|
def port(self) -> int:
|
2019-11-17 23:36:36 +01:00
|
|
|
return self._server.port
|
|
|
|
|
|
|
|
@property
|
2021-08-28 17:52:10 +02:00
|
|
|
def host(self) -> str:
|
2019-11-17 23:36:36 +01:00
|
|
|
return self._server.host
|
|
|
|
|
2021-08-22 06:04:11 +02:00
|
|
|
def set_responses(self, responses: Iterable["WSGIApplication"]) -> None:
|
2019-11-17 23:36:36 +01:00
|
|
|
assert not self._running, "responses cannot be set on running server"
|
|
|
|
self._server.mock.side_effect = responses
|
|
|
|
|
2021-08-08 02:14:17 +02:00
|
|
|
def start(self) -> None:
|
2019-11-17 23:36:36 +01:00
|
|
|
assert not self._running, "running server cannot be started"
|
|
|
|
self.context.enter_context(server_running(self._server))
|
|
|
|
self.context.enter_context(self._set_running())
|
|
|
|
|
|
|
|
@contextmanager
|
2021-08-28 17:52:10 +02:00
|
|
|
def _set_running(self) -> Iterator[None]:
|
2019-11-17 23:36:36 +01:00
|
|
|
self._running = True
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
self._running = False
|
|
|
|
|
2021-08-08 02:14:17 +02:00
|
|
|
def stop(self) -> None:
|
2019-11-17 23:36:36 +01:00
|
|
|
assert self._running, "idle server cannot be stopped"
|
|
|
|
self.context.close()
|
|
|
|
|
2021-08-22 06:04:11 +02:00
|
|
|
def get_requests(self) -> List[Dict[str, str]]:
|
2021-04-02 11:22:08 +02:00
|
|
|
"""Get environ for each received request."""
|
2019-11-17 23:36:36 +01:00
|
|
|
assert not self._running, "cannot get mock from running server"
|
2021-02-11 07:48:14 +01:00
|
|
|
# Legacy: replace call[0][0] with call.args[0]
|
|
|
|
# when pip drops support for python3.7
|
2021-04-02 11:22:08 +02:00
|
|
|
return [call[0][0] for call in self._server.mock.call_args_list]
|
2019-11-17 23:36:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2021-08-28 17:52:10 +02:00
|
|
|
def mock_server() -> Iterator[MockServer]:
|
2019-11-17 23:36:36 +01:00
|
|
|
server = make_mock_server()
|
|
|
|
test_server = MockServer(server)
|
|
|
|
with test_server.context:
|
|
|
|
yield test_server
|
2020-07-23 11:32:26 +02:00
|
|
|
|
|
|
|
|
2022-03-11 16:04:21 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def proxy(request: pytest.FixtureRequest) -> str:
|
|
|
|
return request.config.getoption("proxy")
|
2022-10-27 15:34:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def enable_user_site(virtualenv: VirtualEnvironment) -> None:
|
|
|
|
virtualenv.user_site_packages = True
|
2023-08-02 04:11:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
class MetadataKind(Enum):
|
|
|
|
"""All the types of values we might be provided for the data-dist-info-metadata
|
|
|
|
attribute from PEP 658."""
|
|
|
|
|
|
|
|
# Valid: will read metadata from the dist instead.
|
|
|
|
No = "none"
|
|
|
|
# Valid: will read the .metadata file, but won't check its hash.
|
|
|
|
Unhashed = "unhashed"
|
|
|
|
# Valid: will read the .metadata file and check its hash matches.
|
|
|
|
Sha256 = "sha256"
|
|
|
|
# Invalid: will error out after checking the hash.
|
|
|
|
WrongHash = "wrong-hash"
|
|
|
|
# Invalid: will error out after failing to fetch the .metadata file.
|
|
|
|
NoFile = "no-file"
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class FakePackage:
|
|
|
|
"""Mock package structure used to generate a PyPI repository.
|
|
|
|
|
|
|
|
FakePackage name and version should correspond to sdists (.tar.gz files) in our test
|
|
|
|
data."""
|
|
|
|
|
|
|
|
name: str
|
|
|
|
version: str
|
|
|
|
filename: str
|
|
|
|
metadata: MetadataKind
|
|
|
|
# This will override any dependencies specified in the actual dist's METADATA.
|
|
|
|
requires_dist: Tuple[str, ...] = ()
|
|
|
|
# This will override the Name specified in the actual dist's METADATA.
|
|
|
|
metadata_name: Optional[str] = None
|
|
|
|
|
|
|
|
def metadata_filename(self) -> str:
|
|
|
|
"""This is specified by PEP 658."""
|
|
|
|
return f"{self.filename}.metadata"
|
|
|
|
|
|
|
|
def generate_additional_tag(self) -> str:
|
|
|
|
"""This gets injected into the <a> tag in the generated PyPI index page for this
|
|
|
|
package."""
|
|
|
|
if self.metadata == MetadataKind.No:
|
|
|
|
return ""
|
|
|
|
if self.metadata in [MetadataKind.Unhashed, MetadataKind.NoFile]:
|
|
|
|
return 'data-dist-info-metadata="true"'
|
|
|
|
if self.metadata == MetadataKind.WrongHash:
|
|
|
|
return 'data-dist-info-metadata="sha256=WRONG-HASH"'
|
|
|
|
assert self.metadata == MetadataKind.Sha256
|
|
|
|
checksum = sha256(self.generate_metadata()).hexdigest()
|
|
|
|
return f'data-dist-info-metadata="sha256={checksum}"'
|
|
|
|
|
|
|
|
def requires_str(self) -> str:
|
|
|
|
if not self.requires_dist:
|
|
|
|
return ""
|
|
|
|
joined = " and ".join(self.requires_dist)
|
|
|
|
return f"Requires-Dist: {joined}"
|
|
|
|
|
|
|
|
def generate_metadata(self) -> bytes:
|
|
|
|
"""This is written to `self.metadata_filename()` and will override the actual
|
|
|
|
dist's METADATA, unless `self.metadata == MetadataKind.NoFile`."""
|
|
|
|
return dedent(
|
|
|
|
f"""\
|
|
|
|
Metadata-Version: 2.1
|
|
|
|
Name: {self.metadata_name or self.name}
|
|
|
|
Version: {self.version}
|
|
|
|
{self.requires_str()}
|
|
|
|
"""
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def fake_packages() -> Dict[str, List[FakePackage]]:
|
|
|
|
"""The package database we generate for testing PEP 658 support."""
|
|
|
|
return {
|
|
|
|
"simple": [
|
|
|
|
FakePackage("simple", "1.0", "simple-1.0.tar.gz", MetadataKind.Sha256),
|
|
|
|
FakePackage("simple", "2.0", "simple-2.0.tar.gz", MetadataKind.No),
|
|
|
|
# This will raise a hashing error.
|
|
|
|
FakePackage("simple", "3.0", "simple-3.0.tar.gz", MetadataKind.WrongHash),
|
|
|
|
],
|
|
|
|
"simple2": [
|
|
|
|
# Override the dependencies here in order to force pip to download
|
|
|
|
# simple-1.0.tar.gz as well.
|
|
|
|
FakePackage(
|
|
|
|
"simple2",
|
|
|
|
"1.0",
|
|
|
|
"simple2-1.0.tar.gz",
|
|
|
|
MetadataKind.Unhashed,
|
|
|
|
("simple==1.0",),
|
|
|
|
),
|
|
|
|
# This will raise an error when pip attempts to fetch the metadata file.
|
|
|
|
FakePackage("simple2", "2.0", "simple2-2.0.tar.gz", MetadataKind.NoFile),
|
|
|
|
# This has a METADATA file with a mismatched name.
|
|
|
|
FakePackage(
|
|
|
|
"simple2",
|
|
|
|
"3.0",
|
|
|
|
"simple2-3.0.tar.gz",
|
|
|
|
MetadataKind.Sha256,
|
|
|
|
metadata_name="not-simple2",
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"colander": [
|
|
|
|
# Ensure we can read the dependencies from a metadata file within a wheel
|
|
|
|
# *without* PEP 658 metadata.
|
|
|
|
FakePackage(
|
|
|
|
"colander",
|
|
|
|
"0.9.9",
|
|
|
|
"colander-0.9.9-py2.py3-none-any.whl",
|
|
|
|
MetadataKind.No,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"compilewheel": [
|
|
|
|
# Ensure we can override the dependencies of a wheel file by injecting PEP
|
|
|
|
# 658 metadata.
|
|
|
|
FakePackage(
|
|
|
|
"compilewheel",
|
|
|
|
"1.0",
|
|
|
|
"compilewheel-1.0-py2.py3-none-any.whl",
|
|
|
|
MetadataKind.Unhashed,
|
|
|
|
("simple==1.0",),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"has-script": [
|
|
|
|
# Ensure we check PEP 658 metadata hashing errors for wheel files.
|
|
|
|
FakePackage(
|
|
|
|
"has-script",
|
|
|
|
"1.0",
|
|
|
|
"has.script-1.0-py2.py3-none-any.whl",
|
|
|
|
MetadataKind.WrongHash,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"translationstring": [
|
|
|
|
FakePackage(
|
|
|
|
"translationstring",
|
|
|
|
"1.1",
|
|
|
|
"translationstring-1.1.tar.gz",
|
|
|
|
MetadataKind.No,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"priority": [
|
|
|
|
# Ensure we check for a missing metadata file for wheels.
|
|
|
|
FakePackage(
|
|
|
|
"priority",
|
|
|
|
"1.0",
|
|
|
|
"priority-1.0-py2.py3-none-any.whl",
|
|
|
|
MetadataKind.NoFile,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
"requires-simple-extra": [
|
|
|
|
# Metadata name is not canonicalized.
|
|
|
|
FakePackage(
|
|
|
|
"requires-simple-extra",
|
|
|
|
"0.1",
|
|
|
|
"requires_simple_extra-0.1-py2.py3-none-any.whl",
|
|
|
|
MetadataKind.Sha256,
|
|
|
|
metadata_name="Requires_Simple.Extra",
|
|
|
|
),
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
def html_index_for_packages(
|
|
|
|
shared_data: TestData,
|
|
|
|
fake_packages: Dict[str, List[FakePackage]],
|
|
|
|
tmpdir_factory: pytest.TempPathFactory,
|
|
|
|
) -> Path:
|
|
|
|
"""Generate a PyPI HTML package index within a local directory pointing to
|
|
|
|
synthetic test data."""
|
|
|
|
html_dir = tmpdir_factory.mktemp("fake_index_html_content")
|
|
|
|
|
|
|
|
# (1) Generate the content for a PyPI index.html.
|
|
|
|
pkg_links = "\n".join(
|
|
|
|
f' <a href="{pkg}/index.html">{pkg}</a>' for pkg in fake_packages.keys()
|
|
|
|
)
|
2023-08-03 11:50:37 +02:00
|
|
|
# Output won't be nicely indented because dedent() acts after f-string
|
|
|
|
# arg insertion.
|
|
|
|
index_html = dedent(
|
|
|
|
f"""\
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta name="pypi:repository-version" content="1.0">
|
|
|
|
<title>Simple index</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
{pkg_links}
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
)
|
2023-08-02 04:11:07 +02:00
|
|
|
# (2) Generate the index.html in a new subdirectory of the temp directory.
|
|
|
|
(html_dir / "index.html").write_text(index_html)
|
|
|
|
|
|
|
|
# (3) Generate subdirectories for individual packages, each with their own
|
|
|
|
# index.html.
|
|
|
|
for pkg, links in fake_packages.items():
|
|
|
|
pkg_subdir = html_dir / pkg
|
|
|
|
pkg_subdir.mkdir()
|
|
|
|
|
|
|
|
download_links: List[str] = []
|
|
|
|
for package_link in links:
|
|
|
|
# (3.1) Generate the <a> tag which pip can crawl pointing to this
|
|
|
|
# specific package version.
|
|
|
|
download_links.append(
|
|
|
|
f' <a href="{package_link.filename}" {package_link.generate_additional_tag()}>{package_link.filename}</a><br/>' # noqa: E501
|
|
|
|
)
|
|
|
|
# (3.2) Copy over the corresponding file in `shared_data.packages`.
|
|
|
|
shutil.copy(
|
|
|
|
shared_data.packages / package_link.filename,
|
|
|
|
pkg_subdir / package_link.filename,
|
|
|
|
)
|
|
|
|
# (3.3) Write a metadata file, if applicable.
|
|
|
|
if package_link.metadata != MetadataKind.NoFile:
|
|
|
|
with open(pkg_subdir / package_link.metadata_filename(), "wb") as f:
|
|
|
|
f.write(package_link.generate_metadata())
|
|
|
|
|
|
|
|
# (3.4) After collating all the download links and copying over the files,
|
|
|
|
# write an index.html with the generated download links for each
|
|
|
|
# copied file for this specific package name.
|
|
|
|
download_links_str = "\n".join(download_links)
|
2023-08-03 11:50:37 +02:00
|
|
|
pkg_index_content = dedent(
|
|
|
|
f"""\
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta name="pypi:repository-version" content="1.0">
|
|
|
|
<title>Links for {pkg}</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Links for {pkg}</h1>
|
|
|
|
{download_links_str}
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
)
|
2023-08-02 04:11:07 +02:00
|
|
|
with open(pkg_subdir / "index.html", "w") as f:
|
|
|
|
f.write(pkg_index_content)
|
|
|
|
|
|
|
|
return html_dir
|
2023-08-02 06:02:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
class OneTimeDownloadHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
|
"""Serve files from the current directory, but error if a file is downloaded more
|
|
|
|
than once."""
|
|
|
|
|
|
|
|
_seen_paths: ClassVar[Set[str]] = set()
|
|
|
|
|
|
|
|
def do_GET(self) -> None:
|
|
|
|
if self.path in self._seen_paths:
|
|
|
|
self.send_error(
|
|
|
|
http.HTTPStatus.NOT_FOUND,
|
|
|
|
f"File {self.path} not available more than once!",
|
|
|
|
)
|
|
|
|
return
|
|
|
|
super().do_GET()
|
|
|
|
if not (self.path.endswith("/") or self.path.endswith(".metadata")):
|
|
|
|
self._seen_paths.add(self.path)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
|
|
def html_index_with_onetime_server(
|
|
|
|
html_index_for_packages: Path,
|
|
|
|
) -> Iterator[http.server.ThreadingHTTPServer]:
|
|
|
|
"""Serve files from a generated pypi index, erroring if a file is downloaded more
|
|
|
|
than once.
|
|
|
|
|
|
|
|
Provide `-i http://localhost:8000` to pip invocations to point them at this server.
|
|
|
|
"""
|
|
|
|
|
|
|
|
class InDirectoryServer(http.server.ThreadingHTTPServer):
|
|
|
|
def finish_request(self, request: Any, client_address: Any) -> None:
|
|
|
|
self.RequestHandlerClass(
|
|
|
|
request, client_address, self, directory=str(html_index_for_packages) # type: ignore[call-arg] # noqa: E501
|
|
|
|
)
|
|
|
|
|
|
|
|
class Handler(OneTimeDownloadHandler):
|
|
|
|
_seen_paths: ClassVar[Set[str]] = set()
|
|
|
|
|
|
|
|
with InDirectoryServer(("", 8000), Handler) as httpd:
|
|
|
|
server_thread = threading.Thread(target=httpd.serve_forever)
|
|
|
|
server_thread.start()
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield httpd
|
|
|
|
finally:
|
|
|
|
httpd.shutdown()
|
|
|
|
server_thread.join()
|