mirror of
https://github.com/uutils/diffutils.git
synced 2026-07-01 08:02:33 -04:00
Compare commits
305 Commits
v0.2.1
...
latest-commit
| Author | SHA1 | Date | |
|---|---|---|---|
| bdf449eaf2 | |||
| f8248801a9 | |||
| 357c99038f | |||
| 59e130aa22 | |||
| 54c8b7aeb9 | |||
| 6f082c6572 | |||
| 34db0ade7c | |||
| d3d0b0c966 | |||
| 87e0aa2828 | |||
| 9f419c31ea | |||
| 95883b462b | |||
| f20af97a09 | |||
| b9b7ea8d2b | |||
| 47798b4b2c | |||
| 445e1ea02f | |||
| e2fb192d52 | |||
| a1d18a0c09 | |||
| 5dd2e9d30c | |||
| e00ff6b108 | |||
| c38fe5f2e5 | |||
| 44565de705 | |||
| 8c8c1db5c6 | |||
| 3e02493701 | |||
| c6e8b46d21 | |||
| 125fc298c5 | |||
| 16673cf466 | |||
| f66ad85757 | |||
| de9bf94d01 | |||
| 940a0e00b6 | |||
| 7ddc6c6c4b | |||
| 8997ac06b8 | |||
| df90e37566 | |||
| f25cad8497 | |||
| a09dcac41d | |||
| 98bf765a98 | |||
| 1e1e968027 | |||
| 4eee9cefa0 | |||
| 67589b9331 | |||
| 83f6d2db7c | |||
| b193ea0c43 | |||
| 5f2ba7a84c | |||
| 15473edcd7 | |||
| 30b6bd2523 | |||
| 590a4b405e | |||
| 418596138e | |||
| eadc8c3dc5 | |||
| 2806ec2029 | |||
| dbd60416e6 | |||
| 392b8fa07b | |||
| 44645e5428 | |||
| 644a794067 | |||
| 19b79efd76 | |||
| 3380bab935 | |||
| a0a05eeba9 | |||
| 611e380266 | |||
| af0dc993b8 | |||
| a680c4f467 | |||
| 0b604f67aa | |||
| cc67cbcc59 | |||
| a95ca0062f | |||
| b59d9be943 | |||
| 3654b82a6d | |||
| 7df02399ba | |||
| 03fe614087 | |||
| 8261d790f4 | |||
| 168dae3aee | |||
| c7d4140fa3 | |||
| dee3bc1d66 | |||
| fce0881e27 | |||
| 45b3072534 | |||
| a3e57c950e | |||
| 1ef6923b7d | |||
| dff98a2969 | |||
| 8105420bb4 | |||
| 5b791e8bf6 | |||
| b31df0b5e8 | |||
| c02273c827 | |||
| 199c7f169c | |||
| 978390c14d | |||
| 87ccc8e4c2 | |||
| 9bc53486df | |||
| 0d7e4d82ae | |||
| 360bff50ed | |||
| 26ee98dfaa | |||
| 009d64acd2 | |||
| b53d4f427c | |||
| 8448fd8068 | |||
| d573c3ae1d | |||
| ca1c4c3618 | |||
| ba1cac3c20 | |||
| 949cccebd4 | |||
| bbdfa1b765 | |||
| 2f1a89173a | |||
| f9553984f4 | |||
| 59920040f6 | |||
| 44c195c0b2 | |||
| fdc69921e6 | |||
| 4ff2d6b182 | |||
| 39e092488b | |||
| dcd3dfd6e0 | |||
| 90bed40046 | |||
| 1575aec22c | |||
| 4f2f869021 | |||
| 3101aa1aff | |||
| 14b062251f | |||
| 0e11811ce1 | |||
| 3de1930bbe | |||
| 889e7bb7cc | |||
| 1910cbfe58 | |||
| c70cc1921c | |||
| 933230e103 | |||
| a316262603 | |||
| 0bf04b4395 | |||
| 7480068e7d | |||
| 763074a804 | |||
| fac8dab182 | |||
| 2e681301b4 | |||
| 50057412bd | |||
| 68292b370d | |||
| 26bcc102c0 | |||
| 50198ef2c1 | |||
| bfdbf6d7b2 | |||
| f75c187971 | |||
| d07c0438b5 | |||
| 72c7802f06 | |||
| c1b66e4a47 | |||
| 9103365691 | |||
| 7574243de1 | |||
| 6f3834d69d | |||
| d8b91fd60e | |||
| 7c9c2a1ab2 | |||
| 63d51bcd69 | |||
| d5bce65a29 | |||
| 9db1eab1d0 | |||
| 2392acfad1 | |||
| 2a899a9fc7 | |||
| 6ec8370b4b | |||
| dbabf399d5 | |||
| b815162b80 | |||
| 12b205e655 | |||
| 8a6504dd83 | |||
| 67ef43083a | |||
| b1738538a8 | |||
| eea6b62b20 | |||
| f08a3bf512 | |||
| e55ee893dd | |||
| 24245ee098 | |||
| 11f815a7c2 | |||
| e9a8141618 | |||
| c9a756eb43 | |||
| 468c4bf934 | |||
| 1e8fdd58d9 | |||
| e98b5e179e | |||
| 1901982375 | |||
| eee6f49920 | |||
| 8a3a977d2c | |||
| fa4e0c6097 | |||
| d362046ae5 | |||
| 7964afa336 | |||
| 80b993141b | |||
| d922313c8c | |||
| 3e246ab36c | |||
| 4b70969ff1 | |||
| 767c6f6c4a | |||
| 1f896ca1ac | |||
| 713bd210ab | |||
| 61314eaf4e | |||
| bf9147733d | |||
| ce8457cbdb | |||
| df778c610b | |||
| d92132e721 | |||
| 99d4d02985 | |||
| e7dc6558c6 | |||
| 8c6a648aef | |||
| 0304391bc5 | |||
| 8de0ca60d1 | |||
| 43b9c524d9 | |||
| 3dc3fdf5cd | |||
| b7261a43f4 | |||
| 37fe1ae808 | |||
| 22d973fce6 | |||
| fe28610f21 | |||
| 3a8eddfe2c | |||
| 476e69ee20 | |||
| 65993d6a13 | |||
| 39d2ece187 | |||
| 46a26e896b | |||
| 14799eea89 | |||
| 831348d1fc | |||
| 00a5c0ba44 | |||
| bf104648c1 | |||
| 5669f164b3 | |||
| 11bf271666 | |||
| 674974d5e6 | |||
| 2ba35db431 | |||
| fcec7277c9 | |||
| b8efad6b90 | |||
| 68e2f51983 | |||
| 4edaee190f | |||
| 7f7821f558 | |||
| 1149a247dd | |||
| 1b311c6673 | |||
| aedd0684d1 | |||
| 54c02bdf0b | |||
| ba7cb0aef9 | |||
| 33783d094e | |||
| 900e1c3a68 | |||
| 0a77fe12b9 | |||
| 86bd05c739 | |||
| 00e18a6b0c | |||
| f6eb0835b0 | |||
| be66ff3299 | |||
| e1c319f96b | |||
| 84ad116845 | |||
| 6dc34fed44 | |||
| 9507ca28d7 | |||
| c325291696 | |||
| c08e0b6e1f | |||
| 72da7fca40 | |||
| 61fb0657c1 | |||
| 096aa1dad9 | |||
| 2d9e625a5b | |||
| d863fe443a | |||
| 6be94d8683 | |||
| 44ef772e4a | |||
| bbfca84e17 | |||
| 281098d751 | |||
| a3a372ff36 | |||
| 5b814f8530 | |||
| 34a5cc7340 | |||
| 6a152cdc7f | |||
| b8fada8faa | |||
| a213272d0c | |||
| 25e4a17421 | |||
| 589039ab4c | |||
| f83fccb542 | |||
| 76c4714f78 | |||
| 88a7568b52 | |||
| b135b6f218 | |||
| 314e3a7320 | |||
| 6a73657b3a | |||
| e9f0630aaf | |||
| e6a0ba28c5 | |||
| 80c9944bf7 | |||
| 043c5f9493 | |||
| 9ff8f89626 | |||
| 0ab824abda | |||
| f60fefaf6e | |||
| 14e77548fd | |||
| f2fd2127ed | |||
| cfc68d58bc | |||
| e0283083f2 | |||
| 8d65c2badd | |||
| 42eb15b87a | |||
| a304ac0a68 | |||
| f916f1ce86 | |||
| 4ed7ea1553 | |||
| 62e10c6d6c | |||
| c68d386170 | |||
| a89f30afa0 | |||
| 0a67bf9fb8 | |||
| 1241db4806 | |||
| 3bc8668f78 | |||
| c90eee442f | |||
| 6c29f02527 | |||
| 790ef1e633 | |||
| 4c1a752f11 | |||
| 54a5407bec | |||
| 02632e915c | |||
| 3f9556aa05 | |||
| a94c6a60cf | |||
| c28973c019 | |||
| a660f7440c | |||
| 6a69a39852 | |||
| bf13d528be | |||
| 329a7e1f4a | |||
| e5de3cd93e | |||
| 0e14b37e38 | |||
| 3c1176082e | |||
| 865f97c78d | |||
| f5b65a5720 | |||
| 1372c5386c | |||
| d891e1034d | |||
| fdc35f6b8e | |||
| 6648963df1 | |||
| a10ef621c8 | |||
| 7939749338 | |||
| 7b3001f1ff | |||
| 416a4be06c | |||
| 9084134f04 | |||
| f42fc82f18 | |||
| 0b2505d249 | |||
| 045435b803 | |||
| b55cbf2ca2 | |||
| 12f3f16792 | |||
| e9e69b86db | |||
| 029d747e14 | |||
| e38055e5b2 | |||
| 61cfe6eec4 | |||
| ea9376aaaf | |||
| 582259a867 | |||
| da05a5254b | |||
| a4f7642d7a | |||
| e72ea046b7 | |||
| 2e84164d2f |
@@ -0,0 +1,178 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
name: Basic CI
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: cargo check
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: cargo check
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: cargo test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: install GNU patch on MacOS
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install gpatch
|
||||||
|
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
|
||||||
|
- name: set up PATH on Windows
|
||||||
|
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
|
||||||
|
- run: cargo test
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: cargo fmt --all -- --check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
name: cargo clippy -- -D warnings
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- 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
|
||||||
|
- 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:
|
||||||
|
TERM: xterm
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- { os: ubuntu-latest , features: unix }
|
||||||
|
- { os: macos-latest , features: macos }
|
||||||
|
- { os: windows-latest , features: windows }
|
||||||
|
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; }
|
||||||
|
# target-specific options
|
||||||
|
# * CODECOV_FLAGS
|
||||||
|
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
||||||
|
outputs CODECOV_FLAGS
|
||||||
|
|
||||||
|
- name: install GNU patch on MacOS
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install gpatch
|
||||||
|
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
|
||||||
|
- name: set up PATH on Windows
|
||||||
|
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --all-features --no-fail-fast
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: "0"
|
||||||
|
RUSTC_WRAPPER: ""
|
||||||
|
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: |
|
||||||
|
git clone https://github.com/mozilla/grcov.git ~/grcov/
|
||||||
|
cd ~/grcov
|
||||||
|
# Hardcode the version of crossbeam-epoch. See
|
||||||
|
# https://github.com/uutils/coreutils/issues/3680
|
||||||
|
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
|
||||||
|
cargo install --path .
|
||||||
|
cd -
|
||||||
|
# Uncomment when the upstream issue
|
||||||
|
# https://github.com/mozilla/grcov/issues/849 is fixed
|
||||||
|
# uses: actions-rs/install@v0.1
|
||||||
|
# with:
|
||||||
|
# crate: grcov
|
||||||
|
# version: latest
|
||||||
|
# use-tool-cache: false
|
||||||
|
- name: Generate coverage data (via `grcov`)
|
||||||
|
id: coverage
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
## Generate coverage data
|
||||||
|
COVERAGE_REPORT_DIR="target/debug"
|
||||||
|
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
|
||||||
|
mkdir -p "${COVERAGE_REPORT_DIR}"
|
||||||
|
# display coverage files
|
||||||
|
grcov . --output-type files --binary-path "${COVERAGE_REPORT_DIR}" | sort --unique
|
||||||
|
# generate coverage report
|
||||||
|
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch
|
||||||
|
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||||
|
- name: Upload coverage results (to Codecov.io)
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ${{ steps.coverage.outputs.report }}
|
||||||
|
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||||
|
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||||
|
name: codecov-umbrella
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Fuzzing
|
||||||
|
|
||||||
|
# spell-checker:ignore fuzzer
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fuzz-build:
|
||||||
|
name: Build the fuzzers
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: 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 fuzz build
|
||||||
|
|
||||||
|
fuzz-run:
|
||||||
|
needs: fuzz-build
|
||||||
|
name: Run the fuzzers
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
env:
|
||||||
|
RUN_FOR: 60
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
test-target:
|
||||||
|
- { name: fuzz_cmp, should_pass: true }
|
||||||
|
- { name: fuzz_cmp_args, should_pass: true }
|
||||||
|
- { name: fuzz_ed, should_pass: true }
|
||||||
|
- { name: fuzz_normal, should_pass: true }
|
||||||
|
- { name: fuzz_patch, should_pass: true }
|
||||||
|
- { name: fuzz_side, should_pass: true }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: 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: Restore Cached Corpus
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
key: corpus-cache-${{ matrix.test-target.name }}
|
||||||
|
path: |
|
||||||
|
fuzz/corpus/${{ matrix.test-target.name }}
|
||||||
|
- name: Run ${{ matrix.test-target.name }} for XX seconds
|
||||||
|
shell: bash
|
||||||
|
continue-on-error: ${{ !matrix.test-target.name.should_pass }}
|
||||||
|
run: |
|
||||||
|
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:
|
||||||
|
key: corpus-cache-${{ matrix.test-target.name }}
|
||||||
|
path: |
|
||||||
|
fuzz/corpus/${{ matrix.test-target.name }}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||||
|
#
|
||||||
|
# Copyright 2022-2024, axodotdev
|
||||||
|
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||||
|
#
|
||||||
|
# CI that:
|
||||||
|
#
|
||||||
|
# * checks for a Git Tag that looks like a release
|
||||||
|
# * builds artifacts with dist (archives, installers, hashes)
|
||||||
|
# * uploads those artifacts to temporary workflow zip
|
||||||
|
# * on success, uploads the artifacts to a GitHub Release
|
||||||
|
#
|
||||||
|
# Note that the GitHub Release will be created with a generated
|
||||||
|
# title/body based on your changelogs.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
"contents": "write"
|
||||||
|
|
||||||
|
# This task will run whenever you push a git tag that looks like a version
|
||||||
|
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||||
|
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||||
|
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||||
|
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||||
|
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||||
|
# (dist-able) packages in the workspace with that version (this mode is
|
||||||
|
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||||
|
# packages versioned/released in lockstep).
|
||||||
|
#
|
||||||
|
# If you push multiple tags at once, separate instances of this workflow will
|
||||||
|
# spin up, creating an independent announcement for each one. However, GitHub
|
||||||
|
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||||
|
# mistake.
|
||||||
|
#
|
||||||
|
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||||
|
# will be marked as a prerelease.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||||
|
plan:
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.plan.outputs.manifest }}
|
||||||
|
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||||
|
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||||
|
publishing: ${{ !github.event.pull_request }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install dist
|
||||||
|
# we specify bash to get pipefail; it guards against the `curl` command
|
||||||
|
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||||
|
shell: bash
|
||||||
|
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
|
||||||
|
- name: Cache dist
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/dist
|
||||||
|
# sure would be cool if github gave us proper conditionals...
|
||||||
|
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||||
|
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||||
|
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||||
|
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||||
|
- id: plan
|
||||||
|
run: |
|
||||||
|
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
cat plan-dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-plan-dist-manifest
|
||||||
|
path: plan-dist-manifest.json
|
||||||
|
|
||||||
|
# Build and packages all the platform-specific things
|
||||||
|
build-local-artifacts:
|
||||||
|
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||||
|
# Let the initial task tell us to not run (currently very blunt)
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# Target platforms/runners are computed by dist in create-release.
|
||||||
|
# Each member of the matrix has the following arguments:
|
||||||
|
#
|
||||||
|
# - runner: the github runner
|
||||||
|
# - dist-args: cli flags to pass to dist
|
||||||
|
# - install-dist: expression to run to install dist on the runner
|
||||||
|
#
|
||||||
|
# Typically there will be:
|
||||||
|
# - 1 "global" task that builds universal installers
|
||||||
|
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||||
|
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
container: ${{ matrix.container && matrix.container.image || null }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- name: enable windows longpaths
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install Rust non-interactively if not already installed
|
||||||
|
if: ${{ matrix.container }}
|
||||||
|
run: |
|
||||||
|
if ! command -v cargo > /dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
fi
|
||||||
|
- name: Install dist
|
||||||
|
run: ${{ matrix.install_dist.run }}
|
||||||
|
# Get the dist-manifest
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
${{ matrix.packages_install }}
|
||||||
|
- name: Build artifacts
|
||||||
|
run: |
|
||||||
|
# Actually do builds and make zips and whatnot
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
- id: cargo-dist
|
||||||
|
name: Post-build
|
||||||
|
# We force bash here just because github makes it really hard to get values up
|
||||||
|
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||||
|
# inconsistent syntax between shell and powershell.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
|
||||||
|
# Build and package all the platform-agnostic(ish) things
|
||||||
|
build-global-artifacts:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: cargo-dist
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-build-global
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
# Determines if we should publish/announce
|
||||||
|
host:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
- build-global-artifacts
|
||||||
|
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||||
|
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.host.outputs.manifest }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Fetch artifacts from scratch-storage
|
||||||
|
- name: Fetch artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: host
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||||
|
echo "artifacts uploaded and released successfully"
|
||||||
|
cat dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
# Overwrite the previous copy
|
||||||
|
name: artifacts-dist-manifest
|
||||||
|
path: dist-manifest.json
|
||||||
|
# Create a GitHub Release while uploading all files to it
|
||||||
|
- name: "Download GitHub Artifacts"
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
# Remove the granular manifests
|
||||||
|
rm -f artifacts/*-dist-manifest.json
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||||
|
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||||
|
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||||
|
RELEASE_COMMIT: "${{ github.sha }}"
|
||||||
|
run: |
|
||||||
|
# Write and read notes from a file to avoid quoting breaking things
|
||||||
|
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||||
|
|
||||||
|
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||||
|
|
||||||
|
announce:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- host
|
||||||
|
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||||
|
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||||
|
# "host" however must run to completion, no skipping allowed!
|
||||||
|
if: ${{ always() && needs.host.result == 'success' }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: recursive
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
Generated
+1253
File diff suppressed because it is too large
Load Diff
+50
-12
@@ -1,18 +1,56 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "unified-diff"
|
name = "diffutils"
|
||||||
version = "0.2.1"
|
version = "0.5.0"
|
||||||
authors = [
|
edition = "2021"
|
||||||
"Michael Howell <michael@notriddle.com>",
|
description = "A CLI app for generating diff files"
|
||||||
"The Rust Project Developers"
|
|
||||||
]
|
|
||||||
edition = "2018"
|
|
||||||
description = "An implementation of the GNU unified diff format"
|
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/notriddle/rust-unified-diff"
|
repository = "https://github.com/uutils/diffutils"
|
||||||
exclude = [ "fuzz" ]
|
|
||||||
|
[lib]
|
||||||
|
name = "diffutilslib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "unified-diff"
|
name = "diffutils"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diff = "0.1.10"
|
chrono = "0.4.38"
|
||||||
|
diff = "0.1.13"
|
||||||
|
itoa = "1.0.11"
|
||||||
|
regex = "1.10.4"
|
||||||
|
same-file = "1.0.6"
|
||||||
|
unicode-width = "0.2.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert_cmd = "2.0.14"
|
||||||
|
divan = { version = "4.3.0", package = "codspeed-divan-compat" }
|
||||||
|
pretty_assertions = "1.4.0"
|
||||||
|
predicates = "3.1.0"
|
||||||
|
rand = "0.10.0"
|
||||||
|
tempfile = "3.26.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[profile.release-fast]
|
||||||
|
inherits = "release"
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
# The profile that 'dist' will build with
|
||||||
|
[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,3 +1,6 @@
|
|||||||
|
Copyright (c) Michael Howell
|
||||||
|
Copyright (c) uutils developers
|
||||||
|
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
Copyright (c) Michael Howell
|
||||||
|
Copyright (c) uutils developers
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
person obtaining a copy of this software and associated
|
person obtaining a copy of this software and associated
|
||||||
documentation files (the "Software"), to deal in the
|
documentation files (the "Software"), to deal in the
|
||||||
|
|||||||
@@ -1,41 +1,57 @@
|
|||||||
A GNU unified diff generator. Oracle tested against GNU patch 2.7.6
|
[](https://crates.io/crates/diffutils)
|
||||||
|
[](https://discord.gg/wQVJbvJ)
|
||||||
|
[](https://github.com/uutils/diffutils/blob/main/LICENSE)
|
||||||
|
[](https://deps.rs/repo/github/uutils/diffutils)
|
||||||
|
[](https://codspeed.io/uutils/diffutils?utm_source=badge)
|
||||||
|
|
||||||
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs,
|
[](https://codecov.io/gh/uutils/diffutils)
|
||||||
but it implements a different format.
|
|
||||||
|
|
||||||
|
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) (diff, cmp, diff3, sdiff) in Rust.
|
||||||
|
|
||||||
|
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools.
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
|
||||||
|
|
||||||
|
Clone the repository and build the project using Cargo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/uutils/diffutils.git
|
||||||
|
cd diffutils
|
||||||
|
cargo build --release
|
||||||
```
|
```
|
||||||
~/unified-diff$ cargo run Cargo.lock Cargo.toml
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
cat <<EOF >fruits_old.txt
|
||||||
|
Apple
|
||||||
|
Banana
|
||||||
|
Cherry
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF >fruits_new.txt
|
||||||
|
Apple
|
||||||
|
Fig
|
||||||
|
Cherry
|
||||||
|
EOF
|
||||||
|
|
||||||
|
$ cargo run -- -u fruits_old.txt fruits_new.txt
|
||||||
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
|
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
|
||||||
Running `target/debug/unified-diff Cargo.lock Cargo.toml`
|
Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt`
|
||||||
--- Cargo.lock
|
--- fruits_old.txt
|
||||||
+++ Cargo.toml
|
+++ fruits_new.txt
|
||||||
@@ -1,14 +1,14 @@
|
@@ -1,3 +1,3 @@
|
||||||
-# This file is automatically @generated by Cargo.
|
Apple
|
||||||
-# It is not intended for manual editing.
|
-Banana
|
||||||
-[[package]]
|
+Fig
|
||||||
-name = "diff"
|
Cherry
|
||||||
-version = "0.1.12"
|
|
||||||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
-checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
|
||||||
-
|
|
||||||
-[[package]]
|
|
||||||
+[package]
|
|
||||||
name = "unified-diff"
|
|
||||||
version = "0.1.0"
|
|
||||||
-dependencies = [
|
|
||||||
- "diff",
|
|
||||||
+authors = [
|
|
||||||
+ "Michael Howell <michael@notriddle.com>",
|
|
||||||
+ "The Rust Project Developers"
|
|
||||||
]
|
|
||||||
+edition = "2018"
|
|
||||||
+
|
|
||||||
+[[bin]]
|
|
||||||
+name = "unified-diff"
|
|
||||||
+
|
|
||||||
+[dependencies]
|
|
||||||
+diff = "0.1.10"
|
|
||||||
~/unified-diff$ rustup override set nightly
|
|
||||||
~/unified-diff$ cargo fuzz run fuzz_patch
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
diffutils is licensed under the MIT and Apache Licenses - see the `LICENSE-MIT` or `LICENSE-APACHE` files for details
|
||||||
|
|||||||
@@ -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(¶ms).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(¶ms).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(¶ms).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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["cargo:."]
|
||||||
|
|
||||||
|
# Config for 'dist'
|
||||||
|
[dist]
|
||||||
|
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||||
|
cargo-dist-version = "0.30.3"
|
||||||
|
# CI backends to support
|
||||||
|
ci = "github"
|
||||||
|
# The installers to generate for each app
|
||||||
|
installers = []
|
||||||
|
# Target platforms to build apps for (Rust target-triple syntax)
|
||||||
|
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||||
Generated
+474
@@ -0,0 +1,474 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"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"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diffutils"
|
||||||
|
version = "0.5.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"const_format",
|
||||||
|
"diff",
|
||||||
|
"itoa",
|
||||||
|
"regex",
|
||||||
|
"same-file",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.83"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.178"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libfuzzer-sys"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.112"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
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"
|
||||||
|
dependencies = [
|
||||||
|
"diffutils",
|
||||||
|
"libfuzzer-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.1+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
+32
-5
@@ -2,7 +2,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "unified-diff-fuzz"
|
name = "unified-diff-fuzz"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
authors = ["Automatically generated"]
|
|
||||||
publish = false
|
publish = false
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -10,18 +9,46 @@ edition = "2018"
|
|||||||
cargo-fuzz = true
|
cargo-fuzz = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libfuzzer-sys = "0.3"
|
libfuzzer-sys = "0.4.7"
|
||||||
|
diffutils = { path = "../" }
|
||||||
[dependencies.unified-diff]
|
|
||||||
path = ".."
|
|
||||||
|
|
||||||
# Prevent this from interfering with workspaces
|
# Prevent this from interfering with workspaces
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["."]
|
members = ["."]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_cmp"
|
||||||
|
path = "fuzz_targets/fuzz_cmp.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_cmp_args"
|
||||||
|
path = "fuzz_targets/fuzz_cmp_args.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fuzz_patch"
|
name = "fuzz_patch"
|
||||||
path = "fuzz_targets/fuzz_patch.rs"
|
path = "fuzz_targets/fuzz_patch.rs"
|
||||||
test = false
|
test = false
|
||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_normal"
|
||||||
|
path = "fuzz_targets/fuzz_normal.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_ed"
|
||||||
|
path = "fuzz_targets/fuzz_ed.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_side"
|
||||||
|
path = "fuzz_targets/fuzz_side.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"-l"
|
||||||
|
"--verbose"
|
||||||
|
"-b"
|
||||||
|
"--print-bytes"
|
||||||
|
"-lb"
|
||||||
|
"-bl"
|
||||||
|
"-n"
|
||||||
|
"--bytes"
|
||||||
|
"--bytes="
|
||||||
|
"--bytes=1024"
|
||||||
|
"--bytes=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"
|
||||||
|
"-i"
|
||||||
|
"--ignore-initial"
|
||||||
|
"--ignore-initial="
|
||||||
|
"--ignore-initial=1024"
|
||||||
|
"--ignore-initial=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"
|
||||||
|
"-s"
|
||||||
|
"-q"
|
||||||
|
"--quiet"
|
||||||
|
"--silent"
|
||||||
|
"-"
|
||||||
|
"--"
|
||||||
|
"1kB"
|
||||||
|
"1G"
|
||||||
|
"1GB"
|
||||||
|
"1T"
|
||||||
|
"1TB"
|
||||||
|
"1P"
|
||||||
|
"1PB"
|
||||||
|
"1Z"
|
||||||
|
"1ZB"
|
||||||
|
"1Y"
|
||||||
|
"1YB"
|
||||||
|
"1Y"
|
||||||
|
"0"
|
||||||
|
"1:2"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
use diffutilslib::cmp::{self, Cmp};
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn os(s: &str) -> OsString {
|
||||||
|
OsString::from(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||||
|
let args = vec!["cmp", "-l", "-b", "target/fuzz.cmp.a", "target/fuzz.cmp.b"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| os(s))
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
let (from, to) = x;
|
||||||
|
|
||||||
|
File::create("target/fuzz.cmp.a")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&from)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
File::create("target/fuzz.cmp.b")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&to)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let params =
|
||||||
|
cmp::parse_params(args).unwrap_or_else(|e| panic!("Failed to parse params: {}", e));
|
||||||
|
let ret = cmp::cmp(¶ms);
|
||||||
|
if from == to && !matches!(ret, Ok(Cmp::Equal)) {
|
||||||
|
panic!(
|
||||||
|
"target/fuzz.cmp.a and target/fuzz.cmp.b are equal, but cmp returned {:?}.",
|
||||||
|
ret
|
||||||
|
);
|
||||||
|
} else if from != to && !matches!(ret, Ok(Cmp::Different)) {
|
||||||
|
panic!(
|
||||||
|
"target/fuzz.cmp.a and target/fuzz.cmp.b are different, but cmp returned {:?}.",
|
||||||
|
ret
|
||||||
|
);
|
||||||
|
} else if ret.is_err() {
|
||||||
|
panic!(
|
||||||
|
"target/fuzz.cmp.a and target/fuzz.cmp.b caused cmp to error ({:?}).",
|
||||||
|
ret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
use diffutilslib::cmp;
|
||||||
|
|
||||||
|
use libfuzzer_sys::Corpus;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
|
||||||
|
fn os(s: &str) -> OsString {
|
||||||
|
OsString::from(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|x: Vec<OsString>| -> Corpus {
|
||||||
|
if x.len() > 6 {
|
||||||
|
// Make sure we try to parse an option when we get longer args. x[0] will be
|
||||||
|
// the executable name.
|
||||||
|
if ![os("-l"), os("-b"), os("-s"), os("-n"), os("-i")].contains(&x[1]) {
|
||||||
|
return Corpus::Reject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = cmp::parse_params(x.into_iter().peekable());
|
||||||
|
Corpus::Keep
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
use diffutilslib::ed_diff;
|
||||||
|
use diffutilslib::ed_diff::DiffError;
|
||||||
|
use diffutilslib::params::Params;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||||
|
let mut output = ed_diff::diff(expected, actual, &Params::default())?;
|
||||||
|
writeln!(&mut output, "w {filename}").unwrap();
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||||
|
let (mut from, mut to) = x;
|
||||||
|
from.push(b'\n');
|
||||||
|
to.push(b'\n');
|
||||||
|
if let Ok(s) = String::from_utf8(from.clone()) {
|
||||||
|
if !s.is_ascii() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if s.find(|x| x < ' ' && x != '\n').is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(s) = String::from_utf8(to.clone()) {
|
||||||
|
if !s.is_ascii() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if s.find(|x| x < ' ' && x != '\n').is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let diff = diff_w(&from, &to, "target/fuzz.file").unwrap();
|
||||||
|
File::create("target/fuzz.file.original")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&from)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file.expected")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&to)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&from)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.ed")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let output = Command::new("ed")
|
||||||
|
.arg("target/fuzz.file")
|
||||||
|
.stdin(File::open("target/fuzz.ed").unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let result = fs::read("target/fuzz.file").unwrap();
|
||||||
|
if result != to {
|
||||||
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
use diffutilslib::normal_diff;
|
||||||
|
use diffutilslib::params::Params;
|
||||||
|
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||||
|
let (from, to) = x;
|
||||||
|
/*if let Ok(s) = String::from_utf8(from.clone()) {
|
||||||
|
if !s.is_ascii() { return }
|
||||||
|
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let Ok(s) = String::from_utf8(to.clone()) {
|
||||||
|
if !s.is_ascii() { return }
|
||||||
|
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}*/
|
||||||
|
let diff = normal_diff::diff(&from, &to, &Params::default());
|
||||||
|
File::create("target/fuzz.file.original")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&from)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file.expected")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&to)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&from)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.diff")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--binary")
|
||||||
|
.arg("--fuzz=0")
|
||||||
|
.arg("--normal")
|
||||||
|
.arg("target/fuzz.file")
|
||||||
|
.stdin(File::open("target/fuzz.diff").unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let result = fs::read("target/fuzz.file").unwrap();
|
||||||
|
if result != to {
|
||||||
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#[macro_use] extern crate libfuzzer_sys;
|
#[macro_use]
|
||||||
extern crate unified_diff;
|
extern crate libfuzzer_sys;
|
||||||
|
use diffutilslib::params::Params;
|
||||||
|
use diffutilslib::unified_diff;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -20,7 +21,16 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
|||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}*/
|
}*/
|
||||||
let diff = unified_diff::diff(&from, "a/fuzz.file", &to, "target/fuzz.file", context as usize);
|
let diff = unified_diff::diff(
|
||||||
|
&from,
|
||||||
|
&to,
|
||||||
|
&Params {
|
||||||
|
from: "a/fuzz.file".into(),
|
||||||
|
to: "target/fuzz.file".into(),
|
||||||
|
context_count: context as usize,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
File::create("target/fuzz.file.original")
|
File::create("target/fuzz.file.original")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_all(&from)
|
.write_all(&from)
|
||||||
@@ -45,11 +55,18 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
|||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let result = fs::read("target/fuzz.file").unwrap();
|
let result = fs::read("target/fuzz.file").unwrap();
|
||||||
if result != to {
|
if result != to {
|
||||||
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
panic!(
|
||||||
|
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
|
||||||
|
use diffutilslib::side_diff;
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use diffutilslib::params::Params;
|
||||||
|
|
||||||
|
fuzz_target!(|x: (Vec<u8>, Vec<u8>, /* usize, usize */ bool)| {
|
||||||
|
let (original, new, /* width, tabsize, */ expand) = x;
|
||||||
|
|
||||||
|
// if width == 0 || tabsize == 0 {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
let params = Params {
|
||||||
|
// width,
|
||||||
|
// tabsize,
|
||||||
|
expand_tabs: expand,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut output_buf = vec![];
|
||||||
|
side_diff::diff(&original, &new, &mut output_buf, ¶ms);
|
||||||
|
File::create("target/fuzz.file.original")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&original)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file.new")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&new)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.file")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&original)
|
||||||
|
.unwrap();
|
||||||
|
File::create("target/fuzz.diff")
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&output_buf)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
+1211
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,796 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::params::Params;
|
||||||
|
use crate::utils::do_write_line;
|
||||||
|
use crate::utils::get_modification_time;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum DiffLine {
|
||||||
|
Context(Vec<u8>),
|
||||||
|
Change(Vec<u8>),
|
||||||
|
Add(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Mismatch {
|
||||||
|
pub line_number_expected: usize,
|
||||||
|
pub line_number_actual: usize,
|
||||||
|
pub expected: Vec<DiffLine>,
|
||||||
|
pub actual: Vec<DiffLine>,
|
||||||
|
pub expected_missing_nl: bool,
|
||||||
|
pub actual_missing_nl: bool,
|
||||||
|
pub expected_all_context: bool,
|
||||||
|
pub actual_all_context: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mismatch {
|
||||||
|
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||||
|
Mismatch {
|
||||||
|
line_number_expected,
|
||||||
|
line_number_actual,
|
||||||
|
expected: Vec::new(),
|
||||||
|
actual: Vec::new(),
|
||||||
|
expected_missing_nl: false,
|
||||||
|
actual_missing_nl: false,
|
||||||
|
expected_all_context: false,
|
||||||
|
actual_all_context: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a diff between the expected output and actual output.
|
||||||
|
fn make_diff(
|
||||||
|
expected: &[u8],
|
||||||
|
actual: &[u8],
|
||||||
|
context_size: usize,
|
||||||
|
stop_early: bool,
|
||||||
|
) -> Vec<Mismatch> {
|
||||||
|
let mut line_number_expected = 1;
|
||||||
|
let mut line_number_actual = 1;
|
||||||
|
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||||
|
let mut lines_since_mismatch = context_size + 1;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut mismatch = Mismatch::new(0, 0);
|
||||||
|
|
||||||
|
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||||
|
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||||
|
|
||||||
|
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||||
|
// ^ means that underflow here is impossible
|
||||||
|
let expected_lines_count = expected_lines.len() - 1;
|
||||||
|
let actual_lines_count = actual_lines.len() - 1;
|
||||||
|
|
||||||
|
if expected_lines.last() == Some(&&b""[..]) {
|
||||||
|
expected_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual_lines.last() == Some(&&b""[..]) {
|
||||||
|
actual_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rust only allows allocations to grow to isize::MAX, and this is bigger than that.
|
||||||
|
let mut expected_lines_change_idx: usize = !0;
|
||||||
|
|
||||||
|
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||||
|
match result {
|
||||||
|
diff::Result::Left(str) => {
|
||||||
|
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len(),
|
||||||
|
line_number_actual - context_queue.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_lines_change_idx = mismatch.expected.len();
|
||||||
|
mismatch.expected.push(DiffLine::Add(str.to_vec()));
|
||||||
|
if line_number_expected > expected_lines_count {
|
||||||
|
mismatch.expected_missing_nl = true;
|
||||||
|
}
|
||||||
|
line_number_expected += 1;
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
}
|
||||||
|
diff::Result::Right(str) => {
|
||||||
|
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len(),
|
||||||
|
line_number_actual - context_queue.len(),
|
||||||
|
);
|
||||||
|
expected_lines_change_idx = !0;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(DiffLine::Add(content)) =
|
||||||
|
mismatch.expected.get_mut(expected_lines_change_idx)
|
||||||
|
{
|
||||||
|
let content = std::mem::take(content);
|
||||||
|
mismatch.expected[expected_lines_change_idx] = DiffLine::Change(content);
|
||||||
|
expected_lines_change_idx = expected_lines_change_idx.wrapping_sub(1); // if 0, becomes !0
|
||||||
|
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||||
|
} else {
|
||||||
|
mismatch.actual.push(DiffLine::Add(str.to_vec()));
|
||||||
|
}
|
||||||
|
if line_number_actual > actual_lines_count {
|
||||||
|
mismatch.actual_missing_nl = true;
|
||||||
|
}
|
||||||
|
line_number_actual += 1;
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
}
|
||||||
|
diff::Result::Both(str, _) => {
|
||||||
|
expected_lines_change_idx = !0;
|
||||||
|
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||||
|
if (line_number_actual > actual_lines_count)
|
||||||
|
&& (line_number_expected > expected_lines_count)
|
||||||
|
{
|
||||||
|
if context_queue.len() < context_size {
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
if lines_since_mismatch < context_size {
|
||||||
|
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||||
|
mismatch.expected_missing_nl = true;
|
||||||
|
mismatch.actual_missing_nl = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else if line_number_actual > actual_lines_count {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len(),
|
||||||
|
line_number_actual - context_queue.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||||
|
mismatch.actual_missing_nl = true;
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else if line_number_expected > expected_lines_count {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len(),
|
||||||
|
line_number_actual - context_queue.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||||
|
mismatch.expected_missing_nl = true;
|
||||||
|
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else {
|
||||||
|
debug_assert!(context_queue.len() <= context_size);
|
||||||
|
if context_queue.len() >= context_size {
|
||||||
|
let _ = context_queue.pop_front();
|
||||||
|
}
|
||||||
|
if lines_since_mismatch < context_size {
|
||||||
|
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||||
|
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||||
|
} else if context_size > 0 {
|
||||||
|
context_queue.push_back(str);
|
||||||
|
}
|
||||||
|
lines_since_mismatch += 1;
|
||||||
|
}
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop_early && !results.is_empty() {
|
||||||
|
// Optimization: stop analyzing the files as soon as there are any differences
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(mismatch);
|
||||||
|
results.remove(0);
|
||||||
|
|
||||||
|
if results.is_empty() && expected_lines_count != actual_lines_count {
|
||||||
|
let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len());
|
||||||
|
// empty diff and only expected lines has a missing line at end
|
||||||
|
if expected_lines_count != expected_lines.len() {
|
||||||
|
mismatch.expected.push(DiffLine::Change(
|
||||||
|
expected_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.expected_missing_nl = true;
|
||||||
|
mismatch.actual.push(DiffLine::Change(
|
||||||
|
actual_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
results.push(mismatch);
|
||||||
|
} else if actual_lines_count != actual_lines.len() {
|
||||||
|
mismatch.expected.push(DiffLine::Change(
|
||||||
|
expected_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.actual.push(DiffLine::Change(
|
||||||
|
actual_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.actual_missing_nl = true;
|
||||||
|
results.push(mismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hunks with pure context lines get truncated to empty
|
||||||
|
for mismatch in &mut results {
|
||||||
|
if !mismatch
|
||||||
|
.expected
|
||||||
|
.iter()
|
||||||
|
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||||
|
{
|
||||||
|
mismatch.expected_all_context = true;
|
||||||
|
}
|
||||||
|
if !mismatch
|
||||||
|
.actual
|
||||||
|
.iter()
|
||||||
|
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||||
|
{
|
||||||
|
mismatch.actual_all_context = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||||
|
let from_modified_time = get_modification_time(¶ms.from.to_string_lossy());
|
||||||
|
let to_modified_time = get_modification_time(¶ms.to.to_string_lossy());
|
||||||
|
let mut output = format!(
|
||||||
|
"*** {0}\t{1}\n--- {2}\t{3}\n",
|
||||||
|
params.from.to_string_lossy(),
|
||||||
|
from_modified_time,
|
||||||
|
params.to.to_string_lossy(),
|
||||||
|
to_modified_time
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
|
||||||
|
if diff_results.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
if params.brief {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
for result in diff_results {
|
||||||
|
let mut line_number_expected = result.line_number_expected;
|
||||||
|
let mut line_number_actual = result.line_number_actual;
|
||||||
|
let mut expected_count = result.expected.len();
|
||||||
|
let mut actual_count = result.actual.len();
|
||||||
|
if expected_count == 0 {
|
||||||
|
line_number_expected -= 1;
|
||||||
|
expected_count = 1;
|
||||||
|
}
|
||||||
|
if actual_count == 0 {
|
||||||
|
line_number_actual -= 1;
|
||||||
|
actual_count = 1;
|
||||||
|
}
|
||||||
|
let end_line_number_expected = expected_count + line_number_expected - 1;
|
||||||
|
let end_line_number_actual = actual_count + line_number_actual - 1;
|
||||||
|
let exp_start = if end_line_number_expected == line_number_expected {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{line_number_expected},")
|
||||||
|
};
|
||||||
|
let act_start = if end_line_number_actual == line_number_actual {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{line_number_actual},")
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"***************\n*** {exp_start}{end_line_number_expected} ****"
|
||||||
|
)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
if !result.expected_all_context {
|
||||||
|
for line in result.expected {
|
||||||
|
match line {
|
||||||
|
DiffLine::Context(e) => {
|
||||||
|
write!(output, " ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Change(e) => {
|
||||||
|
write!(output, "! ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Add(e) => {
|
||||||
|
write!(output, "- ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.expected_missing_nl {
|
||||||
|
writeln!(output, r"\ No newline at end of file")
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(output, "--- {act_start}{end_line_number_actual} ----")
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
if !result.actual_all_context {
|
||||||
|
for line in result.actual {
|
||||||
|
match line {
|
||||||
|
DiffLine::Context(e) => {
|
||||||
|
write!(output, " ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Change(e) => {
|
||||||
|
write!(output, "! ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Add(e) => {
|
||||||
|
write!(output, "+ ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.actual_missing_nl {
|
||||||
|
writeln!(output, r"\ No newline at end of file")
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
#[test]
|
||||||
|
fn test_permutations() {
|
||||||
|
// test all possible six-line files.
|
||||||
|
let target = "target/context-diff/";
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alef".into(),
|
||||||
|
to: (&format!("{target}/alef")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/ab.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--context")
|
||||||
|
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alef")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_empty_lines() {
|
||||||
|
let target = "target/context-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alef_".into(),
|
||||||
|
to: (&format!("{target}/alef_")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/ab_.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--context")
|
||||||
|
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alef_")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_missing_lines() {
|
||||||
|
let target = "target/context-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
if alef.is_empty() && bet.is_empty() {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alefx".into(),
|
||||||
|
to: (&format!("{target}/alefx")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/abx.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefx")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betx")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--context")
|
||||||
|
.stdin(File::open(format!("{target}/abx.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefx")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_reverse() {
|
||||||
|
let target = "target/context-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"a\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"c\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"e\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alefr".into(),
|
||||||
|
to: (&format!("{target}/alefr")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/abr.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--context")
|
||||||
|
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefr")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop_early() {
|
||||||
|
use crate::assert_diff_eq;
|
||||||
|
|
||||||
|
let from_filename = "foo";
|
||||||
|
let from = ["a", "b", "c", ""].join("\n");
|
||||||
|
let to_filename = "bar";
|
||||||
|
let to = ["a", "d", "c", ""].join("\n");
|
||||||
|
|
||||||
|
let diff_full = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_full = [
|
||||||
|
"*** foo\tTIMESTAMP",
|
||||||
|
"--- bar\tTIMESTAMP",
|
||||||
|
"***************",
|
||||||
|
"*** 1,3 ****",
|
||||||
|
" a",
|
||||||
|
"! b",
|
||||||
|
" c",
|
||||||
|
"--- 1,3 ----",
|
||||||
|
" a",
|
||||||
|
"! d",
|
||||||
|
" c",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
assert_diff_eq!(diff_full, expected_full);
|
||||||
|
|
||||||
|
let diff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n");
|
||||||
|
assert_diff_eq!(diff_brief, expected_brief);
|
||||||
|
|
||||||
|
let nodiff_full = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(nodiff_full.is_empty());
|
||||||
|
|
||||||
|
let nodiff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(nodiff_brief.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use crate::params::{parse_params, Format};
|
||||||
|
use crate::utils::report_failure_to_read_input_file;
|
||||||
|
use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff};
|
||||||
|
use std::env::ArgsOs;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, stdout, Read, Write};
|
||||||
|
use std::iter::Peekable;
|
||||||
|
use std::process::{exit, ExitCode};
|
||||||
|
|
||||||
|
// Exit codes are documented at
|
||||||
|
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
|
||||||
|
// An exit status of 0 means no differences were found,
|
||||||
|
// 1 means some differences were found,
|
||||||
|
// and 2 means trouble.
|
||||||
|
pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
|
||||||
|
let params = parse_params(opts).unwrap_or_else(|error| {
|
||||||
|
eprintln!("{error}");
|
||||||
|
exit(2);
|
||||||
|
});
|
||||||
|
// if from and to are the same file, no need to perform any comparison
|
||||||
|
let maybe_report_identical_files = || {
|
||||||
|
if params.report_identical_files {
|
||||||
|
println!(
|
||||||
|
"Files {} and {} are identical",
|
||||||
|
params.from.to_string_lossy(),
|
||||||
|
params.to.to_string_lossy(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if params.from == "-" && params.to == "-"
|
||||||
|
|| same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false)
|
||||||
|
{
|
||||||
|
maybe_report_identical_files();
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read files
|
||||||
|
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
|
||||||
|
if filepath == "-" {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
io::stdin().read_to_end(&mut content).and(Ok(content))
|
||||||
|
} else {
|
||||||
|
fs::read(filepath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut io_error = false;
|
||||||
|
let from_content = match read_file_contents(¶ms.from) {
|
||||||
|
Ok(from_content) => from_content,
|
||||||
|
Err(e) => {
|
||||||
|
report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e);
|
||||||
|
io_error = true;
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let to_content = match read_file_contents(¶ms.to) {
|
||||||
|
Ok(to_content) => to_content,
|
||||||
|
Err(e) => {
|
||||||
|
report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e);
|
||||||
|
io_error = true;
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if io_error {
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// run diff
|
||||||
|
let result: Vec<u8> = match params.format {
|
||||||
|
Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms),
|
||||||
|
Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms),
|
||||||
|
Format::Context => context_diff::diff(&from_content, &to_content, ¶ms),
|
||||||
|
Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| {
|
||||||
|
eprintln!("{error}");
|
||||||
|
exit(2);
|
||||||
|
}),
|
||||||
|
Format::SideBySide => {
|
||||||
|
let mut output = stdout().lock();
|
||||||
|
side_diff::diff(&from_content, &to_content, &mut output, ¶ms)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if params.brief && !result.is_empty() {
|
||||||
|
println!(
|
||||||
|
"Files {} and {} differ",
|
||||||
|
params.from.to_string_lossy(),
|
||||||
|
params.to.to_string_lossy()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
io::stdout().write_all(&result).unwrap();
|
||||||
|
}
|
||||||
|
if result.is_empty() {
|
||||||
|
maybe_report_identical_files();
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else {
|
||||||
|
ExitCode::from(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+449
@@ -0,0 +1,449 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::params::Params;
|
||||||
|
use crate::utils::do_write_line;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Mismatch {
|
||||||
|
pub line_number_expected: usize,
|
||||||
|
pub line_number_actual: usize,
|
||||||
|
pub expected: Vec<Vec<u8>>,
|
||||||
|
pub actual: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum DiffError {
|
||||||
|
MissingNL,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DiffError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
std::fmt::Display::fmt("No newline at end of file", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DiffError> for String {
|
||||||
|
fn from(_: DiffError) -> String {
|
||||||
|
"No newline at end of file".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mismatch {
|
||||||
|
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||||
|
Mismatch {
|
||||||
|
line_number_expected,
|
||||||
|
line_number_actual,
|
||||||
|
expected: Vec::new(),
|
||||||
|
actual: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a diff between the expected output and actual output.
|
||||||
|
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<Mismatch>, DiffError> {
|
||||||
|
let mut line_number_expected = 1;
|
||||||
|
let mut line_number_actual = 1;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
|
||||||
|
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||||
|
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||||
|
|
||||||
|
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||||
|
// ^ means that underflow here is impossible
|
||||||
|
let _expected_lines_count = expected_lines.len() - 1;
|
||||||
|
let _actual_lines_count = actual_lines.len() - 1;
|
||||||
|
|
||||||
|
if expected_lines.last() == Some(&&b""[..]) {
|
||||||
|
expected_lines.pop();
|
||||||
|
} else {
|
||||||
|
return Err(DiffError::MissingNL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual_lines.last() == Some(&&b""[..]) {
|
||||||
|
actual_lines.pop();
|
||||||
|
} else {
|
||||||
|
return Err(DiffError::MissingNL);
|
||||||
|
}
|
||||||
|
|
||||||
|
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||||
|
match result {
|
||||||
|
diff::Result::Left(str) => {
|
||||||
|
if !mismatch.actual.is_empty() {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
}
|
||||||
|
mismatch.expected.push(str.to_vec());
|
||||||
|
line_number_expected += 1;
|
||||||
|
}
|
||||||
|
diff::Result::Right(str) => {
|
||||||
|
mismatch.actual.push(str.to_vec());
|
||||||
|
line_number_actual += 1;
|
||||||
|
}
|
||||||
|
diff::Result::Both(_str, _) => {
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
} else {
|
||||||
|
mismatch.line_number_expected = line_number_expected;
|
||||||
|
mismatch.line_number_actual = line_number_actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop_early && !results.is_empty() {
|
||||||
|
// Optimization: stop analyzing the files as soon as there are any differences
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||||
|
results.push(mismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result<Vec<u8>, DiffError> {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
let diff_results = make_diff(expected, actual, params.brief)?;
|
||||||
|
if params.brief && !diff_results.is_empty() {
|
||||||
|
write!(&mut output, "\0").unwrap();
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
|
let mut lines_offset = 0;
|
||||||
|
for result in diff_results {
|
||||||
|
let line_number_expected: isize = result.line_number_expected as isize + lines_offset;
|
||||||
|
let _line_number_actual: isize = result.line_number_actual as isize + lines_offset;
|
||||||
|
let expected_count: isize = result.expected.len() as isize;
|
||||||
|
let actual_count: isize = result.actual.len() as isize;
|
||||||
|
match (expected_count, actual_count) {
|
||||||
|
(0, 0) => unreachable!(),
|
||||||
|
(0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(),
|
||||||
|
(_, 0) => writeln!(
|
||||||
|
&mut output,
|
||||||
|
"{},{}d",
|
||||||
|
line_number_expected,
|
||||||
|
expected_count + line_number_expected - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
(1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(),
|
||||||
|
_ => writeln!(
|
||||||
|
&mut output,
|
||||||
|
"{},{}c",
|
||||||
|
line_number_expected,
|
||||||
|
expected_count + line_number_expected - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
lines_offset += actual_count - expected_count;
|
||||||
|
if actual_count != 0 {
|
||||||
|
for actual in &result.actual {
|
||||||
|
if actual == b"." {
|
||||||
|
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
|
||||||
|
} else {
|
||||||
|
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||||
|
writeln!(&mut output).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(&mut output, ".").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||||
|
let mut output = diff(expected, actual, &Params::default())?;
|
||||||
|
writeln!(&mut output, "w {filename}").unwrap();
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let from = b"a\n";
|
||||||
|
let to = b"b\n";
|
||||||
|
let diff = diff(from, to, &Params::default()).unwrap();
|
||||||
|
let expected = ["1c", "b", ".", ""].join("\n");
|
||||||
|
assert_eq!(diff, expected.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations() {
|
||||||
|
let target = "target/ed-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap();
|
||||||
|
File::create(format!("{target}/ab.ed"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
#[cfg(not(windows))] // there's no ed on windows
|
||||||
|
{
|
||||||
|
use std::process::Command;
|
||||||
|
let output = Command::new("ed")
|
||||||
|
.arg(format!("{target}/alef"))
|
||||||
|
.stdin(File::open(format!("{target}/ab.ed")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = std::fs::read(format!("{target}/alef")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_empty_lines() {
|
||||||
|
let target = "target/ed-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap();
|
||||||
|
File::create(format!("{target}/ab_.ed"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
#[cfg(not(windows))] // there's no ed on windows
|
||||||
|
{
|
||||||
|
use std::process::Command;
|
||||||
|
let output = Command::new("ed")
|
||||||
|
.arg(format!("{target}/alef_"))
|
||||||
|
.stdin(File::open(format!("{target}/ab_.ed")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = std::fs::read(format!("{target}/alef_")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_reverse() {
|
||||||
|
let target = "target/ed-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"a\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"c\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"e\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
|
||||||
|
File::create(format!("{target}/abr.ed"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
#[cfg(not(windows))] // there's no ed on windows
|
||||||
|
{
|
||||||
|
use std::process::Command;
|
||||||
|
let output = Command::new("ed")
|
||||||
|
.arg(format!("{target}/alefr"))
|
||||||
|
.stdin(File::open(format!("{target}/abr.ed")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = std::fs::read(format!("{target}/alefr")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop_early() {
|
||||||
|
let from = ["a", "b", "c", ""].join("\n");
|
||||||
|
let to = ["a", "d", "c", ""].join("\n");
|
||||||
|
|
||||||
|
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()).unwrap();
|
||||||
|
let expected_full = ["2c", "d", ".", ""].join("\n");
|
||||||
|
assert_eq!(diff_full, expected_full.as_bytes());
|
||||||
|
|
||||||
|
let diff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let expected_brief = "\0".as_bytes();
|
||||||
|
assert_eq!(diff_brief, expected_brief);
|
||||||
|
|
||||||
|
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()).unwrap();
|
||||||
|
assert!(nodiff_full.is_empty());
|
||||||
|
|
||||||
|
let nodiff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(nodiff_brief.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-781
@@ -1,782 +1,16 @@
|
|||||||
use std::collections::VecDeque;
|
pub mod cmp;
|
||||||
use std::io::Write;
|
pub mod context_diff;
|
||||||
|
pub mod ed_diff;
|
||||||
|
pub mod macros;
|
||||||
|
pub mod normal_diff;
|
||||||
|
pub mod params;
|
||||||
|
pub mod side_diff;
|
||||||
|
pub mod unified_diff;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
// Re-export the public functions/types you need
|
||||||
pub enum DiffLine {
|
pub use context_diff::diff as context_diff;
|
||||||
Context(Vec<u8>),
|
pub use ed_diff::diff as ed_diff;
|
||||||
Expected(Vec<u8>),
|
pub use normal_diff::diff as normal_diff;
|
||||||
Actual(Vec<u8>),
|
pub use side_diff::diff as side_by_side_diff;
|
||||||
MissingNL,
|
pub use unified_diff::diff as unified_diff;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
struct Mismatch {
|
|
||||||
pub line_number_expected: u32,
|
|
||||||
pub line_number_actual: u32,
|
|
||||||
pub lines: Vec<DiffLine>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mismatch {
|
|
||||||
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
|
|
||||||
Mismatch {
|
|
||||||
line_number_expected,
|
|
||||||
line_number_actual,
|
|
||||||
lines: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produces a diff between the expected output and actual output.
|
|
||||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
|
||||||
let mut line_number_expected = 1;
|
|
||||||
let mut line_number_actual = 1;
|
|
||||||
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
|
||||||
let mut lines_since_mismatch = context_size + 1;
|
|
||||||
let mut results = Vec::new();
|
|
||||||
let mut mismatch = Mismatch::new(0, 0);
|
|
||||||
|
|
||||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
|
||||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
|
||||||
|
|
||||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
|
||||||
// ^ means that underflow here is impossible
|
|
||||||
let expected_lines_count = expected_lines.len() as u32 - 1;
|
|
||||||
let actual_lines_count = actual_lines.len() as u32 - 1;
|
|
||||||
|
|
||||||
if expected_lines.last() == Some(&&b""[..]) {
|
|
||||||
expected_lines.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if actual_lines.last() == Some(&&b""[..]) {
|
|
||||||
actual_lines.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
|
||||||
match result {
|
|
||||||
diff::Result::Left(str) => {
|
|
||||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
|
||||||
results.push(mismatch);
|
|
||||||
mismatch = Mismatch::new(
|
|
||||||
line_number_expected - context_queue.len() as u32,
|
|
||||||
line_number_actual - context_queue.len() as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(line) = context_queue.pop_front() {
|
|
||||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
|
|
||||||
mismatch.lines.pop();
|
|
||||||
match mismatch.lines.pop() {
|
|
||||||
Some(DiffLine::Actual(res)) => {
|
|
||||||
// We have to make sure that Actual (the + lines)
|
|
||||||
// always come after Expected (the - lines)
|
|
||||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
|
||||||
if line_number_expected > expected_lines_count {
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL)
|
|
||||||
}
|
|
||||||
mismatch.lines.push(DiffLine::Actual(res));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
}
|
|
||||||
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
|
||||||
if line_number_expected > expected_lines_count {
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line_number_expected += 1;
|
|
||||||
lines_since_mismatch = 0;
|
|
||||||
}
|
|
||||||
diff::Result::Right(str) => {
|
|
||||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
|
||||||
results.push(mismatch);
|
|
||||||
mismatch = Mismatch::new(
|
|
||||||
line_number_expected - context_queue.len() as u32,
|
|
||||||
line_number_actual - context_queue.len() as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(line) = context_queue.pop_front() {
|
|
||||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
|
||||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
|
||||||
}
|
|
||||||
|
|
||||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
|
||||||
if line_number_actual > actual_lines_count {
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL)
|
|
||||||
}
|
|
||||||
line_number_actual += 1;
|
|
||||||
lines_since_mismatch = 0;
|
|
||||||
}
|
|
||||||
diff::Result::Both(str, _) => {
|
|
||||||
// if one of them is missing a newline and the other isn't, then they don't actually match
|
|
||||||
if (line_number_actual > actual_lines_count)
|
|
||||||
&& (line_number_expected > expected_lines_count)
|
|
||||||
{
|
|
||||||
if context_queue.len() < context_size {
|
|
||||||
while let Some(line) = context_queue.pop_front() {
|
|
||||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
|
||||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
|
||||||
}
|
|
||||||
if lines_since_mismatch < context_size {
|
|
||||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines_since_mismatch = 0;
|
|
||||||
} else if line_number_actual > actual_lines_count {
|
|
||||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
|
||||||
results.push(mismatch);
|
|
||||||
mismatch = Mismatch::new(
|
|
||||||
line_number_expected - context_queue.len() as u32,
|
|
||||||
line_number_actual - context_queue.len() as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
while let Some(line) = context_queue.pop_front() {
|
|
||||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
|
||||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
|
||||||
}
|
|
||||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
|
||||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
lines_since_mismatch = 0;
|
|
||||||
} else if line_number_expected > expected_lines_count {
|
|
||||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
|
||||||
results.push(mismatch);
|
|
||||||
mismatch = Mismatch::new(
|
|
||||||
line_number_expected - context_queue.len() as u32,
|
|
||||||
line_number_actual - context_queue.len() as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
while let Some(line) = context_queue.pop_front() {
|
|
||||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
|
||||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
|
||||||
}
|
|
||||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
|
||||||
lines_since_mismatch = 0;
|
|
||||||
} else {
|
|
||||||
debug_assert!(context_queue.len() <= context_size);
|
|
||||||
if context_queue.len() >= context_size {
|
|
||||||
let _ = context_queue.pop_front();
|
|
||||||
}
|
|
||||||
if lines_since_mismatch < context_size {
|
|
||||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
|
||||||
} else if context_size > 0 {
|
|
||||||
context_queue.push_back(str);
|
|
||||||
}
|
|
||||||
lines_since_mismatch += 1;
|
|
||||||
}
|
|
||||||
line_number_expected += 1;
|
|
||||||
line_number_actual += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(mismatch);
|
|
||||||
results.remove(0);
|
|
||||||
|
|
||||||
if results.len() == 0 && expected_lines_count != actual_lines_count {
|
|
||||||
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
|
|
||||||
// empty diff and only expected lines has a missing line at end
|
|
||||||
if expected_lines_count != expected_lines.len() as u32 {
|
|
||||||
mismatch.lines.push(DiffLine::Expected(
|
|
||||||
expected_lines
|
|
||||||
.pop()
|
|
||||||
.expect("can't be empty; produced by split()")
|
|
||||||
.to_vec(),
|
|
||||||
));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
mismatch.lines.push(DiffLine::Actual(
|
|
||||||
actual_lines
|
|
||||||
.pop()
|
|
||||||
.expect("can't be empty; produced by split()")
|
|
||||||
.to_vec(),
|
|
||||||
));
|
|
||||||
results.push(mismatch);
|
|
||||||
} else if actual_lines_count != actual_lines.len() as u32 {
|
|
||||||
mismatch.lines.push(DiffLine::Expected(
|
|
||||||
expected_lines
|
|
||||||
.pop()
|
|
||||||
.expect("can't be empty; produced by split()")
|
|
||||||
.to_vec(),
|
|
||||||
));
|
|
||||||
mismatch.lines.push(DiffLine::Actual(
|
|
||||||
actual_lines
|
|
||||||
.pop()
|
|
||||||
.expect("can't be empty; produced by split()")
|
|
||||||
.to_vec(),
|
|
||||||
));
|
|
||||||
mismatch.lines.push(DiffLine::MissingNL);
|
|
||||||
results.push(mismatch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff(
|
|
||||||
expected: &[u8],
|
|
||||||
expected_filename: &str,
|
|
||||||
actual: &[u8],
|
|
||||||
actual_filename: &str,
|
|
||||||
context_size: usize,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let mut output =
|
|
||||||
format!("--- {}\t\n+++ {}\t\n", expected_filename, actual_filename).into_bytes();
|
|
||||||
let diff_results = make_diff(expected, actual, context_size);
|
|
||||||
if diff_results.len() == 0 {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
for result in diff_results {
|
|
||||||
let mut line_number_expected = result.line_number_expected;
|
|
||||||
let mut line_number_actual = result.line_number_actual;
|
|
||||||
let mut expected_count = 0;
|
|
||||||
let mut actual_count = 0;
|
|
||||||
for line in &result.lines {
|
|
||||||
match line {
|
|
||||||
DiffLine::Expected(_) => {
|
|
||||||
expected_count += 1;
|
|
||||||
}
|
|
||||||
DiffLine::Context(_) => {
|
|
||||||
expected_count += 1;
|
|
||||||
actual_count += 1;
|
|
||||||
}
|
|
||||||
DiffLine::Actual(_) => {
|
|
||||||
actual_count += 1;
|
|
||||||
}
|
|
||||||
DiffLine::MissingNL => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Let's imagine this diff file
|
|
||||||
//
|
|
||||||
// --- a/something
|
|
||||||
// +++ b/something
|
|
||||||
// @@ -2,0 +3,1 @@
|
|
||||||
// + x
|
|
||||||
//
|
|
||||||
// In the unified diff format as implemented by GNU diff and patch,
|
|
||||||
// this is an instruction to insert the x *after* the preexisting line 2,
|
|
||||||
// not before. You can demonstrate it this way:
|
|
||||||
//
|
|
||||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
|
|
||||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
|
||||||
// $ patch -p1 < diff
|
|
||||||
// patching file something
|
|
||||||
// $ cat something
|
|
||||||
// a
|
|
||||||
// b
|
|
||||||
// x
|
|
||||||
// c
|
|
||||||
// d
|
|
||||||
//
|
|
||||||
// Notice how the x winds up at line 3, not line 2. This requires contortions to
|
|
||||||
// work with our diffing algorithm, which keeps track of the "intended destination line",
|
|
||||||
// not a line that things are supposed to be placed after. It's changing the first number,
|
|
||||||
// not the second, that actually affects where the x goes.
|
|
||||||
//
|
|
||||||
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
|
|
||||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
|
|
||||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
|
||||||
// $ patch -p1 < diff
|
|
||||||
// patching file something
|
|
||||||
// $ cat something
|
|
||||||
// a
|
|
||||||
// b
|
|
||||||
// c
|
|
||||||
// x
|
|
||||||
// d
|
|
||||||
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
|
|
||||||
// # actually being read
|
|
||||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
|
|
||||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
|
||||||
// $ patch -p1 < diff
|
|
||||||
// patching file something
|
|
||||||
// $ cat something
|
|
||||||
// a
|
|
||||||
// b
|
|
||||||
// x
|
|
||||||
// c
|
|
||||||
// d
|
|
||||||
//
|
|
||||||
// Now watch what happens if I add a context line:
|
|
||||||
//
|
|
||||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
|
|
||||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
|
||||||
// $ patch -p1 < diff
|
|
||||||
// patching file something
|
|
||||||
// Hunk #1 succeeded at 3 (offset 1 line).
|
|
||||||
//
|
|
||||||
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
|
|
||||||
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
|
|
||||||
// line that the x will wind up on, and not the line immediately before.
|
|
||||||
//
|
|
||||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
|
|
||||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
|
||||||
// $ patch -p1 < diff
|
|
||||||
// patching file something
|
|
||||||
// $ cat something
|
|
||||||
// a
|
|
||||||
// b
|
|
||||||
// x
|
|
||||||
// c
|
|
||||||
// d
|
|
||||||
//
|
|
||||||
// I made this comment because this stuff is not obvious from GNU's
|
|
||||||
// documentation on the format at all.
|
|
||||||
if expected_count == 0 {
|
|
||||||
line_number_expected -= 1
|
|
||||||
}
|
|
||||||
if actual_count == 0 {
|
|
||||||
line_number_actual -= 1
|
|
||||||
}
|
|
||||||
let exp_ct = if expected_count == 1 {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(",{}", expected_count)
|
|
||||||
};
|
|
||||||
let act_ct = if actual_count == 1 {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(",{}", actual_count)
|
|
||||||
};
|
|
||||||
writeln!(
|
|
||||||
output,
|
|
||||||
"@@ -{}{} +{}{} @@",
|
|
||||||
line_number_expected, exp_ct, line_number_actual, act_ct
|
|
||||||
)
|
|
||||||
.expect("write to Vec is infallible");
|
|
||||||
for line in result.lines {
|
|
||||||
match line {
|
|
||||||
DiffLine::Expected(e) => {
|
|
||||||
write!(output, "-").expect("write to Vec is infallible");
|
|
||||||
output.write_all(&e).expect("write to Vec is infallible");
|
|
||||||
writeln!(output).unwrap();
|
|
||||||
}
|
|
||||||
DiffLine::Context(c) => {
|
|
||||||
write!(output, " ").expect("write to Vec is infallible");
|
|
||||||
output.write_all(&c).expect("write to Vec is infallible");
|
|
||||||
writeln!(output).unwrap();
|
|
||||||
}
|
|
||||||
DiffLine::Actual(r) => {
|
|
||||||
write!(output, "+",).expect("write to Vec is infallible");
|
|
||||||
output.write_all(&r).expect("write to Vec is infallible");
|
|
||||||
writeln!(output).unwrap();
|
|
||||||
}
|
|
||||||
DiffLine::MissingNL => {
|
|
||||||
writeln!(output, r"\ No newline at end of file")
|
|
||||||
.expect("write to Vec is infallible");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permutations() {
|
|
||||||
// test all possible six-line files.
|
|
||||||
for &a in &[0, 1, 2] {
|
|
||||||
for &b in &[0, 1, 2] {
|
|
||||||
for &c in &[0, 1, 2] {
|
|
||||||
for &d in &[0, 1, 2] {
|
|
||||||
for &e in &[0, 1, 2] {
|
|
||||||
for &f in &[0, 1, 2] {
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
|
||||||
let mut alef = Vec::new();
|
|
||||||
let mut bet = Vec::new();
|
|
||||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
|
||||||
.unwrap();
|
|
||||||
if a != 2 {
|
|
||||||
bet.write_all(b"b\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
|
||||||
.unwrap();
|
|
||||||
if b != 2 {
|
|
||||||
bet.write_all(b"d\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
|
||||||
.unwrap();
|
|
||||||
if c != 2 {
|
|
||||||
bet.write_all(b"f\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
|
||||||
.unwrap();
|
|
||||||
if d != 2 {
|
|
||||||
bet.write_all(b"h\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
|
||||||
.unwrap();
|
|
||||||
if e != 2 {
|
|
||||||
bet.write_all(b"j\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
|
||||||
.unwrap();
|
|
||||||
if f != 2 {
|
|
||||||
bet.write_all(b"l\n").unwrap();
|
|
||||||
}
|
|
||||||
// This test diff is intentionally reversed.
|
|
||||||
// We want it to turn the alef into bet.
|
|
||||||
let diff = diff(&alef, "a/alef", &bet, "target/alef", 2);
|
|
||||||
File::create("target/ab.diff")
|
|
||||||
.unwrap()
|
|
||||||
.write_all(&diff)
|
|
||||||
.unwrap();
|
|
||||||
let mut fa = File::create("target/alef").unwrap();
|
|
||||||
fa.write_all(&alef[..]).unwrap();
|
|
||||||
let mut fb = File::create("target/bet").unwrap();
|
|
||||||
fb.write_all(&bet[..]).unwrap();
|
|
||||||
let _ = fa;
|
|
||||||
let _ = fb;
|
|
||||||
let output = Command::new("patch")
|
|
||||||
.arg("-p0")
|
|
||||||
.stdin(File::open("target/ab.diff").unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
if !output.status.success() {
|
|
||||||
panic!("{:?}", output);
|
|
||||||
}
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
let alef = fs::read("target/alef").unwrap();
|
|
||||||
assert_eq!(alef, bet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permutations_missing_line_ending() {
|
|
||||||
// test all possible six-line files with missing newlines.
|
|
||||||
for &a in &[0, 1, 2] {
|
|
||||||
for &b in &[0, 1, 2] {
|
|
||||||
for &c in &[0, 1, 2] {
|
|
||||||
for &d in &[0, 1, 2] {
|
|
||||||
for &e in &[0, 1, 2] {
|
|
||||||
for &f in &[0, 1, 2] {
|
|
||||||
for &g in &[0, 1, 2] {
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
|
||||||
let mut alef = Vec::new();
|
|
||||||
let mut bet = Vec::new();
|
|
||||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
|
||||||
.unwrap();
|
|
||||||
if a != 2 {
|
|
||||||
bet.write_all(b"b\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
|
||||||
.unwrap();
|
|
||||||
if b != 2 {
|
|
||||||
bet.write_all(b"d\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
|
||||||
.unwrap();
|
|
||||||
if c != 2 {
|
|
||||||
bet.write_all(b"f\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
|
||||||
.unwrap();
|
|
||||||
if d != 2 {
|
|
||||||
bet.write_all(b"h\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
|
||||||
.unwrap();
|
|
||||||
if e != 2 {
|
|
||||||
bet.write_all(b"j\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
|
||||||
.unwrap();
|
|
||||||
if f != 2 {
|
|
||||||
bet.write_all(b"l\n").unwrap();
|
|
||||||
}
|
|
||||||
match g {
|
|
||||||
0 => {
|
|
||||||
alef.pop();
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
bet.pop();
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
alef.pop();
|
|
||||||
bet.pop();
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
// This test diff is intentionally reversed.
|
|
||||||
// We want it to turn the alef into bet.
|
|
||||||
let diff = diff(&alef, "a/alefn", &bet, "target/alefn", 2);
|
|
||||||
File::create("target/abn.diff")
|
|
||||||
.unwrap()
|
|
||||||
.write_all(&diff)
|
|
||||||
.unwrap();
|
|
||||||
let mut fa = File::create("target/alefn").unwrap();
|
|
||||||
fa.write_all(&alef[..]).unwrap();
|
|
||||||
let mut fb = File::create("target/betn").unwrap();
|
|
||||||
fb.write_all(&bet[..]).unwrap();
|
|
||||||
let _ = fa;
|
|
||||||
let _ = fb;
|
|
||||||
let output = Command::new("patch")
|
|
||||||
.arg("-p0")
|
|
||||||
.stdin(File::open("target/abn.diff").unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
if !output.status.success() {
|
|
||||||
panic!("{:?}", output);
|
|
||||||
}
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
let alef = fs::read("target/alefn").unwrap();
|
|
||||||
assert_eq!(alef, bet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permutations_empty_lines() {
|
|
||||||
// test all possible six-line files with missing newlines.
|
|
||||||
for &a in &[0, 1, 2] {
|
|
||||||
for &b in &[0, 1, 2] {
|
|
||||||
for &c in &[0, 1, 2] {
|
|
||||||
for &d in &[0, 1, 2] {
|
|
||||||
for &e in &[0, 1, 2] {
|
|
||||||
for &f in &[0, 1, 2] {
|
|
||||||
for &g in &[0, 1, 2, 3] {
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
|
||||||
let mut alef = Vec::new();
|
|
||||||
let mut bet = Vec::new();
|
|
||||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
|
||||||
if a != 2 {
|
|
||||||
bet.write_all(b"b\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
|
||||||
if b != 2 {
|
|
||||||
bet.write_all(b"d\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
|
||||||
if c != 2 {
|
|
||||||
bet.write_all(b"f\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
|
||||||
if d != 2 {
|
|
||||||
bet.write_all(b"h\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
|
||||||
if e != 2 {
|
|
||||||
bet.write_all(b"j\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
|
||||||
if f != 2 {
|
|
||||||
bet.write_all(b"l\n").unwrap();
|
|
||||||
}
|
|
||||||
match g {
|
|
||||||
0 => {
|
|
||||||
alef.pop();
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
bet.pop();
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
alef.pop();
|
|
||||||
bet.pop();
|
|
||||||
}
|
|
||||||
3 => {}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
// This test diff is intentionally reversed.
|
|
||||||
// We want it to turn the alef into bet.
|
|
||||||
let diff = diff(&alef, "a/alef_", &bet, "target/alef_", 2);
|
|
||||||
File::create("target/ab_.diff")
|
|
||||||
.unwrap()
|
|
||||||
.write_all(&diff)
|
|
||||||
.unwrap();
|
|
||||||
let mut fa = File::create("target/alef_").unwrap();
|
|
||||||
fa.write_all(&alef[..]).unwrap();
|
|
||||||
let mut fb = File::create("target/bet_").unwrap();
|
|
||||||
fb.write_all(&bet[..]).unwrap();
|
|
||||||
let _ = fa;
|
|
||||||
let _ = fb;
|
|
||||||
let output = Command::new("patch")
|
|
||||||
.arg("-p0")
|
|
||||||
.stdin(File::open("target/ab_.diff").unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
if !output.status.success() {
|
|
||||||
panic!("{:?}", output);
|
|
||||||
}
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
let alef = fs::read("target/alef_").unwrap();
|
|
||||||
assert_eq!(alef, bet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permutations_missing_lines() {
|
|
||||||
// test all possible six-line files.
|
|
||||||
for &a in &[0, 1, 2] {
|
|
||||||
for &b in &[0, 1, 2] {
|
|
||||||
for &c in &[0, 1, 2] {
|
|
||||||
for &d in &[0, 1, 2] {
|
|
||||||
for &e in &[0, 1, 2] {
|
|
||||||
for &f in &[0, 1, 2] {
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
|
||||||
let mut alef = Vec::new();
|
|
||||||
let mut bet = Vec::new();
|
|
||||||
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
|
||||||
if a != 2 {
|
|
||||||
bet.write_all(b"b\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
|
||||||
if b != 2 {
|
|
||||||
bet.write_all(b"d\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
|
||||||
if c != 2 {
|
|
||||||
bet.write_all(b"f\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
|
||||||
if d != 2 {
|
|
||||||
bet.write_all(b"h\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
|
||||||
if e != 2 {
|
|
||||||
bet.write_all(b"j\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
|
||||||
if f != 2 {
|
|
||||||
bet.write_all(b"l\n").unwrap();
|
|
||||||
}
|
|
||||||
// This test diff is intentionally reversed.
|
|
||||||
// We want it to turn the alef into bet.
|
|
||||||
let diff = diff(&alef, "a/alefx", &bet, "target/alefx", 2);
|
|
||||||
File::create("target/abx.diff")
|
|
||||||
.unwrap()
|
|
||||||
.write_all(&diff)
|
|
||||||
.unwrap();
|
|
||||||
let mut fa = File::create("target/alefx").unwrap();
|
|
||||||
fa.write_all(&alef[..]).unwrap();
|
|
||||||
let mut fb = File::create("target/betx").unwrap();
|
|
||||||
fb.write_all(&bet[..]).unwrap();
|
|
||||||
let _ = fa;
|
|
||||||
let _ = fb;
|
|
||||||
let output = Command::new("patch")
|
|
||||||
.arg("-p0")
|
|
||||||
.stdin(File::open("target/abx.diff").unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
if !output.status.success() {
|
|
||||||
panic!("{:?}", output);
|
|
||||||
}
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
let alef = fs::read("target/alefx").unwrap();
|
|
||||||
assert_eq!(alef, bet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permutations_reverse() {
|
|
||||||
// test all possible six-line files.
|
|
||||||
for &a in &[0, 1, 2] {
|
|
||||||
for &b in &[0, 1, 2] {
|
|
||||||
for &c in &[0, 1, 2] {
|
|
||||||
for &d in &[0, 1, 2] {
|
|
||||||
for &e in &[0, 1, 2] {
|
|
||||||
for &f in &[0, 1, 2] {
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
|
||||||
let mut alef = Vec::new();
|
|
||||||
let mut bet = Vec::new();
|
|
||||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
|
||||||
.unwrap();
|
|
||||||
if a != 2 {
|
|
||||||
bet.write_all(b"a\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
|
||||||
.unwrap();
|
|
||||||
if b != 2 {
|
|
||||||
bet.write_all(b"b\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
|
||||||
.unwrap();
|
|
||||||
if c != 2 {
|
|
||||||
bet.write_all(b"c\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
|
||||||
.unwrap();
|
|
||||||
if d != 2 {
|
|
||||||
bet.write_all(b"d\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
|
||||||
.unwrap();
|
|
||||||
if e != 2 {
|
|
||||||
bet.write_all(b"e\n").unwrap();
|
|
||||||
}
|
|
||||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
|
||||||
.unwrap();
|
|
||||||
if f != 2 {
|
|
||||||
bet.write_all(b"f\n").unwrap();
|
|
||||||
}
|
|
||||||
// This test diff is intentionally reversed.
|
|
||||||
// We want it to turn the alef into bet.
|
|
||||||
let diff = diff(&alef, "a/alefr", &bet, "target/alefr", 2);
|
|
||||||
File::create("target/abr.diff")
|
|
||||||
.unwrap()
|
|
||||||
.write_all(&diff)
|
|
||||||
.unwrap();
|
|
||||||
let mut fa = File::create("target/alefr").unwrap();
|
|
||||||
fa.write_all(&alef[..]).unwrap();
|
|
||||||
let mut fb = File::create("target/betr").unwrap();
|
|
||||||
fb.write_all(&bet[..]).unwrap();
|
|
||||||
let _ = fa;
|
|
||||||
let _ = fb;
|
|
||||||
let output = Command::new("patch")
|
|
||||||
.arg("-p0")
|
|
||||||
.stdin(File::open("target/abr.diff").unwrap())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
if !output.status.success() {
|
|
||||||
panic!("{:?}", output);
|
|
||||||
}
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
let alef = fs::read("target/alefr").unwrap();
|
|
||||||
assert_eq!(alef, bet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// asserts equality of the actual diff and expected diff
|
||||||
|
// considering datetime varitations
|
||||||
|
//
|
||||||
|
// It replaces the modification time in the actual diff
|
||||||
|
// with placeholder "TIMESTAMP" and then asserts the equality
|
||||||
|
//
|
||||||
|
// For eg.
|
||||||
|
// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n
|
||||||
|
// --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n";
|
||||||
|
//
|
||||||
|
// replaced = "*** fruits_old.txt\tTIMESTAMP\n
|
||||||
|
// --- fruits_new.txt\tTIMESTAMP\n";
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_diff_eq {
|
||||||
|
($actual:expr, $expected:expr) => {{
|
||||||
|
use regex::Regex;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
let diff = str::from_utf8(&$actual).unwrap();
|
||||||
|
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
|
||||||
|
let actual = re.replacen(diff, 2, "TIMESTAMP");
|
||||||
|
|
||||||
|
assert_eq!(actual, $expected);
|
||||||
|
}};
|
||||||
|
}
|
||||||
+80
-54
@@ -1,55 +1,81 @@
|
|||||||
// Sample program. Do not use.
|
// This file is part of the uutils diffutils package.
|
||||||
use std::env;
|
//
|
||||||
use std::fs;
|
// For the full copyright and license information, please view the LICENSE-*
|
||||||
use std::io::{self, Write};
|
// files that was distributed with this source code.
|
||||||
use std::process;
|
|
||||||
fn main() {
|
use std::{
|
||||||
let mut o = env::args_os();
|
env::ArgsOs,
|
||||||
// parse CLI
|
ffi::{OsStr, OsString},
|
||||||
let exe = match o.next() {
|
iter::Peekable,
|
||||||
Some(from) => from,
|
path::{Path, PathBuf},
|
||||||
None => {
|
process::ExitCode,
|
||||||
eprintln!("Usage: [exe] [from] [to]");
|
};
|
||||||
process::exit(1);
|
|
||||||
}
|
mod cmp;
|
||||||
};
|
mod context_diff;
|
||||||
let from = match o.next() {
|
mod diff;
|
||||||
Some(from) => from,
|
mod ed_diff;
|
||||||
None => {
|
mod macros;
|
||||||
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
|
mod normal_diff;
|
||||||
process::exit(1);
|
mod params;
|
||||||
}
|
mod side_diff;
|
||||||
};
|
mod unified_diff;
|
||||||
let to = match o.next() {
|
mod utils;
|
||||||
Some(from) => from,
|
|
||||||
None => {
|
/// # Panics
|
||||||
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
|
/// Panics if the binary path cannot be determined
|
||||||
process::exit(1);
|
fn binary_path(args: &mut Peekable<ArgsOs>) -> PathBuf {
|
||||||
}
|
match args.peek() {
|
||||||
};
|
Some(ref s) if !s.is_empty() => PathBuf::from(s),
|
||||||
// read files
|
_ => std::env::current_exe().unwrap(),
|
||||||
let from_content = match fs::read(&from) {
|
}
|
||||||
Ok(from_content) => from_content,
|
}
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to read from-file: {}", e);
|
/// #Panics
|
||||||
process::exit(2);
|
/// Panics if path has no UTF-8 valid name
|
||||||
}
|
fn name(binary_path: &Path) -> &OsStr {
|
||||||
};
|
binary_path.file_stem().unwrap()
|
||||||
let to_content = match fs::read(&to) {
|
}
|
||||||
Ok(to_content) => to_content,
|
|
||||||
Err(e) => {
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
eprintln!("Failed to read to-file: {}", e);
|
|
||||||
process::exit(2);
|
fn usage(name: &str) {
|
||||||
}
|
println!("{name} {VERSION} (multi-call binary)\n");
|
||||||
};
|
println!("Usage: {name} [function [arguments...]]\n");
|
||||||
// run diff
|
println!("Currently defined functions:\n");
|
||||||
io::stdout()
|
println!(" cmp, diff\n");
|
||||||
.write_all(&unified_diff::diff(
|
}
|
||||||
&from_content,
|
|
||||||
&from.to_string_lossy(),
|
fn second_arg_error(name: &OsStr) -> ! {
|
||||||
&to_content,
|
eprintln!("Expected utility name as second argument, got nothing.");
|
||||||
&to.to_string_lossy(),
|
usage(&name.to_string_lossy());
|
||||||
1,
|
std::process::exit(0);
|
||||||
))
|
}
|
||||||
.unwrap();
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let mut args = std::env::args_os().peekable();
|
||||||
|
|
||||||
|
let exe_path = binary_path(&mut args);
|
||||||
|
let exe_name = name(&exe_path);
|
||||||
|
|
||||||
|
let util_name = if exe_name == "diffutils" {
|
||||||
|
// Discard the item we peeked.
|
||||||
|
let _ = args.next();
|
||||||
|
|
||||||
|
args.peek()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| second_arg_error(exe_name))
|
||||||
|
} else {
|
||||||
|
OsString::from(exe_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
match util_name.to_str() {
|
||||||
|
Some("diff") => diff::main(args),
|
||||||
|
Some("cmp") => cmp::main(args),
|
||||||
|
Some(name) => {
|
||||||
|
eprintln!("{name}: utility not supported");
|
||||||
|
ExitCode::from(2)
|
||||||
|
}
|
||||||
|
None => second_arg_error(exe_name),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::params::Params;
|
||||||
|
use crate::utils::do_write_line;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Mismatch {
|
||||||
|
pub line_number_expected: usize,
|
||||||
|
pub line_number_actual: usize,
|
||||||
|
pub expected: Vec<Vec<u8>>,
|
||||||
|
pub actual: Vec<Vec<u8>>,
|
||||||
|
pub expected_missing_nl: bool,
|
||||||
|
pub actual_missing_nl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mismatch {
|
||||||
|
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||||
|
Mismatch {
|
||||||
|
line_number_expected,
|
||||||
|
line_number_actual,
|
||||||
|
expected: Vec::new(),
|
||||||
|
actual: Vec::new(),
|
||||||
|
expected_missing_nl: false,
|
||||||
|
actual_missing_nl: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a diff between the expected output and actual output.
|
||||||
|
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<Mismatch> {
|
||||||
|
let mut line_number_expected = 1;
|
||||||
|
let mut line_number_actual = 1;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
|
||||||
|
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||||
|
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||||
|
|
||||||
|
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||||
|
// ^ means that underflow here is impossible
|
||||||
|
let expected_lines_count = expected_lines.len() - 1;
|
||||||
|
let actual_lines_count = actual_lines.len() - 1;
|
||||||
|
|
||||||
|
if expected_lines.last() == Some(&&b""[..]) {
|
||||||
|
expected_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual_lines.last() == Some(&&b""[..]) {
|
||||||
|
actual_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||||
|
match result {
|
||||||
|
diff::Result::Left(str) => {
|
||||||
|
if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
}
|
||||||
|
mismatch.expected.push(str.to_vec());
|
||||||
|
mismatch.expected_missing_nl = line_number_expected > expected_lines_count;
|
||||||
|
line_number_expected += 1;
|
||||||
|
}
|
||||||
|
diff::Result::Right(str) => {
|
||||||
|
mismatch.actual.push(str.to_vec());
|
||||||
|
mismatch.actual_missing_nl = line_number_actual > actual_lines_count;
|
||||||
|
line_number_actual += 1;
|
||||||
|
}
|
||||||
|
diff::Result::Both(str, _) => {
|
||||||
|
match (
|
||||||
|
line_number_expected > expected_lines_count,
|
||||||
|
line_number_actual > actual_lines_count,
|
||||||
|
) {
|
||||||
|
(true, false) => {
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
mismatch.expected.push(str.to_vec());
|
||||||
|
mismatch.expected_missing_nl = true;
|
||||||
|
mismatch.actual.push(str.to_vec());
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
mismatch.actual.push(str.to_vec());
|
||||||
|
mismatch.actual_missing_nl = true;
|
||||||
|
mismatch.expected.push(str.to_vec());
|
||||||
|
}
|
||||||
|
(true, true) | (false, false) => {
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||||
|
} else {
|
||||||
|
mismatch.line_number_expected = line_number_expected;
|
||||||
|
mismatch.line_number_actual = line_number_actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop_early && !results.is_empty() {
|
||||||
|
// Optimization: stop analyzing the files as soon as there are any differences
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||||
|
results.push(mismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||||
|
// See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html
|
||||||
|
// for details on the syntax of the normal format.
|
||||||
|
let mut output = Vec::new();
|
||||||
|
let diff_results = make_diff(expected, actual, params.brief);
|
||||||
|
if params.brief && !diff_results.is_empty() {
|
||||||
|
write!(&mut output, "\0").unwrap();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
for result in diff_results {
|
||||||
|
let line_number_expected = result.line_number_expected;
|
||||||
|
let line_number_actual = result.line_number_actual;
|
||||||
|
let expected_count = result.expected.len();
|
||||||
|
let actual_count = result.actual.len();
|
||||||
|
match (expected_count, actual_count) {
|
||||||
|
(0, 0) => unreachable!(),
|
||||||
|
(0, _) => writeln!(
|
||||||
|
// 'a' stands for "Add lines"
|
||||||
|
&mut output,
|
||||||
|
"{}a{},{}",
|
||||||
|
line_number_expected - 1,
|
||||||
|
line_number_actual,
|
||||||
|
line_number_actual + actual_count - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
(_, 0) => writeln!(
|
||||||
|
// 'd' stands for "Delete lines"
|
||||||
|
&mut output,
|
||||||
|
"{},{}d{}",
|
||||||
|
line_number_expected,
|
||||||
|
expected_count + line_number_expected - 1,
|
||||||
|
line_number_actual - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
(1, 1) => writeln!(
|
||||||
|
// 'c' stands for "Change lines"
|
||||||
|
// exactly one line replaced by one line
|
||||||
|
&mut output,
|
||||||
|
"{line_number_expected}c{line_number_actual}"
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
(1, _) => writeln!(
|
||||||
|
// one line replaced by multiple lines
|
||||||
|
&mut output,
|
||||||
|
"{}c{},{}",
|
||||||
|
line_number_expected,
|
||||||
|
line_number_actual,
|
||||||
|
actual_count + line_number_actual - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
(_, 1) => writeln!(
|
||||||
|
// multiple lines replaced by one line
|
||||||
|
&mut output,
|
||||||
|
"{},{}c{}",
|
||||||
|
line_number_expected,
|
||||||
|
expected_count + line_number_expected - 1,
|
||||||
|
line_number_actual
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
_ => writeln!(
|
||||||
|
// general case: multiple lines replaced by multiple lines
|
||||||
|
&mut output,
|
||||||
|
"{},{}c{},{}",
|
||||||
|
line_number_expected,
|
||||||
|
expected_count + line_number_expected - 1,
|
||||||
|
line_number_actual,
|
||||||
|
actual_count + line_number_actual - 1
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
for expected in &result.expected {
|
||||||
|
write!(&mut output, "< ").unwrap();
|
||||||
|
do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap();
|
||||||
|
writeln!(&mut output).unwrap();
|
||||||
|
}
|
||||||
|
if result.expected_missing_nl {
|
||||||
|
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||||
|
}
|
||||||
|
if expected_count != 0 && actual_count != 0 {
|
||||||
|
writeln!(&mut output, "---").unwrap();
|
||||||
|
}
|
||||||
|
for actual in &result.actual {
|
||||||
|
write!(&mut output, "> ").unwrap();
|
||||||
|
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||||
|
writeln!(&mut output).unwrap();
|
||||||
|
}
|
||||||
|
if result.actual_missing_nl {
|
||||||
|
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let mut a = Vec::new();
|
||||||
|
a.write_all(b"a\n").unwrap();
|
||||||
|
let mut b = Vec::new();
|
||||||
|
b.write_all(b"b\n").unwrap();
|
||||||
|
let diff = diff(&a, &b, &Params::default());
|
||||||
|
let expected = b"1c1\n< a\n---\n> b\n".to_vec();
|
||||||
|
assert_eq!(diff, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations() {
|
||||||
|
let target = "target/normal-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(&alef, &bet, &Params::default());
|
||||||
|
File::create(format!("{target}/ab.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg(format!("{target}/alef"))
|
||||||
|
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alef")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_missing_line_ending() {
|
||||||
|
let target = "target/normal-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
for &g in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
match g {
|
||||||
|
0 => {
|
||||||
|
alef.pop();
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
alef.pop();
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(&alef, &bet, &Params::default());
|
||||||
|
File::create(format!("{target}/abn.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefn")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betn")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg("--normal")
|
||||||
|
.arg(format!("{target}/alefn"))
|
||||||
|
.stdin(File::open(format!("{target}/abn.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefn")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_empty_lines() {
|
||||||
|
let target = "target/normal-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(&alef, &bet, &Params::default());
|
||||||
|
File::create(format!("{target}/ab_.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg(format!("{target}/alef_"))
|
||||||
|
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alef_")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_reverse() {
|
||||||
|
let target = "target/normal-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"a\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"c\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"e\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(&alef, &bet, &Params::default());
|
||||||
|
File::create(format!("{target}/abr.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.arg(format!("{target}/alefr"))
|
||||||
|
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefr")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop_early() {
|
||||||
|
let from = ["a", "b", "c"].join("\n");
|
||||||
|
let to = ["a", "d", "c"].join("\n");
|
||||||
|
|
||||||
|
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default());
|
||||||
|
let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n");
|
||||||
|
assert_eq!(diff_full, expected_full.as_bytes());
|
||||||
|
|
||||||
|
let diff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let expected_brief = "\0".as_bytes();
|
||||||
|
assert_eq!(diff_brief, expected_brief);
|
||||||
|
|
||||||
|
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default());
|
||||||
|
assert!(nodiff_full.is_empty());
|
||||||
|
|
||||||
|
let nodiff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(nodiff_brief.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+919
@@ -0,0 +1,919 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::iter::Peekable;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum Format {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Unified,
|
||||||
|
Context,
|
||||||
|
Ed,
|
||||||
|
SideBySide,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Params {
|
||||||
|
pub executable: OsString,
|
||||||
|
pub from: OsString,
|
||||||
|
pub to: OsString,
|
||||||
|
pub format: Format,
|
||||||
|
pub context_count: usize,
|
||||||
|
pub report_identical_files: bool,
|
||||||
|
pub brief: bool,
|
||||||
|
pub expand_tabs: bool,
|
||||||
|
pub tabsize: usize,
|
||||||
|
pub width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Params {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
executable: OsString::default(),
|
||||||
|
from: OsString::default(),
|
||||||
|
to: OsString::default(),
|
||||||
|
format: Format::default(),
|
||||||
|
context_count: 3,
|
||||||
|
report_identical_files: false,
|
||||||
|
brief: false,
|
||||||
|
expand_tabs: false,
|
||||||
|
tabsize: 8,
|
||||||
|
width: 130,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
|
||||||
|
// parse CLI
|
||||||
|
|
||||||
|
let Some(executable) = opts.next() else {
|
||||||
|
return Err("Usage: <exe> <from> <to>".to_string());
|
||||||
|
};
|
||||||
|
let mut params = Params {
|
||||||
|
executable,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut from = None;
|
||||||
|
let mut to = None;
|
||||||
|
let mut format = None;
|
||||||
|
let mut context = None;
|
||||||
|
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
|
||||||
|
let width_re = Regex::new(r"--width=(?P<long>\d+)$").unwrap();
|
||||||
|
while let Some(param) = opts.next() {
|
||||||
|
let next_param = opts.peek();
|
||||||
|
if param == "--" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if param == "-" {
|
||||||
|
if from.is_none() {
|
||||||
|
from = Some(param);
|
||||||
|
} else if to.is_none() {
|
||||||
|
to = Some(param);
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Usage: {} <from> <to>",
|
||||||
|
params.executable.to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "-s" || param == "--report-identical-files" {
|
||||||
|
params.report_identical_files = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "-q" || param == "--brief" {
|
||||||
|
params.brief = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "-t" || param == "--expand-tabs" {
|
||||||
|
params.expand_tabs = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "--normal" {
|
||||||
|
if format.is_some() && format != Some(Format::Normal) {
|
||||||
|
return Err("Conflicting output style options".to_string());
|
||||||
|
}
|
||||||
|
format = Some(Format::Normal);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "-e" || param == "--ed" {
|
||||||
|
if format.is_some() && format != Some(Format::Ed) {
|
||||||
|
return Err("Conflicting output style options".to_string());
|
||||||
|
}
|
||||||
|
format = Some(Format::Ed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if param == "-y" || param == "--side-by-side" {
|
||||||
|
if format.is_some() && format != Some(Format::SideBySide) {
|
||||||
|
return Err("Conflicting output style option".to_string());
|
||||||
|
}
|
||||||
|
format = Some(Format::SideBySide);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if width_re.is_match(param.to_string_lossy().as_ref()) {
|
||||||
|
let param = param.into_string().unwrap();
|
||||||
|
let width_str: &str = width_re
|
||||||
|
.captures(param.as_str())
|
||||||
|
.unwrap()
|
||||||
|
.name("long")
|
||||||
|
.unwrap()
|
||||||
|
.as_str();
|
||||||
|
|
||||||
|
params.width = match width_str.parse::<usize>() {
|
||||||
|
Ok(num) => {
|
||||||
|
if num == 0 {
|
||||||
|
return Err("invalid width «0»".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
num
|
||||||
|
}
|
||||||
|
Err(_) => return Err(format!("invalid width «{width_str}»")),
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
|
||||||
|
// Because param matches the regular expression,
|
||||||
|
// it is safe to assume it is valid UTF-8.
|
||||||
|
let param = param.into_string().unwrap();
|
||||||
|
let tabsize_str = tabsize_re
|
||||||
|
.captures(param.as_str())
|
||||||
|
.unwrap()
|
||||||
|
.name("num")
|
||||||
|
.unwrap()
|
||||||
|
.as_str();
|
||||||
|
params.tabsize = match tabsize_str.parse::<usize>() {
|
||||||
|
Ok(num) => {
|
||||||
|
if num == 0 {
|
||||||
|
return Err("invalid tabsize «0»".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
num
|
||||||
|
}
|
||||||
|
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match match_context_diff_params(¶m, next_param, format) {
|
||||||
|
Ok(DiffStyleMatch {
|
||||||
|
is_match,
|
||||||
|
context_count,
|
||||||
|
next_param_consumed,
|
||||||
|
}) => {
|
||||||
|
if is_match {
|
||||||
|
format = Some(Format::Context);
|
||||||
|
if context_count.is_some() {
|
||||||
|
context = context_count;
|
||||||
|
}
|
||||||
|
if next_param_consumed {
|
||||||
|
opts.next();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
match match_unified_diff_params(¶m, next_param, format) {
|
||||||
|
Ok(DiffStyleMatch {
|
||||||
|
is_match,
|
||||||
|
context_count,
|
||||||
|
next_param_consumed,
|
||||||
|
}) => {
|
||||||
|
if is_match {
|
||||||
|
format = Some(Format::Unified);
|
||||||
|
if context_count.is_some() {
|
||||||
|
context = context_count;
|
||||||
|
}
|
||||||
|
if next_param_consumed {
|
||||||
|
opts.next();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
if param.to_string_lossy().starts_with('-') {
|
||||||
|
return Err(format!("unrecognized option: {param:?}"));
|
||||||
|
}
|
||||||
|
if from.is_none() {
|
||||||
|
from = Some(param);
|
||||||
|
} else if to.is_none() {
|
||||||
|
to = Some(param);
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Usage: {} <from> <to>",
|
||||||
|
params.executable.to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.from = if let Some(from) = from {
|
||||||
|
from
|
||||||
|
} else if let Some(param) = opts.next() {
|
||||||
|
param
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Usage: {} <from> <to>",
|
||||||
|
params.executable.to_string_lossy()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
params.to = if let Some(to) = to {
|
||||||
|
to
|
||||||
|
} else if let Some(param) = opts.next() {
|
||||||
|
param
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Usage: {} <from> <to>",
|
||||||
|
params.executable.to_string_lossy()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// diff DIRECTORY FILE => diff DIRECTORY/FILE FILE
|
||||||
|
// diff FILE DIRECTORY => diff FILE DIRECTORY/FILE
|
||||||
|
let mut from_path: PathBuf = PathBuf::from(¶ms.from);
|
||||||
|
let mut to_path: PathBuf = PathBuf::from(¶ms.to);
|
||||||
|
|
||||||
|
if from_path.is_dir() && to_path.is_file() {
|
||||||
|
from_path.push(to_path.file_name().unwrap());
|
||||||
|
params.from = from_path.into_os_string();
|
||||||
|
} else if from_path.is_file() && to_path.is_dir() {
|
||||||
|
to_path.push(from_path.file_name().unwrap());
|
||||||
|
params.to = to_path.into_os_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
params.format = format.unwrap_or(Format::default());
|
||||||
|
if let Some(context_count) = context {
|
||||||
|
params.context_count = context_count;
|
||||||
|
}
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiffStyleMatch {
|
||||||
|
is_match: bool,
|
||||||
|
context_count: Option<usize>,
|
||||||
|
next_param_consumed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_context_diff_params(
|
||||||
|
param: &OsString,
|
||||||
|
next_param: Option<&OsString>,
|
||||||
|
format: Option<Format>,
|
||||||
|
) -> Result<DiffStyleMatch, String> {
|
||||||
|
const CONTEXT_RE: &str = r"^(-[cC](?<num1>\d*)|--context(=(?<num2>\d*))?|-(?<num3>\d+)c)$";
|
||||||
|
let regex = Regex::new(CONTEXT_RE).unwrap();
|
||||||
|
let is_match = regex.is_match(param.to_string_lossy().as_ref());
|
||||||
|
let mut context_count = None;
|
||||||
|
let mut next_param_consumed = false;
|
||||||
|
if is_match {
|
||||||
|
if format.is_some() && format != Some(Format::Context) {
|
||||||
|
return Err("Conflicting output style options".to_string());
|
||||||
|
}
|
||||||
|
let captures = regex.captures(param.to_str().unwrap()).unwrap();
|
||||||
|
let num = captures
|
||||||
|
.name("num1")
|
||||||
|
.or(captures.name("num2"))
|
||||||
|
.or(captures.name("num3"));
|
||||||
|
if let Some(numvalue) = num {
|
||||||
|
if !numvalue.as_str().is_empty() {
|
||||||
|
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if param == "-C" {
|
||||||
|
if let Some(p) = next_param {
|
||||||
|
let size_str = p.to_string_lossy();
|
||||||
|
match size_str.parse::<usize>() {
|
||||||
|
Ok(context_size) => {
|
||||||
|
context_count = Some(context_size);
|
||||||
|
next_param_consumed = true;
|
||||||
|
}
|
||||||
|
Err(_) => return Err(format!("invalid context length '{size_str}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DiffStyleMatch {
|
||||||
|
is_match,
|
||||||
|
context_count,
|
||||||
|
next_param_consumed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_unified_diff_params(
|
||||||
|
param: &OsString,
|
||||||
|
next_param: Option<&OsString>,
|
||||||
|
format: Option<Format>,
|
||||||
|
) -> Result<DiffStyleMatch, String> {
|
||||||
|
const UNIFIED_RE: &str = r"^(-[uU](?<num1>\d*)|--unified(=(?<num2>\d*))?|-(?<num3>\d+)u)$";
|
||||||
|
let regex = Regex::new(UNIFIED_RE).unwrap();
|
||||||
|
let is_match = regex.is_match(param.to_string_lossy().as_ref());
|
||||||
|
let mut context_count = None;
|
||||||
|
let mut next_param_consumed = false;
|
||||||
|
if is_match {
|
||||||
|
if format.is_some() && format != Some(Format::Unified) {
|
||||||
|
return Err("Conflicting output style options".to_string());
|
||||||
|
}
|
||||||
|
let captures = regex.captures(param.to_str().unwrap()).unwrap();
|
||||||
|
let num = captures
|
||||||
|
.name("num1")
|
||||||
|
.or(captures.name("num2"))
|
||||||
|
.or(captures.name("num3"));
|
||||||
|
if let Some(numvalue) = num {
|
||||||
|
if !numvalue.as_str().is_empty() {
|
||||||
|
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if param == "-U" {
|
||||||
|
if let Some(p) = next_param {
|
||||||
|
let size_str = p.to_string_lossy();
|
||||||
|
match size_str.parse::<usize>() {
|
||||||
|
Ok(context_size) => {
|
||||||
|
context_count = Some(context_size);
|
||||||
|
next_param_consumed = true;
|
||||||
|
}
|
||||||
|
Err(_) => return Err(format!("invalid context length '{size_str}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DiffStyleMatch {
|
||||||
|
is_match,
|
||||||
|
context_count,
|
||||||
|
next_param_consumed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn os(s: &str) -> OsString {
|
||||||
|
OsString::from(s)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn basics() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("--normal"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn basics_ed() {
|
||||||
|
for arg in ["-e", "--ed"] {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Ed,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os(arg), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn context_valid() {
|
||||||
|
for args in [vec!["-c"], vec!["--context"], vec!["--context="]] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Context,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for args in [
|
||||||
|
vec!["-c42"],
|
||||||
|
vec!["-C42"],
|
||||||
|
vec!["-C", "42"],
|
||||||
|
vec!["--context=42"],
|
||||||
|
vec!["-42c"],
|
||||||
|
] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Context,
|
||||||
|
context_count: 42,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn context_invalid() {
|
||||||
|
for args in [
|
||||||
|
vec!["-c", "42"],
|
||||||
|
vec!["-c=42"],
|
||||||
|
vec!["-c="],
|
||||||
|
vec!["-C"],
|
||||||
|
vec!["-C=42"],
|
||||||
|
vec!["-C="],
|
||||||
|
vec!["--context42"],
|
||||||
|
vec!["--context", "42"],
|
||||||
|
vec!["-42C"],
|
||||||
|
] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unified_valid() {
|
||||||
|
for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Unified,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for args in [
|
||||||
|
vec!["-u42"],
|
||||||
|
vec!["-U42"],
|
||||||
|
vec!["-U", "42"],
|
||||||
|
vec!["--unified=42"],
|
||||||
|
vec!["-42u"],
|
||||||
|
] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Unified,
|
||||||
|
context_count: 42,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unified_invalid() {
|
||||||
|
for args in [
|
||||||
|
vec!["-u", "42"],
|
||||||
|
vec!["-u=42"],
|
||||||
|
vec!["-u="],
|
||||||
|
vec!["-U"],
|
||||||
|
vec!["-U=42"],
|
||||||
|
vec!["-U="],
|
||||||
|
vec!["--unified42"],
|
||||||
|
vec!["--unified", "42"],
|
||||||
|
vec!["-42U"],
|
||||||
|
] {
|
||||||
|
let mut params = vec!["diff"];
|
||||||
|
params.extend(args);
|
||||||
|
params.extend(["foo", "bar"]);
|
||||||
|
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn context_count() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Unified,
|
||||||
|
context_count: 54,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-u54"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Unified,
|
||||||
|
context_count: 54,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-U54"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Unified,
|
||||||
|
context_count: 54,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
format: Format::Context,
|
||||||
|
context_count: 54,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-c54"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn report_identical_files() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
report_identical_files: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-s"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
report_identical_files: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[
|
||||||
|
os("diff"),
|
||||||
|
os("--report-identical-files"),
|
||||||
|
os("foo"),
|
||||||
|
os("bar"),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn brief() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("-q"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("--brief"), os("foo"), os("bar"),]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn expand_tabs() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for option in ["-t", "--expand-tabs"] {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
expand_tabs: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os(option), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn tabsize() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
tabsize: 1,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("--tabsize=1"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("bar"),
|
||||||
|
tabsize: 42,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[
|
||||||
|
os("diff"),
|
||||||
|
os("--tabsize=92233720368547758088"),
|
||||||
|
os("foo"),
|
||||||
|
os("bar")
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn double_dash() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("-g"),
|
||||||
|
to: os("-h"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params(
|
||||||
|
[os("diff"), os("--"), os("-g"), os("-h")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn default_to_stdin() {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("foo"),
|
||||||
|
to: os("-"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("-"),
|
||||||
|
to: os("bar"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(Params {
|
||||||
|
executable: os("diff"),
|
||||||
|
from: os("-"),
|
||||||
|
to: os("-"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable())
|
||||||
|
);
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("foo"), os("bar"), os("-")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("-"), os("-"), os("-")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn missing_arguments() {
|
||||||
|
assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err());
|
||||||
|
assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unknown_argument() {
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os("-g"), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(
|
||||||
|
parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err()
|
||||||
|
);
|
||||||
|
assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
assert!(parse_params([].iter().cloned().peekable()).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn conflicting_output_styles() {
|
||||||
|
for (arg1, arg2) in [
|
||||||
|
("-u", "-c"),
|
||||||
|
("-u", "-e"),
|
||||||
|
("-c", "-u"),
|
||||||
|
("-c", "-U42"),
|
||||||
|
("-u", "--normal"),
|
||||||
|
("--normal", "-e"),
|
||||||
|
("--context", "--normal"),
|
||||||
|
] {
|
||||||
|
assert!(parse_params(
|
||||||
|
[os("diff"), os(arg1), os(arg2), os("foo"), os("bar")]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.peekable()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1263
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,945 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::params::Params;
|
||||||
|
use crate::utils::do_write_line;
|
||||||
|
use crate::utils::get_modification_time;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum DiffLine {
|
||||||
|
Context(Vec<u8>),
|
||||||
|
Expected(Vec<u8>),
|
||||||
|
Actual(Vec<u8>),
|
||||||
|
MissingNL,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Mismatch {
|
||||||
|
pub line_number_expected: u32,
|
||||||
|
pub line_number_actual: u32,
|
||||||
|
pub lines: Vec<DiffLine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mismatch {
|
||||||
|
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
|
||||||
|
Mismatch {
|
||||||
|
line_number_expected,
|
||||||
|
line_number_actual,
|
||||||
|
lines: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a diff between the expected output and actual output.
|
||||||
|
fn make_diff(
|
||||||
|
expected: &[u8],
|
||||||
|
actual: &[u8],
|
||||||
|
context_size: usize,
|
||||||
|
stop_early: bool,
|
||||||
|
) -> Vec<Mismatch> {
|
||||||
|
let mut line_number_expected = 1;
|
||||||
|
let mut line_number_actual = 1;
|
||||||
|
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||||
|
let mut lines_since_mismatch = context_size + 1;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut mismatch = Mismatch::new(0, 0);
|
||||||
|
|
||||||
|
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||||
|
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||||
|
|
||||||
|
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||||
|
// ^ means that underflow here is impossible
|
||||||
|
let expected_lines_count = expected_lines.len() as u32 - 1;
|
||||||
|
let actual_lines_count = actual_lines.len() as u32 - 1;
|
||||||
|
|
||||||
|
if expected_lines.last() == Some(&&b""[..]) {
|
||||||
|
expected_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual_lines.last() == Some(&&b""[..]) {
|
||||||
|
actual_lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||||
|
match result {
|
||||||
|
diff::Result::Left(str) => {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len() as u32,
|
||||||
|
line_number_actual - context_queue.len() as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
|
||||||
|
mismatch.lines.pop();
|
||||||
|
match mismatch.lines.pop() {
|
||||||
|
Some(DiffLine::Actual(res)) => {
|
||||||
|
// We have to make sure that Actual (the + lines)
|
||||||
|
// always come after Expected (the - lines)
|
||||||
|
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||||
|
if line_number_expected > expected_lines_count {
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
}
|
||||||
|
mismatch.lines.push(DiffLine::Actual(res));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
}
|
||||||
|
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||||
|
if line_number_expected > expected_lines_count {
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line_number_expected += 1;
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
}
|
||||||
|
diff::Result::Right(str) => {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len() as u32,
|
||||||
|
line_number_actual - context_queue.len() as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||||
|
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||||
|
if line_number_actual > actual_lines_count {
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
}
|
||||||
|
line_number_actual += 1;
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
}
|
||||||
|
diff::Result::Both(str, _) => {
|
||||||
|
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||||
|
if (line_number_actual > actual_lines_count)
|
||||||
|
&& (line_number_expected > expected_lines_count)
|
||||||
|
{
|
||||||
|
if context_queue.len() < context_size {
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||||
|
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
if lines_since_mismatch < context_size {
|
||||||
|
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else if line_number_actual > actual_lines_count {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len() as u32,
|
||||||
|
line_number_actual - context_queue.len() as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||||
|
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||||
|
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else if line_number_expected > expected_lines_count {
|
||||||
|
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||||
|
results.push(mismatch);
|
||||||
|
mismatch = Mismatch::new(
|
||||||
|
line_number_expected - context_queue.len() as u32,
|
||||||
|
line_number_actual - context_queue.len() as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while let Some(line) = context_queue.pop_front() {
|
||||||
|
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||||
|
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||||
|
}
|
||||||
|
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||||
|
lines_since_mismatch = 0;
|
||||||
|
} else {
|
||||||
|
debug_assert!(context_queue.len() <= context_size);
|
||||||
|
if context_queue.len() >= context_size {
|
||||||
|
let _ = context_queue.pop_front();
|
||||||
|
}
|
||||||
|
if lines_since_mismatch < context_size {
|
||||||
|
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||||
|
} else if context_size > 0 {
|
||||||
|
context_queue.push_back(str);
|
||||||
|
}
|
||||||
|
lines_since_mismatch += 1;
|
||||||
|
}
|
||||||
|
line_number_expected += 1;
|
||||||
|
line_number_actual += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop_early && !results.is_empty() {
|
||||||
|
// Optimization: stop analyzing the files as soon as there are any differences
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(mismatch);
|
||||||
|
results.remove(0);
|
||||||
|
|
||||||
|
if results.is_empty() && expected_lines_count != actual_lines_count {
|
||||||
|
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
|
||||||
|
// empty diff and only expected lines has a missing line at end
|
||||||
|
if expected_lines_count != expected_lines.len() as u32 {
|
||||||
|
mismatch.lines.push(DiffLine::Expected(
|
||||||
|
expected_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
mismatch.lines.push(DiffLine::Actual(
|
||||||
|
actual_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
results.push(mismatch);
|
||||||
|
} else if actual_lines_count != actual_lines.len() as u32 {
|
||||||
|
mismatch.lines.push(DiffLine::Expected(
|
||||||
|
expected_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.lines.push(DiffLine::Actual(
|
||||||
|
actual_lines
|
||||||
|
.pop()
|
||||||
|
.expect("can't be empty; produced by split()")
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
mismatch.lines.push(DiffLine::MissingNL);
|
||||||
|
results.push(mismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||||
|
let from_modified_time = get_modification_time(¶ms.from.to_string_lossy());
|
||||||
|
let to_modified_time = get_modification_time(¶ms.to.to_string_lossy());
|
||||||
|
let mut output = format!(
|
||||||
|
"--- {0}\t{1}\n+++ {2}\t{3}\n",
|
||||||
|
params.from.to_string_lossy(),
|
||||||
|
from_modified_time,
|
||||||
|
params.to.to_string_lossy(),
|
||||||
|
to_modified_time
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
|
||||||
|
if diff_results.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
if params.brief {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
for result in diff_results {
|
||||||
|
let mut line_number_expected = result.line_number_expected;
|
||||||
|
let mut line_number_actual = result.line_number_actual;
|
||||||
|
let mut expected_count = 0;
|
||||||
|
let mut actual_count = 0;
|
||||||
|
for line in &result.lines {
|
||||||
|
match line {
|
||||||
|
DiffLine::Expected(_) => {
|
||||||
|
expected_count += 1;
|
||||||
|
}
|
||||||
|
DiffLine::Context(_) => {
|
||||||
|
expected_count += 1;
|
||||||
|
actual_count += 1;
|
||||||
|
}
|
||||||
|
DiffLine::Actual(_) => {
|
||||||
|
actual_count += 1;
|
||||||
|
}
|
||||||
|
DiffLine::MissingNL => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Let's imagine this diff file
|
||||||
|
//
|
||||||
|
// --- a/something
|
||||||
|
// +++ b/something
|
||||||
|
// @@ -2,0 +3,1 @@
|
||||||
|
// + x
|
||||||
|
//
|
||||||
|
// In the unified diff format as implemented by GNU diff and patch,
|
||||||
|
// this is an instruction to insert the x *after* the preexisting line 2,
|
||||||
|
// not before. You can demonstrate it this way:
|
||||||
|
//
|
||||||
|
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
|
||||||
|
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||||
|
// $ patch -p1 < diff
|
||||||
|
// patching file something
|
||||||
|
// $ cat something
|
||||||
|
// a
|
||||||
|
// b
|
||||||
|
// x
|
||||||
|
// c
|
||||||
|
// d
|
||||||
|
//
|
||||||
|
// Notice how the x winds up at line 3, not line 2. This requires contortions to
|
||||||
|
// work with our diffing algorithm, which keeps track of the "intended destination line",
|
||||||
|
// not a line that things are supposed to be placed after. It's changing the first number,
|
||||||
|
// not the second, that actually affects where the x goes.
|
||||||
|
//
|
||||||
|
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
|
||||||
|
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
|
||||||
|
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||||
|
// $ patch -p1 < diff
|
||||||
|
// patching file something
|
||||||
|
// $ cat something
|
||||||
|
// a
|
||||||
|
// b
|
||||||
|
// c
|
||||||
|
// x
|
||||||
|
// d
|
||||||
|
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
|
||||||
|
// # actually being read
|
||||||
|
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
|
||||||
|
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||||
|
// $ patch -p1 < diff
|
||||||
|
// patching file something
|
||||||
|
// $ cat something
|
||||||
|
// a
|
||||||
|
// b
|
||||||
|
// x
|
||||||
|
// c
|
||||||
|
// d
|
||||||
|
//
|
||||||
|
// Now watch what happens if I add a context line:
|
||||||
|
//
|
||||||
|
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
|
||||||
|
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||||
|
// $ patch -p1 < diff
|
||||||
|
// patching file something
|
||||||
|
// Hunk #1 succeeded at 3 (offset 1 line).
|
||||||
|
//
|
||||||
|
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
|
||||||
|
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
|
||||||
|
// line that the x will wind up on, and not the line immediately before.
|
||||||
|
//
|
||||||
|
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
|
||||||
|
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||||
|
// $ patch -p1 < diff
|
||||||
|
// patching file something
|
||||||
|
// $ cat something
|
||||||
|
// a
|
||||||
|
// b
|
||||||
|
// x
|
||||||
|
// c
|
||||||
|
// d
|
||||||
|
//
|
||||||
|
// I made this comment because this stuff is not obvious from GNU's
|
||||||
|
// documentation on the format at all.
|
||||||
|
if expected_count == 0 {
|
||||||
|
line_number_expected -= 1;
|
||||||
|
}
|
||||||
|
if actual_count == 0 {
|
||||||
|
line_number_actual -= 1;
|
||||||
|
}
|
||||||
|
let exp_ct = if expected_count == 1 {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(",{expected_count}")
|
||||||
|
};
|
||||||
|
let act_ct = if actual_count == 1 {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(",{actual_count}")
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@"
|
||||||
|
)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
for line in result.lines {
|
||||||
|
match line {
|
||||||
|
DiffLine::Expected(e) => {
|
||||||
|
write!(output, "-").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Context(c) => {
|
||||||
|
write!(output, " ").expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &c, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::Actual(r) => {
|
||||||
|
write!(output, "+",).expect("write to Vec is infallible");
|
||||||
|
do_write_line(&mut output, &r, params.expand_tabs, params.tabsize)
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
}
|
||||||
|
DiffLine::MissingNL => {
|
||||||
|
writeln!(output, r"\ No newline at end of file")
|
||||||
|
.expect("write to Vec is infallible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations() {
|
||||||
|
let target = "target/unified-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alef".into(),
|
||||||
|
to: (&format!("{target}/alef")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/ab.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
println!(
|
||||||
|
"diff: {:?}",
|
||||||
|
String::from_utf8(diff.clone())
|
||||||
|
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"alef: {:?}",
|
||||||
|
String::from_utf8(alef.clone())
|
||||||
|
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"bet: {:?}",
|
||||||
|
String::from_utf8(bet.clone())
|
||||||
|
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
let alef = fs::read(format!("{target}/alef")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_missing_line_ending() {
|
||||||
|
let target = "target/unified-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
for &g in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
match g {
|
||||||
|
0 => {
|
||||||
|
alef.pop();
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
alef.pop();
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alefn".into(),
|
||||||
|
to: (&format!("{target}/alefn")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/abn.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefn")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betn")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.stdin(File::open(format!("{target}/abn.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefn")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_empty_lines() {
|
||||||
|
let target = "target/unified-diff/";
|
||||||
|
// test all possible six-line files with missing newlines.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
for &g in &[0, 1, 2, 3] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
match g {
|
||||||
|
0 => {
|
||||||
|
alef.pop();
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
alef.pop();
|
||||||
|
bet.pop();
|
||||||
|
}
|
||||||
|
3 => {}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alef_".into(),
|
||||||
|
to: (&format!("{target}/alef_")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/ab_.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alef_")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_missing_lines() {
|
||||||
|
let target = "target/unified-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"h\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"j\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"l\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alefx".into(),
|
||||||
|
to: (&format!("{target}/alefx")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/abx.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefx")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betx")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.stdin(File::open(format!("{target}/abx.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefx")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_permutations_reverse() {
|
||||||
|
let target = "target/unified-diff/";
|
||||||
|
// test all possible six-line files.
|
||||||
|
let _ = std::fs::create_dir(target);
|
||||||
|
for &a in &[0, 1, 2] {
|
||||||
|
for &b in &[0, 1, 2] {
|
||||||
|
for &c in &[0, 1, 2] {
|
||||||
|
for &d in &[0, 1, 2] {
|
||||||
|
for &e in &[0, 1, 2] {
|
||||||
|
for &f in &[0, 1, 2] {
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Command;
|
||||||
|
let mut alef = Vec::new();
|
||||||
|
let mut bet = Vec::new();
|
||||||
|
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||||
|
.unwrap();
|
||||||
|
if a != 2 {
|
||||||
|
bet.write_all(b"a\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||||
|
.unwrap();
|
||||||
|
if b != 2 {
|
||||||
|
bet.write_all(b"b\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||||
|
.unwrap();
|
||||||
|
if c != 2 {
|
||||||
|
bet.write_all(b"c\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||||
|
.unwrap();
|
||||||
|
if d != 2 {
|
||||||
|
bet.write_all(b"d\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||||
|
.unwrap();
|
||||||
|
if e != 2 {
|
||||||
|
bet.write_all(b"e\n").unwrap();
|
||||||
|
}
|
||||||
|
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||||
|
.unwrap();
|
||||||
|
if f != 2 {
|
||||||
|
bet.write_all(b"f\n").unwrap();
|
||||||
|
}
|
||||||
|
// This test diff is intentionally reversed.
|
||||||
|
// We want it to turn the alef into bet.
|
||||||
|
let diff = diff(
|
||||||
|
&alef,
|
||||||
|
&bet,
|
||||||
|
&Params {
|
||||||
|
from: "a/alefr".into(),
|
||||||
|
to: (&format!("{target}/alefr")).into(),
|
||||||
|
context_count: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
File::create(format!("{target}/abr.diff"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(&diff)
|
||||||
|
.unwrap();
|
||||||
|
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||||
|
fa.write_all(&alef[..]).unwrap();
|
||||||
|
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||||
|
fb.write_all(&bet[..]).unwrap();
|
||||||
|
let _ = fa;
|
||||||
|
let _ = fb;
|
||||||
|
let output = Command::new("patch")
|
||||||
|
.arg("-p0")
|
||||||
|
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(output.status.success(), "{output:?}");
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
let alef = fs::read(format!("{target}/alefr")).unwrap();
|
||||||
|
assert_eq!(alef, bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop_early() {
|
||||||
|
use crate::assert_diff_eq;
|
||||||
|
|
||||||
|
let from_filename = "foo";
|
||||||
|
let from = ["a", "b", "c", ""].join("\n");
|
||||||
|
let to_filename = "bar";
|
||||||
|
let to = ["a", "d", "c", ""].join("\n");
|
||||||
|
|
||||||
|
let diff_full = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_full = [
|
||||||
|
"--- foo\tTIMESTAMP",
|
||||||
|
"+++ bar\tTIMESTAMP",
|
||||||
|
"@@ -1,3 +1,3 @@",
|
||||||
|
" a",
|
||||||
|
"-b",
|
||||||
|
"+d",
|
||||||
|
" c",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
assert_diff_eq!(diff_full, expected_full);
|
||||||
|
|
||||||
|
let diff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
to.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n");
|
||||||
|
assert_diff_eq!(diff_brief, expected_brief);
|
||||||
|
|
||||||
|
let nodiff_full = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(nodiff_full.is_empty());
|
||||||
|
|
||||||
|
let nodiff_brief = diff(
|
||||||
|
from.as_bytes(),
|
||||||
|
from.as_bytes(),
|
||||||
|
&Params {
|
||||||
|
from: from_filename.into(),
|
||||||
|
to: to_filename.into(),
|
||||||
|
brief: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(nodiff_brief.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use std::{ffi::OsString, io::Write};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
/// Replace tabs by spaces in the input line.
|
||||||
|
/// Correctly handle multi-bytes characters.
|
||||||
|
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
|
||||||
|
#[must_use]
|
||||||
|
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
|
||||||
|
let tab = b'\t';
|
||||||
|
let ntabs = line.iter().filter(|c| **c == tab).count();
|
||||||
|
if ntabs == 0 {
|
||||||
|
return line.to_vec();
|
||||||
|
}
|
||||||
|
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
let mut iter = line.split(|c| *c == tab).peekable();
|
||||||
|
while let Some(chunk) = iter.next() {
|
||||||
|
match String::from_utf8(chunk.to_vec()) {
|
||||||
|
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
|
||||||
|
Err(_) => offset += chunk.len(),
|
||||||
|
}
|
||||||
|
result.extend_from_slice(chunk);
|
||||||
|
if iter.peek().is_some() {
|
||||||
|
result.resize(result.len() + tabsize - offset % tabsize, b' ');
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a single line to an output stream, expanding tabs to space if necessary.
|
||||||
|
/// This assumes that line does not contain any line breaks
|
||||||
|
/// (if it does and tabs are to be expanded to spaces, the result is undefined).
|
||||||
|
pub fn do_write_line(
|
||||||
|
output: &mut Vec<u8>,
|
||||||
|
line: &[u8],
|
||||||
|
expand_tabs: bool,
|
||||||
|
tabsize: usize,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
if expand_tabs {
|
||||||
|
output.write_all(do_expand_tabs(line, tabsize).as_slice())
|
||||||
|
} else {
|
||||||
|
output.write_all(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the modification time of the input file specified by file path
|
||||||
|
/// If an error occurs, it returns the current system time
|
||||||
|
pub fn get_modification_time(file_path: &str) -> String {
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use std::fs;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
let modification_time: SystemTime = fs::metadata(file_path)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(SystemTime::now());
|
||||||
|
|
||||||
|
let modification_time: DateTime<Local> = modification_time.into();
|
||||||
|
let modification_time: String = modification_time
|
||||||
|
.format("%Y-%m-%d %H:%M:%S%.9f %z")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
modification_time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_failure_to_read_input_file(
|
||||||
|
executable: &OsString,
|
||||||
|
filepath: &OsString,
|
||||||
|
error: &std::io::Error,
|
||||||
|
) -> String {
|
||||||
|
// std::io::Error's display trait outputs "{detail} (os error {code})"
|
||||||
|
// but we want only the {detail} (error string) part
|
||||||
|
let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap();
|
||||||
|
format!(
|
||||||
|
"{}: {}: {}",
|
||||||
|
executable.to_string_lossy(),
|
||||||
|
filepath.to_string_lossy(),
|
||||||
|
error_code_re.replace(error.to_string().as_str(), ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_failure_to_read_input_file(
|
||||||
|
executable: &OsString,
|
||||||
|
filepath: &OsString,
|
||||||
|
error: &std::io::Error,
|
||||||
|
) {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
format_failure_to_read_input_file(executable, filepath, error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod expand_tabs {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
|
||||||
|
assert_eq!(
|
||||||
|
do_expand_tabs(line.as_bytes(), tabsize),
|
||||||
|
expected.as_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basics() {
|
||||||
|
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
|
||||||
|
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
|
||||||
|
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
|
||||||
|
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_chars() {
|
||||||
|
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
|
||||||
|
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
|
||||||
|
|
||||||
|
// Note: The Woman Scientist emoji (👩🔬) is a ZWJ sequence combining
|
||||||
|
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
|
||||||
|
// it is displayed as a single emoji and has a print size of 2 columns.
|
||||||
|
// Terminal emulators tend to not support this, and display the two emojis
|
||||||
|
// side by side, thus accounting for a print size of 4 columns, but the
|
||||||
|
// unicode_width crate reports a correct size of 2.
|
||||||
|
assert_tab_expansion("foo\t👩🔬\tbaz", 6, "foo 👩🔬 baz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_utf8() {
|
||||||
|
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
|
||||||
|
assert_eq!(
|
||||||
|
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
|
||||||
|
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod write_line {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
|
||||||
|
let mut output: Vec<u8> = Vec::new();
|
||||||
|
assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok());
|
||||||
|
assert_eq!(output, expected.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basics() {
|
||||||
|
assert_line_written("foo bar baz", false, 8, "foo bar baz");
|
||||||
|
assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz");
|
||||||
|
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod modification_time {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_time() {
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
let temp = NamedTempFile::new().unwrap();
|
||||||
|
// set file modification time equal to current time
|
||||||
|
let current = SystemTime::now();
|
||||||
|
let _ = temp.as_file().set_modified(current);
|
||||||
|
|
||||||
|
// format current time
|
||||||
|
let current: DateTime<Local> = current.into();
|
||||||
|
let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string();
|
||||||
|
|
||||||
|
// verify
|
||||||
|
assert_eq!(
|
||||||
|
current,
|
||||||
|
get_modification_time(&temp.path().to_string_lossy())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_file() {
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
let invalid_file = "target/utils/invalid-file";
|
||||||
|
|
||||||
|
// store current time before calling `get_modification_time`
|
||||||
|
// Because the file is invalid, it will return SystemTime::now()
|
||||||
|
// which will be greater than previously saved time
|
||||||
|
let current_time: DateTime<Local> = SystemTime::now().into();
|
||||||
|
let m_time: DateTime<Local> = get_modification_time(invalid_file).parse().unwrap();
|
||||||
|
|
||||||
|
assert!(m_time > current_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,892 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use assert_cmd::cargo::cargo_bin_cmd;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
use std::fs::File;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::{tempdir, NamedTempFile};
|
||||||
|
|
||||||
|
// Integration tests for the diffutils command
|
||||||
|
mod common {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_param() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("patch");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::eq("patch: utility not supported\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::starts_with(
|
||||||
|
"Expected utility name as second argument, got nothing.\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
for subcmd in ["diff", "cmp"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg(subcmd);
|
||||||
|
cmd.arg("--foobar");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::starts_with(
|
||||||
|
"unrecognized option: \"--foobar\"",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_read_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let file = NamedTempFile::new()?;
|
||||||
|
|
||||||
|
let nofile = NamedTempFile::new()?;
|
||||||
|
let nopath = nofile.into_temp_path();
|
||||||
|
std::fs::remove_file(&nopath)?;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let error_message = "No such file or directory";
|
||||||
|
#[cfg(windows)]
|
||||||
|
let error_message = "The system cannot find the file specified.";
|
||||||
|
|
||||||
|
for subcmd in ["diff", "cmp"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg(subcmd);
|
||||||
|
cmd.arg(&nopath).arg(file.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::ends_with(format!(
|
||||||
|
": {}: {error_message}\n",
|
||||||
|
&nopath.as_os_str().to_string_lossy()
|
||||||
|
)));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg(subcmd);
|
||||||
|
cmd.arg(file.path()).arg(&nopath);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::ends_with(format!(
|
||||||
|
": {}: {error_message}\n",
|
||||||
|
&nopath.as_os_str().to_string_lossy()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg(&nopath).arg(&nopath);
|
||||||
|
cmd.assert().code(predicate::eq(2)).failure().stderr(
|
||||||
|
predicate::str::contains(format!(
|
||||||
|
": {}: {error_message}\n",
|
||||||
|
&nopath.as_os_str().to_string_lossy()
|
||||||
|
))
|
||||||
|
.count(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod diff {
|
||||||
|
use diffutilslib::assert_diff_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_differences() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let file = NamedTempFile::new()?;
|
||||||
|
for option in ["", "-u", "-c", "-e"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
if !option.is_empty() {
|
||||||
|
cmd.arg(option);
|
||||||
|
}
|
||||||
|
cmd.arg(file.path()).arg(file.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_differences_report_identical_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// same file
|
||||||
|
let mut file1 = NamedTempFile::new()?;
|
||||||
|
file1.write_all("foo\n".as_bytes())?;
|
||||||
|
for option in ["", "-u", "-c", "-e"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
if !option.is_empty() {
|
||||||
|
cmd.arg(option);
|
||||||
|
}
|
||||||
|
cmd.arg("-s").arg(file1.path()).arg(file1.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::eq(format!(
|
||||||
|
"Files {} and {} are identical\n",
|
||||||
|
file1.path().to_string_lossy(),
|
||||||
|
file1.path().to_string_lossy(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
// two files with the same content
|
||||||
|
let mut file2 = NamedTempFile::new()?;
|
||||||
|
file2.write_all("foo\n".as_bytes())?;
|
||||||
|
for option in ["", "-u", "-c", "-e"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
if !option.is_empty() {
|
||||||
|
cmd.arg(option);
|
||||||
|
}
|
||||||
|
cmd.arg("-s").arg(file1.path()).arg(file2.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::eq(format!(
|
||||||
|
"Files {} and {} are identical\n",
|
||||||
|
file1.path().to_string_lossy(),
|
||||||
|
file2.path().to_string_lossy(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn differences() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut file1 = NamedTempFile::new()?;
|
||||||
|
file1.write_all("foo\n".as_bytes())?;
|
||||||
|
let mut file2 = NamedTempFile::new()?;
|
||||||
|
file2.write_all("bar\n".as_bytes())?;
|
||||||
|
for option in ["", "-u", "-c", "-e"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
if !option.is_empty() {
|
||||||
|
cmd.arg(option);
|
||||||
|
}
|
||||||
|
cmd.arg(file1.path()).arg(file2.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::str::is_empty().not());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn differences_brief() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut file1 = NamedTempFile::new()?;
|
||||||
|
file1.write_all("foo\n".as_bytes())?;
|
||||||
|
let mut file2 = NamedTempFile::new()?;
|
||||||
|
file2.write_all("bar\n".as_bytes())?;
|
||||||
|
for option in ["", "-u", "-c", "-e"] {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
if !option.is_empty() {
|
||||||
|
cmd.arg(option);
|
||||||
|
}
|
||||||
|
cmd.arg("-q").arg(file1.path()).arg(file2.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::eq(format!(
|
||||||
|
"Files {} and {} differ\n",
|
||||||
|
file1.path().to_string_lossy(),
|
||||||
|
file2.path().to_string_lossy()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_newline() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut file1 = NamedTempFile::new()?;
|
||||||
|
file1.write_all("foo".as_bytes())?;
|
||||||
|
let mut file2 = NamedTempFile::new()?;
|
||||||
|
file2.write_all("bar".as_bytes())?;
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-e").arg(file1.path()).arg(file2.path());
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::starts_with("No newline at end of file"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut file1 = NamedTempFile::new()?;
|
||||||
|
file1.write_all("foo\n".as_bytes())?;
|
||||||
|
let mut file2 = NamedTempFile::new()?;
|
||||||
|
file2.write_all("bar\n".as_bytes())?;
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u")
|
||||||
|
.arg(file1.path())
|
||||||
|
.arg("-")
|
||||||
|
.write_stdin("bar\n");
|
||||||
|
cmd.assert().code(predicate::eq(1)).failure();
|
||||||
|
|
||||||
|
let output = cmd.output().unwrap().stdout;
|
||||||
|
assert_diff_eq!(
|
||||||
|
output,
|
||||||
|
format!(
|
||||||
|
"--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||||
|
file1.path().to_string_lossy()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u")
|
||||||
|
.arg("-")
|
||||||
|
.arg(file2.path())
|
||||||
|
.write_stdin("foo\n");
|
||||||
|
cmd.assert().code(predicate::eq(1)).failure();
|
||||||
|
|
||||||
|
let output = cmd.output().unwrap().stdout;
|
||||||
|
assert_diff_eq!(
|
||||||
|
output,
|
||||||
|
format!(
|
||||||
|
"--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||||
|
file2.path().to_string_lossy()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u").arg("-").arg("-");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u")
|
||||||
|
.arg(file1.path())
|
||||||
|
.arg("/dev/stdin")
|
||||||
|
.write_stdin("bar\n");
|
||||||
|
cmd.assert().code(predicate::eq(1)).failure();
|
||||||
|
|
||||||
|
let output = cmd.output().unwrap().stdout;
|
||||||
|
assert_diff_eq!(
|
||||||
|
output,
|
||||||
|
format!(
|
||||||
|
"--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||||
|
file1.path().to_string_lossy()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let directory = tmp_dir.path().join("d");
|
||||||
|
let _ = std::fs::create_dir(&directory);
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"a\n").unwrap();
|
||||||
|
|
||||||
|
let da_path = directory.join("a");
|
||||||
|
let mut da = File::create(&da_path).unwrap();
|
||||||
|
da.write_all(b"da\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u").arg(&directory).arg(&a_path);
|
||||||
|
cmd.assert().code(predicate::eq(1)).failure();
|
||||||
|
|
||||||
|
let output = cmd.output().unwrap().stdout;
|
||||||
|
assert_diff_eq!(
|
||||||
|
output,
|
||||||
|
format!(
|
||||||
|
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n",
|
||||||
|
da_path.display(),
|
||||||
|
a_path.display()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("diff");
|
||||||
|
cmd.arg("-u").arg(&a_path).arg(&directory);
|
||||||
|
cmd.assert().code(predicate::eq(1)).failure();
|
||||||
|
|
||||||
|
let output = cmd.output().unwrap().stdout;
|
||||||
|
assert_diff_eq!(
|
||||||
|
output,
|
||||||
|
format!(
|
||||||
|
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n",
|
||||||
|
a_path.display(),
|
||||||
|
da_path.display()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cmp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_incompatible_params() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-s");
|
||||||
|
cmd.arg("/etc/passwd").arg("/etc/group");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(2))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::ends_with(
|
||||||
|
": options -l and -s are incompatible\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"a\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path);
|
||||||
|
cmd.write_stdin("a\n");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path);
|
||||||
|
cmd.write_stdin("b\n");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(" - differ: char 1, line 1\n"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_equal_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"a\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"a\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_one_file_empty() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"a\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let _ = File::create(&b_path).unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains(" EOF on "))
|
||||||
|
.stderr(predicate::str::ends_with(" which is empty\n"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_immediate_difference() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"abc\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"bcd\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(
|
||||||
|
" differ: byte 1, line 1 is 141 a 142 b\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::eq("1 141 142\n2 142 143\n3 143 144\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::eq(
|
||||||
|
"1 141 a 142 b\n2 142 b 143 c\n3 143 c 144 d\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_newline_difference() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"abc\ndefg").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"abc\ndef\ng").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(" differ: char 8, line 2\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(
|
||||||
|
" differ: byte 8, line 2 is 147 g 12 ^J\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::str::starts_with("8 147 12\n"))
|
||||||
|
.stderr(predicate::str::contains(" EOF on"))
|
||||||
|
.stderr(predicate::str::ends_with(" after byte 8\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::str::starts_with("8 147 g 12 ^J\n"))
|
||||||
|
.stderr(predicate::str::contains(" EOF on"))
|
||||||
|
.stderr(predicate::str::ends_with(" after byte 8\n"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_max_bytes() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"abc efg ijkl\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"abcdefghijkl\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-n");
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-n");
|
||||||
|
cmd.arg("4");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::eq("4 40 144 d\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-n");
|
||||||
|
cmd.arg("13");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::eq(" 4 40 144 d\n 8 40 150 h\n"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_skip_args_parsing() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"---abc\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"###abc\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-i");
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
// Positional skips should be ignored
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-i");
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.arg("1").arg("1");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
// Single positional argument should only affect first file.
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.env("LC_ALL", "C");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.arg("3");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_skip_suffix_parsing() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
writeln!(a, "{}c", "a".repeat(1024)).unwrap();
|
||||||
|
a.flush().unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
writeln!(b, "{}c", "b".repeat(1024)).unwrap();
|
||||||
|
b.flush().unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("--ignore-initial=1K");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_skip() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(b"abc efg ijkl\n").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(b"abcdefghijkl\n").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-i");
|
||||||
|
cmd.arg("8");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::is_empty());
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg("-i");
|
||||||
|
cmd.arg("4");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::is_empty())
|
||||||
|
.stdout(predicate::str::ends_with(
|
||||||
|
" differ: byte 4, line 1 is 40 150 h\n",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_binary() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
let mut bytes = vec![0, 15, 31, 32, 33, 40, 64, 126, 127, 128, 129, 200, 254, 255];
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(&bytes).unwrap();
|
||||||
|
|
||||||
|
bytes.reverse();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(&bytes).unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-l");
|
||||||
|
cmd.arg("-b");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::eq(concat!(
|
||||||
|
" 1 0 ^@ 377 M-^?\n",
|
||||||
|
" 2 17 ^O 376 M-~\n",
|
||||||
|
" 3 37 ^_ 310 M-H\n",
|
||||||
|
" 4 40 201 M-^A\n",
|
||||||
|
" 5 41 ! 200 M-^@\n",
|
||||||
|
" 6 50 ( 177 ^?\n",
|
||||||
|
" 7 100 @ 176 ~\n",
|
||||||
|
" 8 176 ~ 100 @\n",
|
||||||
|
" 9 177 ^? 50 (\n",
|
||||||
|
"10 200 M-^@ 41 !\n",
|
||||||
|
"11 201 M-^A 40 \n",
|
||||||
|
"12 310 M-H 37 ^_\n",
|
||||||
|
"13 376 M-~ 17 ^O\n",
|
||||||
|
"14 377 M-^? 0 ^@\n"
|
||||||
|
)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn cmp_fast_paths() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tmp_dir = tempdir()?;
|
||||||
|
|
||||||
|
// This test mimics one found in the GNU cmp test suite. It is used for
|
||||||
|
// validating the /dev/null optimization.
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let a = File::create(&a_path).unwrap();
|
||||||
|
a.set_len(14 * 1024 * 1024 * 1024 * 1024).unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let b = File::create(&b_path).unwrap();
|
||||||
|
b.set_len(15 * 1024 * 1024 * 1024 * 1024).unwrap();
|
||||||
|
|
||||||
|
let dev_null = OpenOptions::new().write(true).open("/dev/null").unwrap();
|
||||||
|
|
||||||
|
let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils"))
|
||||||
|
.arg("cmp")
|
||||||
|
.arg(&a_path)
|
||||||
|
.arg(&b_path)
|
||||||
|
.stdout(dev_null)
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Bound the runtime to a very short time that still allows for some resource
|
||||||
|
// constraint to slow it down while also allowing very fast systems to exit as
|
||||||
|
// early as possible.
|
||||||
|
const MAX_TRIES: u8 = 50;
|
||||||
|
for tries in 0..=MAX_TRIES {
|
||||||
|
if tries == MAX_TRIES {
|
||||||
|
panic!("cmp took too long to run, /dev/null optimization probably not working")
|
||||||
|
}
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
assert_eq!(status.code(), Some(1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(None) => (),
|
||||||
|
Err(e) => panic!("{e:#?}"),
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two stdins should be equal
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg("-");
|
||||||
|
cmd.arg("-");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(0))
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::is_empty())
|
||||||
|
.stderr(predicate::str::is_empty());
|
||||||
|
|
||||||
|
// Files with longer than block size equal segments should still report
|
||||||
|
// the correct line number for the difference. Assumes 8KB block size (see
|
||||||
|
// https://github.com/rust-lang/rust/blob/master/library/std/src/sys_common/io.rs),
|
||||||
|
// create a 24KB equality.
|
||||||
|
let mut bytes = " ".repeat(4095);
|
||||||
|
bytes.push('\n');
|
||||||
|
bytes.push_str(&" ".repeat(4096));
|
||||||
|
|
||||||
|
let bytes = bytes.repeat(3);
|
||||||
|
let bytes = bytes.as_bytes();
|
||||||
|
|
||||||
|
let a_path = tmp_dir.path().join("a");
|
||||||
|
let mut a = File::create(&a_path).unwrap();
|
||||||
|
a.write_all(bytes).unwrap();
|
||||||
|
a.write_all(b"A").unwrap();
|
||||||
|
|
||||||
|
let b_path = tmp_dir.path().join("b");
|
||||||
|
let mut b = File::create(&b_path).unwrap();
|
||||||
|
b.write_all(bytes).unwrap();
|
||||||
|
b.write_all(b"B").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||||
|
cmd.arg("cmp");
|
||||||
|
cmd.arg(&a_path).arg(&b_path);
|
||||||
|
cmd.env("LC_ALL", "en_US");
|
||||||
|
cmd.assert()
|
||||||
|
.code(predicate::eq(1))
|
||||||
|
.failure()
|
||||||
|
.stdout(predicate::str::ends_with(" differ: byte 24577, line 4\n"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Print the test results written to a JSON file by run-upstream-testsuite.sh
|
||||||
|
# in a markdown format. The printout includes the name of the test, the result,
|
||||||
|
# the URL to the test script and the contents of stdout and stderr.
|
||||||
|
# It can be used verbatim as the description when filing an issue for a test
|
||||||
|
# with an unexpected result.
|
||||||
|
|
||||||
|
json="test-results.json"
|
||||||
|
[[ -n $1 ]] && json="$1"
|
||||||
|
|
||||||
|
codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; }
|
||||||
|
|
||||||
|
jq -c '.tests[]' "$json" | while read -r test
|
||||||
|
do
|
||||||
|
name=$(echo "$test" | jq -r '.test')
|
||||||
|
echo "# test: $name"
|
||||||
|
result=$(echo "$test" | jq -r '.result')
|
||||||
|
echo "result: $result"
|
||||||
|
url=$(echo "$test" | jq -r '.url')
|
||||||
|
echo "url: $url"
|
||||||
|
if [[ "$result" != "SKIP" ]]
|
||||||
|
then
|
||||||
|
stdout=$(echo "$test" | jq -r '.stdout' | base64 -d)
|
||||||
|
if [[ -n "$stdout" ]]
|
||||||
|
then
|
||||||
|
echo "## stdout"
|
||||||
|
codeblock "$stdout"
|
||||||
|
fi
|
||||||
|
stderr=$(echo "$test" | jq -r '.stderr' | base64 -d)
|
||||||
|
if [[ -n "$stderr" ]]
|
||||||
|
then
|
||||||
|
echo "## stderr"
|
||||||
|
codeblock "$stderr"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
Executable
+154
@@ -0,0 +1,154 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Run the GNU upstream test suite for diffutils against a local build of the
|
||||||
|
# Rust implementation, print out a summary of the test results, and writes a
|
||||||
|
# JSON file ('test-results.json') containing detailed information about the
|
||||||
|
# test run.
|
||||||
|
|
||||||
|
# The JSON file contains metadata about the test run, and for each test the
|
||||||
|
# result as well as the contents of stdout, stderr, and of all the files
|
||||||
|
# written by the test script, if any (excluding subdirectories).
|
||||||
|
|
||||||
|
# The script takes a shortcut to fetch only the test suite from the upstream
|
||||||
|
# repository and carefully avoids running the autotools machinery which is
|
||||||
|
# time-consuming and resource-intensive, and doesn't offer the option to not
|
||||||
|
# build the upstream binaries. As a consequence, the environment in which the
|
||||||
|
# tests are run might not match exactly that used when the upstream tests are
|
||||||
|
# run through the autotools.
|
||||||
|
|
||||||
|
# By default it expects a release build of the diffutils binary, but a
|
||||||
|
# different build profile can be specified as an argument
|
||||||
|
# (e.g. 'dev' or 'test').
|
||||||
|
# Unless overridden by the $TESTS environment variable, all tests in the test
|
||||||
|
# suite will be run. Tests targeting a command that is not yet implemented
|
||||||
|
# (e.g. diff3 or sdiff) are skipped.
|
||||||
|
|
||||||
|
scriptpath=$(dirname "$(readlink -f "$0")")
|
||||||
|
rev=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Allow passing a specific profile as parameter (default to "release")
|
||||||
|
profile="release"
|
||||||
|
[[ -n $1 ]] && profile="$1"
|
||||||
|
|
||||||
|
# Verify that the diffutils binary was built for the requested profile
|
||||||
|
binary="$scriptpath/../target/$profile/diffutils"
|
||||||
|
if [[ ! -x "$binary" ]]
|
||||||
|
then
|
||||||
|
echo "Missing build for profile $profile"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Work in a temporary directory
|
||||||
|
tempdir=$(mktemp -d)
|
||||||
|
cd "$tempdir"
|
||||||
|
|
||||||
|
# Check out the upstream test suite
|
||||||
|
gitserver="https://git.savannah.gnu.org"
|
||||||
|
testsuite="$gitserver/git/diffutils.git"
|
||||||
|
echo "Fetching upstream test suite from $testsuite"
|
||||||
|
git clone -n --depth=1 --filter=tree:0 "$testsuite" &> /dev/null
|
||||||
|
cd diffutils
|
||||||
|
git sparse-checkout set --no-cone tests &> /dev/null
|
||||||
|
git checkout &> /dev/null
|
||||||
|
upstreamrev=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Ensure that calling `diff` invokes the built `diffutils` binary instead of
|
||||||
|
# the upstream `diff` binary that is most likely installed on the system
|
||||||
|
mkdir src
|
||||||
|
cd src
|
||||||
|
ln -s "$binary" diff
|
||||||
|
ln -s "$binary" cmp
|
||||||
|
cd ../tests
|
||||||
|
|
||||||
|
# Fetch tests/init.sh from the gnulib repository (needed since
|
||||||
|
# https://git.savannah.gnu.org/cgit/diffutils.git/commit/tests?id=1d2456f539)
|
||||||
|
curl -s "$gitserver/gitweb/?p=gnulib.git;a=blob_plain;f=tests/init.sh;hb=HEAD" -o init.sh
|
||||||
|
|
||||||
|
if [[ -n "$TESTS" ]]
|
||||||
|
then
|
||||||
|
tests="$TESTS"
|
||||||
|
else
|
||||||
|
# Get a list of all upstream tests (default if $TESTS isn't set)
|
||||||
|
echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am
|
||||||
|
tests=$(make -f Makefile.am printtests)
|
||||||
|
fi
|
||||||
|
total=$(echo "$tests" | wc -w)
|
||||||
|
echo "Running $total tests"
|
||||||
|
export LC_ALL=C
|
||||||
|
export KEEP=yes
|
||||||
|
timestamp=$(date -Iseconds)
|
||||||
|
urlroot="$gitserver/cgit/diffutils.git/tree/tests/"
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
skipped=0
|
||||||
|
normal="$(tput sgr0)"
|
||||||
|
for test in $tests
|
||||||
|
do
|
||||||
|
result="FAIL"
|
||||||
|
url="$urlroot$test?id=$upstreamrev"
|
||||||
|
# Run only the tests that invoke `diff` or `cmp`,
|
||||||
|
# because other binaries aren't implemented yet
|
||||||
|
if ! grep -E -s -q "(diff3|sdiff)" "$test"
|
||||||
|
then
|
||||||
|
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS"
|
||||||
|
if [[ $? = 77 ]]
|
||||||
|
then
|
||||||
|
result="SKIP"
|
||||||
|
else
|
||||||
|
json+="{\"test\":\"$test\",\"result\":\"$result\","
|
||||||
|
json+="\"url\":\"$url\","
|
||||||
|
json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\","
|
||||||
|
json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\","
|
||||||
|
json+="\"files\":{"
|
||||||
|
cd gt-$test.*
|
||||||
|
# Note: this doesn't include the contents of subdirectories,
|
||||||
|
# but there isn't much value added in doing so
|
||||||
|
for file in *
|
||||||
|
do
|
||||||
|
[[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\","
|
||||||
|
done
|
||||||
|
json="${json%,}}},"
|
||||||
|
cd - > /dev/null
|
||||||
|
[[ "$result" = "PASS" ]] && (( passed++ ))
|
||||||
|
[[ "$result" = "FAIL" ]] && (( failed++ ))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
result="SKIP"
|
||||||
|
fi
|
||||||
|
color=2 # green
|
||||||
|
[[ "$result" = "FAIL" ]] && color=1 # red
|
||||||
|
if [[ $result = "SKIP" ]]
|
||||||
|
then
|
||||||
|
(( skipped++ ))
|
||||||
|
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
|
||||||
|
color=3 # yellow
|
||||||
|
fi
|
||||||
|
printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -n "Summary: TOTAL: $total / "
|
||||||
|
echo -n "$(tput setaf 2)PASS$normal: $passed / "
|
||||||
|
echo -n "$(tput setaf 1)FAIL$normal: $failed / "
|
||||||
|
echo "$(tput setaf 3)SKIP$normal: $skipped"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
json="\"tests\":[${json%,}]"
|
||||||
|
metadata="\"timestamp\":\"$timestamp\","
|
||||||
|
metadata+="\"revision\":\"$rev\","
|
||||||
|
metadata+="\"upstream-revision\":\"$upstreamrev\","
|
||||||
|
if [[ -n "$GITHUB_ACTIONS" ]]
|
||||||
|
then
|
||||||
|
metadata+="\"branch\":\"$GITHUB_REF\","
|
||||||
|
fi
|
||||||
|
json="{$metadata $json}"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cd "$scriptpath"
|
||||||
|
rm -rf "$tempdir"
|
||||||
|
|
||||||
|
resultsfile="test-results.json"
|
||||||
|
echo "$json" | jq > "$resultsfile"
|
||||||
|
echo "Results written to $scriptpath/$resultsfile"
|
||||||
|
|
||||||
|
(( failed > 0 )) && exit 1
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user