Rift Star on GitHubv0.3.0

Rift / Design

Correctness

A diff tool that is fast and wrong is worse than none. Each guarantee below has a check in the repository that fails when the guarantee breaks.

DESIGN.md lists the guarantees under "Guarantees" and what is left out under "Non-guarantees".

What is guaranteed

  1. The output is a correct edit script. Applied to A, it yields exactly B, approximate regions included.
  2. Equal means compared. A segment or a line is reported equal only after Rift compared its bytes. A matching 64-bit hash is never enough.
  3. Approximate regions are marked in the unified diff (approx and the reason after the hunk header), in --stats, and in the terminal view.
  4. Peak heap stays under --mem.
  5. Exit codes follow diff: 0 for identical files, 1 for different ones, 2 for an error.

And what is not: the diff is not minimal, it can differ between two values of --mem, and moved blocks are listed only when one of their lines was sampled.

Which check covers which guarantee

Guarantees and the checks that cover them A grid with five guarantees as rows and five kinds of check as columns: property tests, integration tests, fuzzing, the CI memory job and Miri. A correct edit script is checked by property tests, integration tests and fuzzing. Equal means compared is checked by property tests and integration tests. Approximate regions marked is checked by integration tests. Peak heap under --mem is checked by integration tests, fuzzing and the CI memory job. The unsafe code is checked by Miri. propertyintegrationfuzzingCI memoryMiri edit script yields Bequal means comparedapprox is markedpeak heap under --memunsafe code is sound
A filled dot means a check in that column fails when the guarantee in that row breaks. Exit codes have an integration test of their own, exit_codes_follow_diff.

Property tests

proptest drives the refiner with 10,000 cases per property. script_applied_to_a_yields_b and script_for_mutated_input_yields_b replay the script against A and require B, the second with random edits on a base text and a cost cutoff from 0 to 3, so the approximate path runs too. myers_and_dp_match_imara_minimal turns patience off and requires an edit count equal to the longest common subsequence and no larger than the imara-diff crate's. colliding_hashes_on_random_input hashes each line to one of three values on purpose, so almost every hash match is false, and the script must still be exact: the byte check after Myers is what saves it.

Smaller suites cover the rest of the pipeline. random_pairs_replay_under_any_budget runs whole diffs at 16, 24 and 64 MiB; the_large_segment_path_replays_random_pairs sends random pairs through resampling with job limits from 2 to 64 KiB; patch_applies_our_diff_to_a_and_gets_b writes the unified diff and runs patch and gpatch, when present, on it; random_scripts_validate_and_apply_to_b checks the validator against the applier.

Sources: crates/rift-core/src/refine/tests.rs, crates/rift-core/src/diff/tests.rs, crates/rift-core/src/unified/tests.rs, crates/rift-core/src/script/mod.rs.

Integration tests on real files

The rift binary is tested as you run it. generated_pairs_patch_from_a_to_b diffs generated SQL, CSV, NDJSON and text pairs, with long lines, CRLF endings, a missing final newline, duplicated and low-entropy runs and moves, at --mem 16M and 256M, and applies each diff with patch. pathological_inputs covers empty files, files of blank lines and a missing final newline. a_single_1gib_line, run by hand in release mode, diffs two files of one 1 GiB line each, checks the heap stays under 256 MiB, and patches A into B with GNU patch.

differential_against_git_patience compares Rift with git diff --no-index --patience on 40 generated 64 KiB pairs. Both must patch A into B, and Rift may change at most 10% more lines than git. In a run on 2026-11-07 on an Apple M1 (cargo test --release -p rift-diff --test full differential_against_git_patience -- --nocapture), the two changed the same number of lines: 1,241 each.

Equality is tested where hashes lie. a_false_anchor_is_demoted_and_never_equal uses a source whose bytes change between the scan and the comparison, which looks to the skeleton like a hash collision, and requires the line to land in a differing segment. approximate_regions_are_marked_and_still_patch requires the approx mark and a working patch on a region with no unique line.

Some tests compare whole outputs byte for byte. no_moves_leaves_the_unified_diff_unchanged requires the same unified diff with and without --no-moves; in key mode, reordered_rows_are_identical requires that a file whose rows were only shuffled reports no change.

Sources: crates/rift-cli/tests/full.rs, crates/rift-cli/tests/moves.rs, crates/rift-cli/tests/keyed.rs; crates/rift-core/tests/skeleton.rs.

Fuzzing: 11 targets, every night

The fuzz crate under fuzz/ has 11 cargo-fuzz targets. Each one checks a property beyond "does not crash":

TargetProperty
scanline count, checkpoints and samples match a plain split, for any chunk size and thread count
keyed_csv, keyed_ndjson, keyed_copya small and a large budget report the same changes; a file against itself reports nothing; peak heap within the small budget
keyed_reader1, 4 and 16 chunks read the same records
pipelinethe unified diff applied to A gives B; the verdict matches A == B; peak heap within --mem
unifiedthe writer's output applied to A gives B
tui_renderno control, bidirectional or zero-width character reaches the terminal
sanitizethe line sanitizer hides nothing and keeps to its width
mcp_request, mcp_toolsno panic or hang; answers keep their size bounds

The Fuzz workflow runs all 11 every night at 04:23 UTC, 600 seconds per target by default, one job per target, with a 2 GiB RSS limit. It keeps each target's corpus between runs and uploads crashing inputs. Inputs that once failed are kept in fuzz/regressions/, and a replay test runs them through the same checks on any OS. fuzz/run-linux.sh runs the same targets on a Linux machine for longer, each pinned to its own cores.

Sources: fuzz/README.md; fuzz/fuzz_targets/; .github/workflows/fuzz.yml.

Miri

The only unsafe code in rift-core is the counting allocator, and the only other unsafe in the binary is two kernel read-ahead hints; the other crates forbid it. Miri runs the rift-core library tests every night at 03:41 UTC: most of them under stacked borrows, and the scan, skeleton, moves and key-mode tests under tree borrows, with 8 cases per property test. A failing nightly run opens an issue, or comments on the open one. On a pull request, or a push to any branch but main, the CI job unsafe-diff looks for unsafe in the Rust lines the change adds, and when it finds one the same Miri jobs run as part of CI.

Sources: .github/workflows/miri.yml, .github/workflows/miri-run.yml, .github/workflows/ci.yml (jobs unsafe-diff and miri); crates/rift-core/src/alloc.rs; crates/rift-cli/src/source.rs, hints.

Memory, to the byte

Two kinds of test hold the heap to --mem. The heap tests in rift-core/tests (scan_heap.rs, refine_heap.rs, moves_heap.rs, unified_heap.rs and others) read the counting allocator's peak around one operation and hold it to a bound: the reservation or estimate that should cover it, or a few KiB when a tiny pair runs under a 1 TiB budget. Each sits alone in its test binary, since the counter is process-wide.

The CI memory job runs on macOS and Linux. bench/ci-mem.sh generates a 1 GiB pair, runs rift --summary, the full diff and key mode at --mem 256M, takes the exact byte count from the peak heap line, and fails when it is larger than 268,435,456. It prints the peak resident set next to it.

Sources: bench/ci-mem.sh; .github/workflows/ci.yml, job memory; crates/rift-core/tests/.