1#![expect(
18 clippy::panic,
19 clippy::unwrap_used,
20 clippy::expect_used,
21 reason = "testing code"
22)]
23
24#[derive(Debug)]
29pub struct ExpectedErrorMessage<'a> {
30 error: &'a str,
32 help: Option<&'a str>,
34 prefix: bool,
38 underlines: Vec<(&'a str, Option<&'a str>)>,
47 source: Option<&'a str>,
49}
50
51#[derive(Debug)]
53pub struct ExpectedErrorMessageBuilder<'a> {
54 error: &'a str,
56 help: Option<&'a str>,
58 prefix: bool,
60 underlines: Vec<(&'a str, Option<&'a str>)>,
62 source: Option<&'a str>,
64}
65
66impl<'a> ExpectedErrorMessageBuilder<'a> {
67 pub fn error(msg: &'a str) -> Self {
70 Self {
71 error: msg,
72 help: None,
73 prefix: false,
74 underlines: vec![],
75 source: None,
76 }
77 }
78
79 pub fn error_starts_with(msg: &'a str) -> Self {
85 Self {
86 error: msg,
87 help: None,
88 prefix: true,
89 underlines: vec![],
90 source: None,
91 }
92 }
93
94 pub fn help(self, msg: &'a str) -> Self {
97 Self {
98 help: Some(msg),
99 ..self
100 }
101 }
102
103 pub fn exactly_one_underline(self, snippet: &'a str) -> Self {
107 Self {
108 underlines: vec![(snippet, None)],
109 ..self
110 }
111 }
112
113 pub fn exactly_one_underline_with_label(self, snippet: &'a str, label: &'a str) -> Self {
117 Self {
118 underlines: vec![(snippet, Some(label))],
119 ..self
120 }
121 }
122
123 pub fn exactly_two_underlines(self, snippet1: &'a str, snippet2: &'a str) -> Self {
128 Self {
129 underlines: vec![(snippet1, None), (snippet2, None)],
130 ..self
131 }
132 }
133
134 pub fn with_underlines_or_labels(
139 self,
140 labels: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
141 ) -> Self {
142 Self {
143 underlines: labels.into_iter().collect(),
144 ..self
145 }
146 }
147
148 pub fn source(self, msg: &'a str) -> Self {
151 Self {
152 source: Some(msg),
153 ..self
154 }
155 }
156
157 pub fn build(self) -> ExpectedErrorMessage<'a> {
159 ExpectedErrorMessage {
160 error: self.error,
161 help: self.help,
162 prefix: self.prefix,
163 underlines: self.underlines,
164 source: self.source,
165 }
166 }
167}
168
169impl<'a> ExpectedErrorMessage<'a> {
170 pub fn matches(&self, error: &impl miette::Diagnostic) -> bool {
174 self.matches_error(error)
175 && self.matches_help(error)
176 && self.matches_source(error)
177 && self.matches_underlines(error)
178 }
179
180 fn matches_error(&self, error: &impl miette::Diagnostic) -> bool {
182 let e_string = error.to_string();
183 if self.prefix {
184 e_string.starts_with(self.error)
185 } else {
186 e_string == self.error
187 }
188 }
189
190 #[track_caller]
192 fn expect_error_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
193 let e_string = error.to_string();
194 if self.prefix {
195 assert!(
196 e_string.starts_with(self.error),
197 "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not start with the expected prefix\n actual error: {error}\n expected prefix: {}", src.into(),
199 self.error,
200 );
201 } else {
202 assert_eq!(
203 &e_string,
204 self.error,
205 "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not match expected", src.into(),
207 );
208 }
209 }
210
211 fn matches_help(&self, error: &impl miette::Diagnostic) -> bool {
213 let h_string = error.help().map(|h| h.to_string());
214 if self.prefix {
215 match (h_string.as_deref(), self.help) {
216 (Some(actual), Some(expected)) => actual.starts_with(expected),
217 (None, None) => true,
218 _ => false,
219 }
220 } else {
221 h_string.as_deref() == self.help
222 }
223 }
224
225 fn matches_source(&self, error: &impl miette::Diagnostic) -> bool {
227 let s_string = error.source().map(|s| s.to_string());
228 if self.prefix {
229 match (s_string.as_deref(), self.source) {
230 (Some(actual), Some(expected)) => actual.starts_with(expected),
231 (None, None) => true,
232 _ => false,
233 }
234 } else {
235 s_string.as_deref() == self.source
236 }
237 }
238
239 #[track_caller]
241 fn expect_help_or_source_matches(
242 &self,
243 src: impl Into<OriginalInput<'a>>,
244 error: &miette::Report,
245 h_or_s: &str,
246 actual: Option<&str>,
247 expected: Option<&str>,
248 ) {
249 if self.prefix {
250 match (actual, expected) {
251 (Some(actual), Some(expected)) => {
252 assert!(
253 actual.starts_with(expected),
254 "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual {h_or_s} did not start with the expected prefix\n actual {h_or_s}: {actual}\n expected {h_or_s}: {expected}", src.into(),
256 )
257 }
258 (None, None) => (),
259 (Some(actual), None) => panic!(
260 "for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not expect a {h_or_s} message, but found one: {actual}", src.into(),
262 ),
263 (None, Some(expected)) => panic!(
264 "for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not find a {h_or_s} message, but expected one: {expected}", src.into(),
266 ),
267 }
268 } else {
269 assert_eq!(
270 actual,
271 expected,
272 "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual {h_or_s} did not match expected", src.into(),
274 );
275 }
276 }
277
278 #[track_caller]
280 fn expect_help_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
281 let h_string = error.help().map(|h| h.to_string());
282 self.expect_help_or_source_matches(src, error, "help", h_string.as_deref(), self.help);
283 }
284
285 #[track_caller]
287 fn expect_source_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
288 let s_string = error.source().map(|s| s.to_string());
289 self.expect_help_or_source_matches(src, error, "source", s_string.as_deref(), self.source);
290 }
291
292 fn matches_underlines(&self, err: &impl miette::Diagnostic) -> bool {
294 let expected_num_labels = self.underlines.len();
295 let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
296 if expected_num_labels != actual_num_labels {
297 return false;
298 }
299 if expected_num_labels != 0 {
300 let src = err
301 .source_code()
302 .expect("err.source_code() should be `Some` if we are expecting underlines");
303 for (expected, actual) in self
304 .underlines
305 .iter()
306 .zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
307 {
308 let (expected_snippet, expected_label) = expected;
309 let actual_snippet = {
310 let span = actual.inner();
311 std::str::from_utf8(src.read_span(span, 0, 0).expect("should read span").data())
312 .expect("should be utf8 encoded")
313 };
314 let actual_label = actual.label();
315 if expected_snippet != &actual_snippet {
316 return false;
317 }
318 if expected_label != &actual_label {
319 return false;
320 }
321 }
322 }
323 true
324 }
325
326 #[track_caller]
328 fn expect_underlines_match(&self, err: &miette::Report) {
329 let expected_num_labels = self.underlines.len();
330 let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
331 assert_eq!(expected_num_labels, actual_num_labels, "in the following error:\n{err:?}\n\nexpected {expected_num_labels} underlines but found {actual_num_labels}"); if expected_num_labels != 0 {
333 let src = err
334 .source_code()
335 .expect("err.source_code() should be `Some` if we are expecting underlines");
336 for (expected, actual) in self
337 .underlines
338 .iter()
339 .zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
340 {
341 let (expected_snippet, expected_label) = expected;
342 let actual_snippet = {
343 let span = actual.inner();
344 std::str::from_utf8(src.read_span(span, 0, 0).expect("should read span").data())
345 .expect("should be utf8 encoded")
346 };
347 let actual_label = actual.label();
348 assert_eq!(
349 expected_snippet,
350 &actual_snippet,
351 "in the following error:\n{err:?}\n\nexpected underlined portion to be:\n {expected_snippet}\nbut it was:\n {actual_snippet}", );
353 assert_eq!(
354 expected_label,
355 &actual_label,
356 "in the following error:\n{err:?}\n\nexpected underlined help text to be:\n {expected_label:?}\nbut it was:\n {actual_label:?}", );
358 }
359 }
360 }
361}
362
363impl std::fmt::Display for ExpectedErrorMessage<'_> {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 if self.prefix {
366 writeln!(f, "expected error to start with: {}", self.error)?;
367 match self.help {
368 Some(help) => writeln!(f, "expected help to start with: {help}")?,
369 None => writeln!(f, " with no help message")?,
370 }
371 } else {
372 writeln!(f, "expected error: {}", self.error)?;
373 match self.help {
374 Some(help) => writeln!(f, "expected help: {help}")?,
375 None => writeln!(f, " with no help message")?,
376 }
377 }
378 if self.underlines.is_empty() {
379 writeln!(f, "and expected no source locations / underlined segments.")?;
380 } else {
381 writeln!(f, "and expected the following underlined segments:")?;
382 for (underline, label) in &self.underlines {
383 writeln!(f, " {underline}")?;
384 if let Some(label) = label {
385 writeln!(f, " with label {label}")?
386 }
387 }
388 }
389 Ok(())
390 }
391}
392
393#[derive(Debug)]
396pub enum OriginalInput<'a> {
397 String(&'a str),
399 Json(&'a serde_json::Value),
402}
403
404impl<'a> From<&'a str> for OriginalInput<'a> {
405 fn from(value: &'a str) -> Self {
406 Self::String(value)
407 }
408}
409
410impl<'a> From<&'a serde_json::Value> for OriginalInput<'a> {
411 fn from(value: &'a serde_json::Value) -> Self {
412 Self::Json(value)
413 }
414}
415
416impl std::fmt::Display for OriginalInput<'_> {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 match self {
419 Self::String(s) => write!(f, "{s}"),
420 Self::Json(val) => write!(f, "{}", serde_json::to_string_pretty(val).unwrap()),
421 }
422 }
423}
424
425#[track_caller] pub fn expect_err<'a>(
432 src: impl Into<OriginalInput<'a>> + Copy,
433 err: &miette::Report,
434 msg: &ExpectedErrorMessage<'a>,
435) {
436 msg.expect_error_matches(src, err);
437 msg.expect_help_matches(src, err);
438 msg.expect_source_matches(src, err);
439 msg.expect_underlines_match(err);
440}
441
442#[macro_export]
444macro_rules! assert_deep_eq {
445 ( $self:expr , $other:expr ) => {
446 assert!(
447 $self.deep_eq(&$other),
448 "expected that {:?} would be structurally equal to {:?}",
449 $self,
450 $other
451 )
452 };
453}
454
455#[macro_export]
457macro_rules! assert_not_deep_eq {
458 ( $self:expr , $other:expr ) => {
459 assert!(
460 !$self.deep_eq(&$other),
461 "expected that {:?} would not be structurally equal to {:?}",
462 $self,
463 $other
464 )
465 };
466}