rassert/
core.rs

1use std::fmt::Debug;
2
3/// Starts a new expectation chain for the supplied expression.
4#[macro_export]
5macro_rules! expect {
6    ($tested:expr) => {
7        $crate::blank_chain!($tested)
8    };
9}
10
11// Originates from the assert_matches library https://github.com/murarth/assert_matches
12// Licensed under the Apache 2.0 Software License.
13#[macro_export]
14macro_rules! expect_matches {
15    ( $e:expr , $($pat:pat)|+ ) => {
16        match $e {
17            $($pat)|+ => $crate::blank_chain!($e),
18            ref e => $crate::failure_chain!($e, format!("Expected {:?} to match {}",
19                e, stringify!($($pat)|+)))
20        }
21    };
22    ( $e:expr , $($pat:pat)|+ if $cond:expr ) => {
23        match $e {
24            $($pat)|+ if $cond => $crate::blank_chain!($e),
25            ref e => $crate::failure_chain!($e, format!("Expected {:?} to match {}",
26                e, stringify!($($pat)|+ if $cond)))
27        }
28    };
29    ( $e:expr , $($pat:pat)|+ , $($arg:tt)* ) => {
30        match $e {
31            $($pat)|+ => $crate::blank_chain!($e),
32            ref e => $crate::failure_chain!($e, format!("Expected {:?} to match {}: {}",
33                e, stringify!($($pat)|+), format_args!($($arg)*)))
34        }
35    };
36    ( $e:expr , $($pat:pat)|+ if $cond:expr , $($arg:tt)* ) => {
37        match $e {
38            $($pat)|+ if $cond => $crate::blank_chain!($e),
39            ref e => $crate::failure_chain!($e, format!("Expected {:?} to match {}: {}",
40                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*)))
41        }
42    };
43}
44
45#[macro_export]
46macro_rules! blank_chain {
47    ($tested:expr) => {
48        $crate::ExpectationChain::from_expression($crate::ExpressionUnderTest {
49            actual: $tested,
50            tested_expression: std::stringify!($tested),
51            location: $crate::SourceLocation {
52                file: file!(),
53                line: line!(),
54                column: column!(),
55            },
56        })
57    };
58}
59
60#[macro_export]
61macro_rules! failure_chain {
62    ($tested:expr, $message:expr) => {
63        $crate::blank_chain!($tested).failure($message)
64    };
65}
66
67#[derive(Debug, PartialEq, Copy, Clone)]
68pub struct SourceLocation {
69    pub file: &'static str,
70    pub line: u32,
71    pub column: u32,
72}
73
74pub struct ExpressionUnderTest<'a, T> {
75    pub actual: &'a T,
76    pub tested_expression: &'static str,
77    pub location: SourceLocation,
78}
79
80pub trait Expectation<T> {
81    fn test(&self, actual: &T) -> bool;
82
83    fn message(&self, expression: &str, actual: &T) -> String;
84}
85
86pub struct ExpectationChain<'a, T> {
87    expression: ExpressionUnderTest<'a, T>,
88
89    in_negated_mode: bool,
90
91    negations: Vec<bool>,
92
93    soft_mode: bool,
94
95    expectations: Vec<Box<dyn Expectation<T> + 'a>>,
96}
97
98impl<'a, T> ExpectationChain<'a, T> {
99    pub fn from_expression(expression: ExpressionUnderTest<'a, T>) -> Self {
100        Self {
101            expression,
102            in_negated_mode: false,
103            negations: vec![],
104            soft_mode: false,
105            expectations: vec![],
106        }
107    }
108
109    #[allow(clippy::should_implement_trait)]
110    pub fn not(mut self) -> Self {
111        self.in_negated_mode = !self.in_negated_mode;
112
113        self
114    }
115
116    pub fn and(self) -> Self {
117        self
118    }
119
120    pub fn expecting(mut self, expectation: impl Expectation<T> + 'a) -> Self {
121        self.expectations.push(Box::new(expectation));
122
123        self.negations.push(self.in_negated_mode);
124
125        self.in_negated_mode = false;
126
127        self
128    }
129
130    pub fn failure(self, message: String) -> Self {
131        self.expecting(ExpectMatchFailure {
132            preset_message: message,
133        })
134    }
135
136    pub fn soft(mut self) -> Self {
137        self.soft_mode = true;
138
139        self
140    }
141
142    pub fn conclude_result(self) -> Result<(), String> {
143        let location = self.expression.location;
144        let mut message = format!(
145            "{}:{}:{}\nwhen testing expression\n\n",
146            location.file, location.line, location.column
147        );
148        message.push_str(&format!("    {}\n\n", self.expression.tested_expression));
149
150        let mut had_failure = false;
151        for i in 0..self.expectations.len() {
152            let expectation = self.expectations.get(i).unwrap();
153            let is_negated = self.negations.get(i).unwrap();
154
155            if !(is_negated ^ expectation.test(self.expression.actual)) {
156                had_failure = true;
157
158                let failure_message =
159                    expectation.message(self.expression.tested_expression, self.expression.actual);
160                let failure_message = if *is_negated {
161                    indented("  ", &format!("NOT {}", failure_message))
162                } else {
163                    indented("  ", &failure_message)
164                };
165
166                message.push_str(&failure_message);
167
168                if !self.soft_mode {
169                    break;
170                }
171            }
172        }
173
174        if had_failure {
175            Err(message)
176        } else {
177            Ok(())
178        }
179    }
180
181    pub fn conclude_panic(self) {
182        if let Err(message) = self.conclude_result() {
183            eprintln!("{}", message);
184            panic!()
185        }
186    }
187}
188
189fn indented(indentation: &str, s: &str) -> String {
190    let result: Vec<String> = s
191        .split('\n')
192        .map(|s| format!("{}{}", indentation, s))
193        .collect();
194
195    format!("{}\n", result.join("\n"))
196}
197
198struct ExpectMatchFailure {
199    preset_message: String,
200}
201
202impl<T> Expectation<T> for ExpectMatchFailure {
203    fn test(&self, _actual: &T) -> bool {
204        false
205    }
206
207    fn message(&self, _expression: &str, _actual: &T) -> String {
208        self.preset_message.clone()
209    }
210}