Verify mutex recovery produces usable inner value after poisoning. 5 tests covering value preservation, mutable recovery, Arc clone, and snapshot-through-guard patterns.
140 lines
4.2 KiB
Rust
140 lines
4.2 KiB
Rust
//! 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<T>` by panicking while holding its lock,
|
|
/// then catches the panic so the test can continue.
|
|
fn poison_mutex<T: Default>(mx: &Mutex<T>) {
|
|
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<T>(mx: &Mutex<T>) -> 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<Vec<f32>> = 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<Mutex<T>> 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);
|
|
}
|