diff --git a/src/id.rs b/src/id.rs index 5ba5c58..56e5f60 100644 --- a/src/id.rs +++ b/src/id.rs @@ -35,27 +35,3 @@ impl LockId { Self(NEXT_ID.fetch_add(1, Ordering::Relaxed)) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ids_are_unique() { - let a = LockId::next(); - let b = LockId::next(); - let c = LockId::next(); - assert_ne!(a, b); - assert_ne!(b, c); - assert_ne!(a, c); - } - - #[test] - fn ids_are_monotonically_increasing() { - let a = LockId::next(); - let b = LockId::next(); - let c = LockId::next(); - assert!(a < b); - assert!(b < c); - } -} diff --git a/tests/integration.rs b/tests/integration.rs index 3fe4258..2d06d60 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2,7 +2,7 @@ //! //! Uses the default `StdMutex` backend (std feature, enabled by default). -#![allow(clippy::unwrap_used)] // Tests use unwrap for clarity +#![allow(clippy::unwrap_used, clippy::expect_used)] use serial_test::serial; use surelock::{ @@ -377,7 +377,7 @@ fn new_after_linear_chain() { #[test] fn new_after_siblings() { - let root: Mutex = Mutex::new(0); + let root: Mutex = Mutex::new(100); let mut child_a = Mutex::new_after(10u32, &root); // Level<1> let mut child_b = Mutex::new_after(20u32, &root); // Level<1> (same level) @@ -390,8 +390,8 @@ fn new_after_siblings() { *b += root_val; }); - assert_eq!(*child_a.get_mut(), 10); - assert_eq!(*child_b.get_mut(), 20); + assert_eq!(*child_a.get_mut(), 110); + assert_eq!(*child_b.get_mut(), 120); } #[test] @@ -501,29 +501,6 @@ fn mutex_after_different_level_parents() { }); } -#[test] -fn mutex_after_different_levels() { - let root: Mutex = Mutex::new(1); // Level<0> - let mid = Mutex::new_after(2u32, &root); // Level<1> - let deep = Mutex::new_after(3u32, &mid); // Level<2> - let shallow = Mutex::new_after(4u32, &root); // Level<1> - - // After both deep (Level<2>) and shallow (Level<1>) - // max(Level<2>, Level<1>) + 1 = Level<3> - let combined = Mutex::new_after(0u32, (&deep, &shallow)); - - let result = lock_scope(|key| { - let (r, key) = key.lock(&root, |g| *g); - let (c, _key) = key.lock(&combined, |mut g| { - *g = r; - *g - }); - c - }); - - assert_eq!(result, 1); -} - // -- Type alias levels -- #[test] @@ -540,6 +517,48 @@ fn type_alias_levels() { }); } +// -- Concurrent multi-thread acquisition -- + +#[test] +fn concurrent_threads_no_deadlock() { + use std::sync::Arc; + + let a: Arc> = Arc::new(Mutex::new(0)); + let b: Arc> = Arc::new(Mutex::new(0)); + + let handles: Vec<_> = (0..8) + .map(|i| { + let a = a.clone(); + let b = b.clone(); + std::thread::spawn(move || { + let mut handle = KeyHandle::claim(); + for _ in 0..100 { + handle.scope(|key| { + // Every thread acquires in the same order (LockSet sorts by LockId) + // regardless of which Arc reference they hold. + let set = LockSet::new((&*a, &*b)); + let ((mut ga, mut gb), _key) = key.acquire(&set); + *ga += i; + *gb += i; + }); + } + }) + }) + .collect(); + + for h in handles { + h.join().expect("thread panicked"); + } + + // Verify both mutexes were updated correctly + // Each thread i adds i*100 to each mutex (100 iterations) + // Total: sum(0..8) * 100 = 28 * 100 = 2800 + let mut a = Arc::try_unwrap(a).expect("arc still shared"); + let mut b = Arc::try_unwrap(b).expect("arc still shared"); + assert_eq!(*a.get_mut(), 2800); + assert_eq!(*b.get_mut(), 2800); +} + // -- Mutable access without locking -- #[test] diff --git a/tests/lock_order.rs b/tests/lock_order.rs index d794c63..674ef10 100644 --- a/tests/lock_order.rs +++ b/tests/lock_order.rs @@ -1,30 +1,13 @@ -//! Tests for manual lock level declarations. -//! -//! Verifies that manually declared levels with `LockAfter` impls -//! produce the expected trait bounds. When `Level` is -//! implemented, these will become type aliases. +//! Compile-time tests verifying that `Level` types produce the +//! expected `LockAfter` trait bounds. #![allow(dead_code, clippy::missing_const_for_fn)] -use surelock::level::{Base, Bottom, IsLevel, LockAfter}; +use surelock::level::{Base, Bottom, IsLevel, Level, LockAfter}; -struct Config; -impl IsLevel for Config {} -impl LockAfter for Config {} -impl LockAfter for Config {} - -struct Account; -impl IsLevel for Account {} -impl LockAfter for Account {} -impl LockAfter for Account {} -impl LockAfter for Account {} - -struct Transaction; -impl IsLevel for Transaction {} -impl LockAfter for Transaction {} -impl LockAfter for Transaction {} -impl LockAfter for Transaction {} -impl LockAfter for Transaction {} +type Config = Level<1>; +type Account = Level<2>; +type Transaction = Level<3>; fn _assert_level_impls() where @@ -63,5 +46,12 @@ where { } +fn _assert_skipping() +where + Level<5>: LockAfter>, + Level<15>: LockAfter>, +{ +} + #[test] fn lock_levels_compile() {} diff --git a/tests/property.rs b/tests/property.rs index 95a5604..1e42df9 100644 --- a/tests/property.rs +++ b/tests/property.rs @@ -20,76 +20,52 @@ fn lockid_monotonicity() { }); } -/// `LockId`s are unique: no two mutexes ever share the same ID. -#[test] -fn lockid_uniqueness() { - bolero::check!().with_type::().for_each(|&count| { - let n = (count as usize).min(64); - let mutexes: Vec> = (0..n).map(|_| Mutex::new(())).collect(); - for i in 0..mutexes.len() { - for j in (i + 1)..mutexes.len() { - assert_ne!( - mutexes[i].id(), - mutexes[j].id(), - "LockId must be unique across all mutexes" - ); - } - } - }); -} - -/// `LockSet` always acquires in sorted `LockId` order, regardless of -/// the order the mutexes were provided in. +/// For any pair of mutexes provided in any order, `LockSet` acquires +/// both successfully and the guards correspond to the original mutexes. #[test] fn lockset_sort_invariant_2tuple() { - // Create mutexes in arbitrary order and verify the LockSet - // always sorts them by LockId. - bolero::check!().with_type::().for_each(|&reverse| { - let a: Mutex = Mutex::new(1); - let b: Mutex = Mutex::new(2); + bolero::check!() + .with_type::<(u32, u32, bool)>() + .for_each(|&(val_a, val_b, reverse)| { + let a: Mutex = Mutex::new(val_a); + let b: Mutex = Mutex::new(val_b); - // Provide in original or reversed order - let set = if reverse { - LockSet::new((&b, &a)) - } else { - LockSet::new((&a, &b)) - }; - - // The LockSet sorts internally -- the guards should always - // correspond to the original mutexes regardless of input order. - lock_scope(|key| { - let ((g1, g2), _key) = key.acquire(&set); - if reverse { - // When reversed, first element was &b, second was &a - // But guards come back in tuple position order - assert_eq!(*g1, 2); - assert_eq!(*g2, 1); + let set = if reverse { + LockSet::new((&b, &a)) } else { - assert_eq!(*g1, 1); - assert_eq!(*g2, 2); - } + LockSet::new((&a, &b)) + }; + + lock_scope(|key| { + let ((g1, g2), _key) = key.acquire(&set); + if reverse { + assert_eq!(*g1, val_b, "first guard should match b"); + assert_eq!(*g2, val_a, "second guard should match a"); + } else { + assert_eq!(*g1, val_a, "first guard should match a"); + assert_eq!(*g2, val_b, "second guard should match b"); + } + }); }); - }); } -/// For any N mutexes, `LockSet`'s internal `sorted_indices` is a valid -/// permutation of 0..N with no duplicates. +/// For any slice of N mutexes, `LockSet` acquires all of them and +/// the guards contain the correct values (sum is preserved). #[test] -fn lockset_indices_are_valid_permutation() { +fn lockset_slice_preserves_values() { bolero::check!().with_type::().for_each(|&count| { - let n = (count as usize).clamp(2, 12); - let mutexes: Vec> = (0..n).map(|_| Mutex::new(())).collect(); - - // Create pairs for a 2-element LockSet from different positions - for i in 0..n { - for j in (i + 1)..n { - let set = LockSet::new((&mutexes[i], &mutexes[j])); + let n = (count as usize).clamp(1, 16); + let mutexes: Vec> = (0..n).map(|i| Mutex::new(i as u64)).collect(); + let expected_sum: u64 = (0..n as u64).sum(); - // Verify the set can be acquired without panic - lock_scope(|key| { - let (_, _key) = key.acquire(&set); - }); - } - } + let set = LockSet::new(mutexes.as_slice()); + lock_scope(|key| { + let (guards, _key) = key.acquire(&set); + let actual_sum: u64 = guards.iter().map(|g| **g).sum(); + assert_eq!( + actual_sum, expected_sum, + "sum of locked values must match sum of original values" + ); + }); }); }