1use ff_decode::DecodeError;
4use ff_format::{ErrorSeverity, MediaError};
5use thiserror::Error;
6
7#[derive(Error, Debug)]
10pub enum AnalysisError {
11 #[error("analysis failed: {reason}")]
14 Failed {
15 reason: String,
17 },
18
19 #[error("BPM detection failed: {reason}")]
24 BpmDetectionFailed {
25 reason: String,
27 },
28
29 #[error(transparent)]
31 Decode(#[from] DecodeError),
32}
33
34impl MediaError for AnalysisError {
35 fn severity(&self) -> ErrorSeverity {
38 match self {
39 Self::Decode(e) => e.severity(),
40 Self::Failed { .. } | Self::BpmDetectionFailed { .. } => ErrorSeverity::Fatal,
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn failed_should_display_reason() {
51 let e = AnalysisError::Failed {
52 reason: "interval must be non-zero".to_string(),
53 };
54 let msg = e.to_string();
55 assert!(msg.contains("analysis failed"), "unexpected message: {msg}");
56 assert!(
57 msg.contains("interval must be non-zero"),
58 "expected reason in message: {msg}"
59 );
60 }
61
62 #[test]
63 fn failed_should_be_fatal_and_not_recoverable() {
64 let e = AnalysisError::Failed {
65 reason: "zero interval".to_string(),
66 };
67 assert!(e.is_fatal());
68 assert!(!e.is_recoverable());
69 }
70
71 #[test]
72 fn bpm_detection_failed_should_display_reason() {
73 let e = AnalysisError::BpmDetectionFailed {
74 reason: "no audio stream".to_string(),
75 };
76 let msg = e.to_string();
77 assert!(
78 msg.contains("BPM detection failed"),
79 "unexpected message: {msg}"
80 );
81 assert!(
82 msg.contains("no audio stream"),
83 "expected reason in message: {msg}"
84 );
85 }
86
87 #[test]
88 fn bpm_detection_failed_should_be_fatal_and_not_recoverable() {
89 let e = AnalysisError::BpmDetectionFailed {
90 reason: "clip too short".to_string(),
91 };
92 assert!(e.is_fatal());
93 assert!(!e.is_recoverable());
94 }
95
96 #[test]
97 fn decode_variant_should_delegate_severity_to_inner() {
98 let inner = DecodeError::decoding_failed("corrupt frame");
100 let recoverable = inner.is_recoverable();
101 let e = AnalysisError::Decode(inner);
102 assert_eq!(e.is_recoverable(), recoverable);
103 assert!(e.is_recoverable());
104 }
105
106 #[test]
107 fn decode_variant_should_convert_via_from() {
108 let e: AnalysisError = DecodeError::FileNotFound {
109 path: std::path::PathBuf::from("missing.mp4"),
110 }
111 .into();
112 assert!(matches!(e, AnalysisError::Decode(_)));
113 assert!(e.is_fatal());
114 }
115}