Skip to main content

ff_pipeline/
error.rs

1//! Error types for pipeline operations.
2//!
3//! This module provides [`PipelineError`], which covers all failure modes that
4//! can arise when building or running a [`Pipeline`](crate::Pipeline).
5
6/// Errors that can occur while building or running a pipeline.
7///
8/// # Error Categories
9///
10/// - **Downstream errors**: [`Decode`](Self::Decode), [`Filter`](Self::Filter),
11///   [`Encode`](Self::Encode) — propagated from the underlying crates via `#[from]`
12/// - **Configuration errors**: [`NoInput`](Self::NoInput), [`NoOutput`](Self::NoOutput),
13///   [`SecondaryInputWithoutFilter`](Self::SecondaryInputWithoutFilter)
14///   — returned by [`PipelineBuilder::build`](crate::PipelineBuilder::build)
15/// - **Runtime control**: [`Cancelled`](Self::Cancelled) — returned by
16///   [`Pipeline::run`](crate::Pipeline::run) when the progress callback returns `false`
17/// - **Availability**: [`FrameNotAvailable`](Self::FrameNotAvailable) — no frame at position
18#[derive(Debug, thiserror::Error)]
19pub enum PipelineError {
20    /// A decoding step failed.
21    ///
22    /// Wraps [`ff_decode::DecodeError`] and is produced automatically via `#[from]`
23    /// when a decode operation inside the pipeline returns an error.
24    #[error("decode failed: {0}")]
25    Decode(#[from] ff_decode::DecodeError),
26
27    /// A filter graph step failed.
28    ///
29    /// Wraps [`ff_filter::FilterError`] and is produced automatically via `#[from]`
30    /// when the filter graph inside the pipeline returns an error.
31    #[error("filter failed: {0}")]
32    Filter(#[from] ff_filter::FilterError),
33
34    /// An encoding step failed.
35    ///
36    /// Wraps [`ff_encode::EncodeError`] and is produced automatically via `#[from]`
37    /// when an encode operation inside the pipeline returns an error.
38    #[error("encode failed: {0}")]
39    Encode(#[from] ff_encode::EncodeError),
40
41    /// No input path was provided to the builder.
42    ///
43    /// At least one call to [`PipelineBuilder::input`](crate::PipelineBuilder::input)
44    /// is required before [`PipelineBuilder::build`](crate::PipelineBuilder::build).
45    #[error("no input specified")]
46    NoInput,
47
48    /// No output path and config were provided to the builder.
49    ///
50    /// A call to [`PipelineBuilder::output`](crate::PipelineBuilder::output) is
51    /// required before [`PipelineBuilder::build`](crate::PipelineBuilder::build).
52    #[error("no output specified")]
53    NoOutput,
54
55    /// `secondary_input()` was called but no filter graph was provided.
56    ///
57    /// A secondary input only makes sense when a multi-slot filter is set via
58    /// [`PipelineBuilder::filter`](crate::PipelineBuilder::filter).
59    #[error("secondary input provided without a filter graph")]
60    SecondaryInputWithoutFilter,
61
62    /// The pipeline was cancelled by the progress callback.
63    ///
64    /// Returned by [`Pipeline::run`](crate::Pipeline::run) when the
65    /// [`ProgressCallback`](crate::ProgressCallback) returns `false`.
66    #[error("pipeline cancelled by caller")]
67    Cancelled,
68
69    /// An I/O error (e.g. creating the output directory for thumbnails).
70    #[error("i/o error: {0}")]
71    Io(#[from] std::io::Error),
72
73    /// No frame was available at the requested position.
74    ///
75    /// Returned by thumbnail and seek-and-decode operations when the decoder
76    /// reports `Ok(None)` — the position is past the end of the stream or no
77    /// decodable frame exists at that point.
78    #[error("no frame available at the requested position")]
79    FrameNotAvailable,
80}
81
82#[cfg(test)]
83mod tests {
84    use std::error::Error;
85
86    use super::PipelineError;
87
88    // --- Display messages: unit variants ---
89
90    #[test]
91    fn no_input_should_display_correct_message() {
92        let err = PipelineError::NoInput;
93        assert_eq!(err.to_string(), "no input specified");
94    }
95
96    #[test]
97    fn no_output_should_display_correct_message() {
98        let err = PipelineError::NoOutput;
99        assert_eq!(err.to_string(), "no output specified");
100    }
101
102    #[test]
103    fn cancelled_should_display_correct_message() {
104        let err = PipelineError::Cancelled;
105        assert_eq!(err.to_string(), "pipeline cancelled by caller");
106    }
107
108    // --- Display messages: wrapping variants ---
109
110    #[test]
111    fn decode_should_prefix_inner_message() {
112        let err = PipelineError::Decode(ff_decode::DecodeError::decoding_failed("test error"));
113        assert!(err.to_string().starts_with("decode failed:"));
114    }
115
116    #[test]
117    fn filter_should_prefix_inner_message() {
118        let err = PipelineError::Filter(ff_filter::FilterError::BuildFailed);
119        assert_eq!(
120            err.to_string(),
121            "filter failed: failed to build filter graph"
122        );
123    }
124
125    #[test]
126    fn encode_should_prefix_inner_message() {
127        let err = PipelineError::Encode(ff_encode::EncodeError::Cancelled);
128        assert_eq!(err.to_string(), "encode failed: Encoding cancelled by user");
129    }
130
131    // --- From conversions ---
132
133    #[test]
134    fn decode_error_should_convert_into_pipeline_error() {
135        let inner = ff_decode::DecodeError::decoding_failed("test error");
136        let err: PipelineError = inner.into();
137        assert!(matches!(err, PipelineError::Decode(_)));
138    }
139
140    #[test]
141    fn filter_error_should_convert_into_pipeline_error() {
142        let inner = ff_filter::FilterError::BuildFailed;
143        let err: PipelineError = inner.into();
144        assert!(matches!(err, PipelineError::Filter(_)));
145    }
146
147    #[test]
148    fn encode_error_should_convert_into_pipeline_error() {
149        let inner = ff_encode::EncodeError::Cancelled;
150        let err: PipelineError = inner.into();
151        assert!(matches!(err, PipelineError::Encode(_)));
152    }
153
154    // --- std::error::Error::source() ---
155
156    #[test]
157    fn decode_should_expose_source() {
158        let err = PipelineError::Decode(ff_decode::DecodeError::decoding_failed("test error"));
159        assert!(err.source().is_some());
160    }
161
162    #[test]
163    fn filter_should_expose_source() {
164        let err = PipelineError::Filter(ff_filter::FilterError::BuildFailed);
165        assert!(err.source().is_some());
166    }
167
168    #[test]
169    fn encode_should_expose_source() {
170        let err = PipelineError::Encode(ff_encode::EncodeError::Cancelled);
171        assert!(err.source().is_some());
172    }
173
174    #[test]
175    fn unit_variants_should_have_no_source() {
176        assert!(PipelineError::NoInput.source().is_none());
177        assert!(PipelineError::NoOutput.source().is_none());
178        assert!(PipelineError::Cancelled.source().is_none());
179    }
180
181    // --- Io variant ---
182
183    #[test]
184    fn io_error_should_convert_into_pipeline_error() {
185        let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
186        let err: PipelineError = inner.into();
187        assert!(matches!(err, PipelineError::Io(_)));
188    }
189
190    #[test]
191    fn io_error_should_display_correct_message() {
192        let inner = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
193        let err: PipelineError = inner.into();
194        assert_eq!(err.to_string(), "i/o error: access denied");
195    }
196
197    #[test]
198    fn io_error_should_expose_source() {
199        let inner = std::io::Error::new(std::io::ErrorKind::Other, "some error");
200        let err: PipelineError = inner.into();
201        assert!(err.source().is_some());
202    }
203}