1use crate::runtime::Runtime;
2use std::ops::Range;
3
4#[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(); let column_number: usize = (self.file_position.column - 1).try_into().unwrap(); let line_byte_offset = if line_number == 0 {
88 0
89 } else {
90 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 != '!') .nth(1) .expect("Failed to locate macro")
105 .0; 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 let trimmed = haystack.trim_start();
147 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 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}