Skip to content

Diffs and test reports

Module: rs_rich.ext.diff (Rust: rich_ext::diff): a diff engine, text and source diff views, git patches, and test-run reports.

The engine

diff_lines, diff_words, diff_chars and diff_sequences return (tag, (old_start, old_end), (new_start, new_end)) operations, tags being "equal", "delete", "insert". Ranges are indices into your lists or strings. group_hunks groups them into hunks with context.

from rs_rich.console import Console
from rs_rich.ext import diff

console = Console(width=60)
print(diff.diff_words("café au lait", "café noir"))
print([hunk.header for hunk in diff.group_hunks(diff.diff_lines(list("abcdefgh"), list("abcXefgh")), context=1)])
[('equal', (0, 5), (0, 5)), ('delete', (5, 12), (5, 5)), ('insert', (12, 12), (5, 9))]
['@@ -3,3 +3,3 @@']

Text diffs

TextDiff compares two texts line by line; unified() is the classic diff -u text. DiffView renders it, unified or layout="side_by_side", with word-level highlights; ansi=True compares text that contains ANSI styles and reports lines whose only change is their style.

OLD = '[server]\nhost = "0.0.0.0"\nport = 8080\nlog = "info"\n'
NEW = '[server]\nhost = "0.0.0.0"\nport = 8443\nlog = "debug"\ntls = true\n'
text_diff = diff.TextDiff(OLD, NEW, context=1)
print(text_diff.stats())
print(text_diff.unified("a/server.toml", "b/server.toml"), end="")
console.print(diff.DiffView(OLD, NEW, layout="side_by_side", titles=("before", "after"), context=1))
(3, 2)
--- a/server.toml
+++ b/server.toml
@@ -2,3 +2,4 @@
 host = "0.0.0.0"
-port = 8080
-log = "info"
+port = 8443
+log = "debug"
+tls = true
before                       │ after
@@ -2,3 +2,4 @@
2   host = "0.0.0.0"         │ 2   host = "0.0.0.0"
3 - port = 8080              │ 3 + port = 8443
4 - log = "info"             │ 4 + log = "debug"
                             │ 5 + tls = true

SourceDiff is the same for code, syntax-highlighted, with an optional link template for line numbers.

Git patches

parse_patch reads git diff output into a Patch of FilePatches; PatchView renders it with a file tree, and can carry annotations (review comments, lints) on lines.

PATCH = """\
diff --git a/src/lib.rs b/src/lib.rs
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,3 @@
 pub fn total(items: &[u32]) -> u32 {
-    let mut sum = 0;
+    let mut sum: u32 = 0;
     for item in items {
"""
patch = diff.parse_patch(PATCH)
print([(f.path, f.status) for f in patch.files], patch.stats())
console.print(diff.PatchView(
    patch,
    tree=False,
    annotations=[diff.Annotation("src/lib.rs", 2, "consider `iter().sum()`", level="note")],
))
[('src/lib.rs', 'modified')] (1, 1)
modified src/lib.rs  +1 -1
@@ -1,3 +1,3 @@
1 1   pub fn total(items: &[u32]) -> u32 {
2   -     let mut sum = 0;
  2 +     let mut sum: u32 = 0;
      note: consider `iter().sum()`
3 3       for item in items {

1 file changed, 1 insertion(+), 1 deletion(-)

KeepFiles(["*.md"]) is a transform stage that keeps matching files of a patch; TemplateLinks makes file and line links from URL templates.

Test reports

TestRun.from_libtest (cargo's JSON test output) and TestRun.from_junit parse test results; TestReport renders a run, or JUnit XML text directly, with the failures' expected and actual values diffed. Runs can also be built from TestSuite and TestCase.

run = diff.TestRun([diff.TestSuite("parser", [
    diff.TestCase("empty"),
    diff.TestCase("quoted", status="failed", message="assertion failed", expected="1\n2\n3", actual="1\n2\n4"),
])])
print(run.totals())
console.print(diff.TestReport(run))
{'errored': 0, 'failed': 1, 'passed': 1, 'skipped': 0, 'total': 2}
FAILED parser > quoted
  assertion failed
  --- expected
  +++ actual
  @@ -1,3 +1,3 @@
  1 1   1
  2 2   2
  3   - 3
    3 + 4

┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━┓
┃ Suite  ┃ Passed ┃ Failed ┃ Skipped ┃ Time ┃
┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━┩
│ parser │ 1      │ 1      │ 0       │      │
└────────┴────────┴────────┴─────────┴──────┘
1 passed, 1 failed