use crate::report::Styled; pub fn write_diff( writer: &mut dyn std::fmt::Write, expected: &crate::Data, actual: &crate::Data, expected_name: Option<&dyn std::fmt::Display>, actual_name: Option<&dyn std::fmt::Display>, palette: crate::report::Palette, ) -> Result<(), std::fmt::Error> { #[allow(unused_mut)] let mut rendered = false; #[cfg(feature = "diff")] if let (Some(expected), Some(actual)) = (expected.render(), actual.render()) { write_diff_inner( writer, &expected, &actual, expected_name, actual_name, palette, )?; rendered = true; } if !rendered { if let Some(expected_name) = expected_name { writeln!(writer, "{} {}:", expected_name, palette.error("(expected)"))?; } else { writeln!(writer, "{}:", palette.error("Expected"))?; } writeln!(writer, "{}", palette.error(&expected))?; if let Some(actual_name) = actual_name { writeln!(writer, "{} {}:", actual_name, palette.info("(actual)"))?; } else { writeln!(writer, "{}:", palette.info("Actual"))?; } writeln!(writer, "{}", palette.info(&actual))?; } Ok(()) } #[cfg(feature = "diff")] fn write_diff_inner( writer: &mut dyn std::fmt::Write, expected: &str, actual: &str, expected_name: Option<&dyn std::fmt::Display>, actual_name: Option<&dyn std::fmt::Display>, palette: crate::report::Palette, ) -> Result<(), std::fmt::Error> { let timeout = std::time::Duration::from_millis(500); let min_elide = 20; let context = 5; let changes = similar::TextDiff::configure() .algorithm(similar::Algorithm::Patience) .timeout(timeout) .newline_terminated(false) .diff_lines(expected, actual); writeln!(writer)?; if let Some(expected_name) = expected_name { writeln!( writer, "{}", palette.error(format_args!("{:->4} expected: {}", "", expected_name)) )?; } else { writeln!(writer, "{}", palette.error(format_args!("--- Expected")))?; } if let Some(actual_name) = actual_name { writeln!( writer, "{}", palette.info(format_args!("{:+>4} actual: {}", "", actual_name)) )?; } else { writeln!(writer, "{}", palette.info(format_args!("+++ Actual")))?; } let changes = changes .ops() .iter() .flat_map(|op| changes.iter_inline_changes(op)) .collect::>(); let tombstones = if min_elide < changes.len() { let mut tombstones = vec![true; changes.len()]; let mut counter = context; for (i, change) in changes.iter().enumerate() { match change.tag() { similar::ChangeTag::Insert | similar::ChangeTag::Delete => { counter = context; tombstones[i] = false; } similar::ChangeTag::Equal => { if counter != 0 { tombstones[i] = false; counter -= 1; } } } } let mut counter = context; for (i, change) in changes.iter().enumerate().rev() { match change.tag() { similar::ChangeTag::Insert | similar::ChangeTag::Delete => { counter = context; tombstones[i] = false; } similar::ChangeTag::Equal => { if counter != 0 { tombstones[i] = false; counter -= 1; } } } } tombstones } else { Vec::new() }; let mut elided = false; for (i, change) in changes.into_iter().enumerate() { if tombstones.get(i).copied().unwrap_or(false) { if !elided { let sign = "⋮"; write!(writer, "{:>4} ", " ",)?; write!(writer, "{:>4} ", " ",)?; writeln!(writer, "{}", palette.hint(sign))?; } elided = true; } else { elided = false; match change.tag() { similar::ChangeTag::Insert => { write_change(writer, change, "+", palette.actual, palette.info, palette)?; } similar::ChangeTag::Delete => { write_change( writer, change, "-", palette.expected, palette.error, palette, )?; } similar::ChangeTag::Equal => { write_change(writer, change, "|", palette.hint, palette.hint, palette)?; } } } } Ok(()) } #[cfg(feature = "diff")] fn write_change( writer: &mut dyn std::fmt::Write, change: similar::InlineChange, sign: &str, em_style: crate::report::Style, style: crate::report::Style, palette: crate::report::Palette, ) -> Result<(), std::fmt::Error> { if let Some(index) = change.old_index() { write!(writer, "{:>4} ", palette.hint(index + 1),)?; } else { write!(writer, "{:>4} ", " ",)?; } if let Some(index) = change.new_index() { write!(writer, "{:>4} ", palette.hint(index + 1),)?; } else { write!(writer, "{:>4} ", " ",)?; } write!(writer, "{} ", Styled::new(sign, style))?; for &(emphasized, change) in change.values() { let cur_style = if emphasized { em_style } else { style }; write!(writer, "{}", Styled::new(change, cur_style))?; } if change.missing_newline() { writeln!(writer, "{}", Styled::new("∅", em_style))?; } Ok(()) } #[cfg(test)] mod test { use super::*; #[cfg(feature = "diff")] #[test] fn diff_eq() { let expected = "Hello\nWorld\n"; let expected_name = "A"; let actual = "Hello\nWorld\n"; let actual_name = "B"; let palette = crate::report::Palette::plain(); let mut actual_diff = String::new(); write_diff_inner( &mut actual_diff, expected, actual, Some(&expected_name), Some(&actual_name), palette, ) .unwrap(); let expected_diff = " ---- expected: A ++++ actual: B 1 1 | Hello 2 2 | World "; assert_eq!(expected_diff, actual_diff); } #[cfg(feature = "diff")] #[test] fn diff_ne_line_missing() { let expected = "Hello\nWorld\n"; let expected_name = "A"; let actual = "Hello\n"; let actual_name = "B"; let palette = crate::report::Palette::plain(); let mut actual_diff = String::new(); write_diff_inner( &mut actual_diff, expected, actual, Some(&expected_name), Some(&actual_name), palette, ) .unwrap(); let expected_diff = " ---- expected: A ++++ actual: B 1 1 | Hello 2 - World "; assert_eq!(expected_diff, actual_diff); } #[cfg(feature = "diff")] #[test] fn diff_eq_trailing_extra_newline() { let expected = "Hello\nWorld"; let expected_name = "A"; let actual = "Hello\nWorld\n"; let actual_name = "B"; let palette = crate::report::Palette::plain(); let mut actual_diff = String::new(); write_diff_inner( &mut actual_diff, expected, actual, Some(&expected_name), Some(&actual_name), palette, ) .unwrap(); let expected_diff = " ---- expected: A ++++ actual: B 1 1 | Hello 2 - World∅ 2 + World "; assert_eq!(expected_diff, actual_diff); } #[cfg(feature = "diff")] #[test] fn diff_eq_trailing_newline_missing() { let expected = "Hello\nWorld\n"; let expected_name = "A"; let actual = "Hello\nWorld"; let actual_name = "B"; let palette = crate::report::Palette::plain(); let mut actual_diff = String::new(); write_diff_inner( &mut actual_diff, expected, actual, Some(&expected_name), Some(&actual_name), palette, ) .unwrap(); let expected_diff = " ---- expected: A ++++ actual: B 1 1 | Hello 2 - World 2 + World∅ "; assert_eq!(expected_diff, actual_diff); } #[cfg(feature = "diff")] #[test] fn diff_eq_elided() { let mut expected = String::new(); expected.push_str("Hello\n"); for i in 0..20 { expected.push_str(&i.to_string()); expected.push('\n'); } expected.push_str("World\n"); for i in 0..20 { expected.push_str(&i.to_string()); expected.push('\n'); } expected.push_str("!\n"); let expected_name = "A"; let mut actual = String::new(); actual.push_str("Goodbye\n"); for i in 0..20 { actual.push_str(&i.to_string()); actual.push('\n'); } actual.push_str("Moon\n"); for i in 0..20 { actual.push_str(&i.to_string()); actual.push('\n'); } actual.push_str("?\n"); let actual_name = "B"; let palette = crate::report::Palette::plain(); let mut actual_diff = String::new(); write_diff_inner( &mut actual_diff, &expected, &actual, Some(&expected_name), Some(&actual_name), palette, ) .unwrap(); let expected_diff = " ---- expected: A ++++ actual: B 1 - Hello 1 + Goodbye 2 2 | 0 3 3 | 1 4 4 | 2 5 5 | 3 6 6 | 4 ⋮ 17 17 | 15 18 18 | 16 19 19 | 17 20 20 | 18 21 21 | 19 22 - World 22 + Moon 23 23 | 0 24 24 | 1 25 25 | 2 26 26 | 3 27 27 | 4 ⋮ 38 38 | 15 39 39 | 16 40 40 | 17 41 41 | 18 42 42 | 19 43 - ! 43 + ? "; assert_eq!(expected_diff, actual_diff); } }