#![cfg_attr(
feature = "serde",
doc = r#"
- [`assert_serde_eq!`]: diffs `Serialize` on assertion failure.
"#
)]
use std::borrow::Cow;
use std::fmt::{self, Display};
use std::time::Duration;
use console::{style, Style};
use similar::{Algorithm, ChangeTag, TextDiff};
#[cfg(feature = "serde")]
#[doc(hidden)]
pub mod serde_impl;
#[doc(hidden)]
pub mod print;
fn get_max_string_length() -> usize {
use std::sync::atomic::{AtomicUsize, Ordering};
static TRUNCATE: AtomicUsize = AtomicUsize::new(!0);
let rv = TRUNCATE.load(Ordering::Relaxed);
if rv != !0 {
return rv;
}
let rv: usize = std::env::var("SIMILAR_ASSERTS_MAX_STRING_LENGTH")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(200);
TRUNCATE.store(rv, Ordering::Relaxed);
rv
}
pub struct SimpleDiff<'a> {
pub(crate) left_short: Cow<'a, str>,
pub(crate) right_short: Cow<'a, str>,
pub(crate) left_expanded: Option<Cow<'a, str>>,
pub(crate) right_expanded: Option<Cow<'a, str>>,
pub(crate) left_label: &'a str,
pub(crate) right_label: &'a str,
}
impl<'a> SimpleDiff<'a> {
pub fn from_str(
left: &'a str,
right: &'a str,
left_label: &'a str,
right_label: &'a str,
) -> SimpleDiff<'a> {
SimpleDiff {
left_short: left.into(),
right_short: right.into(),
left_expanded: None,
right_expanded: None,
left_label,
right_label,
}
}
#[doc(hidden)]
pub fn __from_macro(
left_short: Option<Cow<'a, str>>,
right_short: Option<Cow<'a, str>>,
left_expanded: Option<Cow<'a, str>>,
right_expanded: Option<Cow<'a, str>>,
left_label: &'a str,
right_label: &'a str,
) -> SimpleDiff<'a> {
SimpleDiff {
left_short: left_short.unwrap_or_else(|| "<unprintable object>".into()),
right_short: right_short.unwrap_or_else(|| "<unprintable object>".into()),
left_expanded,
right_expanded,
left_label,
right_label,
}
}
fn left(&self) -> &str {
self.left_expanded.as_deref().unwrap_or(&self.left_short)
}
fn right(&self) -> &str {
self.right_expanded.as_deref().unwrap_or(&self.right_short)
}
fn label_padding(&self) -> usize {
self.left_label
.chars()
.count()
.max(self.right_label.chars().count())
}
#[doc(hidden)]
#[track_caller]
pub fn fail_assertion(&self, hint: &dyn Display) {
let len = get_max_string_length();
let (left, left_truncated) = truncate_str(&self.left_short, len);
let (right, right_truncated) = truncate_str(&self.right_short, len);
panic!(
"assertion failed: `({} == {})`{}'\
\n {:>label_padding$}: `{:?}`{}\
\n {:>label_padding$}: `{:?}`{}\
\n\n{}\n",
self.left_label,
self.right_label,
hint,
self.left_label,
DebugStrTruncated(left, left_truncated),
if left_truncated { " (truncated)" } else { "" },
self.right_label,
DebugStrTruncated(right, right_truncated),
if right_truncated { " (truncated)" } else { "" },
&self,
label_padding = self.label_padding(),
);
}
}
fn truncate_str(s: &str, chars: usize) -> (&str, bool) {
if chars == 0 {
return (s, false);
}
s.char_indices()
.enumerate()
.find_map(|(idx, (offset, _))| {
if idx == chars {
Some((&s[..offset], true))
} else {
None
}
})
.unwrap_or((s, false))
}
struct DebugStrTruncated<'s>(&'s str, bool);
impl<'s> fmt::Debug for DebugStrTruncated<'s> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.1 {
let s = format!("{}...", self.0);
fmt::Debug::fmt(&s, f)
} else {
fmt::Debug::fmt(&self.0, f)
}
}
}
fn trailing_newline(s: &str) -> &str {
if s.ends_with("\r\n") {
"\r\n"
} else if s.ends_with("\r") {
"\r"
} else if s.ends_with("\n") {
"\n"
} else {
""
}
}
fn detect_newlines(s: &str) -> (bool, bool, bool) {
let mut last_char = None;
let mut detected_crlf = false;
let mut detected_cr = false;
let mut detected_lf = false;
for c in s.chars() {
if c == '\n' {
if last_char.take() == Some('\r') {
detected_crlf = true;
} else {
detected_lf = true;
}
}
if last_char == Some('\r') {
detected_cr = true;
}
last_char = Some(c);
}
if last_char == Some('\r') {
detected_cr = true;
}
(detected_cr, detected_crlf, detected_lf)
}
#[allow(clippy::match_like_matches_macro)]
fn newlines_matter(left: &str, right: &str) -> bool {
if trailing_newline(left) != trailing_newline(right) {
return true;
}
let (cr1, crlf1, lf1) = detect_newlines(left);
let (cr2, crlf2, lf2) = detect_newlines(right);
match (cr1 || cr2, crlf1 || crlf2, lf1 || lf2) {
(false, false, false) => false,
(true, false, false) => false,
(false, true, false) => false,
(false, false, true) => false,
_ => true,
}
}
impl<'a> fmt::Display for SimpleDiff<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let left = self.left();
let right = self.right();
let newlines_matter = newlines_matter(left, right);
if left == right {
writeln!(
f,
"{}: the two values are the same in string form.",
style("Invisible differences").bold(),
)?;
return Ok(());
}
let diff = TextDiff::configure()
.timeout(Duration::from_millis(200))
.algorithm(Algorithm::Patience)
.diff_lines(left, right);
writeln!(
f,
"{} ({}{}|{}{}):",
style("Differences").bold(),
style("-").red().dim(),
style(self.left_label).red(),
style("+").green().dim(),
style(self.right_label).green(),
)?;
for (idx, group) in diff.grouped_ops(4).into_iter().enumerate() {
if idx > 0 {
writeln!(f, "@ {}", style("~~~").dim())?;
}
for op in group {
for change in diff.iter_inline_changes(&op) {
let (marker, style) = match change.tag() {
ChangeTag::Delete => ('-', Style::new().red()),
ChangeTag::Insert => ('+', Style::new().green()),
ChangeTag::Equal => (' ', Style::new().dim()),
};
write!(f, "{}", style.apply_to(marker).dim().bold())?;
for &(emphasized, value) in change.values() {
let value = if newlines_matter {
Cow::Owned(
value
.replace("\r", "␍\r")
.replace("\n", "␊\n")
.replace("␍\r␊\n", "␍␊\r\n"),
)
} else {
Cow::Borrowed(value)
};
if emphasized {
write!(f, "{}", style.clone().underlined().bold().apply_to(value))?;
} else {
write!(f, "{}", style.apply_to(value))?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
}
}
Ok(())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __assert_eq {
(
$method:ident,
$left_label:ident,
$left:expr,
$right_label:ident,
$right:expr,
$hint_suffix:expr
) => {{
match (&($left), &($right)) {
(left_val, right_val) =>
{
#[allow(unused_mut)]
if !(*left_val == *right_val) {
use $crate::print::{PrintMode, PrintObject};
let left_label = stringify!($left_label);
let right_label = stringify!($right_label);
let mut left_val_tup1 = (&left_val,);
let mut right_val_tup1 = (&right_val,);
let mut left_val_tup2 = (&left_val,);
let mut right_val_tup2 = (&right_val,);
let left_short = left_val_tup1.print_object(PrintMode::Default);
let right_short = right_val_tup1.print_object(PrintMode::Default);
let left_expanded = left_val_tup2.print_object(PrintMode::Expanded);
let right_expanded = right_val_tup2.print_object(PrintMode::Expanded);
let diff = $crate::SimpleDiff::__from_macro(
left_short,
right_short,
left_expanded,
right_expanded,
left_label,
right_label,
);
diff.fail_assertion(&$hint_suffix);
}
}
}
}};
}
#[macro_export]
macro_rules! assert_eq {
($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({
$crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, "");
});
($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({
$crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, format_args!(": {}", format_args!($($arg)*)));
});
($left:expr, $right:expr $(,)?) => ({
$crate::assert_eq!(left: $left, right: $right);
});
($left:expr, $right:expr, $($arg:tt)*) => ({
$crate::assert_eq!(left: $left, right: $right, $($arg)*);
});
}
#[macro_export]
#[doc(hidden)]
#[deprecated(since = "1.4.0", note = "use assert_eq! instead")]
macro_rules! assert_str_eq {
($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({
$crate::assert_eq!($left_label: $left, $right_label: $right);
});
($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({
$crate::assert_eq!($left_label: $left, $right_label: $right, $($arg)*);
});
($left:expr, $right:expr $(,)?) => ({
$crate::assert_eq!($left, $right);
});
($left:expr, $right:expr, $($arg:tt)*) => ({
$crate::assert_eq!($left, $right, $($arg)*);
});
}
#[test]
fn test_newlines_matter() {
assert!(newlines_matter("\r\n", "\n"));
assert!(newlines_matter("foo\n", "foo"));
assert!(newlines_matter("foo\r\nbar", "foo\rbar"));
assert!(newlines_matter("foo\r\nbar", "foo\nbar"));
assert!(newlines_matter("foo\r\nbar\n", "foobar"));
assert!(newlines_matter("foo\nbar\r\n", "foo\nbar\r\n"));
assert!(newlines_matter("foo\nbar\n", "foo\nbar"));
assert!(!newlines_matter("foo\nbar", "foo\nbar"));
assert!(!newlines_matter("foo\nbar\n", "foo\nbar\n"));
assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar"));
assert!(!newlines_matter("foo\r\nbar\r\n", "foo\r\nbar\r\n"));
assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar"));
}
#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("foobar", 20), ("foobar", false));
assert_eq!(truncate_str("foobar", 2), ("fo", true));
assert_eq!(truncate_str("🔥🔥🔥🔥🔥", 2), ("🔥🔥", true));
}