philiprehberger_diff_assert/
lib.rs1use std::fmt;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37enum DiffLine<'a> {
38 Same(&'a str),
40 Removed(&'a str),
42 Added(&'a str),
44}
45
46fn lcs_table<'a>(left: &[&'a str], right: &[&'a str]) -> Vec<Vec<usize>> {
51 let m = left.len();
52 let n = right.len();
53 let mut table = vec![vec![0usize; n + 1]; m + 1];
54
55 for i in 1..=m {
56 for j in 1..=n {
57 if left[i - 1] == right[j - 1] {
58 table[i][j] = table[i - 1][j - 1] + 1;
59 } else {
60 table[i][j] = std::cmp::max(table[i - 1][j], table[i][j - 1]);
61 }
62 }
63 }
64
65 table
66}
67
68fn compute_diff<'a>(left: &'a str, right: &'a str) -> Vec<DiffLine<'a>> {
70 let left_lines: Vec<&str> = left.lines().collect();
71 let right_lines: Vec<&str> = right.lines().collect();
72
73 let table = lcs_table(&left_lines, &right_lines);
74 let mut result = Vec::new();
75
76 let mut i = left_lines.len();
77 let mut j = right_lines.len();
78
79 while i > 0 || j > 0 {
81 if i > 0 && j > 0 && left_lines[i - 1] == right_lines[j - 1] {
82 result.push(DiffLine::Same(left_lines[i - 1]));
83 i -= 1;
84 j -= 1;
85 } else if j > 0 && (i == 0 || table[i][j - 1] >= table[i - 1][j]) {
86 result.push(DiffLine::Added(right_lines[j - 1]));
87 j -= 1;
88 } else {
89 result.push(DiffLine::Removed(left_lines[i - 1]));
90 i -= 1;
91 }
92 }
93
94 result.reverse();
95 result
96}
97
98fn format_diff(diff: &[DiffLine<'_>], use_color: bool) -> String {
100 let mut output = String::new();
101
102 for (idx, line) in diff.iter().enumerate() {
103 if idx > 0 {
104 output.push('\n');
105 }
106 match line {
107 DiffLine::Same(text) => {
108 output.push_str(" ");
109 output.push_str(text);
110 }
111 DiffLine::Removed(text) => {
112 if use_color {
113 output.push_str("\x1b[31m- ");
114 output.push_str(text);
115 output.push_str("\x1b[0m");
116 } else {
117 output.push_str("- ");
118 output.push_str(text);
119 }
120 }
121 DiffLine::Added(text) => {
122 if use_color {
123 output.push_str("\x1b[32m+ ");
124 output.push_str(text);
125 output.push_str("\x1b[0m");
126 } else {
127 output.push_str("+ ");
128 output.push_str(text);
129 }
130 }
131 }
132 }
133
134 output
135}
136
137fn should_use_color() -> bool {
139 std::env::var("NO_COLOR").is_err()
140}
141
142pub fn diff_strings(left: &str, right: &str) -> String {
159 let diff = compute_diff(left, right);
160 format_diff(&diff, should_use_color())
161}
162
163pub fn diff_strings_no_color(left: &str, right: &str) -> String {
176 let diff = compute_diff(left, right);
177 format_diff(&diff, false)
178}
179
180pub fn diff_debug<T: fmt::Debug>(left: &T, right: &T) -> String {
196 let left_str = format!("{:#?}", left);
197 let right_str = format!("{:#?}", right);
198 diff_strings(&left_str, &right_str)
199}
200
201#[macro_export]
226macro_rules! assert_eq_diff {
227 ($left:expr, $right:expr $(,)?) => {
228 match (&$left, &$right) {
229 (left_val, right_val) => {
230 if !(*left_val == *right_val) {
231 let left_str = format!("{:#?}", left_val);
232 let right_str = format!("{:#?}", right_val);
233 let diff = $crate::diff_strings(&left_str, &right_str);
234 panic!(
235 "assertion `left == right` failed\n\n--- left\n+++ right\n\n{}\n",
236 diff
237 );
238 }
239 }
240 }
241 };
242 ($left:expr, $right:expr, $($arg:tt)+) => {
243 match (&$left, &$right) {
244 (left_val, right_val) => {
245 if !(*left_val == *right_val) {
246 let left_str = format!("{:#?}", left_val);
247 let right_str = format!("{:#?}", right_val);
248 let diff = $crate::diff_strings(&left_str, &right_str);
249 panic!(
250 "assertion `left == right` failed: {}\n\n--- left\n+++ right\n\n{}\n",
251 format_args!($($arg)+),
252 diff
253 );
254 }
255 }
256 }
257 };
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn assert_eq_diff_passes_when_equal() {
266 assert_eq_diff!("hello", "hello");
267 }
268
269 #[test]
270 fn assert_eq_diff_passes_with_equal_structs() {
271 #[derive(Debug, PartialEq)]
272 struct Point {
273 x: i32,
274 y: i32,
275 }
276
277 let a = Point { x: 1, y: 2 };
278 let b = Point { x: 1, y: 2 };
279 assert_eq_diff!(a, b);
280 }
281
282 #[test]
283 #[should_panic(expected = "assertion `left == right` failed")]
284 fn assert_eq_diff_panics_on_inequality() {
285 assert_eq_diff!("hello", "world");
286 }
287
288 #[test]
289 #[should_panic(expected = "custom error message")]
290 fn assert_eq_diff_custom_message() {
291 assert_eq_diff!(1, 2, "custom error message: {}", 42);
292 }
293
294 #[test]
295 fn diff_strings_identical_returns_all_same() {
296 let result = diff_strings_no_color("hello\nworld", "hello\nworld");
297 assert_eq!(result, " hello\n world");
298 }
299
300 #[test]
301 fn diff_strings_with_added_lines() {
302 let result = diff_strings_no_color("a\nc", "a\nb\nc");
303 assert_eq!(result, " a\n+ b\n c");
304 }
305
306 #[test]
307 fn diff_strings_with_removed_lines() {
308 let result = diff_strings_no_color("a\nb\nc", "a\nc");
309 assert_eq!(result, " a\n- b\n c");
310 }
311
312 #[test]
313 fn diff_strings_with_mixed_changes() {
314 let result = diff_strings_no_color("a\nb\nc\nd", "a\nx\nc\ny");
315 assert_eq!(result, " a\n- b\n+ x\n c\n- d\n+ y");
316 }
317
318 #[test]
319 fn diff_strings_no_color_has_no_ansi_codes() {
320 let result = diff_strings_no_color("hello", "world");
321 assert!(!result.contains("\x1b["));
322 assert!(result.contains("- hello"));
323 assert!(result.contains("+ world"));
324 }
325
326 #[test]
327 fn diff_debug_with_structs() {
328 #[derive(Debug)]
329 struct Config {
330 name: String,
331 value: i32,
332 }
333
334 let left = Config {
335 name: "alpha".to_string(),
336 value: 10,
337 };
338 let right = Config {
339 name: "beta".to_string(),
340 value: 10,
341 };
342
343 let result = diff_debug(&left, &right);
344 assert!(!result.is_empty());
346 }
347
348 #[test]
349 fn diff_strings_empty_inputs() {
350 let result = diff_strings_no_color("", "");
351 assert_eq!(result, "");
352 }
353
354 #[test]
355 fn diff_strings_left_empty() {
356 let result = diff_strings_no_color("", "hello");
357 assert!(result.contains("+ hello"));
359 }
360}