//! Poisoned-mutex survival test for audio callbacks (TODO-016). //! //! Verifies that the `unwrap_or_else(|e| e.into_inner())` recovery //! pattern used throughout chanora_audio produces usable values //! rather than panicking when a mutex is poisoned. use std::panic; use std::sync::{Arc, Mutex}; /// Simulates a simple config guard behind a mutex, matching the /// pattern used by `audio_processing_config` in the engine. #[derive(Debug, Clone, PartialEq)] struct DummyConfig { gain: f32, muted: bool, } impl Default for DummyConfig { fn default() -> Self { Self { gain: 1.0, muted: false, } } } /// Poisons a `Mutex` by panicking while holding its lock, /// then catches the panic so the test can continue. fn poison_mutex(mx: &Mutex) { let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| { let _guard = mx.lock().unwrap(); panic!("deliberate poison"); })); } /// Helper that mirrors the exact recovery pattern used in production: /// `lock().unwrap_or_else(|e| e.into_inner())`. fn recover(mx: &Mutex) -> std::sync::MutexGuard<'_, T> { mx.lock().unwrap_or_else(|e| e.into_inner()) } // --------------------------------------------------------------------------- // Test 1: Basic poison recovery returns the inner value. // --------------------------------------------------------------------------- #[test] fn poison_recovery_returns_inner_value() { let mx = Mutex::new(DummyConfig::default()); { let mut g = mx.lock().unwrap(); g.gain = 0.5; g.muted = true; } poison_mutex(&mx); assert!(mx.is_poisoned()); let guard = recover(&mx); assert_eq!(guard.gain, 0.5); assert!(guard.muted); } // --------------------------------------------------------------------------- // Test 2: Recovered guard is mutable and usable (simulates an audio // callback writing silence to the output buffer after recovery). // --------------------------------------------------------------------------- #[test] fn recovered_guard_is_mutable() { let mx: Mutex> = Mutex::new(vec![0.0; 256]); { let mut g = mx.lock().unwrap(); g.fill(0.75); } poison_mutex(&mx); { let mut guard = recover(&mx); guard.fill(0.0); } let guard = recover(&mx); assert!(guard.iter().all(|&s| s == 0.0), "expected silence after recovery"); } // --------------------------------------------------------------------------- // Test 3: Multi-lock scenario — recover from a poisoned mutex, mutate // it, and verify subsequent reads see the updated state. // --------------------------------------------------------------------------- #[test] fn recovered_state_persists_across_locks() { let mx = Mutex::new(42u32); poison_mutex(&mx); *recover(&mx) = 99; assert_eq!(*recover(&mx), 99); assert!(mx.is_poisoned(), "mutex stays poisoned but remains usable"); } // --------------------------------------------------------------------------- // Test 4: Arc> pattern — mirrors the audio engine's shared // state where multiple callbacks hold Arc clones. // --------------------------------------------------------------------------- #[test] fn shared_arc_mutex_recovery() { let mx = Arc::new(Mutex::new(DummyConfig::default())); { let mut g = mx.lock().unwrap(); g.gain = 0.8; } poison_mutex(&mx); let mx2 = Arc::clone(&mx); let guard = mx2.lock().unwrap_or_else(|e| e.into_inner()); assert_eq!(guard.gain, 0.8); } // --------------------------------------------------------------------------- // Test 5: Snapshot-then-clone pattern — mirrors the engine's // `audio_processing_config_snapshot()` which clones through the guard. // --------------------------------------------------------------------------- #[test] fn snapshot_clone_through_poisoned_mutex() { let mx = Mutex::new(DummyConfig { gain: 0.42, muted: true, }); poison_mutex(&mx); let snapshot = mx.lock().unwrap_or_else(|e| e.into_inner()).clone(); assert_eq!(snapshot.gain, 0.42); assert!(snapshot.muted); let snapshot2 = mx.lock().unwrap_or_else(|e| e.into_inner()).clone(); assert_eq!(snapshot, snapshot2); }