blob: f5b681f3a21b861d6b57eb1feac7c868d6bfab7b [file] [log] [blame]
//! Tests for `CacheLock`.
use crate::config::ConfigBuilder;
use cargo::util::cache_lock::{CacheLockMode, CacheLocker};
use cargo_test_support::paths::{self, CargoPathExt};
use cargo_test_support::{retry, thread_wait_timeout, threaded_timeout};
use std::thread::JoinHandle;
/// Helper to verify that it is OK to acquire the given lock (it shouldn't block).
fn verify_lock_is_ok(mode: CacheLockMode) {
let root = paths::root();
threaded_timeout(10, move || {
let config = ConfigBuilder::new().root(root).build();
let locker = CacheLocker::new();
// This would block if it is held.
let _lock = locker.lock(&config, mode).unwrap();
assert!(locker.is_locked(mode));
});
}
/// Helper to acquire two locks from the same locker.
fn a_b_nested(a: CacheLockMode, b: CacheLockMode) {
let config = ConfigBuilder::new().build();
let locker = CacheLocker::new();
let lock1 = locker.lock(&config, a).unwrap();
assert!(locker.is_locked(a));
let lock2 = locker.lock(&config, b).unwrap();
assert!(locker.is_locked(b));
drop(lock2);
drop(lock1);
// Verify locks were unlocked.
verify_lock_is_ok(CacheLockMode::Shared);
verify_lock_is_ok(CacheLockMode::DownloadExclusive);
verify_lock_is_ok(CacheLockMode::MutateExclusive);
}
/// Helper to acquire two locks from separate lockers, verifying that they
/// don't block each other.
fn a_then_b_separate_not_blocked(a: CacheLockMode, b: CacheLockMode, verify: CacheLockMode) {
let config = ConfigBuilder::new().build();
let locker1 = CacheLocker::new();
let lock1 = locker1.lock(&config, a).unwrap();
assert!(locker1.is_locked(a));
let locker2 = CacheLocker::new();
let lock2 = locker2.lock(&config, b).unwrap();
assert!(locker2.is_locked(b));
let thread = verify_lock_would_block(verify);
// Unblock the thread.
drop(lock1);
drop(lock2);
// Verify the thread is unblocked.
thread_wait_timeout::<()>(100, thread);
}
/// Helper to acquire two locks from separate lockers, verifying that the
/// second one blocks.
fn a_then_b_separate_blocked(a: CacheLockMode, b: CacheLockMode) {
let config = ConfigBuilder::new().build();
let locker = CacheLocker::new();
let lock = locker.lock(&config, a).unwrap();
assert!(locker.is_locked(a));
let thread = verify_lock_would_block(b);
// Unblock the thread.
drop(lock);
// Verify the thread is unblocked.
thread_wait_timeout::<()>(100, thread);
}
/// Helper to verify that acquiring the given mode would block.
///
/// Always call `thread_wait_timeout` on the result.
#[must_use]
fn verify_lock_would_block(mode: CacheLockMode) -> JoinHandle<()> {
let root = paths::root();
// Spawn a thread that will block on the lock.
let thread = std::thread::spawn(move || {
let config = ConfigBuilder::new().root(root).build();
let locker2 = CacheLocker::new();
let lock2 = locker2.lock(&config, mode).unwrap();
assert!(locker2.is_locked(mode));
drop(lock2);
});
// Verify that it blocked.
retry(100, || {
if let Ok(s) = std::fs::read_to_string(paths::root().join("shell.out")) {
if s.trim().starts_with("Blocking waiting for file lock on") {
return Some(());
} else {
eprintln!("unexpected output: {s}");
// Try again, it might have been partially written.
}
}
None
});
thread
}
#[test]
fn new_is_unlocked() {
let locker = CacheLocker::new();
assert!(!locker.is_locked(CacheLockMode::Shared));
assert!(!locker.is_locked(CacheLockMode::DownloadExclusive));
assert!(!locker.is_locked(CacheLockMode::MutateExclusive));
}
#[cargo_test]
fn multiple_shared() {
// Test that two nested shared locks from the same locker are safe to acquire.
a_b_nested(CacheLockMode::Shared, CacheLockMode::Shared);
}
#[cargo_test]
fn multiple_shared_separate() {
// Test that two independent shared locks are safe to acquire at the same time.
a_then_b_separate_not_blocked(
CacheLockMode::Shared,
CacheLockMode::Shared,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
fn multiple_download() {
// That that two nested download locks from the same locker are safe to acquire.
a_b_nested(
CacheLockMode::DownloadExclusive,
CacheLockMode::DownloadExclusive,
);
}
#[cargo_test]
fn multiple_mutate() {
// That that two nested mutate locks from the same locker are safe to acquire.
a_b_nested(
CacheLockMode::MutateExclusive,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
#[should_panic(expected = "lock is not allowed")]
fn download_then_shared() {
// This sequence is not supported.
a_b_nested(CacheLockMode::DownloadExclusive, CacheLockMode::Shared);
}
#[cargo_test]
#[should_panic(expected = "lock upgrade from shared to exclusive not supported")]
fn shared_then_mutate() {
// This sequence is not supported.
a_b_nested(CacheLockMode::Shared, CacheLockMode::MutateExclusive);
}
#[cargo_test]
fn shared_then_download() {
a_b_nested(CacheLockMode::Shared, CacheLockMode::DownloadExclusive);
// Verify drop actually unlocked.
verify_lock_is_ok(CacheLockMode::DownloadExclusive);
verify_lock_is_ok(CacheLockMode::MutateExclusive);
}
#[cargo_test]
fn mutate_then_shared() {
a_b_nested(CacheLockMode::MutateExclusive, CacheLockMode::Shared);
// Verify drop actually unlocked.
verify_lock_is_ok(CacheLockMode::MutateExclusive);
}
#[cargo_test]
fn download_then_mutate() {
a_b_nested(
CacheLockMode::DownloadExclusive,
CacheLockMode::MutateExclusive,
);
// Verify drop actually unlocked.
verify_lock_is_ok(CacheLockMode::DownloadExclusive);
verify_lock_is_ok(CacheLockMode::MutateExclusive);
}
#[cargo_test]
fn mutate_then_download() {
a_b_nested(
CacheLockMode::MutateExclusive,
CacheLockMode::DownloadExclusive,
);
// Verify drop actually unlocked.
verify_lock_is_ok(CacheLockMode::MutateExclusive);
verify_lock_is_ok(CacheLockMode::DownloadExclusive);
}
#[cargo_test]
fn readonly() {
// In a permission denied situation, it should still allow a lock. It just
// silently behaves as-if it was locked.
let cargo_home = paths::home().join(".cargo");
std::fs::create_dir_all(&cargo_home).unwrap();
let mut perms = std::fs::metadata(&cargo_home).unwrap().permissions();
perms.set_readonly(true);
std::fs::set_permissions(&cargo_home, perms).unwrap();
let config = ConfigBuilder::new().build();
let locker = CacheLocker::new();
for mode in [
CacheLockMode::Shared,
CacheLockMode::DownloadExclusive,
CacheLockMode::MutateExclusive,
] {
let _lock1 = locker.lock(&config, mode).unwrap();
// Make sure it can recursively acquire the lock, too.
let _lock2 = locker.lock(&config, mode).unwrap();
}
}
#[cargo_test]
fn download_then_shared_separate() {
a_then_b_separate_not_blocked(
CacheLockMode::DownloadExclusive,
CacheLockMode::Shared,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
fn shared_then_download_separate() {
a_then_b_separate_not_blocked(
CacheLockMode::Shared,
CacheLockMode::DownloadExclusive,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
fn multiple_download_separate() {
// Test that with two independent download locks, the second blocks until
// the first is released.
a_then_b_separate_blocked(
CacheLockMode::DownloadExclusive,
CacheLockMode::DownloadExclusive,
);
}
#[cargo_test]
fn multiple_mutate_separate() {
// Test that with two independent mutate locks, the second blocks until
// the first is released.
a_then_b_separate_blocked(
CacheLockMode::MutateExclusive,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
fn shared_then_mutate_separate() {
a_then_b_separate_blocked(CacheLockMode::Shared, CacheLockMode::MutateExclusive);
}
#[cargo_test]
fn download_then_mutate_separate() {
a_then_b_separate_blocked(
CacheLockMode::DownloadExclusive,
CacheLockMode::MutateExclusive,
);
}
#[cargo_test]
fn mutate_then_download_separate() {
a_then_b_separate_blocked(
CacheLockMode::MutateExclusive,
CacheLockMode::DownloadExclusive,
);
}
#[cargo_test]
fn mutate_then_shared_separate() {
a_then_b_separate_blocked(CacheLockMode::MutateExclusive, CacheLockMode::Shared);
}
#[cargo_test(ignore_windows = "no method to prevent creating or locking a file")]
fn mutate_err_is_atomic() {
// Verifies that when getting a mutate lock, that if the first lock
// succeeds, but the second one fails, that the first lock is released.
let config = ConfigBuilder::new().build();
let locker = CacheLocker::new();
let cargo_home = config.home().as_path_unlocked();
let cache_path = cargo_home.join(".package-cache");
// This is a hacky way to force an error acquiring the download lock. By
// making it a directory, it is unable to open it.
// TODO: Unfortunately this doesn't work on Windows. I don't have any
// ideas on how to simulate an error on Windows.
cache_path.mkdir_p();
match locker.lock(&config, CacheLockMode::MutateExclusive) {
Ok(_) => panic!("did not expect lock to succeed"),
Err(e) => {
let msg = format!("{e:?}");
assert!(msg.contains("failed to open:"), "{msg}");
}
}
assert!(!locker.is_locked(CacheLockMode::MutateExclusive));
assert!(!locker.is_locked(CacheLockMode::DownloadExclusive));
assert!(!locker.is_locked(CacheLockMode::Shared));
cache_path.rm_rf();
verify_lock_is_ok(CacheLockMode::DownloadExclusive);
verify_lock_is_ok(CacheLockMode::Shared);
verify_lock_is_ok(CacheLockMode::MutateExclusive);
}