expect_tests/
expect.rs

1use crate::runtime::Runtime;
2use std::ops::Range;
3
4/// Self-updating string literal.
5#[derive(Debug, Clone, Hash, PartialEq, Eq)]
6pub struct Expect<const N: usize> {
7    #[doc(hidden)]
8    pub file_position: FilePosition,
9    #[doc(hidden)]
10    pub raw_actual: &'static str,
11    #[doc(hidden)]
12    pub expected: [&'static str; N],
13    #[doc(hidden)]
14    pub raw_expected: [&'static str; N],
15    #[doc(hidden)]
16    pub assertion_index: usize,
17}
18
19#[derive(Debug, Clone, Hash, PartialEq, Eq)]
20pub struct FilePosition {
21    #[doc(hidden)]
22    pub file: &'static str,
23    #[doc(hidden)]
24    pub line: u32,
25    #[doc(hidden)]
26    pub column: u32,
27}
28
29impl std::fmt::Display for FilePosition {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}:{}:{}", self.file, self.line, self.column)
32    }
33}
34
35impl<const N: usize> Expect<N> {
36    fn trimmed(&self, text: &str) -> String {
37        if text.contains('\n') {
38            let text = text.strip_prefix('\n').unwrap_or(text);
39            let indent_amount = text
40                .lines()
41                .filter(|line| !line.trim().is_empty())
42                .map(|line| line.len() - line.trim_start().len())
43                .min()
44                .unwrap_or(0);
45
46            let mut trimmed = text
47                .lines()
48                .map(|line| {
49                    if line.len() < indent_amount {
50                        ""
51                    } else {
52                        &line[indent_amount..]
53                    }
54                })
55                .collect::<Vec<&str>>()
56                .join("\n");
57            if text.ends_with('\n') {
58                trimmed.push('\n');
59            }
60            trimmed
61        } else {
62            text.to_string()
63        }
64    }
65
66    pub fn assert_eq(&self, actual: &str) {
67        if let Some(expected) = self.expected.get(self.assertion_index) {
68            let expected = self.trimmed(expected);
69            if expected != actual {
70                Runtime::fail_expect(self, &expected, actual);
71            }
72        } else {
73            Runtime::fail_expect(self, "", actual);
74        }
75    }
76
77    pub fn assert_debug_eq<T>(&self, actual: T)
78    where
79        T: std::fmt::Debug,
80    {
81        let actual = format!("{:#?}", actual);
82        self.assert_eq(&actual)
83    }
84    pub fn find_expect_location(&self, file_contents: &str) -> ExpectLocation<N> {
85        let line_number: usize = (self.file_position.line - 1).try_into().unwrap(); // Zero-indexed
86        let column_number: usize = (self.file_position.column - 1).try_into().unwrap(); // Zero-indexed
87        let line_byte_offset = if line_number == 0 {
88            0
89        } else {
90            // Add 1 to skip the newline character
91            file_contents
92                .match_indices('\n')
93                .nth(line_number - 1)
94                .unwrap()
95                .0
96                + 1
97        };
98        let macro_byte_offset = line_byte_offset
99            + file_contents[line_byte_offset..]
100                .char_indices()
101                .skip(column_number)
102                .skip_while(|&(_, c)| c != '!') // macro location (ex: "expect" and "expect_tokens")
103                .nth(1) // !
104                .expect("Failed to locate macro")
105                .0; // extract index from (index, char)
106
107        fn find_ignore_whitespace(haystack: &str, pattern: &str) -> Option<(usize, usize)> {
108            fn validate(haystack: &str, pattern: &str) -> Option<(usize, usize)> {
109                let s = haystack.trim_start();
110                if s.starts_with(pattern) {
111                    let start_index = haystack.len() - s.len();
112                    Some((start_index, start_index + pattern.len()))
113                } else {
114                    None
115                }
116            }
117
118            fn validate_fallback(haystack: &str, pattern: &str) -> Option<(usize, usize)> {
119                let mut haystack_iterator = haystack.chars().peekable();
120                let mut index = 0;
121                while let Some(_whitespace) = haystack_iterator.next_if(|c| c.is_whitespace()) {
122                    index += 1;
123                }
124                let start_index = index;
125                for pattern_char in pattern.chars().filter(|c| !c.is_whitespace()) {
126                    while let Some(_whitespace) = haystack_iterator.next_if(|c| c.is_whitespace()) {
127                        index += 1;
128                    }
129                    if let Some(c) = haystack_iterator.next()
130                        && c == pattern_char
131                    {
132                        index += 1;
133                    } else {
134                        return None;
135                    }
136                }
137                Some((start_index, index))
138            }
139            // First we're going to skip to the next character, to account for the parentheses
140            // then we're going to trim_start() and see if starts_with directly works
141            //
142            // Because stringify! does not return the raw source code in all
143            // situations, we must have some fallback implementation in case
144            // directly matching does not work. In this fallback case, we are
145            // going to process char by char, ignoring whitespace
146            let trimmed = haystack.trim_start();
147            // trim the left parentheses in expect!()
148            let trimmed = trimmed.get(1..)?;
149
150            let num_trimmed = haystack.len() - trimmed.len();
151            let (start, end) =
152                validate(trimmed, pattern).or_else(|| validate_fallback(trimmed, pattern))?;
153            Some((num_trimmed + start, num_trimmed + end))
154        }
155        let (actual_start, actual_end) =
156            find_ignore_whitespace(&file_contents[macro_byte_offset..], self.raw_actual)
157                .unwrap_or_else(|| {
158                    panic!(
159                        "Unable to find actual: `{}` in `{}`",
160                        self.raw_actual,
161                        &file_contents[macro_byte_offset..]
162                    )
163                });
164        let actual_byte_offset = macro_byte_offset + actual_start;
165        let mut current_offset = macro_byte_offset + actual_end;
166
167        // let actual_byte_offset = macro_byte_offset
168        //     + file_contents[macro_byte_offset..]
169        //         .find(self.raw_actual)
170        //         .unwrap_or_else(|| {
171        //             panic!(
172        //                 "Unable to find actual: `{}` in `{}`",
173        //                 self.raw_actual,
174        //                 &file_contents[macro_byte_offset..]
175        //             )
176        //         });
177        // let actual_range = actual_byte_offset..(actual_byte_offset + self.raw_actual.len());
178        // let mut current_offset = actual_byte_offset + self.raw_actual.len();
179
180        let expected_ranges = self.raw_expected.map(|raw_expected| {
181            let start = current_offset
182                + file_contents[current_offset..]
183                    .find(raw_expected)
184                    .expect("Unable to find expected");
185            let end = start + raw_expected.len();
186            current_offset = end;
187            start..end
188        });
189
190        let start_index = actual_byte_offset;
191        let end_index = current_offset;
192
193        let line_indent = file_contents[line_byte_offset..]
194            .chars()
195            .take_while(|&c| c == ' ')
196            .count();
197
198        ExpectLocation {
199            line_indent,
200            expected_ranges,
201            start_index,
202            end_index,
203        }
204    }
205}
206
207#[derive(Debug)]
208pub struct ExpectLocation<const N: usize> {
209    pub line_indent: usize,
210    pub expected_ranges: [Range<usize>; N],
211    pub start_index: usize,
212    pub end_index: usize,
213}