use crate::types::MixLane;
use crate::{Error, Result};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Operation {
Enumerate,
Start,
Normalize,
Flush,
Reopen,
Rollback,
Stop,
Join,
Link,
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Enumerate => "enumerate",
Self::Start => "start",
Self::Normalize => "normalize",
Self::Flush => "flush",
Self::Reopen => "reopen",
Self::Rollback => "rollback",
Self::Stop => "stop",
Self::Join => "join",
Self::Link => "link",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NativeStatus {
HResult {
call: &'static str,
bits: u32,
},
OsStatus {
call: &'static str,
value: i32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ErrorContext {
operation: Operation,
lane: Option<MixLane>,
native_status: Option<NativeStatus>,
}
impl ErrorContext {
pub fn new(operation: Operation) -> Self {
Self {
operation,
lane: None,
native_status: None,
}
}
pub fn with_lane(mut self, lane: MixLane) -> Self {
self.lane = Some(lane);
self
}
pub fn with_native_status(mut self, status: NativeStatus) -> Self {
self.native_status = Some(status);
self
}
pub fn operation(&self) -> Operation {
self.operation
}
pub fn lane(&self) -> Option<MixLane> {
self.lane
}
pub fn native_status(&self) -> Option<NativeStatus> {
self.native_status
}
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "during {}", self.operation)?;
if let Some(lane) = self.lane {
f.write_str(match lane {
MixLane::Microphone => " (microphone)",
MixLane::SystemAudio => " (system audio)",
})?;
}
match self.native_status {
Some(NativeStatus::HResult { bits, .. }) => write!(f, " (HRESULT 0x{bits:08X})"),
Some(NativeStatus::OsStatus { value, .. }) => write!(f, " (OSStatus {value})"),
None => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ErrorGroup {
primary: Box<Error>,
first_secondary: Box<Error>,
remaining_secondary: Vec<Error>,
}
impl ErrorGroup {
pub fn new(primary: Error, first_secondary: Error, remaining_secondary: Vec<Error>) -> Self {
Self {
primary: Box::new(primary),
first_secondary: Box::new(first_secondary),
remaining_secondary,
}
}
pub fn primary(&self) -> &Error {
&self.primary
}
pub fn secondary(&self) -> impl Iterator<Item = &Error> {
std::iter::once(self.first_secondary.as_ref()).chain(self.remaining_secondary.iter())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ShutdownReport {
primary: Option<Error>,
cleanup: Vec<Error>,
}
impl ShutdownReport {
pub fn new(primary: Option<Error>, cleanup: Vec<Error>) -> Self {
Self { primary, cleanup }
}
pub fn primary(&self) -> Option<&Error> {
self.primary.as_ref()
}
pub fn cleanup(&self) -> &[Error] {
&self.cleanup
}
pub fn result(&self) -> Result<()> {
let first = match self.primary.as_ref() {
Some(primary) => primary,
None => match self.cleanup.iter().find(|error| error.vad_code().is_none()) {
Some(error) => error,
None => return Ok(()),
},
};
let mut causes = std::iter::once(first.clone()).chain(
self.cleanup
.iter()
.filter(|error| !std::ptr::eq(*error, first))
.cloned(),
);
match (causes.next(), causes.next()) {
(None, _) => Ok(()),
(Some(error), None) => Err(error),
(Some(primary), Some(first)) => Err(Error::Multiple(ErrorGroup::new(
primary,
first,
causes.collect(),
))),
}
}
}
#[cfg(test)]
mod vad_tests {
use super::*;
use crate::VadErrorCode;
#[test]
fn vad_only_shutdown_succeeds_and_retains_the_typed_cause() {
let cause = Error::Vad {
code: VadErrorCode::Inference,
}
.with_context(ErrorContext::new(Operation::Flush));
let report = ShutdownReport::new(None, vec![cause.clone()]);
assert!(report.result().is_ok());
assert!(report.primary().is_none());
assert_eq!(report.cleanup(), &[cause]);
assert_eq!(
report.cleanup()[0].vad_code(),
Some(VadErrorCode::Inference)
);
assert!(report.cleanup()[0]
.to_string()
.contains("model inference or reset could not complete"));
assert!(ShutdownReport::new(None, Vec::new()).result().is_ok());
}
#[test]
fn capture_shutdown_message_excludes_isolated_vad_related_causes() {
let capture = Error::DeviceLost
.with_context(ErrorContext::new(Operation::Stop))
.with_context(ErrorContext::new(Operation::Stop));
for operation in [Operation::Normalize, Operation::Flush] {
let vad = Error::Vad {
code: VadErrorCode::Inference,
}
.with_context(ErrorContext::new(operation));
let report = ShutdownReport::new(Some(capture.clone()), vec![vad.clone()]);
let error = report
.result()
.expect_err("capture loss must still fail stop");
assert_eq!(error.to_string(), "device lost during stop during stop");
assert_eq!(error.root(), &Error::DeviceLost);
assert_eq!(report.cleanup(), std::slice::from_ref(&vad));
match error {
Error::Multiple(group) => {
assert_eq!(group.secondary().collect::<Vec<_>>(), [&vad]);
assert_eq!(
group.secondary().next().and_then(Error::vad_code),
Some(VadErrorCode::Inference)
);
}
_ => panic!("the rejecting error must retain typed related failures"),
}
}
}
#[test]
fn capture_shutdown_message_retains_non_vad_related_causes() {
let capture = Error::DeviceLost.with_context(ErrorContext::new(Operation::Stop));
let cleanup = Error::Backend("the backend could not finish its shutdown".into())
.with_context(ErrorContext::new(Operation::Join));
let vad = Error::Vad {
code: VadErrorCode::Inference,
}
.with_context(ErrorContext::new(Operation::Flush));
let report = ShutdownReport::new(Some(capture), vec![vad, cleanup]);
let error = report
.result()
.expect_err("capture loss must still fail stop");
assert_eq!(error.to_string(), "device lost during stop; related failure: backend error: the backend could not finish its shutdown during join");
assert!(matches!(error, Error::Multiple(group) if group.secondary().count() == 2));
}
#[test]
fn real_shutdown_failures_remain_primary_and_retain_vad_related_causes() {
let vad = Error::Vad {
code: VadErrorCode::Conversion,
};
let capture = Error::DeviceLost;
let report = ShutdownReport::new(Some(capture.clone()), vec![vad.clone()]);
let error = report
.result()
.expect_err("capture loss must still fail stop");
assert_eq!(error.root(), &capture);
assert!(
matches!(error, Error::Multiple(ref group) if group.secondary().next() == Some(&vad))
);
let cleanup = Error::Backend("the backend could not finish its shutdown".into());
let report = ShutdownReport::new(None, vec![vad.clone(), cleanup.clone()]);
let error = report
.result()
.expect_err("backend teardown failure must still fail stop");
assert_eq!(error.root(), &cleanup);
assert!(
matches!(error, Error::Multiple(ref group) if group.secondary().next() == Some(&vad))
);
}
}