//! Tests for `CacheLock`. use std::thread::JoinHandle; use cargo::util::cache_lock::{CacheLockMode, CacheLocker}; use cargo_test_support::paths::{self, CargoPathExt}; use cargo_test_support::prelude::*; use cargo_test_support::{retry, thread_wait_timeout, threaded_timeout}; use crate::config::GlobalContextBuilder; /// 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 gctx = GlobalContextBuilder::new().root(root).build(); let locker = CacheLocker::new(); // This would block if it is held. let _lock = locker.lock(&gctx, 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 gctx = GlobalContextBuilder::new().build(); let locker = CacheLocker::new(); let lock1 = locker.lock(&gctx, a).unwrap(); assert!(locker.is_locked(a)); let lock2 = locker.lock(&gctx, 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 gctx = GlobalContextBuilder::new().build(); let locker1 = CacheLocker::new(); let lock1 = locker1.lock(&gctx, a).unwrap(); assert!(locker1.is_locked(a)); let locker2 = CacheLocker::new(); let lock2 = locker2.lock(&gctx, 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 gctx = GlobalContextBuilder::new().build(); let locker = CacheLocker::new(); let lock = locker.lock(&gctx, 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 gctx = GlobalContextBuilder::new().root(root).build(); let locker2 = CacheLocker::new(); let lock2 = locker2.lock(&gctx, 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 gctx = GlobalContextBuilder::new().build(); let locker = CacheLocker::new(); for mode in [ CacheLockMode::Shared, CacheLockMode::DownloadExclusive, CacheLockMode::MutateExclusive, ] { let _lock1 = locker.lock(&gctx, mode).unwrap(); // Make sure it can recursively acquire the lock, too. let _lock2 = locker.lock(&gctx, 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 gctx = GlobalContextBuilder::new().build(); let locker = CacheLocker::new(); let cargo_home = gctx.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(&gctx, 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); }