Skip to main content

test_better_matchers/
diff.rs

1//! Line-oriented diff rendering, behind the default `diff` feature.
2//!
3//! This module produces the *structured, uncolored* diff text that lands in a
4//! [`Mismatch`](crate::Mismatch)'s `diff` field. Color is applied later, and
5//! only by `test-better-core`'s renderer: `matchers` never emits ANSI escapes.
6//!
7//! The output is a unified-style diff: each line is prefixed with ` ` for
8//! unchanged context, `-` for a line present in `expected` but not `actual`,
9//! and `+` for a line present in `actual` but not `expected`. The renderer in
10//! `core` keys its red/green coloring off exactly those markers.
11
12use similar::{ChangeTag, TextDiff};
13
14/// Renders a line-oriented diff between `expected` and `actual`.
15///
16/// The result has no trailing newline, so it composes when the renderer
17/// indents each line.
18///
19/// ```
20/// use test_better_core::TestResult;
21/// use test_better_matchers::{diff_lines, eq, check};
22///
23/// fn main() -> TestResult {
24///     let diff = diff_lines("a\nb\nc", "a\nB\nc");
25///     check!(diff).satisfies(eq(" a\n-b\n+B\n c".to_string()))?;
26///     Ok(())
27/// }
28/// ```
29#[must_use]
30pub fn diff_lines(expected: &str, actual: &str) -> String {
31    let diff = TextDiff::from_lines(expected, actual);
32    let mut out = String::new();
33    for change in diff.iter_all_changes() {
34        let marker = match change.tag() {
35            ChangeTag::Delete => '-',
36            ChangeTag::Insert => '+',
37            ChangeTag::Equal => ' ',
38        };
39        out.push(marker);
40        let value = change.value();
41        out.push_str(value);
42        // `from_lines` keeps line endings, but the final line may lack one.
43        if !value.ends_with('\n') {
44            out.push('\n');
45        }
46    }
47    // Drop the trailing newline so the diff composes cleanly when indented.
48    if out.ends_with('\n') {
49        out.pop();
50    }
51    out
52}
53
54#[cfg(test)]
55mod tests {
56    use test_better_core::TestResult;
57
58    use super::*;
59    use crate::{check, eq, is_false};
60
61    #[test]
62    fn equal_input_is_all_context_lines() -> TestResult {
63        check!(diff_lines("one\ntwo", "one\ntwo")).satisfies(eq(" one\n two".to_string()))?;
64        Ok(())
65    }
66
67    #[test]
68    fn a_changed_line_becomes_a_delete_then_an_insert() -> TestResult {
69        let diff = diff_lines("keep\nold\nkeep", "keep\nnew\nkeep");
70        check!(diff).satisfies(eq(" keep\n-old\n+new\n keep".to_string()))?;
71        Ok(())
72    }
73
74    #[test]
75    fn has_no_trailing_newline() -> TestResult {
76        let diff = diff_lines("a\n", "b\n");
77        check!(diff.ends_with('\n')).satisfies(is_false())?;
78        Ok(())
79    }
80}