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
72pub type Result<T> = std::result::Result<T, OcasError>;
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use proptest::prelude::*;
79
80 mod simple {
81 use super::*;
82
83 #[test]
84 fn display_parse_error_with_span() {
85 let err = OcasError::ParseError {
86 message: "unexpected token".into(),
87 span: Some((0, 3)),
88 };
89 assert_eq!(
90 err.to_string(),
91 "parse error at bytes 0..3: unexpected token"
92 );
93 }
94
95 #[test]
96 fn display_parse_error_without_span() {
97 let err = OcasError::ParseError {
98 message: "unexpected end of input".into(),
99 span: None,
100 };
101 assert_eq!(err.to_string(), "parse error: unexpected end of input");
102 }
103
104 #[test]
105 fn display_numeric_overflow() {
106 let err = OcasError::NumericOverflow;
107 assert_eq!(err.to_string(), "numeric overflow");
108 }
109
110 #[test]
111 fn display_domain_error() {
112 let err = OcasError::DomainError {
113 expected: "integer".into(),
114 found: "rational".into(),
115 };
116 assert_eq!(
117 err.to_string(),
118 "domain error: expected integer, found rational"
119 );
120 }
121 }
122
123 mod medium {
124 use super::*;
125
126 #[test]
127 fn display_unsupported_operation() {
128 let err = OcasError::UnsupportedOperation {
129 message: "symbolic integration not implemented".into(),
130 };
131 assert_eq!(
132 err.to_string(),
133 "unsupported operation: symbolic integration not implemented"
134 );
135 }
136
137 #[test]
138 fn display_backend_error() {
139 let err = OcasError::BackendError {
140 backend: "gmp".into(),
141 message: "division by zero".into(),
142 };
143 assert_eq!(err.to_string(), "backend error (gmp): division by zero");
144 }
145
146 #[test]
147 fn display_invalid_argument() {
148 let err = OcasError::InvalidArgument {
149 name: "threads".into(),
150 reason: "must be greater than zero".into(),
151 };
152 assert_eq!(
153 err.to_string(),
154 "invalid argument 'threads': must be greater than zero"
155 );
156 }
157
158 #[test]
159 fn error_implements_std_error() {
160 let err = OcasError::NumericOverflow;
161 let dyn_err: &dyn std::error::Error = &err;
162 assert_eq!(dyn_err.to_string(), "numeric overflow");
163 }
164 }
165
166 mod complex {
167 use super::*;
168
169 #[test]
170 fn error_clone_and_equality() {
171 let err = OcasError::ParseError {
172 message: "unexpected token".into(),
173 span: Some((0, 3)),
174 };
175 let cloned = err.clone();
176 assert_eq!(err, cloned);
177 }
178
179 #[test]
180 fn result_type_alias_compiles() {
181 fn returns_result() -> Result<i32> {
182 Ok(42)
183 }
184 assert_eq!(returns_result().unwrap(), 42);
185 }
186
187 #[test]
188 fn all_variants_round_trip_through_display() {
189 let errors: Vec<OcasError> = vec![
190 OcasError::ParseError {
191 message: "m".into(),
192 span: Some((1, 2)),
193 },
194 OcasError::ParseError {
195 message: "m".into(),
196 span: None,
197 },
198 OcasError::DomainError {
199 expected: "e".into(),
200 found: "f".into(),
201 },
202 OcasError::NumericOverflow,
203 OcasError::UnsupportedOperation {
204 message: "u".into(),
205 },
206 OcasError::BackendError {
207 backend: "b".into(),
208 message: "m".into(),
209 },
210 OcasError::InvalidArgument {
211 name: "n".into(),
212 reason: "r".into(),
213 },
214 ];
215 for err in errors {
216 assert!(!err.to_string().is_empty());
217 assert_eq!(err.clone(), err);
218 }
219 }
220 }
221
222 mod extreme {
223 use super::*;
224
225 proptest! {
226 #[test]
227 fn unsupported_operation_display_contains_message(message in "[a-zA-Z0-9_ ]{1,64}") {
228 let err = OcasError::UnsupportedOperation { message: message.clone() };
229 let text = err.to_string();
230 prop_assert!(!text.is_empty());
231 prop_assert!(text.contains(&message), "{text} should contain {message}");
232 }
233
234 #[test]
235 fn parse_error_span_is_in_display((start, end) in (0usize..1000, 0usize..1000)) {
236 let err = OcasError::ParseError {
237 message: "test".into(),
238 span: Some((start, end)),
239 };
240 let text = err.to_string();
241 prop_assert!(text.contains("parse error"));
242 }
243 }
244 }
245}