1use crate::types::MixLane;
3use crate::{Error, Result};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum Operation {
10 Enumerate,
12 Start,
14 Normalize,
16 Flush,
18 Reopen,
20 Rollback,
22 Stop,
24 Join,
26 Link,
28}
29impl fmt::Display for Operation {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(match self {
32 Self::Enumerate => "enumerate",
33 Self::Start => "start",
34 Self::Normalize => "normalize",
35 Self::Flush => "flush",
36 Self::Reopen => "reopen",
37 Self::Rollback => "rollback",
38 Self::Stop => "stop",
39 Self::Join => "join",
40 Self::Link => "link",
41 })
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum NativeStatus {
49 HResult {
51 call: &'static str,
53 bits: u32,
55 },
56 OsStatus {
58 call: &'static str,
60 value: i32,
62 },
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct ErrorContext {
69 operation: Operation,
70 lane: Option<MixLane>,
71 native_status: Option<NativeStatus>,
72}
73impl ErrorContext {
74 pub fn new(operation: Operation) -> Self {
76 Self {
77 operation,
78 lane: None,
79 native_status: None,
80 }
81 }
82 pub fn with_lane(mut self, lane: MixLane) -> Self {
84 self.lane = Some(lane);
85 self
86 }
87 pub fn with_native_status(mut self, status: NativeStatus) -> Self {
89 self.native_status = Some(status);
90 self
91 }
92 pub fn operation(&self) -> Operation {
94 self.operation
95 }
96 pub fn lane(&self) -> Option<MixLane> {
98 self.lane
99 }
100 pub fn native_status(&self) -> Option<NativeStatus> {
102 self.native_status
103 }
104}
105impl fmt::Display for ErrorContext {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(f, "during {}", self.operation)?;
108 if let Some(lane) = self.lane {
109 f.write_str(match lane {
110 MixLane::Microphone => " (microphone)",
111 MixLane::SystemAudio => " (system audio)",
112 })?;
113 }
114 match self.native_status {
115 Some(NativeStatus::HResult { bits, .. }) => write!(f, " (HRESULT 0x{bits:08X})"),
116 Some(NativeStatus::OsStatus { value, .. }) => write!(f, " (OSStatus {value})"),
117 None => Ok(()),
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub struct ErrorGroup {
126 primary: Box<Error>,
127 first_secondary: Box<Error>,
128 remaining_secondary: Vec<Error>,
129}
130impl ErrorGroup {
131 pub fn new(primary: Error, first_secondary: Error, remaining_secondary: Vec<Error>) -> Self {
133 Self {
134 primary: Box::new(primary),
135 first_secondary: Box::new(first_secondary),
136 remaining_secondary,
137 }
138 }
139 pub fn primary(&self) -> &Error {
141 &self.primary
142 }
143 pub fn secondary(&self) -> impl Iterator<Item = &Error> {
145 std::iter::once(self.first_secondary.as_ref()).chain(self.remaining_secondary.iter())
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151#[non_exhaustive]
152pub struct ShutdownReport {
153 primary: Option<Error>,
154 cleanup: Vec<Error>,
155}
156impl ShutdownReport {
157 pub fn new(primary: Option<Error>, cleanup: Vec<Error>) -> Self {
159 Self { primary, cleanup }
160 }
161 pub fn primary(&self) -> Option<&Error> {
163 self.primary.as_ref()
164 }
165 pub fn cleanup(&self) -> &[Error] {
167 &self.cleanup
168 }
169 pub fn result(&self) -> Result<()> {
172 let first = match self.primary.as_ref() {
173 Some(primary) => primary,
174 None => match self
175 .cleanup
176 .iter()
177 .find(|error| error.whisper_code().is_none())
178 {
179 Some(error) => error,
180 None => return Ok(()),
181 },
182 };
183 let mut causes = std::iter::once(first.clone()).chain(
184 self.cleanup
185 .iter()
186 .filter(|error| !std::ptr::eq(*error, first))
187 .cloned(),
188 );
189 match (causes.next(), causes.next()) {
190 (None, _) => Ok(()),
191 (Some(error), None) => Err(error),
192 (Some(primary), Some(first)) => Err(Error::Multiple(ErrorGroup::new(
193 primary,
194 first,
195 causes.collect(),
196 ))),
197 }
198 }
199}
200
201#[cfg(test)]
202mod whisper_tests {
203 use super::*;
204 use crate::WhisperCode;
205
206 #[test]
207 fn whisper_only_shutdown_succeeds_and_retains_the_typed_cause() {
208 let cause = Error::WhisperVad {
209 code: WhisperCode::Inference,
210 }
211 .with_context(ErrorContext::new(Operation::Flush));
212 let report = ShutdownReport::new(None, vec![cause.clone()]);
213 assert!(report.result().is_ok());
214 assert!(report.primary().is_none());
215 assert_eq!(report.cleanup(), &[cause]);
216 assert_eq!(
217 report.cleanup()[0].whisper_code(),
218 Some(WhisperCode::Inference)
219 );
220 assert!(report.cleanup()[0]
221 .to_string()
222 .contains("model inference or reset could not complete"));
223 assert!(ShutdownReport::new(None, Vec::new()).result().is_ok());
224 }
225
226 #[test]
227 fn capture_shutdown_message_excludes_isolated_whisper_related_causes() {
228 let capture = Error::DeviceLost
229 .with_context(ErrorContext::new(Operation::Stop))
230 .with_context(ErrorContext::new(Operation::Stop));
231 for operation in [Operation::Normalize, Operation::Flush] {
232 let whisper = Error::WhisperVad {
233 code: WhisperCode::Inference,
234 }
235 .with_context(ErrorContext::new(operation));
236 let report = ShutdownReport::new(Some(capture.clone()), vec![whisper.clone()]);
237 let error = report
238 .result()
239 .expect_err("capture loss must still fail stop");
240 assert_eq!(error.to_string(), "device lost during stop during stop");
241 assert_eq!(error.root(), &Error::DeviceLost);
242 assert_eq!(report.cleanup(), std::slice::from_ref(&whisper));
243 match error {
244 Error::Multiple(group) => {
245 assert_eq!(group.secondary().collect::<Vec<_>>(), [&whisper]);
246 assert_eq!(
247 group.secondary().next().and_then(Error::whisper_code),
248 Some(WhisperCode::Inference)
249 );
250 }
251 _ => panic!("the rejecting error must retain typed related failures"),
252 }
253 }
254 }
255
256 #[test]
257 fn capture_shutdown_message_retains_non_whisper_related_causes() {
258 let capture = Error::DeviceLost.with_context(ErrorContext::new(Operation::Stop));
259 let cleanup = Error::Backend("the backend could not finish its shutdown".into())
260 .with_context(ErrorContext::new(Operation::Join));
261 let whisper = Error::WhisperVad {
262 code: WhisperCode::Inference,
263 }
264 .with_context(ErrorContext::new(Operation::Flush));
265 let report = ShutdownReport::new(Some(capture), vec![whisper, cleanup]);
266 let error = report
267 .result()
268 .expect_err("capture loss must still fail stop");
269 assert_eq!(error.to_string(), "device lost during stop; related failure: backend error: the backend could not finish its shutdown during join");
270 assert!(matches!(error, Error::Multiple(group) if group.secondary().count() == 2));
271 }
272
273 #[test]
274 fn real_shutdown_failures_remain_primary_and_retain_whisper_related_causes() {
275 let whisper = Error::WhisperVad {
276 code: WhisperCode::Conversion,
277 };
278 let capture = Error::DeviceLost;
279 let report = ShutdownReport::new(Some(capture.clone()), vec![whisper.clone()]);
280 let error = report
281 .result()
282 .expect_err("capture loss must still fail stop");
283 assert_eq!(error.root(), &capture);
284 assert!(
285 matches!(error, Error::Multiple(ref group) if group.secondary().next() == Some(&whisper))
286 );
287
288 let cleanup = Error::Backend("the backend could not finish its shutdown".into());
290 let report = ShutdownReport::new(None, vec![whisper.clone(), cleanup.clone()]);
291 let error = report
292 .result()
293 .expect_err("backend teardown failure must still fail stop");
294 assert_eq!(error.root(), &cleanup);
295 assert!(
296 matches!(error, Error::Multiple(ref group) if group.secondary().next() == Some(&whisper))
297 );
298 }
299}