use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
enum DiffLine<'a> {
Same(&'a str),
Removed(&'a str),
Added(&'a str),
}
fn lcs_table<'a>(left: &[&'a str], right: &[&'a str]) -> Vec<Vec<usize>> {
let m = left.len();
let n = right.len();
let mut table = vec![vec![0usize; n + 1]; m + 1];
for i in 1..=m {
for j in 1..=n {
if left[i - 1] == right[j - 1] {
table[i][j] = table[i - 1][j - 1] + 1;
} else {
table[i][j] = std::cmp::max(table[i - 1][j], table[i][j - 1]);
}
}
}
table
}
fn compute_diff<'a>(left: &'a str, right: &'a str) -> Vec<DiffLine<'a>> {
let left_lines: Vec<&str> = left.lines().collect();
let right_lines: Vec<&str> = right.lines().collect();
let table = lcs_table(&left_lines, &right_lines);
let mut result = Vec::new();
let mut i = left_lines.len();
let mut j = right_lines.len();
while i > 0 || j > 0 {
if i > 0 && j > 0 && left_lines[i - 1] == right_lines[j - 1] {
result.push(DiffLine::Same(left_lines[i - 1]));
i -= 1;
j -= 1;
} else if j > 0 && (i == 0 || table[i][j - 1] >= table[i - 1][j]) {
result.push(DiffLine::Added(right_lines[j - 1]));
j -= 1;
} else {
result.push(DiffLine::Removed(left_lines[i - 1]));
i -= 1;
}
}
result.reverse();
result
}
fn format_diff(diff: &[DiffLine<'_>], use_color: bool) -> String {
let mut output = String::new();
for (idx, line) in diff.iter().enumerate() {
if idx > 0 {
output.push('\n');
}
match line {
DiffLine::Same(text) => {
output.push_str(" ");
output.push_str(text);
}
DiffLine::Removed(text) => {
if use_color {
output.push_str("\x1b[31m- ");
output.push_str(text);
output.push_str("\x1b[0m");
} else {
output.push_str("- ");
output.push_str(text);
}
}
DiffLine::Added(text) => {
if use_color {
output.push_str("\x1b[32m+ ");
output.push_str(text);
output.push_str("\x1b[0m");
} else {
output.push_str("+ ");
output.push_str(text);
}
}
}
}
output
}
fn should_use_color() -> bool {
std::env::var("NO_COLOR").is_err()
}
pub fn diff_strings(left: &str, right: &str) -> String {
let diff = compute_diff(left, right);
format_diff(&diff, should_use_color())
}
pub fn diff_strings_no_color(left: &str, right: &str) -> String {
let diff = compute_diff(left, right);
format_diff(&diff, false)
}
pub fn diff_debug<T: fmt::Debug>(left: &T, right: &T) -> String {
let left_str = format!("{:#?}", left);
let right_str = format!("{:#?}", right);
diff_strings(&left_str, &right_str)
}
#[macro_export]
macro_rules! assert_eq_diff {
($left:expr, $right:expr $(,)?) => {
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let left_str = format!("{:#?}", left_val);
let right_str = format!("{:#?}", right_val);
let diff = $crate::diff_strings(&left_str, &right_str);
panic!(
"assertion `left == right` failed\n\n--- left\n+++ right\n\n{}\n",
diff
);
}
}
}
};
($left:expr, $right:expr, $($arg:tt)+) => {
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let left_str = format!("{:#?}", left_val);
let right_str = format!("{:#?}", right_val);
let diff = $crate::diff_strings(&left_str, &right_str);
panic!(
"assertion `left == right` failed: {}\n\n--- left\n+++ right\n\n{}\n",
format_args!($($arg)+),
diff
);
}
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assert_eq_diff_passes_when_equal() {
assert_eq_diff!("hello", "hello");
}
#[test]
fn assert_eq_diff_passes_with_equal_structs() {
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
assert_eq_diff!(a, b);
}
#[test]
#[should_panic(expected = "assertion `left == right` failed")]
fn assert_eq_diff_panics_on_inequality() {
assert_eq_diff!("hello", "world");
}
#[test]
#[should_panic(expected = "custom error message")]
fn assert_eq_diff_custom_message() {
assert_eq_diff!(1, 2, "custom error message: {}", 42);
}
#[test]
fn diff_strings_identical_returns_all_same() {
let result = diff_strings_no_color("hello\nworld", "hello\nworld");
assert_eq!(result, " hello\n world");
}
#[test]
fn diff_strings_with_added_lines() {
let result = diff_strings_no_color("a\nc", "a\nb\nc");
assert_eq!(result, " a\n+ b\n c");
}
#[test]
fn diff_strings_with_removed_lines() {
let result = diff_strings_no_color("a\nb\nc", "a\nc");
assert_eq!(result, " a\n- b\n c");
}
#[test]
fn diff_strings_with_mixed_changes() {
let result = diff_strings_no_color("a\nb\nc\nd", "a\nx\nc\ny");
assert_eq!(result, " a\n- b\n+ x\n c\n- d\n+ y");
}
#[test]
fn diff_strings_no_color_has_no_ansi_codes() {
let result = diff_strings_no_color("hello", "world");
assert!(!result.contains("\x1b["));
assert!(result.contains("- hello"));
assert!(result.contains("+ world"));
}
#[test]
fn diff_debug_with_structs() {
#[derive(Debug)]
struct Config {
name: String,
value: i32,
}
let left = Config {
name: "alpha".to_string(),
value: 10,
};
let right = Config {
name: "beta".to_string(),
value: 10,
};
let result = diff_debug(&left, &right);
assert!(!result.is_empty());
}
#[test]
fn diff_strings_empty_inputs() {
let result = diff_strings_no_color("", "");
assert_eq!(result, "");
}
#[test]
fn diff_strings_left_empty() {
let result = diff_strings_no_color("", "hello");
assert!(result.contains("+ hello"));
}
}