cargo/tests/testsuite/global_cache_tracker.rs
Eric Huss a82794ec4a Fix path issues for running rustup wrapper on Windows.
Cargo likes to modify PATH, which circumvents the ability to choose the
correct "cargo" executable to run on Windows (because Windows uses PATH
for both binary and shared library searching).
2024-02-20 16:27:04 -08:00

2022 lines
63 KiB
Rust

//! Tests for last-use tracking and auto-gc.
//!
//! Cargo supports an environment variable called `__CARGO_TEST_LAST_USE_NOW`
//! to have cargo pretend that the current time is the given time (in seconds
//! since the unix epoch). This is used throughout these tests to simulate
//! what happens when time passes. The [`days_ago_unix`] and
//! [`months_ago_unix`] functions help with setting this value.
use super::config::GlobalContextBuilder;
use cargo::core::global_cache_tracker::{self, DeferredGlobalLastUse, GlobalCacheTracker};
use cargo::util::cache_lock::CacheLockMode;
use cargo::util::interning::InternedString;
use cargo::GlobalContext;
use cargo_test_support::paths::{self, CargoPathExt};
use cargo_test_support::registry::{Package, RegistryBuilder};
use cargo_test_support::{
basic_manifest, cargo_process, execs, git, process, project, retry, sleep_ms,
thread_wait_timeout, Execs, Project,
};
use itertools::Itertools;
use std::fmt::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::{Duration, SystemTime};
/// Helper to create a simple `foo` project which depends on a registry
/// dependency called `bar`.
fn basic_foo_bar_project() -> Project {
Package::new("bar", "1.0.0").publish();
project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build()
}
/// Helper to get the names of files in a directory as strings.
fn get_names(glob: &str) -> Vec<String> {
let mut names: Vec<_> = glob::glob(paths::home().join(glob).to_str().unwrap())
.unwrap()
.map(|p| p.unwrap().file_name().unwrap().to_str().unwrap().to_owned())
.collect();
names.sort();
names
}
fn get_registry_names(which: &str) -> Vec<String> {
get_names(&format!(".cargo/registry/{which}/*/*"))
}
fn get_index_names() -> Vec<String> {
get_names(&format!(".cargo/registry/index/*"))
}
fn get_git_db_names() -> Vec<String> {
get_names(&format!(".cargo/git/db/*"))
}
fn get_git_checkout_names(db_name: &str) -> Vec<String> {
get_names(&format!(".cargo/git/checkouts/{db_name}/*"))
}
fn days_ago(n: u64) -> SystemTime {
SystemTime::now() - Duration::from_secs(60 * 60 * 24 * n)
}
/// Helper for simulating running cargo in the past. Use with the
/// __CARGO_TEST_LAST_USE_NOW environment variable.
fn days_ago_unix(n: u64) -> String {
days_ago(n)
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
}
/// Helper for simulating running cargo in the past. Use with the
/// __CARGO_TEST_LAST_USE_NOW environment variable.
fn months_ago_unix(n: u64) -> String {
days_ago_unix(n * 30)
}
/// Populates last-use database and the cache files.
///
/// This makes it easier to more accurately specify exact sizes. Creating
/// specific sizes with `Package` is too difficult.
fn populate_cache(
gctx: &GlobalContext,
test_crates: &[(&str, u64, u64, u64)],
) -> (PathBuf, PathBuf) {
let cache_dir = paths::home().join(".cargo/registry/cache/example.com-a6c4a5adcb232b9a");
let src_dir = paths::home().join(".cargo/registry/src/example.com-a6c4a5adcb232b9a");
GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.rm_rf();
let _lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let mut tracker = GlobalCacheTracker::new(&gctx).unwrap();
let mut deferred = DeferredGlobalLastUse::new();
cache_dir.rm_rf();
cache_dir.mkdir_p();
src_dir.rm_rf();
src_dir.mkdir_p();
paths::home()
.join(".cargo/registry/index/example.com-a6c4a5adcb232b9a")
.mkdir_p();
let mut create = |name: &str, age, crate_size: u64, src_size: u64| {
let crate_filename = InternedString::new(&format!("{name}.crate"));
deferred.mark_registry_crate_used_stamp(
global_cache_tracker::RegistryCrate {
encoded_registry_name: "example.com-a6c4a5adcb232b9a".into(),
crate_filename,
size: crate_size,
},
Some(&days_ago(age)),
);
deferred.mark_registry_src_used_stamp(
global_cache_tracker::RegistrySrc {
encoded_registry_name: "example.com-a6c4a5adcb232b9a".into(),
package_dir: name.into(),
size: Some(src_size),
},
Some(&days_ago(age)),
);
std::fs::write(
cache_dir.join(crate_filename),
"x".repeat(crate_size as usize),
)
.unwrap();
let path = src_dir.join(name);
path.mkdir_p();
std::fs::write(path.join("data"), "x".repeat(src_size as usize)).unwrap()
};
for (name, age, crate_size, src_size) in test_crates {
create(name, *age, *crate_size, *src_size);
}
deferred.save(&mut tracker).unwrap();
(cache_dir, src_dir)
}
fn rustup_cargo() -> Execs {
// Get the path to the rustup cargo wrapper. This is necessary because
// cargo adds the "deps" directory into PATH on Windows, which points to
// the wrong cargo.
let rustup_cargo = Path::new(&std::env::var_os("CARGO_HOME").unwrap()).join("bin/cargo");
execs().with_process_builder(process(rustup_cargo))
}
#[cargo_test]
fn auto_gc_gated() {
// Requires -Zgc to both track last-use data and to run auto-gc.
let p = basic_foo_bar_project();
p.cargo("check")
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
// Check that it did not create a database or delete anything.
let gctx = GlobalContextBuilder::new().build();
assert!(!GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.exists());
assert_eq!(get_index_names().len(), 1);
// Again in the future, shouldn't auto-gc.
p.cargo("check").run();
assert!(!GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.exists());
assert_eq!(get_index_names().len(), 1);
}
#[cargo_test]
fn clean_gc_gated() {
cargo_process("clean gc")
.with_status(101)
.with_stderr(
"\
error: the `cargo clean gc` command is unstable, and only available on the \
nightly channel of Cargo, but this is the `stable` channel
See [..]
See [..]
",
)
.run();
}
#[cargo_test]
fn implies_source() {
// Checks that when a src, crate, or checkout is marked as used, the
// corresponding index or git db also gets marked as used.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let _lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let mut deferred = DeferredGlobalLastUse::new();
let mut tracker = GlobalCacheTracker::new(&gctx).unwrap();
deferred.mark_registry_crate_used(global_cache_tracker::RegistryCrate {
encoded_registry_name: "example.com-a6c4a5adcb232b9a".into(),
crate_filename: "regex-1.8.4.crate".into(),
size: 123,
});
deferred.mark_registry_src_used(global_cache_tracker::RegistrySrc {
encoded_registry_name: "index.crates.io-6f17d22bba15001f".into(),
package_dir: "rand-0.8.5".into(),
size: None,
});
deferred.mark_git_checkout_used(global_cache_tracker::GitCheckout {
encoded_git_name: "cargo-e7ff1db891893a9e".into(),
short_name: "f0a4ee0".into(),
size: None,
});
deferred.save(&mut tracker).unwrap();
let mut indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 2);
indexes.sort_by(|a, b| a.0.encoded_registry_name.cmp(&b.0.encoded_registry_name));
assert_eq!(
indexes[0].0.encoded_registry_name,
"example.com-a6c4a5adcb232b9a"
);
assert_eq!(
indexes[1].0.encoded_registry_name,
"index.crates.io-6f17d22bba15001f"
);
let dbs = tracker.git_db_all().unwrap();
assert_eq!(dbs.len(), 1);
assert_eq!(dbs[0].0.encoded_git_name, "cargo-e7ff1db891893a9e");
}
#[cargo_test]
fn auto_gc_defaults() {
// Checks that the auto-gc deletes old entries, and leaves new ones intact.
Package::new("old", "1.0.0").publish();
Package::new("new", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
old = "1.0"
new = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Populate the last-use data.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0", "old-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["new-1.0.0.crate", "old-1.0.0.crate"]
);
// Run again with just one package. Make sure the old src gets deleted,
// but .crate does not.
p.change_file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
new = "1.0"
"#,
);
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(2))
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["new-1.0.0.crate", "old-1.0.0.crate"]
);
// Run again after the .crate should have aged out.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["new-1.0.0.crate"]);
}
#[cargo_test]
fn auto_gc_config() {
// Can configure auto gc settings.
Package::new("old", "1.0.0").publish();
Package::new("new", "1.0.0").publish();
let p = project()
.file(
".cargo/config.toml",
r#"
[gc.auto]
frequency = "always"
max-src-age = "1 day"
max-crate-age = "3 days"
max-index-age = "3 days"
max-git-co-age = "1 day"
max-git-db-age = "3 days"
"#,
)
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
old = "1.0"
new = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Populate the last-use data.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(4))
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0", "old-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["new-1.0.0.crate", "old-1.0.0.crate"]
);
// Run again with just one package. Make sure the old src gets deleted,
// but .crate does not.
p.change_file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
new = "1.0"
"#,
);
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(2))
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["new-1.0.0.crate", "old-1.0.0.crate"]
);
// Run again after the .crate should have aged out.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["new-1.0.0.crate"]);
}
#[cargo_test]
fn frequency() {
// gc.auto.frequency settings
let p = basic_foo_bar_project();
p.change_file(
".cargo/config.toml",
r#"
[gc.auto]
frequency = "never"
"#,
);
// Populate data in the past.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(get_index_names().len(), 1);
assert_eq!(get_registry_names("src"), ["bar-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["bar-1.0.0.crate"]);
p.change_file("Cargo.toml", &basic_manifest("foo", "0.2.0"));
// Try after the default expiration time, with "never" it shouldn't gc.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_index_names().len(), 1);
assert_eq!(get_registry_names("src"), ["bar-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["bar-1.0.0.crate"]);
// Try again with a setting that allows it to run.
p.cargo("check -Zgc")
.env("CARGO_GC_AUTO_FREQUENCY", "1 day")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_index_names().len(), 0);
assert_eq!(get_registry_names("src").len(), 0);
assert_eq!(get_registry_names("cache").len(), 0);
}
#[cargo_test]
fn auto_gc_index() {
// Deletes the index if it hasn't been used in a while.
let p = basic_foo_bar_project();
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(get_index_names().len(), 1);
// Make sure it stays within the time frame.
p.change_file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
"#,
);
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(2))
.run();
assert_eq!(get_index_names().len(), 1);
// After it expires, it should be deleted.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_index_names().len(), 0);
}
#[cargo_test]
fn auto_gc_git() {
// auto-gc should delete git checkouts and dbs.
// Returns the short git name of a checkout.
let short_id = |repo: &git2::Repository| -> String {
let head = repo.revparse_single("HEAD").unwrap();
let short_id = head.short_id().unwrap();
short_id.as_str().unwrap().to_owned()
};
// Set up a git dependency and fetch it and populate the database,
// 6 months in the past.
let (git_project, git_repo) = git::new_repo("bar", |p| {
p.file("Cargo.toml", &basic_manifest("bar", "1.0.0"))
.file("src/lib.rs", "")
});
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = {{ git = '{}' }}
"#,
git_project.url()
),
)
.file("src/lib.rs", "")
.build();
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(6))
.run();
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 1);
let first_short_oid = short_id(&git_repo);
assert_eq!(
get_git_checkout_names(&db_names[0]),
[first_short_oid.clone()]
);
// Use a new git checkout, should keep both.
git_project.change_file("src/lib.rs", "// modified");
git::add(&git_repo);
git::commit(&git_repo);
p.cargo("update -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(6))
.run();
assert_eq!(get_git_db_names().len(), 1);
let second_short_oid = short_id(&git_repo);
let mut both = vec![first_short_oid, second_short_oid.clone()];
both.sort();
assert_eq!(get_git_checkout_names(&db_names[0]), both);
// In the future, using the second checkout should delete the first.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(get_git_db_names().len(), 1);
assert_eq!(
get_git_checkout_names(&db_names[0]),
[second_short_oid.clone()]
);
// After three months, the db should get deleted.
p.change_file("Cargo.toml", &basic_manifest("foo", "0.2.0"));
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_git_db_names().len(), 0);
assert_eq!(get_git_checkout_names(&db_names[0]).len(), 0);
}
#[cargo_test]
fn auto_gc_various_commands() {
// Checks that auto gc works with a variety of commands.
//
// Auto-gc is only run on a subset of commands. Generally it is run on
// commands that are already doing a lot of work, or heavily involve the
// use of the registry.
Package::new("bar", "1.0.0").publish();
let cmds = ["check", "fetch"];
for cmd in cmds {
eprintln!("checking command {cmd}");
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Populate the last-use data.
p.cargo(cmd)
.arg("-Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 1);
let crates = tracker.registry_crate_all().unwrap();
assert_eq!(crates.len(), 1);
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 1);
drop(lock);
// After everything is aged out, it should all be deleted.
p.change_file("Cargo.toml", &basic_manifest("foo", "0.2.0"));
p.cargo(cmd)
.arg("-Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 0);
let crates = tracker.registry_crate_all().unwrap();
assert_eq!(crates.len(), 0);
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 0);
drop(tracker);
drop(lock);
paths::home().join(".cargo/registry").rm_rf();
GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.rm_rf();
}
}
#[cargo_test]
fn updates_last_use_various_commands() {
// Checks that last-use tracking is updated by various commands.
//
// Not *all* commands update the index tracking, even though they
// technically involve reading the index. There isn't a convenient place
// to ensure it gets saved while avoiding saving too often in other
// commands. For the most part, this should be fine, since these commands
// usually aren't run without running one of the commands that does save
// the tracking. Some of the commands are:
//
// - login, owner, yank, search
// - report future-incompatibilities
// - package --no-verify
// - fetch --locked
Package::new("bar", "1.0.0").publish();
let cmds = [
// name, expected_crates (0=doesn't download)
("check", 1),
("fetch", 1),
("tree", 1),
("generate-lockfile", 0),
("update", 0),
("metadata", 1),
("vendor --respect-source-config", 1),
];
for (cmd, expected_crates) in cmds {
eprintln!("checking command {cmd}");
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Populate the last-use data.
p.cargo(cmd)
.arg("-Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 1);
let crates = tracker.registry_crate_all().unwrap();
assert_eq!(crates.len(), expected_crates);
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), expected_crates);
drop(tracker);
drop(lock);
paths::home().join(".cargo/registry").rm_rf();
GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.rm_rf();
}
}
#[cargo_test]
fn both_git_and_http_index_cleans() {
// Checks that either the git or http index cache gets cleaned.
let _crates_io = RegistryBuilder::new().build();
let _alternative = RegistryBuilder::new().alternative().http_index().build();
Package::new("from_git", "1.0.0").publish();
Package::new("from_http", "1.0.0")
.alternative(true)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
from_git = "1.0"
from_http = { version = "1.0", registry = "alternative" }
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("update -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 2);
assert_eq!(get_index_names().len(), 2);
drop(lock);
// Running in the future without these indexes should delete them.
p.change_file("Cargo.toml", &basic_manifest("foo", "0.2.0"));
p.cargo("clean gc -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 0);
assert_eq!(get_index_names().len(), 0);
drop(lock);
}
#[cargo_test]
fn clean_gc_dry_run() {
// Basic `clean --gc --dry-run` test.
let p = basic_foo_bar_project();
// Populate the last-use data.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
let registry_root = paths::home().join(".cargo/registry");
let glob_registry = |name| -> PathBuf {
let mut paths: Vec<_> = glob::glob(registry_root.join(name).join("*").to_str().unwrap())
.unwrap()
.map(|p| p.unwrap())
.collect();
assert_eq!(paths.len(), 1);
paths.pop().unwrap()
};
let index = glob_registry("index").ls_r();
let src = glob_registry("src").ls_r();
let cache = glob_registry("cache").ls_r();
let expected_files = index
.iter()
.chain(src.iter())
.chain(cache.iter())
.map(|p| p.to_str().unwrap())
.join("\n");
p.cargo("clean gc --dry-run -v -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stdout_unordered(&expected_files)
.with_stderr(
"[SUMMARY] [..] files, [..] total\n\
[WARNING] no files deleted due to --dry-run",
)
.run();
// Again, make sure the information is still tracked.
p.cargo("clean gc --dry-run -v -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stdout_unordered(&expected_files)
.with_stderr(
"[SUMMARY] [..] files, [..] total\n\
[WARNING] no files deleted due to --dry-run",
)
.run();
}
#[cargo_test]
fn clean_default_gc() {
// `clean gc` without options should also gc
let p = basic_foo_bar_project();
// Populate the last-use data.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
p.cargo("clean gc -v -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(
"\
[REMOVING] [ROOT]/home/.cargo/registry/index/[..]
[REMOVING] [ROOT]/home/.cargo/registry/src/[..]
[REMOVING] [ROOT]/home/.cargo/registry/cache/[..]
[REMOVED] [..] files, [..] total
",
)
.run();
}
#[cargo_test]
fn tracks_sizes() {
// Checks that sizes are properly tracked in the db.
Package::new("dep1", "1.0.0")
.file("src/lib.rs", "")
.publish();
Package::new("dep2", "1.0.0")
.file("src/lib.rs", "")
.file("data", &"abcdefghijklmnopqrstuvwxyz".repeat(1000))
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
dep1 = "1.0"
dep2 = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
// Check that the crate sizes are the same as on disk.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let _lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let mut crates = tracker.registry_crate_all().unwrap();
crates.sort_by(|a, b| a.0.crate_filename.cmp(&b.0.crate_filename));
let db_sizes: Vec<_> = crates.iter().map(|c| c.0.size).collect();
let mut actual: Vec<_> = p
.glob(paths::home().join(".cargo/registry/cache/*/*"))
.map(|p| p.unwrap())
.collect();
actual.sort();
let actual_sizes: Vec<_> = actual
.iter()
.map(|path| std::fs::metadata(path).unwrap().len())
.collect();
assert_eq!(db_sizes, actual_sizes);
// Also check the src sizes are computed.
let mut srcs = tracker.registry_src_all().unwrap();
srcs.sort_by(|a, b| a.0.package_dir.cmp(&b.0.package_dir));
let db_sizes: Vec<_> = srcs.iter().map(|c| c.0.size.unwrap()).collect();
let mut actual: Vec<_> = p
.glob(paths::home().join(".cargo/registry/src/*/*"))
.map(|p| p.unwrap())
.collect();
actual.sort();
// .cargo-ok is not tracked in the size.
actual.iter().for_each(|p| p.join(".cargo-ok").rm_rf());
let actual_sizes: Vec<_> = actual
.iter()
.map(|path| cargo_util::du(path, &[]).unwrap())
.collect();
assert_eq!(db_sizes, actual_sizes);
assert!(db_sizes[1] > 26000);
}
#[cargo_test]
fn max_size() {
// Checks --max-crate-size and --max-src-size with various cleaning thresholds.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let test_crates = [
// name, age, crate_size, src_size
("a-1.0.0", 5, 1, 1),
("b-1.0.0", 6, 2, 2),
("c-1.0.0", 3, 3, 3),
("d-1.0.0", 2, 4, 4),
("e-1.0.0", 2, 5, 5),
("f-1.0.0", 9, 6, 6),
("g-1.0.0", 1, 1, 1),
];
// Determine the order things get deleted so they can be verified.
let mut names_by_timestamp: Vec<_> = test_crates
.iter()
.map(|(name, age, _, _)| (days_ago_unix(*age), name))
.collect();
names_by_timestamp.sort();
let names_by_timestamp: Vec<_> = names_by_timestamp
.into_iter()
.map(|(_, name)| name)
.collect();
// This exercises the different boundary conditions.
for (clean_size, files, bytes) in [
(22, 0, 0),
(21, 1, 6),
(16, 1, 6),
(15, 2, 8),
(14, 2, 8),
(13, 3, 9),
(12, 4, 12),
(10, 4, 12),
(9, 5, 16),
(6, 5, 16),
(5, 6, 21),
(1, 6, 21),
(0, 7, 22),
] {
let (removed, kept) = names_by_timestamp.split_at(files);
// --max-crate-size
let (cache_dir, src_dir) = populate_cache(&gctx, &test_crates);
let mut stderr = String::new();
for name in removed {
writeln!(stderr, "[REMOVING] [..]{name}.crate").unwrap();
}
let total_display = if removed.is_empty() {
String::new()
} else {
format!(", {bytes}B total")
};
let files_display = if files == 1 {
format!("1 file")
} else {
format!("{files} files")
};
write!(stderr, "[REMOVED] {files_display}{total_display}").unwrap();
cargo_process(&format!("clean gc -Zgc -v --max-crate-size={clean_size}"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(&stderr)
.run();
for name in kept {
assert!(cache_dir.join(format!("{name}.crate")).exists());
}
for name in removed {
assert!(!cache_dir.join(format!("{name}.crate")).exists());
}
// --max-src-size
populate_cache(&gctx, &test_crates);
let mut stderr = String::new();
for name in removed {
writeln!(stderr, "[REMOVING] [..]{name}").unwrap();
}
let total_display = if files == 0 {
String::new()
} else {
format!(", {bytes}B total")
};
write!(stderr, "[REMOVED] {files_display}{total_display}").unwrap();
cargo_process(&format!("clean gc -Zgc -v --max-src-size={clean_size}"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(&stderr)
.run();
for name in kept {
assert!(src_dir.join(name).exists());
}
for name in removed {
assert!(!src_dir.join(name).exists());
}
}
}
#[cargo_test]
fn max_size_untracked_crate() {
// When a .crate file exists from an older version of cargo that did not
// track sizes, `clean --max-crate-size` should populate the db with the
// sizes.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let cache = paths::home().join(".cargo/registry/cache/example.com-a6c4a5adcb232b9a");
cache.mkdir_p();
paths::home()
.join(".cargo/registry/index/example.com-a6c4a5adcb232b9a")
.mkdir_p();
// Create the `.crate files.
let test_crates = [
// name, size
("a-1.0.0.crate", 1234),
("b-1.0.0.crate", 42),
("c-1.0.0.crate", 0),
];
for (name, size) in test_crates {
std::fs::write(cache.join(name), "x".repeat(size as usize)).unwrap()
}
// This should scan the directory and populate the db with the size information.
cargo_process("clean gc -Zgc -v --max-crate-size=100000")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] 0 files")
.run();
// Check that it stored the size data.
let _lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let crates = tracker.registry_crate_all().unwrap();
let mut actual: Vec<_> = crates
.iter()
.map(|(rc, _time)| (rc.crate_filename.as_str(), rc.size))
.collect();
actual.sort();
assert_eq!(test_crates, actual.as_slice());
}
/// Helper to prepare the max-size test.
fn max_size_untracked_prepare() -> (GlobalContext, Project) {
// First, publish and download a dependency.
let p = basic_foo_bar_project();
p.cargo("fetch").run();
// Pretend it was an older version that did not track last-use.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.rm_rf();
(gctx, p)
}
/// Helper to verify the max-size test.
fn max_size_untracked_verify(gctx: &GlobalContext) {
let actual: Vec<_> = glob::glob(
paths::home()
.join(".cargo/registry/src/*/*")
.to_str()
.unwrap(),
)
.unwrap()
.map(|p| p.unwrap())
.collect();
assert_eq!(actual.len(), 1);
let actual_size = cargo_util::du(&actual[0], &[]).unwrap();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 1);
assert_eq!(srcs[0].0.size, Some(actual_size));
drop(lock);
}
#[cargo_test]
fn max_size_untracked_src_from_use() {
// When a src directory exists from an older version of cargo that did not
// track sizes, doing a build should populate the db with an entry with an
// unknown size. `clean --max-src-size` should then fix the size.
let (gctx, p) = max_size_untracked_prepare();
// Run a command that will update the db with an unknown src size.
p.cargo("tree -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
// Check that it is None.
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 1);
assert_eq!(srcs[0].0.size, None);
drop(lock);
// Fix the size.
p.cargo("clean gc -v --max-src-size=10000 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] 0 files")
.run();
max_size_untracked_verify(&gctx);
}
#[cargo_test]
fn max_size_untracked_src_from_clean() {
// When a src directory exists from an older version of cargo that did not
// track sizes, `clean --max-src-size` should populate the db with the
// sizes.
let (gctx, p) = max_size_untracked_prepare();
// Clean should scan the src and update the db.
p.cargo("clean gc -v --max-src-size=10000 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] 0 files")
.run();
max_size_untracked_verify(&gctx);
}
#[cargo_test]
fn max_download_size() {
// --max-download-size
//
// This creates some sample crates of specific sizes, and then tries
// deleting at various specific size thresholds that exercise different
// edge conditions.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let test_crates = [
// name, age, crate_size, src_size
("d-1.0.0", 4, 4, 5),
("c-1.0.0", 3, 3, 3),
("a-1.0.0", 1, 2, 5),
("b-1.0.0", 1, 1, 7),
];
for (max_size, num_deleted, files_deleted, bytes) in [
(30, 0, 0, 0),
(29, 1, 1, 5),
(24, 2, 2, 9),
(20, 3, 3, 12),
(1, 7, 7, 29),
(0, 8, 8, 30),
] {
populate_cache(&gctx, &test_crates);
// Determine the order things will be deleted.
let delete_order: Vec<String> = test_crates
.iter()
.flat_map(|(name, _, _, _)| [name.to_string(), format!("{name}.crate")])
.collect();
let (removed, _kept) = delete_order.split_at(num_deleted);
let mut stderr = String::new();
for name in removed {
writeln!(stderr, "[REMOVING] [..]{name}").unwrap();
}
let files_display = if files_deleted == 1 {
format!("1 file")
} else {
format!("{files_deleted} files")
};
let total_display = if removed.is_empty() {
String::new()
} else {
format!(", {bytes}B total")
};
write!(stderr, "[REMOVED] {files_display}{total_display}",).unwrap();
cargo_process(&format!("clean gc -Zgc -v --max-download-size={max_size}"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(&stderr)
.run();
}
}
#[cargo_test]
fn package_cache_lock_during_build() {
// Verifies that a shared lock is held during a build. Resolution and
// downloads should be OK while that is held, but mutation should block.
//
// This works by launching a build with a build script that will pause.
// Then it performs other cargo commands and verifies their behavior.
Package::new("bar", "1.0.0").publish();
let p_foo = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.file(
"build.rs",
r#"
fn main() {
std::fs::write("blocking", "").unwrap();
let path = std::path::Path::new("ready");
loop {
if path.exists() {
break;
} else {
std::thread::sleep(std::time::Duration::from_millis(100))
}
}
}
"#,
)
.build();
let p_foo2 = project()
.at("foo2")
.file(
"Cargo.toml",
r#"
[package]
name = "foo2"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Start a build that will pause once the build starts.
let mut foo_child = p_foo
.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.build_command()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
// Wait for it to enter build script.
retry(100, || p_foo.root().join("blocking").exists().then_some(()));
// Start a build with a different target directory. It should not block,
// even though it gets a download lock, and then a shared lock.
//
// Also verify that auto-gc gets disabled.
p_foo2
.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("CARGO_GC_AUTO_FREQUENCY", "always")
.env("CARGO_LOG", "gc=debug")
.with_stderr_contains("[UPDATING] `dummy-registry` index")
.with_stderr_contains("[CHECKING] bar v1.0.0")
.with_stderr_contains("[CHECKING] foo2 v0.1.0 [..]")
.with_stderr_contains("[FINISHED] [..]")
.with_stderr_contains("[..]unable to acquire mutate lock, auto gc disabled")
.run();
// Ensure that the first build really blocked.
assert!(matches!(foo_child.try_wait(), Ok(None)));
// Cleaning while a command is running should block.
let mut clean_cmd = p_foo2
.cargo("clean gc --max-download-size=0 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.build_command();
clean_cmd.stderr(Stdio::piped());
let mut clean_child = clean_cmd.spawn().unwrap();
// Give the clean command a chance to finish (it shouldn't).
sleep_ms(500);
// They should both still be running.
assert!(matches!(foo_child.try_wait(), Ok(None)));
assert!(matches!(clean_child.try_wait(), Ok(None)));
// Let the original build finish.
p_foo.change_file("ready", "");
// Wait for clean to finish.
let thread = std::thread::spawn(|| clean_child.wait_with_output().unwrap());
let output = thread_wait_timeout(100, thread);
assert!(output.status.success());
// Validate the output of the clean.
execs()
.with_stderr(
"\
[BLOCKING] waiting for file lock on package cache mutation
[REMOVED] [..]
",
)
.run_output(&output);
}
#[cargo_test]
fn read_only_locking_auto_gc() {
// Tests the behavior for auto-gc on a read-only directory.
let p = basic_foo_bar_project();
// Populate cache.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
let cargo_home = paths::home().join(".cargo");
let mut perms = std::fs::metadata(&cargo_home).unwrap().permissions();
// Test when it can't update auto-gc db.
perms.set_readonly(true);
std::fs::set_permissions(&cargo_home, perms.clone()).unwrap();
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[CHECKING] bar v1.0.0
[CHECKING] foo v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
// Try again without the last-use existing (such as if the cache was
// populated by an older version of cargo).
perms.set_readonly(false);
std::fs::set_permissions(&cargo_home, perms.clone()).unwrap();
let gctx = GlobalContextBuilder::new().build();
GlobalCacheTracker::db_path(&gctx)
.into_path_unlocked()
.rm_rf();
perms.set_readonly(true);
std::fs::set_permissions(&cargo_home, perms.clone()).unwrap();
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[FINISHED] [..]")
.run();
perms.set_readonly(false);
std::fs::set_permissions(&cargo_home, perms).unwrap();
}
#[cargo_test]
fn delete_index_also_deletes_crates() {
// Checks that when an index is delete that src and cache directories also get deleted.
let p = basic_foo_bar_project();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(get_registry_names("src"), ["bar-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["bar-1.0.0.crate"]);
p.cargo("clean gc")
.arg("--max-index-age=0 days")
.arg("-Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] [..]")
.run();
assert_eq!(get_registry_names("src").len(), 0);
assert_eq!(get_registry_names("cache").len(), 0);
}
#[cargo_test]
fn clean_syncs_missing_files() {
// When files go missing in the cache, clean operations that need to track
// the size should also remove them from the database.
Package::new("bar", "1.0.0").publish();
Package::new("baz", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
baz = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
// Verify things are tracked.
let gctx = GlobalContextBuilder::new().unstable_flag("gc").build();
let lock = gctx
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&gctx).unwrap();
let crates = tracker.registry_crate_all().unwrap();
assert_eq!(crates.len(), 2);
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 2);
drop(lock);
// Remove the files.
for pattern in [
".cargo/registry/cache/*/bar-1.0.0.crate",
".cargo/registry/src/*/bar-1.0.0",
] {
p.glob(paths::home().join(pattern))
.map(|p| p.unwrap())
.next()
.unwrap()
.rm_rf();
}
// Clean should update the db.
p.cargo("clean gc -v --max-download-size=1GB -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] 0 files")
.run();
// Verify
let crates = tracker.registry_crate_all().unwrap();
assert_eq!(crates.len(), 1);
let srcs = tracker.registry_src_all().unwrap();
assert_eq!(srcs.len(), 1);
}
#[cargo_test]
fn offline_doesnt_auto_gc() {
// When running offline, auto-gc shouldn't run.
let p = basic_foo_bar_project();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
// Remove the dependency.
p.change_file("Cargo.toml", &basic_manifest("foo", "0.1.0"));
// Run offline, make sure it doesn't delete anything
p.cargo("check --offline -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[CHECKING] foo v0.1.0[..]\n[FINISHED][..]")
.run();
assert_eq!(get_registry_names("src"), ["bar-1.0.0"]);
assert_eq!(get_registry_names("cache"), ["bar-1.0.0.crate"]);
// Run online, make sure auto-gc runs.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[FINISHED][..]")
.run();
assert_eq!(get_registry_names("src"), &[] as &[String]);
assert_eq!(get_registry_names("cache"), &[] as &[String]);
}
#[cargo_test]
fn can_handle_future_schema() -> anyhow::Result<()> {
// It should work when a future version of cargo has made schema changes
// to the database.
let p = basic_foo_bar_project();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
// Modify the schema to pretend this is done by a future version of cargo.
let gctx = GlobalContextBuilder::new().build();
let db_path = GlobalCacheTracker::db_path(&gctx).into_path_unlocked();
let conn = rusqlite::Connection::open(&db_path)?;
let user_version: u32 =
conn.query_row("SELECT user_version FROM pragma_user_version", [], |row| {
row.get(0)
})?;
conn.execute("ALTER TABLE global_data ADD COLUMN foo DEFAULT 123", [])?;
conn.pragma_update(None, "user_version", &(user_version + 1))?;
drop(conn);
// Verify it doesn't blow up.
p.cargo("clean gc --max-download-size=0 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr("[REMOVED] 4 files, [..] total")
.run();
Ok(())
}
#[cargo_test]
fn clean_max_git_age() {
// --max-git-*-age flags
let (git_a, git_a_repo) = git::new_repo("git_a", |p| {
p.file("Cargo.toml", &basic_manifest("git_a", "1.0.0"))
.file("src/lib.rs", "")
});
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
git_a = {{ git = '{}' }}
"#,
git_a.url()
),
)
.file("src/lib.rs", "")
.build();
// Populate last-use tracking.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(4))
.run();
// Update git_a to create a separate checkout.
git_a.change_file("src/lib.rs", "// test");
git::add(&git_a_repo);
git::commit(&git_a_repo);
// Update last-use tracking, where the first git checkout will stay "old".
p.cargo("update -p git_a -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(2))
.with_stderr(
"\
[UPDATING] git repository [..]
[UPDATING] git_a v1.0.0 [..]
",
)
.run();
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 1);
let db_name = &db_names[0];
let co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 2);
// Delete the first checkout
p.cargo("clean gc -v -Zgc")
.arg("--max-git-co-age=3 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/git_a-[..]/[..]
[REMOVED] [..]
",
)
.run();
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 1);
let co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 1);
// delete the second checkout
p.cargo("clean gc -v -Zgc")
.arg("--max-git-co-age=0 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/git_a-[..]/[..]
[REMOVED] [..]
",
)
.run();
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 1);
let co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 0);
// delete the db
p.cargo("clean gc -v -Zgc")
.arg("--max-git-db-age=1 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/db/git_a-[..]
[REMOVING] [ROOT]/home/.cargo/git/checkouts/git_a-[..]
[REMOVED] [..]
",
)
.run();
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 0);
let co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 0);
}
#[cargo_test]
fn clean_max_src_crate_age() {
// --max-src-age and --max-crate-age flags
let p = basic_foo_bar_project();
// Populate last-use tracking.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(4))
.run();
// Update bar to create a separate copy with a different timestamp.
Package::new("bar", "1.0.1").publish();
p.cargo("update -p bar -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(2))
.with_stderr(
"\
[UPDATING] `dummy-registry` index
[UPDATING] bar v1.0.0 -> v1.0.1
",
)
.run();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(2))
.with_stderr(
"\
[DOWNLOADING] crates ...
[DOWNLOADED] bar v1.0.1 [..]
",
)
.run();
assert_eq!(get_registry_names("src"), ["bar-1.0.0", "bar-1.0.1"]);
assert_eq!(
get_registry_names("cache"),
["bar-1.0.0.crate", "bar-1.0.1.crate"]
);
// Delete the old src.
p.cargo("clean gc -v -Zgc")
.arg("--max-src-age=3 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [..]/bar-1.0.0
[REMOVED] [..]
",
)
.run();
// delete the second src
p.cargo("clean gc -v -Zgc")
.arg("--max-src-age=0 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [..]/bar-1.0.1
[REMOVED] [..]
",
)
.run();
// delete the old crate
p.cargo("clean gc -v -Zgc")
.arg("--max-crate-age=3 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [..]/bar-1.0.0.crate
[REMOVED] [..]
",
)
.run();
// delete the seecond crate
p.cargo("clean gc -v -Zgc")
.arg("--max-crate-age=0 days")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [..]/bar-1.0.1.crate
[REMOVED] [..]
",
)
.run();
}
#[cargo_test]
fn clean_max_git_size() {
// clean --max-git-size
//
// Creates two checkouts. The sets a size threshold to delete one. And
// then with 0 max size to delete everything.
let (git_project, git_repo) = git::new_repo("bar", |p| {
p.file("Cargo.toml", &basic_manifest("bar", "1.0.0"))
.file("src/lib.rs", "")
});
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = {{ git = '{}' }}
"#,
git_project.url()
),
)
.file("src/lib.rs", "")
.build();
// Fetch and populate db.
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(3))
.run();
// Figure out the name of the first checkout.
let git_root = paths::home().join(".cargo/git");
let db_names = get_git_db_names();
assert_eq!(db_names.len(), 1);
let db_name = &db_names[0];
let co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 1);
let first_co_name = &co_names[0];
// Make an update and create a new checkout.
git_project.change_file("src/lib.rs", "// modified");
git::add(&git_repo);
git::commit(&git_repo);
p.cargo("update -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
// Use a different time so that the first checkout timestamp is less
// than the second.
.env("__CARGO_TEST_LAST_USE_NOW", days_ago_unix(2))
.run();
// Figure out the threshold to use.
let mut co_names = get_git_checkout_names(&db_name);
assert_eq!(co_names.len(), 2);
co_names.retain(|name| name != first_co_name);
assert_eq!(co_names.len(), 1);
let second_co_name = &co_names[0];
let second_co_path = git_root
.join("checkouts")
.join(db_name)
.join(second_co_name);
let second_co_size = cargo_util::du(&second_co_path, &["!.git"]).unwrap();
let db_size = cargo_util::du(&git_root.join("db").join(db_name), &[]).unwrap();
let threshold = db_size + second_co_size;
p.cargo(&format!("clean gc --max-git-size={threshold} -Zgc -v"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(&format!(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/{db_name}/{first_co_name}
[REMOVED] [..]
"
))
.run();
// And then try cleaning everything.
p.cargo("clean gc --max-git-size=0 -Zgc -v")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(&format!(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/{db_name}/{second_co_name}
[REMOVING] [ROOT]/home/.cargo/git/db/{db_name}
[REMOVED] [..]
"
))
.run();
}
// Helper for setting up fake git sizes for git size cleaning.
fn setup_fake_git_sizes(db_name: &str, db_size: usize, co_sizes: &[usize]) {
let base_git = paths::home().join(".cargo/git");
let db_path = base_git.join("db").join(db_name);
db_path.mkdir_p();
std::fs::write(db_path.join("test"), "x".repeat(db_size)).unwrap();
let base_co = base_git.join("checkouts").join(db_name);
for (i, size) in co_sizes.iter().enumerate() {
let co_name = format!("co{i}");
let co_path = base_co.join(co_name);
co_path.mkdir_p();
std::fs::write(co_path.join("test"), "x".repeat(*size)).unwrap();
}
}
#[cargo_test]
fn clean_max_git_size_untracked() {
// If there are git directories that aren't tracked in the database,
// `--max-git-size` should pick it up.
//
// The db_name of "example" depends on the sorting order of the names ("e"
// should be after "c"), so that the db comes after the checkouts.
setup_fake_git_sizes("example", 5000, &[1000, 2000]);
cargo_process(&format!("clean gc -Zgc -v --max-git-size=7000"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/example/co0
[REMOVED] [..]
",
)
.run();
cargo_process(&format!("clean gc -Zgc -v --max-git-size=5000"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/example/co1
[REMOVED] [..]
",
)
.run();
cargo_process(&format!("clean gc -Zgc -v --max-git-size=0"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/db/example
[REMOVED] [..]
",
)
.run();
}
#[cargo_test]
fn clean_max_git_size_deletes_co_from_db() {
// In the scenario where it thinks it needs to delete the db, it should
// also delete all the checkouts.
//
// The db_name of "abc" depends on the sorting order of the names ("a"
// should be before "c"), so that the db comes before the checkouts.
setup_fake_git_sizes("abc", 5000, &[1000, 2000]);
// This deletes everything because it tries to delete the db, which then
// deletes all checkouts.
cargo_process(&format!("clean gc -Zgc -v --max-git-size=3000"))
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/db/abc
[REMOVING] [ROOT]/home/.cargo/git/checkouts/abc/co1
[REMOVING] [ROOT]/home/.cargo/git/checkouts/abc/co0
[REMOVED] [..]
",
)
.run();
}
#[cargo_test]
fn handles_missing_index() {
// Checks behavior when index is missing.
let p = basic_foo_bar_project();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
paths::home().join(".cargo/registry/index").rm_rf();
cargo_process("clean gc -v --max-download-size=0 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr_unordered(
"\
[REMOVING] [ROOT]/home/.cargo/registry/cache/[..]
[REMOVING] [ROOT]/home/.cargo/registry/src/[..]
[REMOVED] [..]
",
)
.run();
}
#[cargo_test]
fn handles_missing_git_db() {
// Checks behavior when git db is missing.
let git_project = git::new("bar", |p| {
p.file("Cargo.toml", &basic_manifest("bar", "1.0.0"))
.file("src/lib.rs", "")
});
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = {{ git = '{}' }}
"#,
git_project.url()
),
)
.file("src/lib.rs", "")
.build();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
paths::home().join(".cargo/git/db").rm_rf();
cargo_process("clean gc -v --max-git-size=0 -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.with_stderr(
"\
[REMOVING] [ROOT]/home/.cargo/git/checkouts/[..]
[REMOVED] [..]
",
)
.run();
}
#[cargo_test]
fn clean_gc_quiet_is_quiet() {
// Checks that --quiet works with `cargo clean gc`, since there was a
// subtle issue with how the flag is defined as a global flag.
let p = basic_foo_bar_project();
p.cargo("fetch -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
p.cargo("clean gc --quiet -Zgc --dry-run")
.masquerade_as_nightly_cargo(&["gc"])
.with_stdout("")
.with_stderr("")
.run();
// Verify exact same command without -q would actually display something.
p.cargo("clean gc -Zgc --dry-run")
.masquerade_as_nightly_cargo(&["gc"])
.with_stdout("")
.with_stderr(
"\
[SUMMARY] [..] files, [..] total
[WARNING] no files deleted due to --dry-run
",
)
.run();
}
#[cargo_test(requires_rustup_stable)]
fn compatible_with_older_cargo() {
// Ensures that db stays backwards compatible across versions.
// T-4 months: Current version, build the database.
Package::new("old", "1.0.0").publish();
Package::new("middle", "1.0.0").publish();
Package::new("new", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
old = "1.0"
middle = "1.0"
new = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Populate the last-use data.
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(4))
.run();
assert_eq!(
get_registry_names("src"),
["middle-1.0.0", "new-1.0.0", "old-1.0.0"]
);
assert_eq!(
get_registry_names("cache"),
["middle-1.0.0.crate", "new-1.0.0.crate", "old-1.0.0.crate"]
);
// T-2 months: Stable version, make sure it reads and deletes old src.
p.change_file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
new = "1.0"
middle = "1.0"
"#,
);
rustup_cargo()
.args(&["+stable", "check", "-Zgc"])
.cwd(p.root())
.masquerade_as_nightly_cargo(&["gc"])
.env("__CARGO_TEST_LAST_USE_NOW", months_ago_unix(2))
.run();
assert_eq!(get_registry_names("src"), ["middle-1.0.0", "new-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["middle-1.0.0.crate", "new-1.0.0.crate", "old-1.0.0.crate"]
);
// T-0 months: Current version, make sure it can read data from stable,
// deletes old crate and middle src.
p.change_file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
new = "1.0"
"#,
);
p.cargo("check -Zgc")
.masquerade_as_nightly_cargo(&["gc"])
.run();
assert_eq!(get_registry_names("src"), ["new-1.0.0"]);
assert_eq!(
get_registry_names("cache"),
["middle-1.0.0.crate", "new-1.0.0.crate"]
);
}
#[cargo_test(requires_rustup_stable)]
fn forward_compatible() {
// Checks that db created in an older version can be read in a newer version.
Package::new("bar", "1.0.0").publish();
let git_project = git::new("from_git", |p| {
p.file("Cargo.toml", &basic_manifest("from_git", "1.0.0"))
.file("src/lib.rs", "")
});
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
[dependencies]
bar = "1.0.0"
from_git = {{ git = '{}' }}
"#,
git_project.url()
),
)
.file("src/lib.rs", "")
.build();
rustup_cargo()
.args(&["+stable", "check", "-Zgc"])
.cwd(p.root())
.masquerade_as_nightly_cargo(&["gc"])
.run();
let config = GlobalContextBuilder::new().unstable_flag("gc").build();
let lock = config
.acquire_package_cache_lock(CacheLockMode::MutateExclusive)
.unwrap();
let tracker = GlobalCacheTracker::new(&config).unwrap();
// Don't want to check the actual index name here, since although the
// names are semi-stable, they might change over long periods of time.
let indexes = tracker.registry_index_all().unwrap();
assert_eq!(indexes.len(), 1);
let crates = tracker.registry_crate_all().unwrap();
let names: Vec<_> = crates
.iter()
.map(|(krate, _timestamp)| krate.crate_filename)
.collect();
assert_eq!(names, &["bar-1.0.0.crate"]);
let srcs = tracker.registry_src_all().unwrap();
let names: Vec<_> = srcs
.iter()
.map(|(src, _timestamp)| src.package_dir)
.collect();
assert_eq!(names, &["bar-1.0.0"]);
let dbs: Vec<_> = tracker.git_db_all().unwrap();
assert_eq!(dbs.len(), 1);
let cos: Vec<_> = tracker.git_checkout_all().unwrap();
assert_eq!(cos.len(), 1);
drop(lock);
}