Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,27 @@ jobs:
run: cargo doc --no-deps --workspace --document-private-items
env:
RUSTDOCFLAGS: -D warnings

fuzz:
name: Fuzz Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@v4

- name: Install nightly toolchain
uses: dtolnay/rust-toolchain@nightly

- uses: Swatinem/rust-cache@v2

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Run fuzz tests
run: |
# Run each fuzz target for 45 seconds (total ~3 minutes for 4 targets)
for target in unified_diff_printer postprocess_heuristics comprehensive_diff diff_compute_with; do
cargo fuzz run $target --release -- -max_total_time=45
done
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ The sourcecode of the helix editor.
<img src='plots/helix_comparison.svg' width="700">
<img src='plots/helix_speedup.svg' width="700">

## Testing

`imara-diff` includes comprehensive fuzz testing using [cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.html) to ensure robustness against arbitrary inputs. Fuzz tests cover all major algorithms (Myers, Histogram, MyersMinimal), postprocessing with different heuristics, and unified diff printing. See the [fuzz/README.md](fuzz/README.md) for more details on running fuzz tests locally.

## Stability Policy

Expand Down
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
184 changes: 184 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[package]
name = "imara-diff-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }

[dependencies.imara-diff]
path = ".."
features = ["unified_diff"]

[[bin]]
name = "comprehensive_diff"
path = "fuzz_targets/comprehensive_diff.rs"
test = false
doc = false
bench = false

[[bin]]
name = "diff_compute_with"
path = "fuzz_targets/diff_compute_with.rs"
test = false
doc = false
bench = false

[[bin]]
name = "postprocess_heuristics"
path = "fuzz_targets/postprocess_heuristics.rs"
test = false
doc = false
bench = false

[[bin]]
name = "unified_diff_printer"
path = "fuzz_targets/unified_diff_printer.rs"
test = false
doc = false
bench = false
41 changes: 41 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Fuzz Testing

This directory contains fuzz tests for imara-diff using [cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.html).

## Running Fuzz Tests

### Prerequisites
- Nightly Rust toolchain: `rustup install nightly`
- cargo-fuzz: `cargo install cargo-fuzz`

### Running a specific target
```bash
# Run for a specific time (e.g., 60 seconds)
cargo +nightly fuzz run comprehensive_diff -- -max_total_time=60

# Run with a specific number of runs
cargo +nightly fuzz run comprehensive_diff -- -runs=1000000
```

### Running all targets
```bash
for target in comprehensive_diff diff_compute_with postprocess_heuristics unified_diff_printer; do
cargo +nightly fuzz run --release $target -- -max_total_time=60
done
```

### Analyzing coverage
```bash
cargo +nightly fuzz coverage comprehensive_diff
```

## CI Integration

Fuzz tests are automatically run in CI for 3 minutes total (45 seconds per target × 4 targets) to ensure no regressions in robustness.

## Adding New Fuzz Targets

1. Create a new file in `fuzz_targets/` directory
2. Add a new `[[bin]]` entry in `Cargo.toml`
3. Update the CI workflow to run the new target
4. Update this README with a description of the new target
72 changes: 72 additions & 0 deletions fuzz/fuzz_targets/comprehensive_diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#![no_main]

use imara_diff::{Algorithm, Diff, InternedInput};
use libfuzzer_sys::fuzz_target;

use libfuzzer_sys::arbitrary;

#[derive(arbitrary::Arbitrary, Debug)]
struct Input<'a> {
before: &'a [u8],
before_str: &'a str,
after: &'a [u8],
after_str: &'a str,
}

/// Tests all three diff algorithms (Myers, Histogram, MyersMinimal) with:
/// - Computing diffs on arbitrary string inputs
/// - Postprocessing with no heuristic and line heuristic
/// - Unified diff printing
/// - Basic queries (count_additions, count_removals, is_added, is_removed)
/// - Hunks iteration
fn do_fuzz(
Input {
before,
before_str,
after,
after_str,
}: Input<'_>,
) {
// Create interned input
let input = InternedInput::new(before, after);

// Test all three diff algorithms
for algorithm in [
Algorithm::Histogram,
Algorithm::Myers,
Algorithm::MyersMinimal,
] {
// Compute diff
let mut diff = Diff::compute(algorithm, &input);

// Test basic queries
let _ = diff.count_additions();
let _ = diff.count_removals();

// Test hunks iteration
for hunk in diff.hunks() {
let _ = hunk.is_pure_insertion();
let _ = hunk.is_pure_removal();
let _ = hunk.invert();
}

// Test postprocessing with no heuristic
diff.postprocess_no_heuristic(&input);

// Test postprocessing with line heuristic
diff.postprocess_lines(&input);
}

let input = InternedInput::new(before_str, after_str);
let mut word_input = InternedInput::default();
let mut word_diff = Diff::default();

let diff = Diff::compute(Algorithm::Myers, &input);
for hunk in diff.hunks() {
hunk.latin_word_diff(&input, &mut word_input, &mut word_diff);
}
}

fuzz_target!(|input: Input<'_>| {
do_fuzz(input);
});
Loading