Files
fluxer/fluxer_desktop/native/mac-app-audio/src/source_state.rs
T

231 lines
6.4 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum State {
Idle = 0,
Starting = 1,
Running = 2,
Stopping = 3,
Stopped = 4,
}
impl State {
fn from_u32(v: u32) -> State {
match v {
0 => State::Idle,
1 => State::Starting,
2 => State::Running,
3 => State::Stopping,
_ => State::Stopped,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TransitionError {
IllegalTransition,
DoubleStart,
StopBeforeStart,
StartWhileStopping,
}
pub fn is_allowed(from: State, to: State) -> bool {
match from {
State::Idle => matches!(to, State::Starting | State::Stopped),
State::Starting => matches!(to, State::Running | State::Stopped),
State::Running => matches!(to, State::Stopping | State::Stopped),
State::Stopping => matches!(to, State::Stopped),
State::Stopped => false,
}
}
pub struct Machine {
state: AtomicU32,
}
impl Machine {
pub fn new() -> Self {
Self {
state: AtomicU32::new(State::Idle as u32),
}
}
pub fn current(&self) -> State {
State::from_u32(self.state.load(Ordering::Acquire))
}
fn cas(&self, from: State, to: State) -> Result<(), TransitionError> {
if !is_allowed(from, to) {
return Err(TransitionError::IllegalTransition);
}
self.state
.compare_exchange(from as u32, to as u32, Ordering::AcqRel, Ordering::Acquire)
.map(|_| ())
.map_err(|_| TransitionError::IllegalTransition)
}
pub fn request_start(&self) -> Result<(), TransitionError> {
match self.current() {
State::Idle => self.cas(State::Idle, State::Starting),
State::Starting | State::Running => Err(TransitionError::DoubleStart),
State::Stopping => Err(TransitionError::StartWhileStopping),
State::Stopped => Err(TransitionError::IllegalTransition),
}
}
pub fn mark_running(&self) -> Result<(), TransitionError> {
self.cas(State::Starting, State::Running)
}
pub fn request_stop(&self) -> Result<(), TransitionError> {
match self.current() {
State::Running => self.cas(State::Running, State::Stopping),
State::Idle => Err(TransitionError::StopBeforeStart),
State::Starting | State::Stopping | State::Stopped => {
Err(TransitionError::IllegalTransition)
}
}
}
pub fn mark_stopped(&self) -> Result<(), TransitionError> {
self.cas(State::Stopping, State::Stopped)
}
pub fn cancel_idle(&self) -> Result<(), TransitionError> {
self.cas(State::Idle, State::Stopped)
}
pub fn mark_fatal(&self) -> State {
loop {
let raw = self.state.load(Ordering::Acquire);
let prev = State::from_u32(raw);
if prev == State::Stopped {
return prev;
}
if self
.state
.compare_exchange(
raw,
State::Stopped as u32,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
return prev;
}
}
}
}
impl Default for Machine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_allowed_exhaustive() {
let all = [
State::Idle,
State::Starting,
State::Running,
State::Stopping,
State::Stopped,
];
let allowed: &[(State, State)] = &[
(State::Idle, State::Starting),
(State::Idle, State::Stopped),
(State::Starting, State::Running),
(State::Starting, State::Stopped),
(State::Running, State::Stopping),
(State::Running, State::Stopped),
(State::Stopping, State::Stopped),
];
for &from in &all {
for &to in &all {
let expected = allowed.iter().any(|p| p.0 == from && p.1 == to);
assert_eq!(expected, is_allowed(from, to), "{:?} -> {:?}", from, to);
}
}
}
#[test]
fn happy_path() {
let m = Machine::new();
assert_eq!(State::Idle, m.current());
m.request_start().unwrap();
assert_eq!(State::Starting, m.current());
m.mark_running().unwrap();
assert_eq!(State::Running, m.current());
m.request_stop().unwrap();
assert_eq!(State::Stopping, m.current());
m.mark_stopped().unwrap();
assert_eq!(State::Stopped, m.current());
}
#[test]
fn double_start_rejected() {
let m = Machine::new();
m.request_start().unwrap();
assert_eq!(Err(TransitionError::DoubleStart), m.request_start());
m.mark_running().unwrap();
assert_eq!(Err(TransitionError::DoubleStart), m.request_start());
}
#[test]
fn stop_before_start_rejected() {
let m = Machine::new();
assert_eq!(Err(TransitionError::StopBeforeStart), m.request_stop());
}
#[test]
fn start_while_stopping_rejected() {
let m = Machine::new();
m.request_start().unwrap();
m.mark_running().unwrap();
m.request_stop().unwrap();
assert_eq!(Err(TransitionError::StartWhileStopping), m.request_start());
}
#[test]
fn cancel_idle_short_circuits() {
let m = Machine::new();
m.cancel_idle().unwrap();
assert_eq!(State::Stopped, m.current());
assert_eq!(Err(TransitionError::IllegalTransition), m.request_start());
}
#[test]
fn mark_fatal_forces_stopped() {
for &start in &[
State::Idle,
State::Starting,
State::Running,
State::Stopping,
] {
let m = Machine::new();
m.state.store(start as u32, Ordering::Release);
let prev = m.mark_fatal();
assert_eq!(start, prev);
assert_eq!(State::Stopped, m.current());
}
}
#[test]
fn mark_fatal_idempotent() {
let m = Machine::new();
m.state.store(State::Stopped as u32, Ordering::Release);
let prev = m.mark_fatal();
assert_eq!(State::Stopped, prev);
assert_eq!(State::Stopped, m.current());
}
}