uv/crates/uv/tests/it/cache_prune.rs

448 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use indoc::indoc;
use uv_static::EnvVars;
use crate::common::TestContext;
use crate::common::uv_snapshot;
/// `cache prune` should be a no-op if there's nothing out-of-date in the cache.
#[test]
fn prune_no_op() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("anyio")?;
// Install a requirement, to populate the cache.
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
let filters: Vec<_> = context
.filters()
.into_iter()
.chain(std::iter::once((r"Removed \d+ files", "Removed [N] files")))
.collect();
uv_snapshot!(&filters, context.prune().arg("--verbose"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
No unused entries found
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
Ok(())
}
/// `cache prune` should remove any stale top-level directories from the cache.
#[test]
fn prune_stale_directory() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("anyio")?;
// Install a requirement, to populate the cache.
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
// Add a stale directory to the cache.
let simple = context.cache_dir.child("simple-v4");
simple.create_dir_all()?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain(std::iter::once((r"Removed \d+ files", "Removed [N] files")))
.collect();
uv_snapshot!(&filters, context.prune().arg("--verbose"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling cache bucket: [CACHE_DIR]/simple-v4
Removed 1 directory
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
Ok(())
}
/// `cache prune` should remove all cached environments from the cache.
#[test]
fn prune_cached_env() {
let context = TestContext::new("3.12").with_filtered_counts();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
let filters: Vec<_> = context
.filters()
.into_iter()
.chain(std::iter::once((r"Removed \d+ files", "Removed [N] files")))
.collect();
uv_snapshot!(&filters, context.tool_run()
.arg("pytest@8.0.0")
.arg("--version")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
pytest 8.0.0
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ iniconfig==2.0.0
+ packaging==24.0
+ pluggy==1.4.0
+ pytest==8.0.0
"###);
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([
// The cache entry does not have a stable key, so we filter it out
(
r"\[CACHE_DIR\](\\|\/)(.*?)(\\|\/).*",
"[CACHE_DIR]/$2/[ENTRY]",
),
])
.collect();
uv_snapshot!(filters, context.prune().arg("--verbose"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling cache environment: [CACHE_DIR]/environments-v2/[ENTRY]
DEBUG Removing dangling cache archive: [CACHE_DIR]/archive-v0/[ENTRY]
Removed [N] files ([SIZE])
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
}
/// `cache prune` should remove any stale symlink from the cache.
#[test]
fn prune_stale_symlink() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("anyio")?;
// Install a requirement, to populate the cache.
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
// Remove the wheels directory, causing the symlink to become stale.
let wheels = context.cache_dir.child("wheels-v5");
fs_err::remove_dir_all(wheels)?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([
// The cache entry does not have a stable key, so we filter it out
(
r"\[CACHE_DIR\](\\|\/)(.*?)(\\|\/).*",
"[CACHE_DIR]/$2/[ENTRY]",
),
])
.collect();
uv_snapshot!(filters, context.prune().arg("--verbose"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling cache archive: [CACHE_DIR]/archive-v0/[ENTRY]
Removed 44 files ([SIZE])
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
Ok(())
}
#[tokio::test]
async fn prune_force() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("typing-extensions\niniconfig")?;
// Install a requirement, to populate the cache.
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
// When unlocked, `--force` should still take a lock
uv_snapshot!(context.filters(), context.prune().arg("--verbose").arg("--force"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
No unused entries found
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
// Add a stale directory to the cache.
let simple = context.cache_dir.child("simple-v4");
simple.create_dir_all()?;
// When locked, `--force` should proceed without blocking
let _cache = uv_cache::Cache::from_path(context.cache_dir.path())
.with_exclusive_lock()
.await;
uv_snapshot!(context.filters(), context.prune().arg("--verbose").arg("--force"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Lock is busy for `[CACHE_DIR]/`
DEBUG Cache is currently in use, proceeding due to `--force`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling cache bucket: [CACHE_DIR]/simple-v4
Removed 1 directory
");
Ok(())
}
/// `cache prune --ci` should remove all unzipped archives.
#[test]
fn prune_unzipped() -> Result<()> {
let context = TestContext::new("3.12").with_exclude_newer("2025-01-01T00:00Z");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc! { r"
source-distribution==0.0.1
iniconfig
" })?;
let filters: Vec<_> = std::iter::once((r"Removed \d+ files", "Removed [N] files"))
.chain(context.filters())
.collect();
// Install a requirement, to populate the cache.
uv_snapshot!(&filters, context.pip_install().arg("-r").arg("requirements.txt").arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ source-distribution==0.0.1
"###);
uv_snapshot!(&filters, context.prune().arg("--ci"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Pruning cache at: [CACHE_DIR]/
Removed [N] files ([SIZE])
"###);
context.venv().arg("--clear").assert().success();
// Reinstalling the source distribution should not require re-downloading the source
// distribution.
requirements_txt.write_str(indoc! { r"
source-distribution==0.0.1
" })?;
uv_snapshot!(&filters, context.pip_install().arg("-r").arg("requirements.txt").arg("--offline"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ source-distribution==0.0.1
"###);
// But reinstalling the other package should require a download, since we pruned the wheel.
requirements_txt.write_str(indoc! { r"
iniconfig
" })?;
uv_snapshot!(&filters, context.pip_install().arg("-r").arg("requirements.txt").arg("--offline"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because all versions of iniconfig need to be downloaded from a registry and you require iniconfig, we can conclude that your requirements are unsatisfiable.
hint: Pre-releases are available for `iniconfig` in the requested range (e.g., 0.2.dev0), but pre-releases weren't enabled (try: `--prerelease=allow`)
hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.
");
Ok(())
}
/// `cache prune` should remove any stale source distribution revisions.
#[test]
fn prune_stale_revision() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
context.temp_dir.child("src").child("__init__.py").touch()?;
context.temp_dir.child("README").touch()?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain(std::iter::once((r"Removed \d+ files", "Removed [N] files")))
.collect();
// Install the same package twice, with `--reinstall`.
uv_snapshot!(&filters, context
.pip_install()
.arg(".")
.arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
uv_snapshot!(&filters, context
.pip_install()
.arg(".")
.arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
let filters: Vec<_> = filters
.into_iter()
.chain([
// The cache entry does not have a stable key, so we filter it out
(
r"\[CACHE_DIR\](\\|\/)(.*?)(\\|\/).*",
"[CACHE_DIR]/$2/[ENTRY]",
),
])
.collect();
// Pruning should remove the unused revision.
uv_snapshot!(&filters, context.prune().arg("--verbose"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired exclusive lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling source revision: [CACHE_DIR]/sdists-v9/[ENTRY]
DEBUG Removing dangling cache archive: [CACHE_DIR]/archive-v0/[ENTRY]
Removed [N] files ([SIZE])
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
// Uninstall and reinstall the package. We should use the cached version.
uv_snapshot!(&filters, context
.pip_uninstall()
.arg("."), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- project==0.1.0 (from file://[TEMP_DIR]/)
"###);
uv_snapshot!(&filters, context
.pip_install()
.arg("."), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
Ok(())
}