Compare commits

74 Commits

Author SHA1 Message Date
Sylvestre Ledru 281098d751 Run clippy pedantic fixes 2024-04-03 23:25:48 +02:00
Sylvestre Ledru 34a5cc7340 Merge pull request #45 from oSoMoN/stdin-tests
Unit and integration tests for reading data from standard input
2024-04-03 00:02:21 +02:00
Olivier Tilloy 6a152cdc7f Add an integration test for reading data from stdin 2024-04-02 22:34:42 +02:00
Olivier Tilloy b8fada8faa Add unit tests for missing arguments 2024-04-02 21:10:41 +02:00
Olivier Tilloy a213272d0c Add tests for when '-' is used to signify to use standard input 2024-04-02 20:41:01 +02:00
Sylvestre Ledru 25e4a17421 Merge pull request #44 from oSoMoN/fix-codecov-badge
Fix the link to the Codecov badge
2024-04-02 18:34:51 +02:00
Olivier Tilloy 589039ab4c Fix the link to the Codecov badge 2024-04-02 18:24:09 +02:00
Sylvestre Ledru f83fccb542 Merge pull request #41 from uutils/sylvestre-patch-1
Disable the fail fast (closes: #40)
2024-04-02 07:33:43 +02:00
Sylvestre Ledru 76c4714f78 Disable the fail fast (closes: #40) 2024-04-01 23:15:23 +02:00
Sylvestre Ledru b135b6f218 Merge pull request #37 from oSoMoN/diff-arguments
Pass a Params reference to the various diff() functions, instead of a long list of arguments
2024-04-01 00:46:37 +02:00
Sylvestre Ledru 314e3a7320 Merge pull request #38 from oSoMoN/readme-grammar
README: minor grammar correction
2024-04-01 00:44:36 +02:00
Olivier Tilloy 6a73657b3a README: minor grammar correction 2024-04-01 00:26:30 +02:00
Olivier Tilloy e9f0630aaf Update fuzzers 2024-03-31 23:39:43 +02:00
Olivier Tilloy e6a0ba28c5 Pass a Params reference to the various diff() functions, instead of a long list of arguments 2024-03-31 21:27:28 +02:00
Sylvestre Ledru 0ab824abda Merge pull request #36 from oSoMoN/default-params
Implement the Default trait for Params
2024-03-31 09:26:02 +02:00
Olivier Tilloy f60fefaf6e Implement the Default trait for Params 2024-03-30 23:50:58 +01:00
Sylvestre Ledru 14e77548fd Merge pull request #28 from oSoMoN/implement-expand-tabs-option
Implement `-t`/`--expand-tabs` option (fixes #26) and `--tabsize=NUM` option (fixes #27)
2024-03-30 15:28:53 +01:00
Olivier Tilloy f2fd2127ed Politely ask clippy to not complain about too many arguments 2024-03-28 18:10:31 +01:00
Olivier Tilloy cfc68d58bc Fix fuzzers' invokations 2024-03-28 18:10:31 +01:00
Olivier Tilloy e0283083f2 Implement --tabsize option 2024-03-28 18:10:29 +01:00
Olivier Tilloy 8d65c2badd Implement -t/--expand-tabs option 2024-03-28 18:09:03 +01:00
Sylvestre Ledru a304ac0a68 Merge pull request #30 from cakebaker/fix_clippy_warnings
clippy: fix warnings from useless_vec lint
2024-03-24 19:28:03 +01:00
Daniel Hofstetter f916f1ce86 clippy: fix warnings from useless_vec lint 2024-03-24 14:05:44 +01:00
Olivier Tilloy 4ed7ea1553 Implement -q/--brief option (fixes #19) (#20)
* Implement -q/--brief option

* Optimization: stop analyzing the files as soon as there are any differences

* Unit tests for the stop_early parameter

* Simplify checks
2024-03-19 11:45:06 +01:00
hanbings 62e10c6d6c ci: fuzzers in the Github Actions CI. (#29)
* Add fuzzing CI.

* fuzz dependency change: diffutils -> diffutilslib

* fix fuzz build.
2024-03-18 21:50:33 +01:00
Olivier Tilloy c68d386170 Implement -s/--report-identical-files option (fixes #23) 2024-03-01 14:45:41 +01:00
Olivier Tilloy a89f30afa0 Ed diff: compact ranges of lines where possible (fixes #21) 2024-02-24 11:55:54 +01:00
Olivier Tilloy 0a67bf9fb8 Add integration tests that check the exit codes and stdout/stderr 2024-02-22 17:26:24 +01:00
Olivier Tilloy 1241db4806 Match GNU diff's implementation for exit codes (fixes #17) 2024-02-22 17:26:24 +01:00
renovate[bot] 3bc8668f78 fix(deps): update rust crate libfuzzer-sys to 0.4 (#5)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-17 18:19:02 +01:00
Sylvestre Ledru c90eee442f Add the example section 2024-02-17 17:07:31 +01:00
Sylvestre Ledru 6c29f02527 improve the readme and example 2024-02-17 17:06:14 +01:00
Olivier Tilloy 790ef1e633 Run the GNU test suite in CI (fixes #8) (#13) 2024-02-17 15:27:52 +01:00
Sylvestre Ledru 4c1a752f11 Merge pull request #12 from oSoMoN/trivial-typo-error-message
Fix trivial typo in error message
2024-02-09 21:21:43 +01:00
Olivier Tilloy 54a5407bec Fix trivial typo in error message 2024-02-09 17:51:50 +01:00
Sylvestre Ledru 02632e915c Merge pull request #11 from oSoMoN/compact-range-of-lines
Normal diff: compact ranges of lines where possible (fixes #10)
2024-02-09 13:57:51 +01:00
Olivier Tilloy 3f9556aa05 Add comments to ease readability and maintainability 2024-02-09 10:28:39 +01:00
Olivier Tilloy a94c6a60cf Normal diff: compact ranges of lines where possible (fixes #10) 2024-02-09 10:28:39 +01:00
Sylvestre Ledru c28973c019 coverage: remove the fail fast to continue for linux even if windows fails 2024-02-09 10:28:27 +01:00
Sylvestre Ledru a660f7440c Merge pull request #1 from uutils/renovate/configure
Configure Renovate
2024-01-27 09:54:27 +01:00
Sylvestre Ledru 6a69a39852 add badges 2024-01-27 09:51:04 +01:00
Sylvestre Ledru bf13d528be Merge pull request #2 from cakebaker/ci_replace_actions_rs
ci: replace unmaintained `actions-rs` actions
2024-01-26 17:25:19 +01:00
Daniel Hofstetter 329a7e1f4a ci: use dtolnay/rust-toolchain instead of
unmaintained actions-rs/toolchain
2024-01-26 16:29:22 +01:00
Daniel Hofstetter e5de3cd93e ci: remove actions-rs/cargo & use cargo directly 2024-01-26 16:23:16 +01:00
Sylvestre Ledru 0e14b37e38 refresh the gitignore 2024-01-24 09:31:09 +01:00
Sylvestre Ledru 3c1176082e Ship with Cargo.lock 2024-01-24 09:30:47 +01:00
Sylvestre Ledru 865f97c78d upgrade the rust edition 2024-01-24 09:30:11 +01:00
Sylvestre Ledru f5b65a5720 add debug info 2024-01-23 12:49:20 +01:00
Sylvestre Ledru 1372c5386c use pretty assertions to help with Windows debug 2024-01-23 12:36:57 +01:00
Sylvestre Ledru d891e1034d run clippy pedantic
$ cargo +nightly clippy --allow-dirty --fix -- -W clippy::pedantic
2024-01-23 12:36:38 +01:00
Sylvestre Ledru fdc35f6b8e use mod tests + bring back the function used only for tests 2024-01-23 10:12:08 +01:00
Sylvestre Ledru 6648963df1 remove the unused function diff_w 2024-01-23 10:08:08 +01:00
Sylvestre Ledru a10ef621c8 Ajust the test paths 2024-01-23 10:06:02 +01:00
Sylvestre Ledru 7939749338 remove old config 2024-01-22 22:33:47 +01:00
Sylvestre Ledru 7b3001f1ff adjust the rule 2024-01-22 22:33:29 +01:00
Sylvestre Ledru 416a4be06c remove old files 2024-01-22 22:32:49 +01:00
Sylvestre Ledru 9084134f04 rustfmt the code 2024-01-22 22:32:32 +01:00
Sylvestre Ledru f42fc82f18 adjust the fuzzers to use the new structure 2024-01-22 22:32:11 +01:00
Sylvestre Ledru 0b2505d249 simplify the structure of the crate 2024-01-22 22:01:07 +01:00
Sylvestre Ledru 045435b803 add missing license headers 2024-01-22 21:49:07 +01:00
renovate[bot] b55cbf2ca2 Add renovate.json 2024-01-22 18:51:27 +01:00
Sylvestre Ledru 12f3f16792 add github action 2024-01-22 18:50:30 +01:00
Sylvestre Ledru e9e69b86db update of the README 2024-01-22 18:47:53 +01:00
Sylvestre Ledru 029d747e14 remove trailing spaces 2024-01-22 18:45:41 +01:00
Sylvestre Ledru e38055e5b2 rustfmt the code 2024-01-22 18:41:33 +01:00
Sylvestre Ledru 61cfe6eec4 Fix some clippy warnings 2024-01-22 18:41:01 +01:00
Michael Howell ea9376aaaf Add ed-formatted diff 2021-09-15 19:06:14 -07:00
Michael Howell 582259a867 Refactor into separate bin and lib folders 2021-07-21 13:40:52 -07:00
Michael Howell da05a5254b Fix missingnl behavior on normal-diff 2021-04-03 09:42:35 -07:00
Michael Howell a4f7642d7a Get rid of missing line ending tests
These don't work right with GNU patch. I get assertion errors.
2021-03-09 16:37:42 -07:00
Michael Howell e72ea046b7 Add context and normal diff modes 2021-03-09 16:34:57 -07:00
Michael Howell 2e84164d2f Get rid of sample binary 2021-03-09 11:07:44 -07:00
Michael Howell aa339419e3 Exclude "fuzz" folder 2021-02-25 09:07:43 -07:00
Michael Howell feb8b18ea0 v0.2.0 2021-02-25 09:06:32 -07:00
22 changed files with 4754 additions and 878 deletions
+154
View File
@@ -0,0 +1,154 @@
on: [push, pull_request]
name: Basic CI
env:
CARGO_TERM_COLOR: always
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
- uses: dtolnay/rust-toolchain@stable
- 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
- uses: dtolnay/rust-toolchain@stable
- run: cargo test
fmt:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
clippy:
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
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- run: cargo clippy -- -D warnings
gnu-testsuite:
name: GNU test suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release
# 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
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
id: vars
shell: bash
run: |
## VARs setup
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
# toolchain
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
# * use requested TOOLCHAIN if specified
if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi
outputs TOOLCHAIN
# target-specific options
# * CARGO_FEATURES_OPTION
CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage
# * CODECOV_FLAGS
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS
- name: rust toolchain ~ install
uses: dtolnay/rust-toolchain@nightly
- name: Test
run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
- name: "`grcov` ~ install"
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 --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v3
# if: steps.vars.outputs.HAS_CODECOV_TOKEN
with:
# token: ${{ secrets.CODECOV_TOKEN }}
file: ${{ 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
+72
View File
@@ -0,0 +1,72 @@
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
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target"
- name: Run `cargo-fuzz build`
run: cargo +nightly fuzz build
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_ed, should_pass: true }
- { name: fuzz_normal, should_pass: true }
- { name: fuzz_patch, should_pass: true }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target"
- name: Restore Cached Corpus
uses: actions/cache/restore@v4
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 +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Save Corpus Cache
uses: actions/cache/save@v4
with:
key: corpus-cache-${{ matrix.test-target.name }}
path: |
fuzz/corpus/${{ matrix.test-target.name }}
-1
View File
@@ -1,3 +1,2 @@
/target
Cargo.lock
*.swp
Generated
+434
View File
@@ -0,0 +1,434 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "assert_cmd"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bstr"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "diffutils"
version = "0.3.0"
dependencies = [
"assert_cmd",
"diff",
"predicates",
"pretty_assertions",
"regex",
"same-file",
"tempfile",
"unicode-width",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]]
name = "predicates"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rustix"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[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 = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys",
]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98532992affa02e52709d5b4d145a3668ae10d9081eea4a7f26f719a8476f71"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7269c1442e75af9fa59290383f7665b828efc76c429cc0b7f2ecb33cf51ebae"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f70ab2cebf332b7ecbdd98900c2da5298a8c862472fb35c75fc297eabb9d89b8"
[[package]]
name = "windows_i686_gnu"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "679f235acf6b1639408c0f6db295697a19d103b0cdc88146aa1b992c580c647d"
[[package]]
name = "windows_i686_msvc"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3480ac194b55ae274a7e135c21645656825da4a7f5b6e9286291b2113c94a78b"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42c46bab241c121402d1cb47d028ea3680ee2f359dcc287482dcf7fdddc73363"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc885a4332ee1afb9a1bacf11514801011725570d35675abc229ce7e3afe4d20"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e440c60457f84b0bee09208e62acc7ade264b38c4453f6312b8c9ab1613e73c"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+20 -10
View File
@@ -1,17 +1,27 @@
[package]
name = "unified-diff"
version = "0.1.2"
authors = [
"Michael Howell <michael@notriddle.com>",
"The Rust Project Developers"
]
edition = "2018"
description = "An implementation of the GNU unified diff format"
name = "diffutils"
version = "0.3.0"
edition = "2021"
description = "A CLI app for generating diff files"
license = "MIT OR Apache-2.0"
repository = "https://github.com/notriddle/rust-unified-diff"
repository = "https://github.com/uutils/diffutils"
[lib]
name = "diffutilslib"
path = "src/lib.rs"
[[bin]]
name = "unified-diff"
name = "diffutils"
path = "src/main.rs"
[dependencies]
diff = "0.1.10"
regex = "1.10.3"
same-file = "1.0.6"
unicode-width = "0.1.11"
[dev-dependencies]
pretty_assertions = "1"
assert_cmd = "2.0.14"
predicates = "3.1.0"
tempfile = "3.10.0"
+50 -35
View File
@@ -1,41 +1,56 @@
A GNU unified diff generator. Oracle tested against GNU patch 2.7.6
[![Crates.io](https://img.shields.io/crates/v/diffutils.svg)](https://crates.io/crates/diffutils)
[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE)
[![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils)
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs,
but it implements a different format.
[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils)
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) 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
Running `target/debug/unified-diff Cargo.lock Cargo.toml`
--- Cargo.lock
+++ Cargo.toml
@@ -1,14 +1,14 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-[[package]]
-name = "diff"
-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
Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt`
--- fruits_old.txt
+++ fruits_new.txt
@@ -1,3 +1,3 @@
Apple
-Banana
+Fig
Cherry
```
## License
diffutils is licensed under the MIT and Apache Licenses - see the `LICENSE-MIT` or `LICENSE-APACHE` files for details
+16 -5
View File
@@ -2,7 +2,6 @@
[package]
name = "unified-diff-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
@@ -10,10 +9,8 @@ edition = "2018"
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.unified-diff]
path = ".."
libfuzzer-sys = "0.4"
diffutils = { path = "../" }
# Prevent this from interfering with workspaces
[workspace]
@@ -25,3 +22,17 @@ path = "fuzz_targets/fuzz_patch.rs"
test = 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
+78
View File
@@ -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)
);
}
});
+66
View File
@@ -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)
);
}
});
+24 -7
View File
@@ -1,7 +1,8 @@
#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate unified_diff;
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::params::Params;
use diffutilslib::unified_diff;
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
@@ -20,7 +21,16 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
} else {
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")
.unwrap()
.write_all(&from)
@@ -45,11 +55,18 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
.output()
.unwrap();
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();
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)
);
}
});
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+787
View File
@@ -0,0 +1,787 @@
// 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;
#[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 mut output = format!(
"*** {0}\t\n--- {1}\t\n",
params.from.to_string_lossy(),
params.to.to_string_lossy()
)
.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() {
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\t",
"--- bar\t",
"***************",
"*** 1,3 ****",
" a",
"! b",
" c",
"--- 1,3 ----",
" a",
"! d",
" c",
"",
]
.join("\n");
assert_eq!(diff_full, expected_full.as_bytes());
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\t", "--- bar\t", ""].join("\n");
assert_eq!(diff_brief, expected_brief.as_bytes());
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());
}
}
+440
View File
@@ -0,0 +1,440 @@
// 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::{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_w(&alef, &bet, &format!("{target}/alef")).unwrap();
File::create("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;
let output = Command::new("ed")
.arg(&format!("{target}/alef"))
.stdin(File::open("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 = 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::{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_w(&alef, &bet, "target/alef_").unwrap();
File::create("target/ab_.ed")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("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("ed")
.arg("target/alef_")
.stdin(File::open("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 = fs::read("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::{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_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
File::create("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;
let output = Command::new("ed")
.arg(&format!("{target}/alefr"))
.stdin(File::open("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 = 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());
}
}
+11 -781
View File
@@ -1,782 +1,12 @@
use std::collections::VecDeque;
use std::io::Write;
pub mod context_diff;
pub mod ed_diff;
pub mod normal_diff;
pub mod params;
pub mod unified_diff;
pub mod utils;
#[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) -> 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);
}
}
}
}
}
}
}
// Re-export the public functions/types you need
pub use context_diff::diff as context_diff;
pub use ed_diff::diff as ed_diff;
pub use normal_diff::diff as normal_diff;
pub use unified_diff::diff as unified_diff;
+69 -39
View File
@@ -1,55 +1,85 @@
// Sample program. Do not use.
// 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 std::env;
use std::fs;
use std::io::{self, Write};
use std::process;
fn main() {
let mut o = env::args_os();
// parse CLI
let exe = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: [exe] [from] [to]");
process::exit(1);
}
};
let from = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
process::exit(1);
}
};
let to = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
process::exit(1);
use std::process::{exit, ExitCode};
mod context_diff;
mod ed_diff;
mod normal_diff;
mod params;
mod unified_diff;
mod utils;
// 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.
fn main() -> ExitCode {
let opts = env::args_os();
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 same_file::is_same_file(&params.from, &params.to).unwrap_or(false) {
maybe_report_identical_files();
return ExitCode::SUCCESS;
}
// read files
let from_content = match fs::read(&from) {
let from_content = match fs::read(&params.from) {
Ok(from_content) => from_content,
Err(e) => {
eprintln!("Failed to read from-file: {}", e);
process::exit(2);
eprintln!("Failed to read from-file: {e}");
return ExitCode::from(2);
}
};
let to_content = match fs::read(&to) {
let to_content = match fs::read(&params.to) {
Ok(to_content) => to_content,
Err(e) => {
eprintln!("Failed to read to-file: {}", e);
process::exit(2);
eprintln!("Failed to read to-file: {e}");
return ExitCode::from(2);
}
};
// run diff
io::stdout()
.write_all(&unified_diff::diff(
&from_content,
&from.to_string_lossy(),
&to_content,
&to.to_string_lossy(),
1,
))
.unwrap();
let result: Vec<u8> = match params.format {
Format::Normal => normal_diff::diff(&from_content, &to_content, &params),
Format::Unified => unified_diff::diff(&from_content, &to_content, &params),
Format::Context => context_diff::diff(&from_content, &to_content, &params),
Format::Ed => ed_diff::diff(&from_content, &to_content, &params).unwrap_or_else(|error| {
eprintln!("{error}");
exit(2);
}),
};
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)
}
}
+584
View File
@@ -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());
}
}
+505
View File
@@ -0,0 +1,505 @@
use std::ffi::{OsStr, OsString};
use regex::Regex;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Format {
#[default]
Normal,
Unified,
Context,
Ed,
}
#[cfg(unix)]
fn osstr_bytes(osstr: &OsStr) -> &[u8] {
use std::os::unix::ffi::OsStrExt;
osstr.as_bytes()
}
#[cfg(not(unix))]
fn osstr_bytes(osstr: &OsStr) -> Vec<u8> {
osstr.to_string_lossy().bytes().collect()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Params {
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,
}
impl Default for Params {
fn default() -> Self {
Self {
from: OsString::default(),
to: OsString::default(),
format: Format::default(),
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: 8,
}
}
}
pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
let mut opts = opts.into_iter();
// parse CLI
let Some(exe) = opts.next() else {
return Err("Usage: <exe> <from> <to>".to_string());
};
let mut params = Params::default();
let mut from = None;
let mut to = None;
let mut format = None;
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
while let Some(param) = opts.next() {
if param == "--" {
break;
}
if param == "-" {
if from.is_none() {
from = Some(OsString::from("/dev/stdin"));
} else if to.is_none() {
to = Some(OsString::from("/dev/stdin"));
} else {
return Err(format!("Usage: {} <from> <to>", exe.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 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) => num,
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
};
continue;
}
let p = osstr_bytes(&param);
if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') {
let mut bit = p[1..].iter().copied().peekable();
// Can't use a for loop because `diff -30u` is supposed to make a diff
// with 30 lines of context.
while let Some(b) = bit.next() {
match b {
b'0'..=b'9' => {
params.context_count = (b - b'0') as usize;
while let Some(b'0'..=b'9') = bit.peek() {
params.context_count *= 10;
params.context_count += (bit.next().unwrap() - b'0') as usize;
}
}
b'c' => {
if format.is_some() && format != Some(Format::Context) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Context);
}
b'e' => {
if format.is_some() && format != Some(Format::Ed) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Ed);
}
b'u' => {
if format.is_some() && format != Some(Format::Unified) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Unified);
}
b'U' => {
if format.is_some() && format != Some(Format::Unified) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Unified);
let context_count_maybe = if bit.peek().is_some() {
String::from_utf8(bit.collect::<Vec<u8>>()).ok()
} else {
opts.next().map(|x| x.to_string_lossy().into_owned())
};
if let Some(context_count_maybe) =
context_count_maybe.and_then(|x| x.parse().ok())
{
params.context_count = context_count_maybe;
break;
}
return Err("Invalid context count".to_string());
}
_ => return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))),
}
}
} else if from.is_none() {
from = Some(param);
} else if to.is_none() {
to = Some(param);
} else {
return Err(format!("Usage: {} <from> <to>", exe.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>", exe.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>", exe.to_string_lossy()));
};
params.format = format.unwrap_or(Format::default());
Ok(params)
}
#[cfg(test)]
mod tests {
use super::*;
fn os(s: &str) -> OsString {
OsString::from(s)
}
#[test]
fn basics() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
}
#[test]
fn basics_ed() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Ed,
..Default::default()
}),
parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
);
}
#[test]
fn context_count() {
assert_eq!(
Ok(Params {
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()
)
);
assert_eq!(
Ok(Params {
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()
)
);
assert_eq!(
Ok(Params {
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()
)
);
assert_eq!(
Ok(Params {
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()
)
);
}
#[test]
fn report_identical_files() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
report_identical_files: true,
..Default::default()
}),
parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
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()
)
);
}
#[test]
fn brief() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
brief: true,
..Default::default()
}),
parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
brief: true,
..Default::default()
}),
parse_params(
[os("diff"), os("--brief"), os("foo"), os("bar"),]
.iter()
.cloned()
)
);
}
#[test]
fn expand_tabs() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
for option in ["-t", "--expand-tabs"] {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
expand_tabs: true,
..Default::default()
}),
parse_params(
[os("diff"), os(option), os("foo"), os("bar")]
.iter()
.cloned()
)
);
}
}
#[test]
fn tabsize() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
tabsize: 0,
..Default::default()
}),
parse_params(
[os("diff"), os("--tabsize=0"), os("foo"), os("bar")]
.iter()
.cloned()
)
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
tabsize: 42,
..Default::default()
}),
parse_params(
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
.iter()
.cloned()
)
);
assert!(parse_params(
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[
os("diff"),
os("--tabsize=92233720368547758088"),
os("foo"),
os("bar")
]
.iter()
.cloned()
)
.is_err());
}
#[test]
fn double_dash() {
assert_eq!(
Ok(Params {
from: os("-g"),
to: os("-h"),
..Default::default()
}),
parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned())
);
}
#[test]
fn default_to_stdin() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("/dev/stdin"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("-")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("/dev/stdin"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("-"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("/dev/stdin"),
to: os("/dev/stdin"),
..Default::default()
}),
parse_params([os("diff"), os("-"), os("-")].iter().cloned())
);
assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err());
assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err());
}
#[test]
fn missing_arguments() {
assert!(parse_params([os("diff")].iter().cloned()).is_err());
assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err());
}
#[test]
fn unknown_argument() {
assert!(
parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err()
);
assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err());
assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err());
}
#[test]
fn empty() {
assert!(parse_params([].iter().cloned()).is_err());
}
}
+936
View File
@@ -0,0 +1,936 @@
// 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;
#[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 mut output = format!(
"--- {0}\t\n+++ {1}\t\n",
params.from.to_string_lossy(),
params.to.to_string_lossy()
)
.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() {
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\t",
"+++ bar\t",
"@@ -1,3 +1,3 @@",
" a",
"-b",
"+d",
" c",
"",
]
.join("\n");
assert_eq!(diff_full, expected_full.as_bytes());
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\t", "+++ bar\t", ""].join("\n");
assert_eq!(diff_brief, expected_brief.as_bytes());
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());
}
}
+118
View File
@@ -0,0 +1,118 @@
// 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 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)
}
}
#[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 should have a print size of 2 columns,
// but terminal emulators tend to not support this, and display the two emojis
// side by side, thus accounting for a print size of 4 columns.
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");
}
}
}
+205
View File
@@ -0,0 +1,205 @@
// 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::cmd::Command;
use predicates::prelude::*;
use std::io::Write;
use tempfile::NamedTempFile;
// Integration tests for the diffutils command
#[test]
fn unknown_param() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("--foobar");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Usage: "));
Ok(())
}
#[test]
fn cannot_read_from_file() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("foo.txt").arg("bar.txt");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Failed to read from-file"));
Ok(())
}
#[test]
fn cannot_read_to_file() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg(file.path()).arg("bar.txt");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Failed to read to-file"));
Ok(())
}
#[test]
fn no_differences() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
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 = Command::cargo_bin("diffutils")?;
cmd.arg("-u")
.arg(file1.path())
.arg("-")
.write_stdin("bar\n");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n",
file1.path().to_string_lossy()
)));
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-u")
.arg("-")
.arg(file2.path())
.write_stdin("foo\n");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"--- /dev/stdin\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n",
file2.path().to_string_lossy()
)));
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-u").arg("-").arg("-").write_stdin("foo\n");
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty());
Ok(())
}
+38
View File
@@ -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
+141
View File
@@ -0,0 +1,141 @@
#!/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 overriden 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. cmp, 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
cd ../tests
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
exitcode=0
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`,
# because other binaries aren't implemented yet
if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test"
then
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1
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++ ))
else
result="SKIP"
(( skipped++ ))
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
fi
color=2 # green
[[ "$result" = "FAIL" ]] && color=1 # red
[[ "$result" = "SKIP" ]] && color=3 # yellow
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"
exit $exitcode