1use std::fmt::Write as _;
13use std::path::PathBuf;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::{fmt, fs, io};
16
17const UPDATE_EXPECT_VAR_NAME: &str = if cfg!(test) {
18 "UPDATE_EXPECT_BYTES"
19} else {
20 "UPDATE_EXPECT"
21};
22
23const HELP: &str = "
24You can update all `expect!` tests by running:
25
26 env UPDATE_EXPECT=1 cargo test
27
28To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
29";
30
31static HELP_PRINTED: AtomicBool = AtomicBool::new(false);
32
33fn not_found_to_none<T>(res: io::Result<T>) -> io::Result<Option<T>> {
35 match res {
36 Ok(value) => Ok(Some(value)),
37 Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
38 Err(e) => Err(e),
39 }
40}
41
42fn first_diff_index(a: &[u8], b: &[u8]) -> Option<usize> {
47 a.iter()
48 .zip(b.iter())
49 .position(|(x, y)| x != y)
50 .or_else(|| (a.len() != b.len()).then(|| a.len().min(b.len())))
51}
52
53const BYTE_WINDOW_HALF_SIZE: usize = 4;
54
55struct ByteWindowDisplay<'a> {
56 data: &'a [u8],
57 diff_idx: usize,
58 is_expected: bool,
59}
60impl fmt::Display for ByteWindowDisplay<'_> {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 let start = self.diff_idx.saturating_sub(BYTE_WINDOW_HALF_SIZE);
63 let end = self.data.len().min(self.diff_idx + BYTE_WINDOW_HALF_SIZE);
64
65 let translated_diff_idx = self.diff_idx - start;
67
68 for (i, byte) in self.data[start..=end].iter().enumerate() {
69 if i != 0 {
70 write!(f, " ").unwrap();
71 }
72 if i == translated_diff_idx {
73 let highlight_ansi_code = if self.is_expected { "32" } else { "31" };
74 write!(f, "\x1b[{highlight_ansi_code}m").unwrap();
75 }
76
77 write!(f, "{byte:02x}").unwrap();
78
79 if i == translated_diff_idx {
80 write!(f, "\x1b[0m").unwrap();
81 }
82 }
83
84 write!(f, " {}", CharacterPanel(&self.data[start..=end]))?;
85 Ok(())
86 }
87}
88
89struct CharacterPanel<'a>(&'a [u8]);
91impl fmt::Display for CharacterPanel<'_> {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 for byte in self.0 {
94 let ch = match *byte {
95 0 => '⋄',
96 _ if byte.is_ascii_graphic() => *byte as char,
97 b' ' => ' ',
98 _ if byte.is_ascii_whitespace() => '_',
99 _ if byte.is_ascii() => '•',
100 _ => '×',
101 };
102 f.write_char(ch)?;
103 }
104 Ok(())
105 }
106}
107
108#[derive(Debug)]
113pub struct ExpectFile {
114 #[doc(hidden)]
115 pub path: PathBuf,
116}
117
118impl ExpectFile {
119 pub fn assert_eq(&self, actual: &[u8]) {
129 if let Err(()) = self.assert_eq_nopanic_imp(actual, &mut io::stdout()) {
130 std::panic::resume_unwind(Box::new(()));
132 }
133 }
134 fn assert_eq_nopanic_imp<W: io::Write>(&self, actual: &[u8], writer: &mut W) -> Result<(), ()> {
135 let expected = not_found_to_none(fs::read(&self.path)).unwrap();
136 if expected.as_deref() == Some(actual) {
137 return Ok(());
138 }
139 if std::env::var_os(UPDATE_EXPECT_VAR_NAME).is_some() {
140 writeln!(
141 writer,
142 "\x1b[1m\x1b[92mupdating\x1b[0m: {}",
143 self.path.display()
144 )
145 .unwrap();
146 fs::write(&self.path, actual).unwrap();
147 return Ok(());
148 }
149 let print_help = if cfg!(test) {
150 true } else {
152 !HELP_PRINTED.swap(true, Ordering::SeqCst)
153 };
154 let help = if print_help { HELP } else { "" };
155
156 writeln!(
157 writer,
158 "
159\x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m
160 \x1b[1m\x1b[34m-->\x1b[0m {location}
161{help}
162\x1b[1mExpect\x1b[0m:
163{expect}
164
165\x1b[1mActual\x1b[0m:
166<binary>
167",
168 location = self.path.display(),
169 expect = if expected.is_some() {
170 "<binary>"
171 } else {
172 "\x1b[1mNot found\x1b[0m"
173 },
174 )
175 .unwrap();
176
177 if let Some(expected) = expected {
178 let diff_idx = first_diff_index(&expected, actual).unwrap_or(0);
179
180 writeln!(
181 writer,
182 "\x1b[1mDiff\x1b[0m:
183Binary files differ at byte {diff_idx:#x}
184
185Expect: {expect}
186Actual: {actual}
187 {offset}\x1b[1m^^\x1b[0m",
188 expect = ByteWindowDisplay {
189 data: &expected,
190 diff_idx,
191 is_expected: true
192 },
193 actual = ByteWindowDisplay {
194 data: actual,
195 diff_idx,
196 is_expected: false
197 },
198 offset = " ".repeat(diff_idx.min(BYTE_WINDOW_HALF_SIZE)),
199 )
200 .unwrap();
201 }
202
203 Err(())
204 }
205}
206
207#[macro_export]
214macro_rules! expect_file {
215 [$path:expr] => {
216 $crate::ExpectFile {
217 path: {
218 let path = ::std::path::Path::new($path);
219 if path.is_absolute() {
220 path.to_owned()
221 } else {
222 ::std::path::Path::new(file!()).parent().unwrap().join(path)
223 }
224 },
225 }
226 };
227}
228
229#[derive(Debug)]
233pub struct Expect<'a> {
234 #[doc(hidden)]
235 pub position: Position,
236 #[doc(hidden)]
237 pub data: &'a [u8],
238}
239
240#[derive(Debug)]
242pub struct Position {
243 #[doc(hidden)]
244 pub file: &'static str,
245 #[doc(hidden)]
246 pub line: u32,
247 #[doc(hidden)]
248 pub column: u32,
249}
250
251impl fmt::Display for Position {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "{}:{}:{}", self.file, self.line, self.column)
254 }
255}
256
257#[macro_export]
264macro_rules! expect {
265 [[$data:expr]] => {
266 $crate::Expect {
267 position: $crate::Position {
268 file: file!(),
269 line: line!(),
270 column: column!(),
271 },
272 data: $data,
273 }
274 };
275 [$data:expr] => { $crate::expect![[$data]] };
276 [] => { $crate::expect![[b""]] };
277 [[]] => { $crate::expect![[b""]] };
278}
279
280#[cfg(test)]
281mod tests;