k9/
assertions.rs

1use crate::utils;
2use colored::*;
3
4#[cfg(feature = "regex")]
5pub mod err_matches_regex;
6#[cfg(feature = "regex")]
7pub mod matches_regex;
8
9pub mod equal;
10pub mod err;
11pub mod greater_than;
12pub mod greater_than_or_equal;
13pub mod lesser_than;
14pub mod lesser_than_or_equal;
15pub mod matches_snapshot;
16pub mod ok;
17pub mod snapshot;
18
19#[derive(Debug)]
20pub struct Assertion {
21    /// Description of what's being asserted to provide a bit more context in the error message
22    pub description: Option<String>,
23    /// the name of the assertion macro that was invoked. e.g. `assert_equals`
24    pub name: String,
25    /// string containing all arguments passed to the assertion macro. e.g. "1 + 1, my_var"
26    pub args_str: String,
27    /// Assertion failure message, e.g. `expected blah blah but got blah`
28    pub failure_message: String,
29}
30
31impl Assertion {
32    pub fn get_failure_message(&self) -> String {
33        let message = format!(
34            "
35{separator}
36{assertion_expression}
37{description}
38{failure_message}
39{separator}
40",
41            assertion_expression = self.assertion_expression(),
42            description = utils::add_linebreaks(
43                self.description
44                    .as_ref()
45                    .unwrap_or(&"Assertion Failure!".to_string())
46            ),
47            failure_message = self.failure_message,
48            separator = utils::terminal_separator_line().dimmed(),
49        );
50
51        message
52    }
53
54    pub fn assertion_expression(&self) -> String {
55        format!(
56            "{assertion_name}({args});",
57            assertion_name = format!("{}!", self.name).yellow(),
58            args = self.args_str
59        )
60    }
61}
62
63#[macro_export]
64macro_rules! make_assertion {
65    ($name:expr, $args_str:expr, $failure_message:expr, $description:expr,) => {{
66        let assertion = $crate::assertions::make_assertion_impl(
67            $name,
68            $args_str,
69            $failure_message,
70            $description,
71        );
72        if let Some(assertion) = &assertion {
73            #[allow(clippy::all)]
74            if $crate::config::should_panic() {
75                panic!("{}", assertion.get_failure_message());
76            }
77        }
78        assertion
79    }};
80}
81
82pub fn make_assertion_impl(
83    name: &str,
84    args_str: String,
85    failure_message: Option<String>,
86    description: Option<&str>,
87) -> Option<Assertion> {
88    if let Some(failure_message) = failure_message {
89        let assertion = Assertion {
90            description: description.map(|d| d.into()),
91            failure_message,
92            name: name.to_string(),
93            args_str,
94        };
95        Some(assertion)
96    } else {
97        None
98    }
99}
100
101pub fn initialize_colors() {
102    if crate::config::CONFIG.force_enable_colors {
103        colored::control::set_override(true);
104    }
105}
106
107/// Asserts that two passed arguments are equal.
108/// Panics if they're not, using a pretty printed difference of
109/// [Debug](std::fmt::Debug) representations of the passed arguments.
110///
111/// This is a drop-in replacement for [assert_eq][assert_eq] macro
112///
113/// ```
114/// use k9::assert_equal;
115///
116/// // simple values
117/// assert_equal!(1, 1);
118///
119/// #[derive(Debug, PartialEq)]
120/// struct A {
121///     name: &'static str
122/// }
123///
124/// let a1 = A { name: "Kelly" };
125/// let a2 = A { name: "Kelly" };
126///
127/// assert_equal!(a1, a2);
128/// ```
129///
130/// ```should_panic
131/// # use k9::assert_equal;
132/// # #[derive(Debug, PartialEq)]
133/// # struct A {
134/// #     name: &'static str
135/// # }
136/// let a1 = A { name: "Kelly" };
137/// let a2 = A { name: "Rob" };
138///
139/// // this will print the visual difference between two structs
140/// assert_equal!(a1, a2);
141/// ```
142#[macro_export]
143macro_rules! assert_equal {
144    ($left:expr, $right:expr) => {{
145        use $crate::__macros__::colored::*;
146        $crate::assertions::initialize_colors();
147        let args_str = format!(
148            "{}, {}",
149            stringify!($left).red(),
150            stringify!($right).green(),
151        );
152
153        match  (&$left, &$right) {
154            (left, right) => {
155                let fail = *left != *right;
156                $crate::make_assertion!(
157                    "assert_equal",
158                    args_str,
159                    $crate::assertions::equal::assert_equal(left, right, fail),
160                    None,
161                )
162            }
163        }
164    }};
165    ($left:expr, $right:expr, $($description:expr),*) => {{
166        use $crate::__macros__::colored::*;
167        $crate::assertions::initialize_colors();
168        let description = format!($( $description ),*);
169        let args_str = format!(
170            "{}, {}, {}",
171            stringify!($left).red(),
172            stringify!($right).green(),
173            stringify!($( $description ),* ).dimmed(),
174        );
175        match  (&$left, &$right) {
176            (left, right) => {
177                let fail = *left != *right;
178                $crate::make_assertion!(
179                    "assert_equal",
180                    args_str,
181                    $crate::assertions::equal::assert_equal(left, right, fail),
182                    Some(&description),
183                )
184            }
185        }
186    }};
187}
188
189/// Asserts if left is greater than right.
190/// panics if they are not
191///
192/// ```rust
193/// use k9::assert_greater_than;
194///
195/// assert_greater_than!(2, 1);
196/// ```
197#[macro_export]
198macro_rules! assert_greater_than {
199    ($left:expr, $right:expr) => {{
200        use $crate::__macros__::colored::*;
201        $crate::assertions::initialize_colors();
202        let args_str = format!(
203            "{}, {}",
204            stringify!($left).red(),
205            stringify!($right).green(),
206        );
207        $crate::make_assertion!(
208            "assert_greater_than",
209            args_str,
210            $crate::assertions::greater_than::assert_greater_than($left, $right),
211            None,
212        )
213    }};
214    ($left:expr, $right:expr, $description:expr) => {{
215        use $crate::__macros__::colored::*;
216        $crate::assertions::initialize_colors();
217        let args_str = format!(
218            "{}, {}, {}",
219            stringify!($left).red(),
220            stringify!($right).green(),
221            stringify!($description).dimmed(),
222        );
223        $crate::make_assertion!(
224            "assert_greater_than",
225            args_str,
226            $crate::assertions::greater_than::assert_greater_than($left, $right),
227            Some(&$description),
228        )
229    }};
230}
231
232/// Asserts if left greater than or equal to right.
233/// panics if they are not
234///
235/// ```rust
236/// use k9::assert_greater_than_or_equal;
237///
238/// assert_greater_than_or_equal!(2, 1);
239/// assert_greater_than_or_equal!(1, 1);
240/// ```
241#[macro_export]
242macro_rules! assert_greater_than_or_equal {
243    ($left:expr, $right:expr) => {{
244        use $crate::__macros__::colored::*;
245        $crate::assertions::initialize_colors();
246        let args_str = format!(
247            "{}, {}",
248            stringify!($left).red(),
249            stringify!($right).green(),
250        );
251        $crate::make_assertion!(
252            "assert_greater_than_or_equal",
253            args_str,
254            $crate::assertions::greater_than_or_equal::assert_greater_than_or_equal($left, $right),
255            None,
256        )
257    }};
258    ($left:expr, $right:expr, $description:expr) => {{
259        use $crate::__macros__::colored::*;
260        $crate::assertions::initialize_colors();
261        let args_str = format!(
262            "{}, {}, {}",
263            stringify!($left).red(),
264            stringify!($right).green(),
265            stringify!($description).dimmed(),
266        );
267        $crate::make_assertion!(
268            "assert_greater_than_or_equal",
269            args_str,
270            $crate::assertions::greater_than_or_equal::assert_greater_than_or_equal($left, $right),
271            Some(&$description),
272        )
273    }};
274}
275
276/// Asserts if left is lesser than right.
277/// panics if they are not
278///
279/// ```rust
280/// use k9::assert_lesser_than;
281///
282/// assert_lesser_than!(1, 2);
283/// ```
284#[macro_export]
285macro_rules! assert_lesser_than {
286    ($left:expr, $right:expr) => {{
287        use $crate::__macros__::colored::*;
288        $crate::assertions::initialize_colors();
289        let args_str = format!(
290            "{}, {}",
291            stringify!($left).red(),
292            stringify!($right).green(),
293        );
294        $crate::make_assertion!(
295            "assert_lesser_than",
296            args_str,
297            $crate::assertions::lesser_than::assert_lesser_than($left, $right),
298            None,
299        )
300    }};
301    ($left:expr, $right:expr, $description:expr) => {{
302        use $crate::__macros__::colored::*;
303        $crate::assertions::initialize_colors();
304        let args_str = format!(
305            "{}, {}, {}",
306            stringify!($left).red(),
307            stringify!($right).green(),
308            stringify!($description).dimmed(),
309        );
310        $crate::make_assertion!(
311            "assert_lesser_than",
312            args_str,
313            $crate::assertions::lesser_than::assert_lesser_than($left, $right),
314            Some(&$description),
315        )
316    }};
317}
318
319/// Asserts if left lesser than or equal to right.
320/// panics if they are not
321///
322/// ```rust
323/// use k9::assert_lesser_than_or_equal;
324///
325/// assert_lesser_than_or_equal!(1, 2);
326/// assert_lesser_than_or_equal!(1, 1);
327/// ```
328#[macro_export]
329macro_rules! assert_lesser_than_or_equal {
330    ($left:expr, $right:expr) => {{
331        use $crate::__macros__::colored::*;
332        $crate::assertions::initialize_colors();
333        let args_str = format!(
334            "{}, {}",
335            stringify!($left).red(),
336            stringify!($right).green(),
337        );
338        $crate::make_assertion!(
339            "assert_lesser_than_or_equal",
340            args_str,
341            $crate::assertions::lesser_than_or_equal::assert_lesser_than_or_equal($left, $right),
342            None,
343        )
344    }};
345    ($left:expr, $right:expr, $description:expr) => {{
346        use $crate::__macros__::colored::*;
347        $crate::assertions::initialize_colors();
348        let args_str = format!(
349            "{}, {}, {}",
350            stringify!($left).red(),
351            stringify!($right).green(),
352            stringify!($description).dimmed(),
353        );
354        $crate::make_assertion!(
355            "assert_lesser_than_or_equal",
356            args_str,
357            $crate::assertions::lesser_than_or_equal::assert_lesser_than_or_equal($left, $right),
358            Some(&$description),
359        )
360    }};
361}
362
363/// Asserts that passed `&str` matches a regular expression.
364/// Regular expressions are compiled using `regex` crate.
365///
366/// ```rust
367/// use k9::assert_matches_regex;
368///
369/// assert_matches_regex!("1234-45", "\\d{4}-\\d{2}");
370/// assert_matches_regex!("abc", "abc");
371/// ````
372#[cfg(feature = "regex")]
373#[macro_export]
374macro_rules! assert_matches_regex {
375    ($s:expr, $regex:expr) => {{
376        use $crate::__macros__::colored::*;
377        $crate::assertions::initialize_colors();
378        let args_str = format!("{}, {}", stringify!($s).red(), stringify!($regex).green());
379        $crate::make_assertion!(
380            "assert_matches_regex",
381            args_str,
382            $crate::assertions::matches_regex::assert_matches_regex($s, $regex),
383            None,
384        )
385    }};
386    ($s:expr, $regex:expr, $description:expr) => {{
387        use $crate::__macros__::colored::*;
388        $crate::assertions::initialize_colors();
389        let args_str = format!(
390            "{}, {}, {}",
391            stringify!($s).red(),
392            stringify!($regex).green(),
393            stringify!($description).dimmed()
394        );
395        $crate::make_assertion!(
396            "assert_matches_regex",
397            args_str,
398            $crate::assertions::matches_regex::assert_matches_regex($s, $regex),
399            Some($description),
400        )
401    }};
402}
403
404/// Asserts that the passed `Result` argument is an `Err` and
405/// and the debug string of that error matches provided regex.
406/// Regular expressions are compiled using `regex` crate.
407///
408/// ```rust
409/// use k9::assert_err_matches_regex;
410/// // Borrowed from Rust by Example: https://doc.rust-lang.org/stable/rust-by-example/std/result.html
411/// fn divide(x: f64, y: f64) -> Result<f64, &'static str> {
412///     if y == 0.0 {
413///         // This operation would `fail`, instead let's return the reason of
414///         // the failure wrapped in `Err`
415///         Err("Cannot divide by 0.")
416///     } else {
417///         // This operation is valid, return the result wrapped in `Ok`
418///         Ok(x / y)
419///     }
420/// }
421/// let division_error = divide(4.0, 0.0);
422/// assert_err_matches_regex!(division_error, "Cannot");
423/// ```
424#[cfg(feature = "regex")]
425#[macro_export]
426macro_rules! assert_err_matches_regex {
427    ($err:expr, $regex:expr) => {{
428        use $crate::__macros__::colored::*;
429        $crate::assertions::initialize_colors();
430        let args_str = format!("{}, {}", stringify!($err).red(), stringify!($regex).green(),);
431        $crate::make_assertion!(
432            "assert_err_matches_regex",
433            args_str,
434            $crate::assertions::err_matches_regex::assert_err_matches_regex($err, $regex),
435            None,
436        )
437    }};
438    ($err:expr, $regex:expr, $context:expr) => {{
439        use $crate::__macros__::colored::*;
440        $crate::assertions::initialize_colors();
441        let args_str = format!(
442            "{}, {}, {}",
443            stringify!($err).red(),
444            stringify!($regex).green(),
445            stringify!($context).dimmed(),
446        );
447        $crate::make_assertion!(
448            "assert_err_matches_regex",
449            args_str,
450            $crate::assertions::err_matches_regex::assert_err_matches_regex($err, $regex),
451            Some($context),
452        )
453    }};
454}
455
456/// Same as [snapshot!()](./macro.snapshot.html) macro, but it takes a string as the
457/// only argument and stores the snapshot in a separate file instead of inlining
458/// it in the source code of the test.
459///
460/// ```rust
461/// #[test]
462/// fn my_test() {
463///     struct A {
464///         name: &'a str,
465///         age: u32
466///     }
467///
468///     let a = A { name: "Lance", age: 9 };
469///
470///     // When first run with `K9_UPDATE_SNAPSHOTS=1` it will
471///     // create `__k9_snapshots__/my_test_file/my_test.snap` file
472///     // with contents being the serialized value of `a`.
473///     // Next time the test is run, if the newly serialized value of a
474///     // is different from the value of that snapshot file, the assertion
475///     // will fail.
476///     assert_matches_snapshot!(a);
477/// }
478/// ```
479#[macro_export]
480macro_rules! assert_matches_snapshot {
481    ($to_snap:expr) => {{
482        use $crate::__macros__::colored::*;
483        $crate::assertions::initialize_colors();
484        let line = line!();
485        let column = column!();
486        let file = file!();
487        let args_str = format!("{}", stringify!($to_snap).red(),);
488        $crate::make_assertion!(
489            "assert_matches_snapshot",
490            args_str,
491            $crate::assertions::matches_snapshot::snap_internal($to_snap, line, column, file),
492            None,
493        )
494    }};
495    ($to_snap:expr, $description:expr) => {{
496        use $crate::__macros__::colored::*;
497        $crate::assertions::initialize_colors();
498        let line = line!();
499        let column = column!();
500        let file = file!();
501        let args_str = format!(
502            "{}, {}",
503            stringify!($to_snap).red(),
504            stringify!($description).dimmed(),
505        );
506        $crate::make_assertion!(
507            "assert_matches_snapshot",
508            args_str,
509            $crate::assertions::matches_snapshot::snap_internal($to_snap, line, column, file),
510            Some($description),
511        )
512    }};
513}
514
515/// Asserts if value is Ok(T).
516/// panics if it is not
517///
518/// ```rust
519/// use k9::assert_ok;
520///
521/// let result: Result<_, ()> = Ok(2);
522/// assert_ok!(result);
523/// ```
524#[macro_export]
525macro_rules! assert_ok {
526    ($left:expr) => {{
527        use $crate::__macros__::colored::*;
528        $crate::assertions::initialize_colors();
529        let args_str = format!("{}", stringify!($left).red());
530        $crate::make_assertion!(
531            "assert_ok",
532            args_str,
533            $crate::assertions::ok::assert_ok($left),
534            None,
535        )
536    }};
537    ($left:expr, $description:expr) => {{
538        use $crate::__macros__::colored::*;
539        $crate::assertions::initialize_colors();
540        let args_str = format!(
541            "{}, {}",
542            stringify!($left).red(),
543            stringify!($description).dimmed(),
544        );
545        $crate::make_assertion!(
546            "assert_ok",
547            args_str,
548            $crate::assertions::ok::assert_ok($left),
549            Some(&$description),
550        )
551    }};
552}
553
554/// Asserts if value is Err(E).
555/// panics if it is not
556///
557/// ```rust
558/// use k9::assert_err;
559///
560/// let result: Result<(), _> = Err("invalid path");
561/// assert_err!(result);
562/// ```
563#[macro_export]
564macro_rules! assert_err {
565    ($left:expr) => {{
566        use $crate::__macros__::colored::*;
567        $crate::assertions::initialize_colors();
568        let args_str = format!("{}", stringify!($left).red());
569        $crate::make_assertion!(
570            "assert_err",
571            args_str,
572            $crate::assertions::err::assert_err($left),
573            None,
574        )
575    }};
576    ($left:expr, $description:expr) => {{
577        use $crate::__macros__::colored::*;
578        $crate::assertions::initialize_colors();
579        let args_str = format!(
580            "{}, {}",
581            stringify!($left).red(),
582            stringify!($description).dimmed(),
583        );
584        $crate::make_assertion!(
585            "assert_err",
586            args_str,
587            $crate::assertions::err::assert_err($left),
588            Some(&$description),
589        )
590    }};
591}
592
593/// Serializes the first argument into a string and compares it with
594/// the second argument, which is a snapshot string that was automatically generated
595/// during previous test runs. Panics if the values are not equal.
596///
597/// If second argument is missing, assertion will always fail by prompting to
598/// re-run the test in `update snapshots` mode.
599///
600/// If run in `update snapshots` mode, serialization of the first argument will
601/// be made into a string literal and inserted into source code as the second
602/// argument of this macro. (It will actually modify the file in the filesystem)
603///
604/// Typical workflow for this assertion is:
605///
606/// ```should_panic
607/// // Step 1:
608/// // - Take a result of some computation and pass it as a single argument to the macro
609/// // - Run the test
610/// // - Test will fail prompting to re-run it in update mode
611/// use std::collections::BTreeMap;
612///
613/// k9::snapshot!((1..=3).rev().enumerate().collect::<BTreeMap<_, _>>());
614/// ```
615///
616/// ```text
617/// # Step 2:
618/// #   Run tests with K9_UPDATE_SNAPSHOTS=1 env variable set
619/// $ K9_UPDATE_SNAPSHOTS=1 cargo test
620/// ```
621///
622/// ```
623/// // Step 3:
624/// // After test run finishes and process exits successfully, the source code of the
625/// //       test file will be updated with the serialized value of the first argument.
626/// // All subsequent runs of this test will pass
627/// use std::collections::BTreeMap;
628///
629/// k9::snapshot!(
630///     (1..=3).rev().enumerate().collect::<BTreeMap<_, _>>(),
631///     "
632/// {
633///     0: 3,
634///     1: 2,
635///     2: 1,
636/// }
637/// "
638/// );
639/// ```
640///
641/// ```should_panic
642/// // If the logic behind first argument ever changes and affects the serialization
643/// // the test will fail and print the difference between the "old" and the "new" values
644/// use std::collections::BTreeMap;
645///
646/// k9::snapshot!(
647///     /// remove `.rev()`
648///     (1..=3).enumerate().collect::<BTreeMap<_, _>>(),
649///     "
650/// {
651///     0: 3,
652///     1: 2,
653///     2: 1,
654/// }
655/// "
656/// );
657/// ```
658///
659/// The test above will now fail with the following message:
660/// ```text
661/// Difference:
662/// {
663/// -     0: 3,
664/// +     0: 1,
665///       1: 2,
666/// -     2: 1,
667/// +     2: 3,
668/// }
669/// ```
670#[macro_export]
671macro_rules! snapshot {
672    ($to_snap:expr) => {{
673        use $crate::__macros__::colored::*;
674        $crate::assertions::initialize_colors();
675        let line = line!();
676        let column = column!();
677        let file = file!();
678        let args_str = format!("{}", stringify!($to_snap).red(),);
679        $crate::make_assertion!(
680            "snapshot",
681            args_str,
682            $crate::assertions::snapshot::snapshot($to_snap, None, line, column, file),
683            None,
684        )
685    }};
686    ($to_snap:expr, $inline_snap:literal) => {{
687        use $crate::__macros__::colored::*;
688        $crate::assertions::initialize_colors();
689        let line = line!();
690        let column = column!();
691        let file = file!();
692        let args_str = format!(
693            "{}, {}",
694            stringify!($to_snap).red(),
695            stringify!($inline_snap).green(),
696        );
697        $crate::make_assertion!(
698            "snapshot",
699            args_str,
700            $crate::assertions::snapshot::snapshot(
701                $to_snap,
702                Some($inline_snap),
703                line,
704                column,
705                file,
706            ),
707            None,
708        )
709    }};
710}