1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00
pip/tests/functional/test_cache.py
2023-08-28 15:04:54 +02:00

415 lines
15 KiB
Python

import os
import shutil
from glob import glob
from typing import Callable, List, Tuple
import pytest
from tests.lib import PipTestEnvironment, TestPipResult
@pytest.fixture
def cache_dir(script: PipTestEnvironment) -> str:
result = script.run(
"python",
"-c",
"from pip._internal.locations import USER_CACHE_DIR;print(USER_CACHE_DIR)",
)
return result.stdout.strip()
@pytest.fixture
def http_cache_dir(cache_dir: str) -> str:
return os.path.normcase(os.path.join(cache_dir, "http"))
@pytest.fixture
def wheel_cache_dir(cache_dir: str) -> str:
return os.path.normcase(os.path.join(cache_dir, "wheels"))
@pytest.fixture
def http_cache_files(http_cache_dir: str) -> List[str]:
destination = os.path.join(http_cache_dir, "arbitrary", "pathname")
if not os.path.exists(destination):
return []
filenames = glob(os.path.join(destination, "*"))
return [os.path.join(destination, filename) for filename in filenames]
@pytest.fixture
def wheel_cache_files(wheel_cache_dir: str) -> List[str]:
destination = os.path.join(wheel_cache_dir, "arbitrary", "pathname")
if not os.path.exists(destination):
return []
filenames = glob(os.path.join(destination, "*.whl"))
return [os.path.join(destination, filename) for filename in filenames]
@pytest.fixture
def populate_http_cache(http_cache_dir: str) -> List[Tuple[str, str]]:
destination = os.path.join(http_cache_dir, "arbitrary", "pathname")
os.makedirs(destination)
files = [
("aaaaaaaaa", os.path.join(destination, "aaaaaaaaa")),
("bbbbbbbbb", os.path.join(destination, "bbbbbbbbb")),
("ccccccccc", os.path.join(destination, "ccccccccc")),
]
for _name, filename in files:
with open(filename, "w"):
pass
return files
@pytest.fixture
def populate_wheel_cache(wheel_cache_dir: str) -> List[Tuple[str, str]]:
destination = os.path.join(wheel_cache_dir, "arbitrary", "pathname")
os.makedirs(destination)
files = [
("yyy-1.2.3", os.path.join(destination, "yyy-1.2.3-py3-none-any.whl")),
("zzz-4.5.6", os.path.join(destination, "zzz-4.5.6-py3-none-any.whl")),
("zzz-4.5.7", os.path.join(destination, "zzz-4.5.7-py3-none-any.whl")),
("zzz-7.8.9", os.path.join(destination, "zzz-7.8.9-py3-none-any.whl")),
]
for _name, filename in files:
with open(filename, "w"):
pass
return files
@pytest.fixture
def empty_wheel_cache(wheel_cache_dir: str) -> None:
if os.path.exists(wheel_cache_dir):
shutil.rmtree(wheel_cache_dir)
def list_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
"""Returns True if any line in `result`, which should be the output of
a `pip cache list` call, matches `wheel_name`.
E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with
`- foo-1.2.3-py3-none-any.whl `."""
lines = result.stdout.splitlines()
expected = f" - {wheel_name}-py3-none-any.whl "
return any(line.startswith(expected) for line in lines)
def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
"""Returns True if any line in `result`, which should be the output of
a `pip cache list --format=abspath` call, is a valid path and belongs to
`wheel_name`.
E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with
`foo-1.2.3-py3-none-any.whl`."""
lines = result.stdout.splitlines()
expected = f"{wheel_name}-py3-none-any.whl"
return any(
(
(os.path.basename(line).startswith(expected) and os.path.exists(line))
for line in lines
)
)
RemoveMatches = Callable[[str, TestPipResult], bool]
@pytest.fixture
def remove_matches_http(http_cache_dir: str) -> RemoveMatches:
"""Returns True if any line in `result`, which should be the output of
a `pip cache purge` call, matches `http_filename`.
E.g., If http_filename is `aaaaaaaaa`, it searches for a line equal to
`Removed <http files cache dir>/arbitrary/pathname/aaaaaaaaa`.
"""
def _remove_matches_http(http_filename: str, result: TestPipResult) -> bool:
lines = result.stdout.splitlines()
# The "/arbitrary/pathname/" bit is an implementation detail of how
# the `populate_http_cache` fixture is implemented.
path = os.path.join(
http_cache_dir,
"arbitrary",
"pathname",
http_filename,
)
expected = f"Removed {path}"
return expected in lines
return _remove_matches_http
@pytest.fixture
def remove_matches_wheel(wheel_cache_dir: str) -> RemoveMatches:
"""Returns True if any line in `result`, which should be the output of
a `pip cache remove`/`pip cache purge` call, matches `wheel_name`.
E.g., If wheel_name is `foo-1.2.3`, it searches for a line equal to
`Removed <wheel cache dir>/arbitrary/pathname/foo-1.2.3-py3-none-any.whl`.
"""
def _remove_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
lines = result.stdout.splitlines()
wheel_filename = f"{wheel_name}-py3-none-any.whl"
# The "/arbitrary/pathname/" bit is an implementation detail of how
# the `populate_wheel_cache` fixture is implemented.
path = os.path.join(
wheel_cache_dir,
"arbitrary",
"pathname",
wheel_filename,
)
expected = f"Removed {path}"
return expected in lines
return _remove_matches_wheel
def test_cache_dir(script: PipTestEnvironment, cache_dir: str) -> None:
result = script.pip("cache", "dir")
assert os.path.normcase(cache_dir) == result.stdout.strip()
def test_cache_dir_too_many_args(script: PipTestEnvironment, cache_dir: str) -> None:
result = script.pip("cache", "dir", "aaa", expect_error=True)
assert result.stdout == ""
# This would be `result.stderr == ...`, but pip prints deprecation
# warnings on Python 2.7, so we check if the _line_ is in stderr.
assert "ERROR: Too many arguments" in result.stderr.splitlines()
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
def test_cache_info(
script: PipTestEnvironment,
http_cache_dir: str,
wheel_cache_dir: str,
wheel_cache_files: List[str],
) -> None:
result = script.pip("cache", "info")
assert f"Package index page cache location: {http_cache_dir}" in result.stdout
assert f"Locally built wheels location: {wheel_cache_dir}" in result.stdout
num_wheels = len(wheel_cache_files)
assert f"Number of locally built wheels: {num_wheels}" in result.stdout
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list(script: PipTestEnvironment) -> None:
"""Running `pip cache list` should return exactly what the
populate_wheel_cache fixture adds."""
result = script.pip("cache", "list")
assert list_matches_wheel("yyy-1.2.3", result)
assert list_matches_wheel("zzz-4.5.6", result)
assert list_matches_wheel("zzz-4.5.7", result)
assert list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list --format=abspath` should return full
paths of exactly what the populate_wheel_cache fixture adds."""
result = script.pip("cache", "list", "--format=abspath")
assert list_matches_wheel_abspath("yyy-1.2.3", result)
assert list_matches_wheel_abspath("zzz-4.5.6", result)
assert list_matches_wheel_abspath("zzz-4.5.7", result)
assert list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("empty_wheel_cache")
def test_cache_list_with_empty_cache(script: PipTestEnvironment) -> None:
"""Running `pip cache list` with an empty cache should print
"No locally built wheels cached." and exit."""
result = script.pip("cache", "list")
assert result.stdout == "No locally built wheels cached.\n"
@pytest.mark.usefixtures("empty_wheel_cache")
def test_cache_list_with_empty_cache_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list --format=abspath` with an empty cache should not
print anything and exit."""
result = script.pip("cache", "list", "--format=abspath")
assert result.stdout.strip() == ""
@pytest.mark.usefixtures("empty_wheel_cache")
def test_cache_purge_with_empty_cache(script: PipTestEnvironment) -> None:
"""Running `pip cache purge` with an empty cache should print a warning
and exit without an error code."""
result = script.pip("cache", "purge", allow_stderr_warning=True)
assert result.stderr == "WARNING: No matching packages\n"
assert result.stdout == "Files removed: 0\n"
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_remove_with_bad_pattern(script: PipTestEnvironment) -> None:
"""Running `pip cache remove` with a bad pattern should print a warning
and exit without an error code."""
result = script.pip("cache", "remove", "aaa", allow_stderr_warning=True)
assert result.stderr == 'WARNING: No matching packages for pattern "aaa"\n'
assert result.stdout == "Files removed: 0\n"
def test_cache_list_too_many_args(script: PipTestEnvironment) -> None:
"""Passing `pip cache list` too many arguments should cause an error."""
script.pip("cache", "list", "aaa", "bbb", expect_error=True)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list_name_match(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7,
zzz-7.8.9, but nothing else."""
result = script.pip("cache", "list", "zzz", "--verbose")
assert not list_matches_wheel("yyy-1.2.3", result)
assert list_matches_wheel("zzz-4.5.6", result)
assert list_matches_wheel("zzz-4.5.7", result)
assert list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list_name_match_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz --format=abspath` should list paths of
zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else."""
result = script.pip("cache", "list", "zzz", "--format=abspath", "--verbose")
assert not list_matches_wheel_abspath("yyy-1.2.3", result)
assert list_matches_wheel_abspath("zzz-4.5.6", result)
assert list_matches_wheel_abspath("zzz-4.5.7", result)
assert list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list_name_and_version_match(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but
nothing else."""
result = script.pip("cache", "list", "zzz-4.5.6", "--verbose")
assert not list_matches_wheel("yyy-1.2.3", result)
assert list_matches_wheel("zzz-4.5.6", result)
assert not list_matches_wheel("zzz-4.5.7", result)
assert not list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_list_name_and_version_match_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz-4.5.6 --format=abspath` should list path of
zzz-4.5.6, but nothing else."""
result = script.pip("cache", "list", "zzz-4.5.6", "--format=abspath", "--verbose")
assert not list_matches_wheel_abspath("yyy-1.2.3", result)
assert list_matches_wheel_abspath("zzz-4.5.6", result)
assert not list_matches_wheel_abspath("zzz-4.5.7", result)
assert not list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_remove_no_arguments(script: PipTestEnvironment) -> None:
"""Running `pip cache remove` with no arguments should cause an error."""
script.pip("cache", "remove", expect_error=True)
def test_cache_remove_too_many_args(script: PipTestEnvironment) -> None:
"""Passing `pip cache remove` too many arguments should cause an error."""
script.pip("cache", "remove", "aaa", "bbb", expect_error=True)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_remove_name_match(
script: PipTestEnvironment, remove_matches_wheel: RemoveMatches
) -> None:
"""Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9,
but nothing else."""
result = script.pip("cache", "remove", "zzz", "--verbose")
assert not remove_matches_wheel("yyy-1.2.3", result)
assert remove_matches_wheel("zzz-4.5.6", result)
assert remove_matches_wheel("zzz-4.5.7", result)
assert remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
def test_cache_remove_name_and_version_match(
script: PipTestEnvironment, remove_matches_wheel: RemoveMatches
) -> None:
"""Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but
nothing else."""
result = script.pip("cache", "remove", "zzz-4.5.6", "--verbose")
assert not remove_matches_wheel("yyy-1.2.3", result)
assert remove_matches_wheel("zzz-4.5.6", result)
assert not remove_matches_wheel("zzz-4.5.7", result)
assert not remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
def test_cache_purge(
script: PipTestEnvironment,
remove_matches_http: RemoveMatches,
remove_matches_wheel: RemoveMatches,
) -> None:
"""Running `pip cache purge` should remove all cached http files and
wheels."""
result = script.pip("cache", "purge", "--verbose")
assert remove_matches_http("aaaaaaaaa", result)
assert remove_matches_http("bbbbbbbbb", result)
assert remove_matches_http("ccccccccc", result)
assert remove_matches_wheel("yyy-1.2.3", result)
assert remove_matches_wheel("zzz-4.5.6", result)
assert remove_matches_wheel("zzz-4.5.7", result)
assert remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
def test_cache_purge_too_many_args(
script: PipTestEnvironment,
http_cache_files: List[str],
wheel_cache_files: List[str],
) -> None:
"""Running `pip cache purge aaa` should raise an error and remove no
cached http files or wheels."""
result = script.pip("cache", "purge", "aaa", "--verbose", expect_error=True)
assert result.stdout == ""
# This would be `result.stderr == ...`, but pip prints deprecation
# warnings on Python 2.7, so we check if the _line_ is in stderr.
assert "ERROR: Too many arguments" in result.stderr.splitlines()
# Make sure nothing was deleted.
for filename in http_cache_files + wheel_cache_files:
assert os.path.exists(filename)
@pytest.mark.parametrize("command", ["info", "list", "remove", "purge"])
def test_cache_abort_when_no_cache_dir(
script: PipTestEnvironment, command: str
) -> None:
"""Running any pip cache command when cache is disabled should
abort and log an informative error"""
result = script.pip("cache", command, "--no-cache-dir", expect_error=True)
assert result.stdout == ""
assert (
"ERROR: pip cache commands can not function"
" since cache is disabled." in result.stderr.splitlines()
)