19 Commits

Author SHA1 Message Date
oech3 bdf449eaf2 Publish binary from main (#163) 2026-03-07 22:04:19 +01:00
oech3 f8248801a9 fuzzing.yml: Avoid non reusable cache generation (#170) 2026-03-07 18:39:56 +01:00
Aster Boese 357c99038f cmp: fix 32-bit usize overflow in test (#173)
Fixes https://github.com/uutils/diffutils/issues/172
2026-03-07 18:35:57 +01:00
codspeed-hq[bot] 59e130aa22 Add CodSpeed performance benchmarking workflow and badge (#189)
Co-authored-by: codspeed-hq[bot] <117304815+codspeed-hq[bot]@users.noreply.github.com>
2026-03-07 15:12:18 +01:00
Gunter Schmidt 54c8b7aeb9 feat: Divan Benchmark (#185)
* feat: Criterion Benchmark

* fix: Replaced Criterion with codspeed drop-in replacement

* feat: uses Divan instead of Criterion

* changed file num lines to file size in kb

---------

Co-authored-by: Gunter Schmidt <gsgit@beadsoft.de>
2026-03-07 14:55:10 +01:00
Ryuji Yasukochi 6f082c6572 fix: rename "Unknown option" to "unrecognized option" for diff and cmp (#179) 2026-02-28 13:43:56 +01:00
renovate[bot] 34db0ade7c chore(deps): update rust crate tempfile to v3.26.0 2026-02-24 10:29:37 +01:00
renovate[bot] d3d0b0c966 chore(deps): update rust crate chrono to v0.4.44 2026-02-23 14:00:09 +01:00
renovate[bot] 87e0aa2828 chore(deps): update rust crate predicates to v3.1.4 2026-02-12 06:27:45 +01:00
renovate[bot] 9f419c31ea chore(deps): update rust crate libfuzzer-sys to v0.4.12 2026-02-10 23:23:31 +01:00
renovate[bot] 95883b462b chore(deps): update rust crate tempfile to v3.25.0 2026-02-10 06:08:40 +01:00
Daniel Hofstetter f20af97a09 Merge pull request #166 from uutils/renovate/regex-1.x-lockfile
chore(deps): update rust crate regex to v1.12.3
2026-02-03 17:06:50 +01:00
renovate[bot] b9b7ea8d2b chore(deps): update rust crate regex to v1.12.3 2026-02-03 14:47:31 +00:00
Sylvestre Ledru 47798b4b2c Merge pull request #164 from oech3/patch-2
Use preinstalled rust, disable incremental build
2026-01-25 17:22:11 +01:00
oech3 445e1ea02f Use preinstalled rust, disable incremental build 2026-01-25 11:58:26 +09:00
renovate[bot] e2fb192d52 chore(deps): update rust crate chrono to v0.4.43 2026-01-15 06:13:15 +01:00
renovate[bot] a1d18a0c09 chore(deps): update rust crate assert_cmd to v2.1.2 2026-01-09 19:40:57 +01:00
Sylvestre Ledru 5dd2e9d30c cmp: stop allocating for byte printing (#153)
This makes verbose comparison of 37MB completely different files 2.34x
faster than our own baseline, putting our cmp at almost 6x faster than
GNU cmp (/opt/homebrew/bin/cmp) on my M4 Pro Mac. The output remains
identical to that of GNU cmp. Mostly equal and smaller files do not
regress.

Benchmark 1: ./bin/baseline/diffutils cmp -lb t/huge t/eguh
  Time (mean ± σ):      1.669 s ±  0.011 s    [User: 1.594 s, System: 0.073 s]
  Range (min … max):    1.654 s …  1.689 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: ./target/release/diffutils cmp -lb t/huge t/eguh
  Time (mean ± σ):     714.2 ms ±   4.1 ms    [User: 629.3 ms, System: 82.7 ms]
  Range (min … max):   707.2 ms … 721.5 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 3: /opt/homebrew/bin/cmp -lb t/huge t/eguh
  Time (mean ± σ):      4.213 s ±  0.050 s    [User: 4.128 s, System: 0.081 s]
  Range (min … max):    4.160 s …  4.316 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 4: /usr/bin/cmp -lb t/huge t/eguh
  Time (mean ± σ):      3.892 s ±  0.048 s    [User: 3.819 s, System: 0.070 s]
  Range (min … max):    3.808 s …  3.976 s    10 runs

  Warning: Ignoring non-zero exit code.

Summary
  ./target/release/diffutils cmp -lb t/huge t/eguh ran
    2.34 ± 0.02 times faster than ./bin/baseline/diffutils cmp -lb t/huge t/eguh
    5.45 ± 0.07 times faster than /usr/bin/cmp -lb t/huge t/eguh
    5.90 ± 0.08 times faster than /opt/homebrew/bin/cmp -lb t/huge t/eguh
2026-01-08 23:33:42 +01:00
Gustavo Noronha Silva e00ff6b108 cmp: stop allocating for byte printing
This makes verbose comparison of 37MB completely different files 2.34x
faster than our own baseline, putting our cmp at almost 6x faster than
GNU cmp (/opt/homebrew/bin/cmp) on my M4 Pro Mac. The output remains
identical to that of GNU cmp. Mostly equal and smaller files do not
regress.

Benchmark 1: ./bin/baseline/diffutils cmp -lb t/huge t/eguh
  Time (mean ± σ):      1.669 s ±  0.011 s    [User: 1.594 s, System: 0.073 s]
  Range (min … max):    1.654 s …  1.689 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: ./target/release/diffutils cmp -lb t/huge t/eguh
  Time (mean ± σ):     714.2 ms ±   4.1 ms    [User: 629.3 ms, System: 82.7 ms]
  Range (min … max):   707.2 ms … 721.5 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 3: /opt/homebrew/bin/cmp -lb t/huge t/eguh
  Time (mean ± σ):      4.213 s ±  0.050 s    [User: 4.128 s, System: 0.081 s]
  Range (min … max):    4.160 s …  4.316 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 4: /usr/bin/cmp -lb t/huge t/eguh
  Time (mean ± σ):      3.892 s ±  0.048 s    [User: 3.819 s, System: 0.070 s]
  Range (min … max):    3.808 s …  3.976 s    10 runs

  Warning: Ignoring non-zero exit code.

Summary
  ./target/release/diffutils cmp -lb t/huge t/eguh ran
    2.34 ± 0.02 times faster than ./bin/baseline/diffutils cmp -lb t/huge t/eguh
    5.45 ± 0.07 times faster than /usr/bin/cmp -lb t/huge t/eguh
    5.90 ± 0.08 times faster than /opt/homebrew/bin/cmp -lb t/huge t/eguh
2026-01-02 11:21:48 -03:00
11 changed files with 1245 additions and 124 deletions
+25 -18
View File
@@ -4,6 +4,7 @@ name: Basic CI
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
check:
@@ -15,7 +16,6 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
test:
@@ -27,7 +27,6 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: install GNU patch on MacOS
if: runner.os == 'macOS'
run: |
@@ -44,8 +43,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
clippy:
@@ -57,17 +54,18 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- run: cargo clippy -- -D warnings
gnu-testsuite:
permissions:
contents: write # Publish diffutils instead of discarding
name: GNU test suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release
- run: |
cargo build --config=profile.release.strip=true --profile=release #-fast
zstd -19 target/release/diffutils -o diffutils-x86_64-unknown-linux-gnu.zst
# do not fail, the report is merely informative (at least until all tests pass reliably)
- run: ./tests/run-upstream-testsuite.sh release || true
env:
@@ -77,6 +75,17 @@ jobs:
name: test-results.json
path: tests/test-results.json
- run: ./tests/print-test-results.sh tests/test-results.json
- name: Publish latest commit
uses: softprops/action-gh-release@v2
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
tag_name: latest-commit
draft: false
prerelease: true
files: |
diffutils-x86_64-unknown-linux-gnu.zst
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
coverage:
name: Code Coverage
@@ -91,26 +100,19 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Initialize workflow variables
env:
# Use -Z
RUSTC_BOOTSTRAP: 1
id: vars
shell: bash
run: |
## VARs setup
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
# toolchain
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
# * use requested TOOLCHAIN if specified
if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi
outputs TOOLCHAIN
# target-specific options
# * CODECOV_FLAGS
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS
- name: rust toolchain ~ install
uses: dtolnay/rust-toolchain@nightly
- run: rustup component add llvm-tools-preview
- name: install GNU patch on MacOS
if: runner.os == 'macOS'
run: |
@@ -128,7 +130,12 @@ jobs:
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw"
# Use -Z
RUSTC_BOOTSTRAP: 1
- name: "`grcov` ~ install"
env:
# Use -Z
RUSTC_BOOTSTRAP: 1
id: build_grcov
shell: bash
run: |
+37
View File
@@ -0,0 +1,37 @@
name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@v0
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Build the benchmark target(s)
run: cargo codspeed build -m simulation
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: cargo codspeed run
+10 -6
View File
@@ -21,15 +21,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
run: |
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
echo "CARGO_INCREMENTAL=0" >> "${GITHUB_ENV}"
cargo install cargo-fuzz --locked
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target"
- name: Run `cargo-fuzz build`
run: cargo +nightly fuzz build
run: cargo fuzz build
fuzz-run:
needs: fuzz-build
@@ -49,9 +51,11 @@ jobs:
- { name: fuzz_side, should_pass: true }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
run: |
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
echo "CARGO_INCREMENTAL=0" >> "${GITHUB_ENV}"
cargo install cargo-fuzz --locked
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
@@ -66,7 +70,7 @@ jobs:
shell: bash
continue-on-error: ${{ !matrix.test-target.name.should_pass }}
run: |
cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
cargo fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Save Corpus Cache
uses: actions/cache/save@v5
with:
Generated
+682 -56
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -23,10 +23,12 @@ same-file = "1.0.6"
unicode-width = "0.2.0"
[dev-dependencies]
pretty_assertions = "1.4.0"
assert_cmd = "2.0.14"
divan = { version = "4.3.0", package = "codspeed-divan-compat" }
pretty_assertions = "1.4.0"
predicates = "3.1.0"
tempfile = "3.10.1"
rand = "0.10.0"
tempfile = "3.26.0"
[profile.release]
lto = "thin"
@@ -40,3 +42,15 @@ panic = "abort"
[profile.dist]
inherits = "release"
lto = "thin"
[[bench]]
name = "bench_diffutils"
path = "benches/bench-diffutils.rs"
harness = false
[features]
# default = ["feat_bench_not_diff"]
# Turn bench for diffutils cmp off
feat_bench_not_cmp = []
# Turn bench for diffutils diff off
feat_bench_not_diff = []
+1
View File
@@ -2,6 +2,7 @@
[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE)
[![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/uutils/diffutils?utm_source=badge)
[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils)
+377
View File
@@ -0,0 +1,377 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
//! Benches for all utils in diffutils.
//!
//! There is a file generator included to create files of different sizes for comparison. \
//! Set the TEMP_DIR const to keep the files. df_to_ files have small changes in them, search for '#'. \
//! File generation up to 1 GB is really fast, Benchmarking above 100 MB takes very long.
/// Generate test files with these sizes in KB.
const FILE_SIZE_KILO_BYTES: [u64; 4] = [100, 1 * MB, 10 * MB, 25 * MB];
// const FILE_SIZE_KILO_BYTES: [u64; 3] = [100, 1 * MB, 5 * MB];
// Empty String to use TempDir (files will be removed after test) or specify dir to keep generated files
const TEMP_DIR: &str = "";
const NUM_DIFF: u64 = 4;
// just for FILE_SIZE_KILO_BYTES
const MB: u64 = 1_000;
const CHANGE_CHAR: u8 = b'#';
#[cfg(not(feature = "feat_bench_not_cmp"))]
mod diffutils_cmp {
use std::hint::black_box;
use diffutilslib::cmp;
use divan::Bencher;
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmp_compare_files_equal(bencher: Bencher, kb: u64) {
let (from, to) = get_context().get_test_files_equal(kb);
let cmd = format!("cmp {from} {to}");
let opts = str_to_options(&cmd).into_iter().peekable();
let params = cmp::parse_params(opts).unwrap();
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| params.clone())
.bench_refs(|params| black_box(cmp::cmp(&params).unwrap()));
}
// bench the actual compare; cmp exits on first difference
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmp_compare_files_different(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_different(bytes);
let cmd = format!("cmp {from} {to} -s");
let opts = str_to_options(&cmd).into_iter().peekable();
let params = cmp::parse_params(opts).unwrap();
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| params.clone())
.bench_refs(|params| black_box(cmp::cmp(&params).unwrap()));
}
// bench original GNU cmp
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_cmp_gnu_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("{from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("cmp", cmd_args));
}
// bench the compiled release version
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_cmp_release_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("cmp {from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
}
}
#[cfg(not(feature = "feat_bench_not_diff"))]
mod diffutils_diff {
// use std::hint::black_box;
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
// use diffutilslib::params;
use divan::Bencher;
// bench the actual compare
// TODO diff does not have a diff function
// #[divan::bench(args = [100_000,10_000])]
// fn diff_compare_files(bencher: Bencher, bytes: u64) {
// let (from, to) = gen_testfiles(lines, 0, "id");
// let cmd = format!("cmp {from} {to}");
// let opts = str_to_options(&cmd).into_iter().peekable();
// let params = params::parse_params(opts).unwrap();
//
// bencher
// // .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
// .with_inputs(|| params.clone())
// .bench_refs(|params| diff::diff(&params).unwrap());
// }
// bench original GNU diff
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_diff_gnu_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("{from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("diff", cmd_args));
}
// bench the compiled release version
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_diff_release_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("diff {from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
}
}
mod parser {
use std::hint::black_box;
use diffutilslib::{cmp, params};
use divan::Bencher;
use crate::prepare::str_to_options;
// bench the time it takes to parse the command line arguments
#[divan::bench]
fn cmp_parser(bencher: Bencher) {
let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
let args = str_to_options(&cmd).into_iter().peekable();
bencher
.with_inputs(|| args.clone())
.bench_values(|data| black_box(cmp::parse_params(data)));
}
// // test the impact on the benchmark if not converting the cmd to Vec<OsString> (doubles for parse)
// #[divan::bench]
// fn cmp_parser_no_prepare() {
// let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
// let args = str_to_options(&cmd).into_iter().peekable();
// let _ = cmp::parse_params(args);
// }
// bench the time it takes to parse the command line arguments
#[divan::bench]
fn diff_parser(bencher: Bencher) {
let cmd = "diff file_1.txt file_2.txt -s --brief --expand-tabs --width=100";
let args = str_to_options(&cmd).into_iter().peekable();
bencher
.with_inputs(|| args.clone())
.bench_values(|data| black_box(params::parse_params(data)));
}
}
mod prepare {
use std::{
ffi::OsString,
fs::{self, File},
io::{BufWriter, Write},
path::Path,
sync::OnceLock,
};
use rand::RngExt;
use tempfile::TempDir;
use crate::{CHANGE_CHAR, FILE_SIZE_KILO_BYTES, NUM_DIFF, TEMP_DIR};
// file lines and .txt will be added
const FROM_FILE: &str = "from_file";
const TO_FILE: &str = "to_file";
const LINE_LENGTH: usize = 60;
/// Contains test data (file names) which only needs to be created once.
#[derive(Debug, Default)]
pub struct BenchContext {
pub tmp_dir: Option<TempDir>,
pub dir: String,
pub files_equal: Vec<(String, String)>,
pub files_different: Vec<(String, String)>,
}
impl BenchContext {
pub fn get_path(&self) -> &Path {
match &self.tmp_dir {
Some(tmp) => tmp.path(),
None => Path::new(&self.dir),
}
}
pub fn get_test_files_equal(&self, kb: u64) -> &(String, String) {
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
&self.files_equal[p]
}
#[allow(unused)]
pub fn get_test_files_different(&self, kb: u64) -> &(String, String) {
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
&self.files_different[p]
}
}
// Since each bench function is separate in Divan it is more difficult to dynamically create test data.
// This keeps the TempDir alive until the program exits and generates the files only once.
static SHARED_CONTEXT: OnceLock<BenchContext> = OnceLock::new();
/// Creates the test files once and provides them to all tests.
pub fn get_context() -> &'static BenchContext {
SHARED_CONTEXT.get_or_init(|| {
let mut ctx = BenchContext::default();
if TEMP_DIR.is_empty() {
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
ctx.tmp_dir = Some(tmp_dir);
} else {
// uses current directory, the generated files are kept
let path = Path::new(TEMP_DIR);
if !path.exists() {
fs::create_dir_all(path).expect("Path {path} could not be created");
}
ctx.dir = TEMP_DIR.to_string();
};
// generate test bytes
for kb in FILE_SIZE_KILO_BYTES {
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, 0, "eq")
.expect("generate_test_files failed");
ctx.files_equal.push(f);
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, NUM_DIFF, "df")
.expect("generate_test_files failed");
ctx.files_different.push(f);
}
ctx
})
}
pub fn str_to_options(opt: &str) -> Vec<OsString> {
let s: Vec<OsString> = opt
.split(" ")
.into_iter()
.filter(|s| !s.is_empty())
.map(|s| OsString::from(s))
.collect();
s
}
/// Generates two test files for comparison with <bytes> size.
///
/// Each line consists of 10 words with 5 letters, giving a line length of 60 bytes.
/// If num_differences is set, '#' will be inserted between the first two words of a line,
/// evenly spaced in the file. 1 will add the change in the last line, so the comparison takes longest.
fn generate_test_files_bytes(
dir: &Path,
bytes: u64,
num_differences: u64,
id: &str,
) -> std::io::Result<(String, String)> {
let id = if id.is_empty() {
"".to_string()
} else {
format!("{id}_")
};
let f1 = format!("{id}{FROM_FILE}_{bytes}.txt");
let f2 = format!("{id}{TO_FILE}_{bytes}.txt");
let from_path = dir.join(f1);
let to_path = dir.join(f2);
generate_file_bytes(&from_path, &to_path, bytes, num_differences)?;
Ok((
from_path.to_string_lossy().to_string(),
to_path.to_string_lossy().to_string(),
))
}
fn generate_file_bytes(
from_name: &Path,
to_name: &Path,
bytes: u64,
num_differences: u64,
) -> std::io::Result<()> {
let file_from = File::create(from_name)?;
let file_to = File::create(to_name)?;
// for int division, lines will be smaller than requested bytes
let n_lines = bytes / LINE_LENGTH as u64;
let change_every_n_lines = if num_differences == 0 {
0
} else {
let c = n_lines / num_differences;
if c == 0 {
1
} else {
c
}
};
// Use a larger 128KB buffer for massive files
let mut writer_from = BufWriter::with_capacity(128 * 1024, file_from);
let mut writer_to = BufWriter::with_capacity(128 * 1024, file_to);
let mut rng = rand::rng();
// Each line: (5 chars * 10 words) + 9 spaces + 1 newline = 60 bytes
let mut line_buffer = [b' '; 60];
line_buffer[59] = b'\n'; // Set the newline once at the end
for i in (0..n_lines).rev() {
// Fill only the letter positions, skipping spaces and the newline
for word_idx in 0..10 {
let start = word_idx * 6; // Each word + space block is 6 bytes
for i in 0..5 {
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
}
}
// Write the raw bytes directly to both files
writer_from.write_all(&line_buffer)?;
// make changes in the file
if num_differences == 0 {
writer_to.write_all(&line_buffer)?;
} else {
if i % change_every_n_lines == 0 && n_lines - i > 2 {
line_buffer[5] = CHANGE_CHAR;
}
writer_to.write_all(&line_buffer)?;
line_buffer[5] = b' ';
}
}
// create last line
let missing = (bytes - n_lines as u64 * LINE_LENGTH as u64) as usize;
if missing > 0 {
for word_idx in 0..10 {
let start = word_idx * 6; // Each word + space block is 6 bytes
for i in 0..5 {
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
}
}
line_buffer[missing - 1] = b'\n';
writer_from.write_all(&line_buffer[0..missing])?;
writer_to.write_all(&line_buffer[0..missing])?;
}
writer_from.flush()?;
writer_to.flush()?;
Ok(())
}
}
mod binary {
use std::process::Command;
use crate::prepare::str_to_options;
pub fn bench_binary(program: &str, cmd_args: &str) -> std::process::ExitStatus {
let args = str_to_options(cmd_args);
Command::new(program)
.args(args)
.status()
.expect("Failed to execute binary")
}
}
fn main() {
// Run registered benchmarks.
divan::main();
}
+29 -2
View File
@@ -69,6 +69,26 @@ dependencies = [
"windows-link",
]
[[package]]
name = "const_format"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -86,6 +106,7 @@ name = "diffutils"
version = "0.5.0"
dependencies = [
"chrono",
"const_format",
"diff",
"itoa",
"regex",
@@ -169,9 +190,9 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
@@ -301,6 +322,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unified-diff-fuzz"
version = "0.0.0"
+64 -38
View File
@@ -217,7 +217,7 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
std::process::exit(0);
}
if param_str.starts_with('-') {
return Err(format!("Unknown option: {param:?}"));
return Err(format!("unrecognized option: {param:?}"));
}
if from.is_none() {
from = Some(param);
@@ -500,12 +500,6 @@ pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
}
}
#[inline]
fn is_ascii_printable(byte: u8) -> bool {
let c = byte as char;
c.is_ascii() && !c.is_ascii_control()
}
#[inline]
fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str {
*buf = [b' ', b' ', b'0'];
@@ -525,32 +519,68 @@ fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str {
}
#[inline]
fn format_byte(byte: u8) -> String {
let mut byte = byte;
let mut quoted = vec![];
if !is_ascii_printable(byte) {
if byte >= 128 {
quoted.push(b'M');
quoted.push(b'-');
byte -= 128;
fn write_visible_byte(output: &mut Vec<u8>, byte: u8) -> usize {
match byte {
// Control characters: ^@, ^A, ..., ^_
0..=31 => {
output.push(b'^');
output.push(byte + 64);
2
}
if byte < 32 {
quoted.push(b'^');
byte += 64;
} else if byte == 127 {
quoted.push(b'^');
byte = b'?';
// Printable ASCII (space through ~)
32..=126 => {
output.push(byte);
1
}
// DEL: ^?
127 => {
output.extend_from_slice(b"^?");
2
}
// High bytes with control equivalents: M-^@, M-^A, ..., M-^_
128..=159 => {
output.push(b'M');
output.push(b'-');
output.push(b'^');
output.push(byte - 64);
4
}
// High bytes: M-<space>, M-!, ..., M-~
160..=254 => {
output.push(b'M');
output.push(b'-');
output.push(byte - 128);
3
}
// Byte 255: M-^?
255 => {
output.extend_from_slice(b"M-^?");
4
}
assert!((byte as char).is_ascii());
}
}
quoted.push(byte);
/// Writes a byte in visible form with right-padding to 4 spaces.
#[inline]
fn write_visible_byte_padded(output: &mut Vec<u8>, byte: u8) {
const SPACES: &[u8] = b" ";
const WIDTH: usize = SPACES.len();
// SAFETY: the checks and shifts we do above match what cat and GNU
let display_width = write_visible_byte(output, byte);
// Add right-padding spaces
let padding = WIDTH.saturating_sub(display_width);
output.extend_from_slice(&SPACES[..padding]);
}
/// Formats a byte as a visible string (for non-performance-critical path)
#[inline]
fn format_visible_byte(byte: u8) -> String {
let mut result = Vec::with_capacity(4);
write_visible_byte(&mut result, byte);
// SAFETY: the checks and shifts in write_visible_byte match what cat and GNU
// cmp do to ensure characters fall inside the ascii range.
unsafe { String::from_utf8_unchecked(quoted) }
unsafe { String::from_utf8_unchecked(result) }
}
// This function has been optimized to not use the Rust fmt system, which
@@ -588,14 +618,7 @@ fn format_verbose_difference(
output.push(b' ');
let from_byte_str = format_byte(from_byte);
let from_byte_padding = 4 - from_byte_str.len();
output.extend_from_slice(from_byte_str.as_bytes());
for _ in 0..from_byte_padding {
output.push(b' ')
}
write_visible_byte_padded(output, from_byte);
output.push(b' ');
@@ -603,7 +626,7 @@ fn format_verbose_difference(
output.push(b' ');
output.extend_from_slice(format_byte(to_byte).as_bytes());
write_visible_byte(output, to_byte);
output.push(b'\n');
} else {
@@ -706,9 +729,9 @@ fn report_difference(from_byte: u8, to_byte: u8, at_byte: usize, at_line: usize,
print!(
" is {:>3o} {:char_width$} {:>3o} {:char_width$}",
from_byte,
format_byte(from_byte),
format_visible_byte(from_byte),
to_byte,
format_byte(to_byte)
format_visible_byte(to_byte)
);
}
println!();
@@ -1062,6 +1085,9 @@ mod tests {
from: os("foo"),
to: os("bar"),
skip_a: Some(1_000_000_000),
#[cfg(target_pointer_width = "32")]
skip_b: Some((2_147_483_647.5 * 2.0) as usize),
#[cfg(target_pointer_width = "64")]
skip_b: Some(1_152_921_504_606_846_976 * 2),
..Default::default()
}),
+1 -1
View File
@@ -195,7 +195,7 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
Err(error) => return Err(error),
}
if param.to_string_lossy().starts_with('-') {
return Err(format!("Unknown option: {param:?}"));
return Err(format!("unrecognized option: {param:?}"));
}
if from.is_none() {
from = Some(param);
+3 -1
View File
@@ -39,7 +39,9 @@ mod common {
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Unknown option: \"--foobar\""));
.stderr(predicate::str::starts_with(
"unrecognized option: \"--foobar\"",
));
}
Ok(())
}