1use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Error)]
19#[non_exhaustive]
20pub enum OcasError {
21 #[error("parse error{}: {message}", match span {
23 Some((start, end)) => format!(" at bytes {start}..{end}"),
24 None => String::new(),
25 })]
26 ParseError {
27 message: String,
29 span: Option<(usize, usize)>,
31 },
32
33 #[error("domain error: expected {expected}, found {found}")]
35 DomainError {
36 expected: String,
38 found: String,
40 },
41
42 #[error("numeric overflow")]
44 NumericOverflow,
45
46 #[error("unsupported operation: {message}")]
48 UnsupportedOperation {
49 message: String,
51 },
52
53 #[error("backend error ({backend}): {message}")]
55 BackendError {
56 backend: String,
58 message: String,
60 },
61
62 #[error("invalid argument '{name}': {reason}")]
64 InvalidArgument {
65 name: String,
67 reason: String,
69 },
70
71 #[error("out of fuel: budget exhausted")]
75 OutOfFuel,
76}
77
78pub 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}