summaryrefslogtreecommitdiffstats
path: root/src/tools/cargo/tests/testsuite/cache_lock.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/cargo/tests/testsuite/cache_lock.rs')
-rw-r--r--src/tools/cargo/tests/testsuite/cache_lock.rs304
1 files changed, 304 insertions, 0 deletions
diff --git a/src/tools/cargo/tests/testsuite/cache_lock.rs b/src/tools/cargo/tests/testsuite/cache_lock.rs
new file mode 100644
index 000000000..f5b681f3a
--- /dev/null
+++ b/src/tools/cargo/tests/testsuite/cache_lock.rs
@@ -0,0 +1,304 @@
+//! 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);
+}