//! `loom::Mutex` tests. All run under the scheduler because `lock()` //! needs to be able to park. use smarm::{run, spawn, yield_now, LockTimeout, Mutex}; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::{Duration, Instant}; // --------------------------------------------------------------------------- // Uncontended fast path // --------------------------------------------------------------------------- #[test] fn lock_free_mutex_succeeds() { let captured = Arc::new(AtomicU32::new(0)); let c = captured.clone(); run(move || { let m = Mutex::new(42u32); { let g = m.lock_timeout(Duration::from_millis(500)).unwrap(); c.store(*g, Ordering::SeqCst); } // After drop we can lock again. let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap(); assert_eq!(*g2, 42); }); assert_eq!(captured.load(Ordering::SeqCst), 42); } #[test] fn try_lock_returns_some_when_free_none_when_held() { let success_flag = Arc::new(AtomicU32::new(0)); let s = success_flag.clone(); run(move || { let m = Mutex::new(0u32); let g = m.try_lock().expect("free"); // Holding the guard; a second try_lock on the same actor should fail. assert!(m.try_lock().is_none()); drop(g); // Now free again. let g2 = m.try_lock().expect("free again"); drop(g2); s.store(1, Ordering::SeqCst); }); assert_eq!(success_flag.load(Ordering::SeqCst), 1); } #[test] fn guard_mutates_value_visible_through_next_lock() { let final_value = Arc::new(AtomicU32::new(0)); let f = final_value.clone(); run(move || { let m = Mutex::new(0u32); { let mut g = m.lock_timeout(Duration::from_millis(500)).unwrap(); *g = 7; } let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap(); f.store(*g2, Ordering::SeqCst); }); assert_eq!(final_value.load(Ordering::SeqCst), 7); } // --------------------------------------------------------------------------- // Contention: a second actor parks until the first releases. // --------------------------------------------------------------------------- #[test] fn contended_lock_parks_until_holder_releases() { // Actor A locks, yields (still holding), then releases. Actor B tries // to lock in between — B should park, then succeed after A drops. let log: Arc>> = Arc::new(StdMutex::new(Vec::new())); let la = log.clone(); let lb = log.clone(); run(move || { let m = Mutex::new(0u32); let m_a = m.clone(); let m_b = m.clone(); let a = spawn(move || { let g = m_a.lock_timeout(Duration::from_millis(500)).unwrap(); la.lock().unwrap().push("A_locked"); // First yield: lets B run past its first yield_now. yield_now(); // Second yield: lets B reach B_try and attempt lock() while we // still hold it, so B parks on the mutex. yield_now(); la.lock().unwrap().push("A_dropping"); drop(g); la.lock().unwrap().push("A_dropped"); }); let b = spawn(move || { // One yield: lets A run and acquire the lock first. yield_now(); lb.lock().unwrap().push("B_try"); let _g = m_b.lock_timeout(Duration::from_millis(500)).unwrap(); lb.lock().unwrap().push("B_locked"); }); a.join().unwrap(); b.join().unwrap(); }); let v = log.lock().unwrap(); // A locks, B tries (parks), A drops, B gets the lock. let pos_a_locked = v.iter().position(|s| *s == "A_locked").unwrap(); let pos_b_try = v.iter().position(|s| *s == "B_try").unwrap(); let pos_a_dropped = v.iter().position(|s| *s == "A_dropped").unwrap(); let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap(); assert!(pos_a_locked < pos_b_try, "log: {:?}", *v); assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v); assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v); } // --------------------------------------------------------------------------- // Timeout: B times out while A holds forever. // --------------------------------------------------------------------------- #[test] fn lock_timeout_returns_err_when_holder_never_releases() { let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false)); let s = saw_err.clone(); run(move || { let m: Mutex = Mutex::new(0); let m_a = m.clone(); let m_b = m.clone(); let a = spawn(move || { // Hold the lock for 100ms, blocking B's attempt with a 20ms timeout. let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap(); smarm::sleep(Duration::from_millis(100)); // _g drops here. }); let b = spawn(move || { // Let A acquire first. yield_now(); let t0 = Instant::now(); let res = m_b.lock_timeout(Duration::from_millis(20)); let elapsed = t0.elapsed(); assert!(matches!(res, Err(LockTimeout)), "got {:?}", res); // Sanity: actually waited approximately the timeout. assert!( elapsed >= Duration::from_millis(15), "timed out too fast: {:?}", elapsed ); assert!( elapsed < Duration::from_millis(80), "timed out far too slow: {:?}", elapsed ); s.store(true, Ordering::SeqCst); }); a.join().unwrap(); b.join().unwrap(); }); assert!(saw_err.load(Ordering::SeqCst)); } // --------------------------------------------------------------------------- // FIFO fairness: when many actors queue, they get the lock in arrival order. // --------------------------------------------------------------------------- #[test] fn waiters_are_granted_the_lock_in_fifo_order() { let order: Arc>> = Arc::new(StdMutex::new(Vec::new())); run({ let order = order.clone(); move || { let m: Mutex<()> = Mutex::new(()); // Holder: takes the lock, yields to let others queue up, then // releases. Each waiter records its arrival order on acquisition. let m_holder = m.clone(); let holder = spawn(move || { let g = m_holder.lock_timeout(Duration::from_millis(500)).unwrap(); // Let waiters pile up. for _ in 0..5 { yield_now(); } drop(g); }); // Spawn 4 waiters in order 1, 2, 3, 4. Each yields once before // calling lock(), so we know the holder ran first. let mut handles = vec![holder]; for id in 1u32..=4 { let m_w = m.clone(); let o = order.clone(); handles.push(spawn(move || { // Stagger the lock attempts so they arrive in order. for _ in 0..id { yield_now(); } let _g = m_w.lock_timeout(Duration::from_millis(500)).unwrap(); o.lock().unwrap().push(id); })); } for h in handles { h.join().unwrap(); } } }); let v = order.lock().unwrap().clone(); assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order"); } // --------------------------------------------------------------------------- // Grant-vs-timeout race: holder drops just before timer would fire — waiter // should get the lock, not LockTimeout. // --------------------------------------------------------------------------- #[test] fn grant_wins_when_holder_releases_before_timeout() { let got_lock = Arc::new(std::sync::atomic::AtomicBool::new(false)); let g = got_lock.clone(); run(move || { let m: Mutex = Mutex::new(0); let m_a = m.clone(); let m_b = m.clone(); let a = spawn(move || { let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap(); // Hold for 10ms, well under B's 100ms timeout. smarm::sleep(Duration::from_millis(10)); }); let b = spawn(move || { yield_now(); let res = m_b.lock_timeout(Duration::from_millis(100)); if res.is_ok() { g.store(true, Ordering::SeqCst); } }); a.join().unwrap(); b.join().unwrap(); }); assert!(got_lock.load(Ordering::SeqCst)); } // --------------------------------------------------------------------------- // Panic in critical section: next waiter still gets the lock (no poisoning). // --------------------------------------------------------------------------- #[test] fn next_waiter_gets_lock_after_holder_panics() { let next_got_it = Arc::new(std::sync::atomic::AtomicBool::new(false)); let n = next_got_it.clone(); run(move || { let m: Mutex = Mutex::new(7); let m_a = m.clone(); let m_b = m.clone(); let a = spawn(move || { let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap(); yield_now(); panic!("holder dies mid-critical-section"); }); let b = spawn(move || { yield_now(); // A is dead but its guard's Drop ran during unwind. We get the lock. let g = m_b.lock_timeout(Duration::from_millis(100)).unwrap(); assert_eq!(*g, 7); n.store(true, Ordering::SeqCst); }); let _ = a.join(); // panic — expected b.join().unwrap(); }); assert!(next_got_it.load(Ordering::SeqCst)); } // --------------------------------------------------------------------------- // Multiple short critical sections under contention all complete (no lost // wakeups, no deadlock). Counts up to N from M actors. // --------------------------------------------------------------------------- #[test] fn many_actors_increment_shared_counter_via_mutex() { const ACTORS: u32 = 8; const PER_ACTOR: u32 = 50; let final_value = Arc::new(AtomicU32::new(0)); let fv = final_value.clone(); run(move || { let m: Mutex = Mutex::new(0); let mut handles = Vec::new(); for _ in 0..ACTORS { let m_i = m.clone(); handles.push(spawn(move || { for _ in 0..PER_ACTOR { let mut g = m_i.lock_timeout(Duration::from_millis(500)).unwrap(); *g += 1; } })); } for h in handles { h.join().unwrap(); } let g = m.lock_timeout(Duration::from_millis(500)).unwrap(); fv.store(*g, Ordering::SeqCst); }); assert_eq!(final_value.load(Ordering::SeqCst), ACTORS * PER_ACTOR); }