1use ff_format::{ErrorSeverity, MediaError};
7
8#[derive(Debug, thiserror::Error)]
21pub enum PipelineError {
22 #[error("decode failed: {0}")]
27 Decode(#[from] ff_decode::DecodeError),
28
29 #[error("filter failed: {0}")]
34 Filter(#[from] ff_filter::FilterError),
35
36 #[error("encode failed: {0}")]
41 Encode(#[from] ff_encode::EncodeError),
42
43 #[error("no input specified")]
48 NoInput,
49
50 #[error("no output specified")]
55 NoOutput,
56
57 #[error("secondary input provided without a filter graph")]
62 SecondaryInputWithoutFilter,
63
64 #[error("pipeline cancelled by caller")]
69 Cancelled,
70
71 #[error("i/o error: {0}")]
73 Io(#[from] std::io::Error),
74
75 #[error("no frame available at the requested position")]
81 FrameNotAvailable,
82}
83
84impl MediaError for PipelineError {
85 fn severity(&self) -> ErrorSeverity {
86 match self {
87 Self::Decode(e) => e.severity(),
88 Self::Filter(e) => e.severity(),
89 Self::Encode(e) => e.severity(),
90 Self::Cancelled | Self::FrameNotAvailable => ErrorSeverity::Other,
91 Self::NoInput | Self::NoOutput | Self::SecondaryInputWithoutFilter | Self::Io(_) => {
92 ErrorSeverity::Fatal
93 }
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::error::Error;
101
102 use ff_format::MediaError;
103
104 use super::PipelineError;
105
106 #[test]
109 fn no_input_should_display_correct_message() {
110 let err = PipelineError::NoInput;
111 assert_eq!(err.to_string(), "no input specified");
112 }
113
114 #[test]
115 fn no_output_should_display_correct_message() {
116 let err = PipelineError::NoOutput;
117 assert_eq!(err.to_string(), "no output specified");
118 }
119
120 #[test]
121 fn cancelled_should_display_correct_message() {
122 let err = PipelineError::Cancelled;
123 assert_eq!(err.to_string(), "pipeline cancelled by caller");
124 }
125
126 #[test]
129 fn decode_should_prefix_inner_message() {
130 let err = PipelineError::Decode(ff_decode::DecodeError::decoding_failed("test error"));
131 assert!(err.to_string().starts_with("decode failed:"));
132 }
133
134 #[test]
135 fn filter_should_prefix_inner_message() {
136 let err = PipelineError::Filter(ff_filter::FilterError::BuildFailed);
137 assert_eq!(
138 err.to_string(),
139 "filter failed: failed to build filter graph"
140 );
141 }
142
143 #[test]
144 fn encode_should_prefix_inner_message() {
145 let err = PipelineError::Encode(ff_encode::EncodeError::Cancelled);
146 assert_eq!(err.to_string(), "encode failed: Encoding cancelled by user");
147 }
148
149 #[test]
152 fn decode_error_should_convert_into_pipeline_error() {
153 let inner = ff_decode::DecodeError::decoding_failed("test error");
154 let err: PipelineError = inner.into();
155 assert!(matches!(err, PipelineError::Decode(_)));
156 }
157
158 #[test]
159 fn filter_error_should_convert_into_pipeline_error() {
160 let inner = ff_filter::FilterError::BuildFailed;
161 let err: PipelineError = inner.into();
162 assert!(matches!(err, PipelineError::Filter(_)));
163 }
164
165 #[test]
166 fn encode_error_should_convert_into_pipeline_error() {
167 let inner = ff_encode::EncodeError::Cancelled;
168 let err: PipelineError = inner.into();
169 assert!(matches!(err, PipelineError::Encode(_)));
170 }
171
172 #[test]
175 fn decode_should_expose_source() {
176 let err = PipelineError::Decode(ff_decode::DecodeError::decoding_failed("test error"));
177 assert!(err.source().is_some());
178 }
179
180 #[test]
181 fn filter_should_expose_source() {
182 let err = PipelineError::Filter(ff_filter::FilterError::BuildFailed);
183 assert!(err.source().is_some());
184 }
185
186 #[test]
187 fn encode_should_expose_source() {
188 let err = PipelineError::Encode(ff_encode::EncodeError::Cancelled);
189 assert!(err.source().is_some());
190 }
191
192 #[test]
193 fn unit_variants_should_have_no_source() {
194 assert!(PipelineError::NoInput.source().is_none());
195 assert!(PipelineError::NoOutput.source().is_none());
196 assert!(PipelineError::Cancelled.source().is_none());
197 }
198
199 #[test]
202 fn io_error_should_convert_into_pipeline_error() {
203 let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
204 let err: PipelineError = inner.into();
205 assert!(matches!(err, PipelineError::Io(_)));
206 }
207
208 #[test]
209 fn io_error_should_display_correct_message() {
210 let inner = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
211 let err: PipelineError = inner.into();
212 assert_eq!(err.to_string(), "i/o error: access denied");
213 }
214
215 #[test]
216 fn io_error_should_expose_source() {
217 let inner = std::io::Error::new(std::io::ErrorKind::Other, "some error");
218 let err: PipelineError = inner.into();
219 assert!(err.source().is_some());
220 }
221
222 #[test]
223 fn pipeline_io_should_be_fatal() {
224 let e: PipelineError = std::io::Error::other("x").into();
225 assert!(e.is_fatal() && !e.is_recoverable());
226 }
227
228 #[test]
229 fn pipeline_cancelled_should_be_other() {
230 let e = PipelineError::Cancelled;
231 assert!(!e.is_fatal() && !e.is_recoverable());
232 }
233
234 #[test]
235 fn pipeline_decode_should_delegate_recoverable() {
236 let e = PipelineError::Decode(ff_decode::DecodeError::decoding_failed("x"));
238 assert!(e.is_recoverable() && !e.is_fatal());
239 }
240}