#![forbid(unsafe_code)]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mode {
Lines,
Trim,
TrimAll,
}
pub fn normalize(s: &str, mode: Mode) -> String {
match mode {
Mode::Lines => s.lines().collect::<Vec<_>>().join("\n"),
Mode::Trim => s
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n"),
Mode::TrimAll => s.split_whitespace().collect(),
}
}
#[doc(hidden)]
#[track_caller]
pub fn assert_str_impl(
left: impl AsRef<str>,
right: impl AsRef<str>,
mode: Mode,
negated: bool,
args: Option<core::fmt::Arguments<'_>>,
) {
let left = normalize(left.as_ref(), mode);
let right = normalize(right.as_ref(), mode);
if (left == right) == negated {
let op = if negated { "!=" } else { "==" };
match args {
Some(args) => panic!(
"assertion failed: `(left {op} right)`\n left: `{left}`,\n right: `{right}`: {args}"
),
None => panic!(
"assertion failed: `(left {op} right)`\n left: `{left}`,\n right: `{right}`"
),
}
}
}
#[macro_export]
macro_rules! assert_str_eq {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::Lines, false, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::Lines,
false,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_str_ne {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::Lines, true, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::Lines,
true,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_str_trim_eq {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::Trim, false, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::Trim,
false,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_str_trim_ne {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::Trim, true, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::Trim,
true,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_str_trim_all_eq {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::TrimAll, false, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::TrimAll,
false,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_str_trim_all_ne {
($left:expr, $right:expr $(,)?) => {
$crate::assert_str_impl(&$left, &$right, $crate::Mode::TrimAll, true, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::assert_str_impl(
&$left,
&$right,
$crate::Mode::TrimAll,
true,
::core::option::Option::Some(::core::format_args!($($arg)+)),
)
};
}
#[cfg(test)]
mod tests {
#![allow(clippy::unnecessary_to_owned)]
#[test]
fn cross_str_equal_simple() {
assert_str_eq!("Line\nLine2", "Line\r\nLine2");
assert_str_eq!("Line\nLine2".to_owned(), "Line\r\nLine2");
assert_str_eq!("Line\nLine2", "Line\r\nLine2".to_owned());
assert_str_eq!("Line\nLine2".to_owned(), "Line\r\nLine2".to_owned());
assert_str_eq!("Line\nLine2", "Line\r\nLine2",);
}
#[test]
fn cross_str_equal_message() {
assert_str_eq!("Line\nLine2".to_owned(), "Line\r\nLine2", "Message");
assert_str_eq!("Line\nLine2", "Line\r\nLine2".to_owned(), "Message");
assert_str_eq!("L\nLine2".to_owned(), "L\r\nLine2".to_owned(), "Message");
assert_str_eq!("Line\nLine2", "Line\r\nLine2", "Message");
}
#[test]
fn cross_str_not_equal_simple() {
assert_str_ne!("Line\nLine2", "Line\r\nLine");
assert_str_ne!("Line\nLine2".to_owned(), "Line\r\nLine");
assert_str_ne!("Line\nLine2", "Line\r\nLine".to_owned());
assert_str_ne!("Line\nLine2", "Line\r\nLine",);
}
#[test]
fn cross_str_not_equal_message() {
assert_str_ne!("Line\nLine2".to_owned(), "Line\r\nLine", "Message");
assert_str_ne!("Line\nLine2", "Line\r\nLine".to_owned(), "Message");
assert_str_ne!("L\nLine2".to_owned(), "L\r\nLine".to_owned(), "Message");
assert_str_ne!("Line\nLine2", "Line\r\nLine", "Message");
}
#[test]
fn cross_str_trim_equal() {
let left = "String \n Line ".to_owned();
let right = "String\r\nLine".to_owned();
assert_str_trim_eq!(left, right);
assert_str_trim_eq!(&left, right);
assert_str_trim_eq!(left, &right);
assert_str_trim_eq!(&left, &right);
assert_str_trim_eq!(left, right,);
}
#[test]
fn cross_str_trim_equal_message() {
let left = "String \n Line ".to_owned();
let right = "String\r\nLine".to_owned();
assert_str_trim_eq!(&left, right, "Message");
assert_str_trim_eq!(left, &right, "Message");
assert_str_trim_eq!(left, right, "Message");
assert_str_trim_eq!(&left, &right, "Message");
}
#[test]
fn cross_str_trim_not_equal() {
let left = "String \n Line ".to_owned();
let right = "String\r\n12".to_owned();
assert_str_trim_ne!(left, right);
assert_str_trim_ne!(&left, right);
assert_str_trim_ne!(left, &right);
assert_str_trim_ne!(&left, &right);
assert_str_trim_ne!(left, right,);
}
#[test]
fn cross_str_trim_not_equal_message() {
let left = "String \n Line ".to_owned();
let right = "String\r\n12".to_owned();
assert_str_trim_ne!(left, right, "Message");
assert_str_trim_ne!(&left, right, "Message");
assert_str_trim_ne!(left, &right, "Message");
assert_str_trim_ne!(&left, &right, "Message");
}
#[test]
fn cross_str_trim_all_equal() {
let left = "String \n Line ".to_owned();
let right = "String\r\nLine".to_owned();
assert_str_trim_all_eq!(left, right);
assert_str_trim_all_eq!(&left, right);
assert_str_trim_all_eq!(left, &right);
assert_str_trim_all_eq!(&left, &right);
assert_str_trim_all_eq!(left, right,);
}
#[test]
fn cross_str_trim_all_equal_message() {
let left = "String \n Line ".to_owned();
let right = "StringLine".to_owned();
assert_str_trim_all_eq!(&left, right, "Message");
assert_str_trim_all_eq!(left, &right, "Message");
assert_str_trim_all_eq!(left, right, "Message");
assert_str_trim_all_eq!(&left, &right, "Message");
}
#[test]
fn cross_str_trim_all_not_equal() {
let left = "String \n Line ".to_owned();
let right = "Stringline".to_owned();
assert_str_trim_all_ne!(left, right);
assert_str_trim_all_ne!(&left, right);
assert_str_trim_all_ne!(left, &right);
assert_str_trim_all_ne!(&left, &right);
assert_str_trim_all_ne!(left, right,);
}
#[test]
fn cross_str_trim_all_not_equal_message() {
let left = "String \n Line ".to_owned();
let right = "String12".to_owned();
assert_str_trim_all_ne!(left, right, "Message");
assert_str_trim_all_ne!(&left, right, "Message");
assert_str_trim_all_ne!(left, &right, "Message");
assert_str_trim_all_ne!(&left, &right, "Message");
}
#[test]
fn trim_all_eq_ignores_internal_whitespace() {
assert_str_trim_all_eq!("a b", "ab");
assert_str_trim_all_eq!("a b", "ab",);
assert_str_trim_all_eq!("a b", "ab", "internal whitespace must be ignored");
}
#[test]
fn trim_all_ne_uses_trim_all_semantics() {
assert_str_trim_all_ne!("abc", "x y z");
assert_str_trim_all_ne!("abc", "x y z",);
assert_str_trim_all_ne!("abc", "x y z", "content differs after removing whitespace");
}
#[test]
#[should_panic(expected = "assertion failed: `(left == right)`")]
fn eq_panics_when_different() {
assert_str_eq!("a", "b");
}
#[test]
#[should_panic(expected = " right: `b`: custom 42")]
fn eq_panics_with_custom_message() {
assert_str_eq!("a", "b", "custom {}", 42);
}
#[test]
#[should_panic(expected = "assertion failed: `(left != right)`")]
fn ne_panics_when_equal() {
assert_str_ne!("a\nb", "a\r\nb");
}
#[test]
#[should_panic(expected = "assertion failed: `(left == right)`")]
fn trim_eq_panics_when_different() {
assert_str_trim_eq!(" a \n b ", "a\nc");
}
#[test]
#[should_panic(expected = "assertion failed: `(left != right)`")]
fn trim_all_ne_trailing_comma_panics_on_whitespace_only_difference() {
assert_str_trim_all_ne!("a b", "ab",);
}
mod normalize_fn {
use crate::{Mode, normalize};
#[test]
fn lines_treats_lf_and_crlf_as_equal() {
assert_eq!(normalize("a\r\nb", Mode::Lines), "a\nb");
assert_eq!(normalize("a\nb", Mode::Lines), "a\nb");
}
#[test]
fn lines_absorbs_a_single_trailing_newline() {
assert_eq!(normalize("a\nb\n", Mode::Lines), "a\nb");
assert_eq!(normalize("a\nb", Mode::Lines), "a\nb");
}
#[test]
fn trim_strips_line_edges_and_drops_blank_lines() {
assert_eq!(normalize(" a \n\n\t b \t", Mode::Trim), "a\nb");
}
#[test]
fn trim_all_removes_every_whitespace_run() {
assert_eq!(normalize("a b\tc\n d ", Mode::TrimAll), "abcd");
}
#[test]
fn empty_input_normalizes_to_empty() {
assert_eq!(normalize("", Mode::Lines), "");
assert_eq!(normalize("", Mode::Trim), "");
assert_eq!(normalize("", Mode::TrimAll), "");
}
}
mod behaviour_edge_cases {
use crate::{Mode, normalize};
#[test]
fn bare_carriage_return_is_not_a_line_break() {
assert_eq!(normalize("a\rb", Mode::Lines), "a\rb");
}
#[test]
fn unicode_line_separator_is_not_split_by_lines() {
assert_eq!(normalize("a\u{2028}b", Mode::Lines), "a\u{2028}b");
}
#[test]
fn byte_order_mark_is_preserved() {
assert_eq!(normalize("\u{feff}hi", Mode::TrimAll), "\u{feff}hi");
}
#[test]
fn non_breaking_space_is_whitespace() {
assert_eq!(normalize("a\u{a0}b", Mode::TrimAll), "ab");
}
#[test]
fn zero_width_space_is_preserved() {
assert_eq!(normalize("a\u{200b}b", Mode::TrimAll), "a\u{200b}b");
}
}
}