use std::fmt::Write as _;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fmt, fs, io};
const UPDATE_EXPECT_VAR_NAME: &str = if cfg!(test) {
"UPDATE_EXPECT_BYTES"
} else {
"UPDATE_EXPECT"
};
const HELP: &str = "
You can update all `expect!` tests by running:
env UPDATE_EXPECT=1 cargo test
To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
";
static HELP_PRINTED: AtomicBool = AtomicBool::new(false);
fn not_found_to_none<T>(res: io::Result<T>) -> io::Result<Option<T>> {
match res {
Ok(value) => Ok(Some(value)),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn first_diff_index(a: &[u8], b: &[u8]) -> Option<usize> {
a.iter()
.zip(b.iter())
.position(|(x, y)| x != y)
.or_else(|| (a.len() != b.len()).then(|| a.len().min(b.len())))
}
const BYTE_WINDOW_HALF_SIZE: usize = 4;
struct ByteWindowDisplay<'a> {
data: &'a [u8],
diff_idx: usize,
is_expected: bool,
}
impl fmt::Display for ByteWindowDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let start = self.diff_idx.saturating_sub(BYTE_WINDOW_HALF_SIZE);
let end = self.data.len().min(self.diff_idx + BYTE_WINDOW_HALF_SIZE);
let translated_diff_idx = self.diff_idx - start;
for (i, byte) in self.data[start..=end].iter().enumerate() {
if i != 0 {
write!(f, " ").unwrap();
}
if i == translated_diff_idx {
let highlight_ansi_code = if self.is_expected { "32" } else { "31" };
write!(f, "\x1b[{highlight_ansi_code}m").unwrap();
}
write!(f, "{byte:02x}").unwrap();
if i == translated_diff_idx {
write!(f, "\x1b[0m").unwrap();
}
}
write!(f, " {}", CharacterPanel(&self.data[start..=end]))?;
Ok(())
}
}
struct CharacterPanel<'a>(&'a [u8]);
impl fmt::Display for CharacterPanel<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
let ch = match *byte {
0 => 'â‹„',
_ if byte.is_ascii_graphic() => *byte as char,
b' ' => ' ',
_ if byte.is_ascii_whitespace() => '_',
_ if byte.is_ascii() => '•',
_ => '×',
};
f.write_char(ch)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ExpectFile {
#[doc(hidden)]
pub path: PathBuf,
}
impl ExpectFile {
pub fn assert_eq(&self, actual: &[u8]) {
if let Err(()) = self.assert_eq_nopanic_imp(actual, &mut io::stdout()) {
std::panic::resume_unwind(Box::new(()));
}
}
fn assert_eq_nopanic_imp<W: io::Write>(&self, actual: &[u8], writer: &mut W) -> Result<(), ()> {
let expected = not_found_to_none(fs::read(&self.path)).unwrap();
if expected.as_deref() == Some(actual) {
return Ok(());
}
if std::env::var_os(UPDATE_EXPECT_VAR_NAME).is_some() {
writeln!(
writer,
"\x1b[1m\x1b[92mupdating\x1b[0m: {}",
self.path.display()
)
.unwrap();
fs::write(&self.path, actual).unwrap();
return Ok(());
}
let print_help = if cfg!(test) {
true } else {
!HELP_PRINTED.swap(true, Ordering::SeqCst)
};
let help = if print_help { HELP } else { "" };
writeln!(
writer,
"
\x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m
\x1b[1m\x1b[34m-->\x1b[0m {location}
{help}
\x1b[1mExpect\x1b[0m:
{expect}
\x1b[1mActual\x1b[0m:
<binary>
",
location = self.path.display(),
expect = if expected.is_some() {
"<binary>"
} else {
"\x1b[1mNot found\x1b[0m"
},
)
.unwrap();
if let Some(expected) = expected {
let diff_idx = first_diff_index(&expected, actual).unwrap_or(0);
writeln!(
writer,
"\x1b[1mDiff\x1b[0m:
Binary files differ at byte {diff_idx:#x}
Expect: {expect}
Actual: {actual}
{offset}\x1b[1m^^\x1b[0m",
expect = ByteWindowDisplay {
data: &expected,
diff_idx,
is_expected: true
},
actual = ByteWindowDisplay {
data: actual,
diff_idx,
is_expected: false
},
offset = " ".repeat(diff_idx.min(BYTE_WINDOW_HALF_SIZE)),
)
.unwrap();
}
Err(())
}
}
#[macro_export]
macro_rules! expect_file {
[$path:expr] => {
$crate::ExpectFile {
path: {
let path = ::std::path::Path::new($path);
if path.is_absolute() {
path.to_owned()
} else {
::std::path::Path::new(file!()).parent().unwrap().join(path)
}
},
}
};
}
#[derive(Debug)]
pub struct Expect<'a> {
#[doc(hidden)]
pub position: Position,
#[doc(hidden)]
pub data: &'a [u8],
}
#[derive(Debug)]
pub struct Position {
#[doc(hidden)]
pub file: &'static str,
#[doc(hidden)]
pub line: u32,
#[doc(hidden)]
pub column: u32,
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[macro_export]
macro_rules! expect {
[[$data:expr]] => {
$crate::Expect {
position: $crate::Position {
file: file!(),
line: line!(),
column: column!(),
},
data: $data,
}
};
[$data:expr] => { $crate::expect![[$data]] };
[] => { $crate::expect![[b""]] };
[[]] => { $crate::expect![[b""]] };
}
#[cfg(test)]
mod tests;