use core::sync::atomic::{AtomicU8, Ordering};
use crate::error::Error;
const FREE: u8 = 0;
const TAKEN: u8 = 1;
const POISONED: u8 = 2;
static STATE: AtomicU8 = AtomicU8::new(FREE);
#[derive(Debug)]
pub struct Claim {
poisoned: bool,
}
impl Claim {
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system takes it; still unit-tested on the host"
)
)]
pub fn take() -> Result<Self, Error> {
match STATE.compare_exchange(FREE, TAKEN, Ordering::Acquire, Ordering::Relaxed) {
Ok(_) => Ok(Self { poisoned: false }),
Err(POISONED) => Err(Error::AudioSystemPoisoned),
Err(_) => Err(Error::AudioSystemExists),
}
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system poisons it; still unit-tested on the host"
)
)]
pub const fn poison(&mut self) {
self.poisoned = true;
}
}
impl Drop for Claim {
fn drop(&mut self) {
let released = if self.poisoned { POISONED } else { FREE };
STATE.store(released, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use std::panic::catch_unwind;
use std::sync::{Mutex, PoisonError};
use std::thread;
use super::*;
static SERIALISE: Mutex<()> = Mutex::new(());
fn serialised() -> impl Drop {
let order = SERIALISE.lock().unwrap_or_else(PoisonError::into_inner);
STATE.store(FREE, Ordering::Release);
order
}
#[test]
fn only_one_claim_exists_at_a_time() {
let _order = serialised();
let first = Claim::take().expect("the claim should be free");
assert_eq!(
Claim::take().unwrap_err(),
Error::AudioSystemExists,
"a second audio system must be refused"
);
drop(first);
Claim::take().expect("dropping the first should have freed the claim");
}
#[test]
fn another_thread_is_refused_too() {
let _order = serialised();
let held = Claim::take().expect("the claim should be free");
let from_other_thread = thread::spawn(|| Claim::take().map(|_| ()))
.join()
.expect("the thread should not panic");
assert_eq!(
from_other_thread.unwrap_err(),
Error::AudioSystemExists,
"the claim is process-wide, not per-thread"
);
drop(held);
}
#[test]
fn a_claim_dropped_while_unwinding_is_released() {
let _order = serialised();
let panicked = catch_unwind(|| {
let _claim = Claim::take().expect("the claim should be free");
panic!("something went wrong during setup");
});
assert!(panicked.is_err(), "the panic should propagate");
Claim::take().expect("a panic must not leave the claim held forever");
}
#[test]
fn a_poisoned_claim_is_never_free_again() {
let _order = serialised();
let mut claim = Claim::take().expect("the claim should be free");
claim.poison();
drop(claim);
assert_eq!(
Claim::take().unwrap_err(),
Error::AudioSystemPoisoned,
"a poisoned audio system must not be handed out again"
);
assert_eq!(
Claim::take().unwrap_err(),
Error::AudioSystemPoisoned,
"and must stay refused however many times it is asked for"
);
}
#[test]
fn poisoning_is_refused_from_other_threads_too() {
let _order = serialised();
let mut claim = Claim::take().expect("the claim should be free");
claim.poison();
drop(claim);
let from_other_thread = thread::spawn(|| Claim::take().map(|_| ()))
.join()
.expect("the thread should not panic");
assert_eq!(
from_other_thread.unwrap_err(),
Error::AudioSystemPoisoned,
"the poison is process-wide, like the claim it replaces"
);
}
#[test]
fn an_unpoisoned_claim_still_frees() {
let _order = serialised();
let claim = Claim::take().expect("the claim should be free");
drop(claim);
Claim::take().expect("only a poisoned claim should refuse the next one");
}
}