Skip to main content

ferro_rs/testing/
expect.rs

1//! Fluent assertion API inspired by Jest's expect
2//!
3//! Provides a fluent API for assertions with clear expected/received output.
4
5use std::fmt::Debug;
6
7std::thread_local! {
8    /// Thread-local storage for current test name (set by test! macro)
9    pub static CURRENT_TEST_NAME: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
10}
11
12/// Set the current test name (called by test! macro)
13pub fn set_current_test_name(name: Option<String>) {
14    CURRENT_TEST_NAME.with(|cell| {
15        *cell.borrow_mut() = name;
16    });
17}
18
19/// Get the current test name for error messages
20fn get_test_name() -> Option<String> {
21    CURRENT_TEST_NAME.with(|cell| cell.borrow().clone())
22}
23
24/// Format the assertion failure header
25fn format_header(location: &str) -> String {
26    if let Some(name) = get_test_name() {
27        format!("\nTest: \"{name}\"\n  at {location}\n")
28    } else {
29        format!("\nassertion failed at {location}\n")
30    }
31}
32
33/// The main Expect wrapper for fluent assertions
34pub struct Expect<T> {
35    value: T,
36    location: &'static str,
37}
38
39impl<T> Expect<T> {
40    /// Create a new Expect wrapper (use the expect! macro instead)
41    pub fn new(value: T, location: &'static str) -> Self {
42        Self { value, location }
43    }
44}
45
46// Equality matchers for Debug + PartialEq types
47impl<T: Debug + PartialEq> Expect<T> {
48    /// Assert that the value equals the expected value
49    ///
50    /// # Example
51    /// ```rust,ignore
52    /// expect!(actual).to_equal(expected);
53    /// ```
54    pub fn to_equal(&self, expected: T) {
55        if self.value != expected {
56            panic!(
57                "{}\n  expect!(actual).to_equal(expected)\n\n  Expected: {:?}\n  Received: {:?}\n",
58                format_header(self.location),
59                expected,
60                self.value
61            );
62        }
63    }
64
65    /// Assert that the value does not equal the unexpected value
66    ///
67    /// # Example
68    /// ```rust,ignore
69    /// expect!(actual).to_not_equal(unexpected);
70    /// ```
71    pub fn to_not_equal(&self, unexpected: T) {
72        if self.value == unexpected {
73            panic!(
74                "{}\n  expect!(actual).to_not_equal(value)\n\n  Expected NOT: {:?}\n  Received: {:?}\n",
75                format_header(self.location),
76                unexpected,
77                self.value
78            );
79        }
80    }
81}
82
83// Boolean matchers
84impl Expect<bool> {
85    /// Assert that the value is true
86    ///
87    /// # Example
88    /// ```rust,ignore
89    /// expect!(condition).to_be_true();
90    /// ```
91    pub fn to_be_true(&self) {
92        if !self.value {
93            panic!(
94                "{}\n  expect!(value).to_be_true()\n\n  Expected: true\n  Received: false\n",
95                format_header(self.location)
96            );
97        }
98    }
99
100    /// Assert that the value is false
101    ///
102    /// # Example
103    /// ```rust,ignore
104    /// expect!(condition).to_be_false();
105    /// ```
106    pub fn to_be_false(&self) {
107        if self.value {
108            panic!(
109                "{}\n  expect!(value).to_be_false()\n\n  Expected: false\n  Received: true\n",
110                format_header(self.location)
111            );
112        }
113    }
114}
115
116// Option matchers
117impl<T: Debug> Expect<Option<T>> {
118    /// Assert that the Option is Some
119    ///
120    /// # Example
121    /// ```rust,ignore
122    /// expect!(option).to_be_some();
123    /// ```
124    pub fn to_be_some(&self) {
125        if self.value.is_none() {
126            panic!(
127                "{}\n  expect!(option).to_be_some()\n\n  Expected: Some(_)\n  Received: None\n",
128                format_header(self.location)
129            );
130        }
131    }
132
133    /// Assert that the Option is None
134    ///
135    /// # Example
136    /// ```rust,ignore
137    /// expect!(option).to_be_none();
138    /// ```
139    pub fn to_be_none(&self) {
140        if let Some(ref v) = self.value {
141            panic!(
142                "{}\n  expect!(option).to_be_none()\n\n  Expected: None\n  Received: Some({:?})\n",
143                format_header(self.location),
144                v
145            );
146        }
147    }
148}
149
150// Option with PartialEq for to_contain
151impl<T: Debug + PartialEq> Expect<Option<T>> {
152    /// Assert that the Option contains the expected value
153    ///
154    /// # Example
155    /// ```rust,ignore
156    /// expect!(Some(5)).to_contain_value(5);
157    /// ```
158    pub fn to_contain_value(&self, expected: T) {
159        match &self.value {
160            Some(v) if *v == expected => {}
161            Some(v) => {
162                panic!(
163                    "{}\n  expect!(option).to_contain_value(expected)\n\n  Expected: Some({:?})\n  Received: Some({:?})\n",
164                    format_header(self.location),
165                    expected,
166                    v
167                );
168            }
169            None => {
170                panic!(
171                    "{}\n  expect!(option).to_contain_value(expected)\n\n  Expected: Some({:?})\n  Received: None\n",
172                    format_header(self.location),
173                    expected
174                );
175            }
176        }
177    }
178}
179
180// Result matchers
181impl<T: Debug, E: Debug> Expect<Result<T, E>> {
182    /// Assert that the Result is Ok
183    ///
184    /// # Example
185    /// ```rust,ignore
186    /// expect!(result).to_be_ok();
187    /// ```
188    pub fn to_be_ok(&self) {
189        if let Err(ref e) = self.value {
190            panic!(
191                "{}\n  expect!(result).to_be_ok()\n\n  Expected: Ok(_)\n  Received: Err({:?})\n",
192                format_header(self.location),
193                e
194            );
195        }
196    }
197
198    /// Assert that the Result is Err
199    ///
200    /// # Example
201    /// ```rust,ignore
202    /// expect!(result).to_be_err();
203    /// ```
204    pub fn to_be_err(&self) {
205        if let Ok(ref v) = self.value {
206            panic!(
207                "{}\n  expect!(result).to_be_err()\n\n  Expected: Err(_)\n  Received: Ok({:?})\n",
208                format_header(self.location),
209                v
210            );
211        }
212    }
213}
214
215// String matchers
216impl Expect<String> {
217    /// Assert that the string contains the substring
218    ///
219    /// # Example
220    /// ```rust,ignore
221    /// expect!(string).to_contain("hello");
222    /// ```
223    pub fn to_contain(&self, substring: &str) {
224        if !self.value.contains(substring) {
225            panic!(
226                "{}\n  expect!(string).to_contain(substring)\n\n  Expected to contain: {:?}\n  Received: {:?}\n",
227                format_header(self.location),
228                substring,
229                self.value
230            );
231        }
232    }
233
234    /// Assert that the string starts with the prefix
235    ///
236    /// # Example
237    /// ```rust,ignore
238    /// expect!(string).to_start_with("hello");
239    /// ```
240    pub fn to_start_with(&self, prefix: &str) {
241        if !self.value.starts_with(prefix) {
242            panic!(
243                "{}\n  expect!(string).to_start_with(prefix)\n\n  Expected to start with: {:?}\n  Received: {:?}\n",
244                format_header(self.location),
245                prefix,
246                self.value
247            );
248        }
249    }
250
251    /// Assert that the string ends with the suffix
252    ///
253    /// # Example
254    /// ```rust,ignore
255    /// expect!(string).to_end_with("world");
256    /// ```
257    pub fn to_end_with(&self, suffix: &str) {
258        if !self.value.ends_with(suffix) {
259            panic!(
260                "{}\n  expect!(string).to_end_with(suffix)\n\n  Expected to end with: {:?}\n  Received: {:?}\n",
261                format_header(self.location),
262                suffix,
263                self.value
264            );
265        }
266    }
267
268    /// Assert that the string has the expected length
269    ///
270    /// # Example
271    /// ```rust,ignore
272    /// expect!(string).to_have_length(5);
273    /// ```
274    pub fn to_have_length(&self, expected: usize) {
275        let actual = self.value.len();
276        if actual != expected {
277            panic!(
278                "{}\n  expect!(string).to_have_length({})\n\n  Expected length: {}\n  Actual length: {}\n  Value: {:?}\n",
279                format_header(self.location),
280                expected,
281                expected,
282                actual,
283                self.value
284            );
285        }
286    }
287
288    /// Assert that the string is empty
289    ///
290    /// # Example
291    /// ```rust,ignore
292    /// expect!(string).to_be_empty();
293    /// ```
294    pub fn to_be_empty(&self) {
295        if !self.value.is_empty() {
296            panic!(
297                "{}\n  expect!(string).to_be_empty()\n\n  Expected: \"\"\n  Received: {:?}\n",
298                format_header(self.location),
299                self.value
300            );
301        }
302    }
303}
304
305// &str matchers
306impl Expect<&str> {
307    /// Assert that the string contains the substring
308    pub fn to_contain(&self, substring: &str) {
309        if !self.value.contains(substring) {
310            panic!(
311                "{}\n  expect!(string).to_contain(substring)\n\n  Expected to contain: {:?}\n  Received: {:?}\n",
312                format_header(self.location),
313                substring,
314                self.value
315            );
316        }
317    }
318
319    /// Assert that the string starts with the prefix
320    pub fn to_start_with(&self, prefix: &str) {
321        if !self.value.starts_with(prefix) {
322            panic!(
323                "{}\n  expect!(string).to_start_with(prefix)\n\n  Expected to start with: {:?}\n  Received: {:?}\n",
324                format_header(self.location),
325                prefix,
326                self.value
327            );
328        }
329    }
330
331    /// Assert that the string ends with the suffix
332    pub fn to_end_with(&self, suffix: &str) {
333        if !self.value.ends_with(suffix) {
334            panic!(
335                "{}\n  expect!(string).to_end_with(suffix)\n\n  Expected to end with: {:?}\n  Received: {:?}\n",
336                format_header(self.location),
337                suffix,
338                self.value
339            );
340        }
341    }
342
343    /// Assert that the string has the expected length
344    pub fn to_have_length(&self, expected: usize) {
345        let actual = self.value.len();
346        if actual != expected {
347            panic!(
348                "{}\n  expect!(string).to_have_length({})\n\n  Expected length: {}\n  Actual length: {}\n  Value: {:?}\n",
349                format_header(self.location),
350                expected,
351                expected,
352                actual,
353                self.value
354            );
355        }
356    }
357
358    /// Assert that the string is empty
359    pub fn to_be_empty(&self) {
360        if !self.value.is_empty() {
361            panic!(
362                "{}\n  expect!(string).to_be_empty()\n\n  Expected: \"\"\n  Received: {:?}\n",
363                format_header(self.location),
364                self.value
365            );
366        }
367    }
368}
369
370// Vec matchers
371impl<T: Debug + PartialEq> Expect<Vec<T>> {
372    /// Assert that the Vec has the expected length
373    ///
374    /// # Example
375    /// ```rust,ignore
376    /// expect!(vec).to_have_length(3);
377    /// ```
378    pub fn to_have_length(&self, expected: usize) {
379        let actual = self.value.len();
380        if actual != expected {
381            panic!(
382                "{}\n  expect!(vec).to_have_length({})\n\n  Expected length: {}\n  Actual length: {}\n",
383                format_header(self.location),
384                expected,
385                expected,
386                actual
387            );
388        }
389    }
390
391    /// Assert that the Vec contains the item
392    ///
393    /// # Example
394    /// ```rust,ignore
395    /// expect!(vec).to_contain(&item);
396    /// ```
397    pub fn to_contain(&self, item: &T) {
398        if !self.value.contains(item) {
399            panic!(
400                "{}\n  expect!(vec).to_contain(item)\n\n  Expected to contain: {:?}\n  Received: {:?}\n",
401                format_header(self.location),
402                item,
403                self.value
404            );
405        }
406    }
407
408    /// Assert that the Vec is empty
409    ///
410    /// # Example
411    /// ```rust,ignore
412    /// expect!(vec).to_be_empty();
413    /// ```
414    pub fn to_be_empty(&self) {
415        if !self.value.is_empty() {
416            panic!(
417                "{}\n  expect!(vec).to_be_empty()\n\n  Expected: []\n  Received: {:?}\n",
418                format_header(self.location),
419                self.value
420            );
421        }
422    }
423}
424
425// Numeric comparison matchers using PartialOrd
426#[allow(clippy::neg_cmp_op_on_partial_ord)]
427impl<T: Debug + PartialOrd> Expect<T> {
428    /// Assert that the value is greater than the expected value
429    ///
430    /// # Example
431    /// ```rust,ignore
432    /// expect!(10).to_be_greater_than(5);
433    /// ```
434    pub fn to_be_greater_than(&self, expected: T) {
435        if !(self.value > expected) {
436            panic!(
437                "{}\n  expect!(value).to_be_greater_than(expected)\n\n  Expected: > {:?}\n  Received: {:?}\n",
438                format_header(self.location),
439                expected,
440                self.value
441            );
442        }
443    }
444
445    /// Assert that the value is less than the expected value
446    ///
447    /// # Example
448    /// ```rust,ignore
449    /// expect!(5).to_be_less_than(10);
450    /// ```
451    pub fn to_be_less_than(&self, expected: T) {
452        if !(self.value < expected) {
453            panic!(
454                "{}\n  expect!(value).to_be_less_than(expected)\n\n  Expected: < {:?}\n  Received: {:?}\n",
455                format_header(self.location),
456                expected,
457                self.value
458            );
459        }
460    }
461
462    /// Assert that the value is greater than or equal to the expected value
463    ///
464    /// # Example
465    /// ```rust,ignore
466    /// expect!(10).to_be_greater_than_or_equal(10);
467    /// ```
468    pub fn to_be_greater_than_or_equal(&self, expected: T) {
469        if !(self.value >= expected) {
470            panic!(
471                "{}\n  expect!(value).to_be_greater_than_or_equal(expected)\n\n  Expected: >= {:?}\n  Received: {:?}\n",
472                format_header(self.location),
473                expected,
474                self.value
475            );
476        }
477    }
478
479    /// Assert that the value is less than or equal to the expected value
480    ///
481    /// # Example
482    /// ```rust,ignore
483    /// expect!(5).to_be_less_than_or_equal(5);
484    /// ```
485    pub fn to_be_less_than_or_equal(&self, expected: T) {
486        if !(self.value <= expected) {
487            panic!(
488                "{}\n  expect!(value).to_be_less_than_or_equal(expected)\n\n  Expected: <= {:?}\n  Received: {:?}\n",
489                format_header(self.location),
490                expected,
491                self.value
492            );
493        }
494    }
495}