Skip to main content

ocas_core/
error.rs

1//! Unified error types for oCAS.
2
3use thiserror::Error;
4
5/// The primary error type returned by oCAS operations.
6///
7/// # Example
8///
9/// ```
10/// use ocas_core::error::OcasError;
11///
12/// let err = OcasError::ParseError {
13///     message: "unexpected token".into(),
14///     span: Some((0, 3)),
15/// };
16/// assert_eq!(err.to_string(), "parse error at bytes 0..3: unexpected token");
17/// ```
18#[derive(Debug, Clone, PartialEq, Error)]
19#[non_exhaustive]
20pub enum OcasError {
21    /// A parsing error with an optional source span.
22    #[error("parse error{}: {message}", match span {
23        Some((start, end)) => format!(" at bytes {start}..{end}"),
24        None => String::new(),
25    })]
26    ParseError {
27        /// Human-readable error message.
28        message: String,
29        /// Optional byte offset into the source string.
30        span: Option<(usize, usize)>,
31    },
32
33    /// An operation was requested on an incompatible domain.
34    #[error("domain error: expected {expected}, found {found}")]
35    DomainError {
36        /// Expected domain or type.
37        expected: String,
38        /// Actual value or type encountered.
39        found: String,
40    },
41
42    /// A numeric overflow or underflow occurred.
43    #[error("numeric overflow")]
44    NumericOverflow,
45
46    /// The requested operation is not yet implemented or supported.
47    #[error("unsupported operation: {message}")]
48    UnsupportedOperation {
49        /// Description of the unsupported operation.
50        message: String,
51    },
52
53    /// A backend library returned an error.
54    #[error("backend error ({backend}): {message}")]
55    BackendError {
56        /// Name of the backend.
57        backend: String,
58        /// Backend-specific error message.
59        message: String,
60    },
61
62    /// An invalid argument was supplied.
63    #[error("invalid argument '{name}': {reason}")]
64    InvalidArgument {
65        /// Name of the argument.
66        name: String,
67        /// Reason the argument is invalid.
68        reason: String,
69    },
70
71    /// A resource budget ([`crate::fuel::Fuel`]) was exhausted before the
72    /// operation could complete. Retrying with a larger budget (or no budget)
73    /// will resume from scratch.
74    #[error("out of fuel: budget exhausted")]
75    OutOfFuel,
76}
77
78/// A convenient result type for oCAS operations.
79pub type Result<T> = std::result::Result<T, OcasError>;
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use proptest::prelude::*;
85
86    mod simple {
87        use super::*;
88
89        #[test]
90        fn display_parse_error_with_span() {
91            let err = OcasError::ParseError {
92                message: "unexpected token".into(),
93                span: Some((0, 3)),
94            };
95            assert_eq!(
96                err.to_string(),
97                "parse error at bytes 0..3: unexpected token"
98            );
99        }
100
101        #[test]
102        fn display_parse_error_without_span() {
103            let err = OcasError::ParseError {
104                message: "unexpected end of input".into(),
105                span: None,
106            };
107            assert_eq!(err.to_string(), "parse error: unexpected end of input");
108        }
109
110        #[test]
111        fn display_numeric_overflow() {
112            let err = OcasError::NumericOverflow;
113            assert_eq!(err.to_string(), "numeric overflow");
114        }
115
116        #[test]
117        fn display_domain_error() {
118            let err = OcasError::DomainError {
119                expected: "integer".into(),
120                found: "rational".into(),
121            };
122            assert_eq!(
123                err.to_string(),
124                "domain error: expected integer, found rational"
125            );
126        }
127    }
128
129    mod medium {
130        use super::*;
131
132        #[test]
133        fn display_unsupported_operation() {
134            let err = OcasError::UnsupportedOperation {
135                message: "symbolic integration not implemented".into(),
136            };
137            assert_eq!(
138                err.to_string(),
139                "unsupported operation: symbolic integration not implemented"
140            );
141        }
142
143        #[test]
144        fn display_backend_error() {
145            let err = OcasError::BackendError {
146                backend: "gmp".into(),
147                message: "division by zero".into(),
148            };
149            assert_eq!(err.to_string(), "backend error (gmp): division by zero");
150        }
151
152        #[test]
153        fn display_invalid_argument() {
154            let err = OcasError::InvalidArgument {
155                name: "threads".into(),
156                reason: "must be greater than zero".into(),
157            };
158            assert_eq!(
159                err.to_string(),
160                "invalid argument 'threads': must be greater than zero"
161            );
162        }
163
164        #[test]
165        fn error_implements_std_error() {
166            let err = OcasError::NumericOverflow;
167            let dyn_err: &dyn std::error::Error = &err;
168            assert_eq!(dyn_err.to_string(), "numeric overflow");
169        }
170    }
171
172    mod complex {
173        use super::*;
174
175        #[test]
176        fn error_clone_and_equality() {
177            let err = OcasError::ParseError {
178                message: "unexpected token".into(),
179                span: Some((0, 3)),
180            };
181            let cloned = err.clone();
182            assert_eq!(err, cloned);
183        }
184
185        #[test]
186        fn result_type_alias_compiles() {
187            fn returns_result() -> Result<i32> {
188                Ok(42)
189            }
190            assert_eq!(returns_result().unwrap(), 42);
191        }
192
193        #[test]
194        fn all_variants_round_trip_through_display() {
195            let errors: Vec<OcasError> = vec![
196                OcasError::ParseError {
197                    message: "m".into(),
198                    span: Some((1, 2)),
199                },
200                OcasError::ParseError {
201                    message: "m".into(),
202                    span: None,
203                },
204                OcasError::DomainError {
205                    expected: "e".into(),
206                    found: "f".into(),
207                },
208                OcasError::NumericOverflow,
209                OcasError::UnsupportedOperation {
210                    message: "u".into(),
211                },
212                OcasError::BackendError {
213                    backend: "b".into(),
214                    message: "m".into(),
215                },
216                OcasError::InvalidArgument {
217                    name: "n".into(),
218                    reason: "r".into(),
219                },
220            ];
221            for err in errors {
222                assert!(!err.to_string().is_empty());
223                assert_eq!(err.clone(), err);
224            }
225        }
226    }
227
228    mod extreme {
229        use super::*;
230
231        proptest! {
232            #[test]
233            fn unsupported_operation_display_contains_message(message in "[a-zA-Z0-9_ ]{1,64}") {
234                let err = OcasError::UnsupportedOperation { message: message.clone() };
235                let text = err.to_string();
236                prop_assert!(!text.is_empty());
237                prop_assert!(text.contains(&message), "{text} should contain {message}");
238            }
239
240            #[test]
241            fn parse_error_span_is_in_display((start, end) in (0usize..1000, 0usize..1000)) {
242                let err = OcasError::ParseError {
243                    message: "test".into(),
244                    span: Some((start, end)),
245                };
246                let text = err.to_string();
247                prop_assert!(text.contains("parse error"));
248            }
249        }
250    }
251}