mirror of
https://github.com/uutils/diffutils.git
synced 2026-06-29 15:15:15 -04:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 281098d751 | |||
| 34a5cc7340 | |||
| 6a152cdc7f | |||
| b8fada8faa | |||
| a213272d0c | |||
| 25e4a17421 | |||
| 589039ab4c | |||
| f83fccb542 | |||
| 76c4714f78 | |||
| b135b6f218 | |||
| 314e3a7320 | |||
| 6a73657b3a | |||
| e9f0630aaf | |||
| e6a0ba28c5 | |||
| 0ab824abda | |||
| f60fefaf6e | |||
| 14e77548fd | |||
| f2fd2127ed | |||
| cfc68d58bc | |||
| e0283083f2 | |||
| 8d65c2badd | |||
| a304ac0a68 | |||
| f916f1ce86 | |||
| 4ed7ea1553 | |||
| 62e10c6d6c | |||
| c68d386170 | |||
| a89f30afa0 | |||
| 0a67bf9fb8 | |||
| 1241db4806 | |||
| 3bc8668f78 | |||
| c90eee442f | |||
| 6c29f02527 | |||
| 790ef1e633 | |||
| 4c1a752f11 | |||
| 54a5407bec | |||
| 02632e915c | |||
| 3f9556aa05 | |||
| a94c6a60cf | |||
| c28973c019 | |||
| a660f7440c | |||
| 6a69a39852 | |||
| b55cbf2ca2 |
@@ -10,6 +10,7 @@ jobs:
|
||||
name: cargo check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
@@ -21,6 +22,7 @@ jobs:
|
||||
name: cargo test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
@@ -41,6 +43,7 @@ jobs:
|
||||
name: cargo clippy -- -D warnings
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
@@ -49,11 +52,28 @@ jobs:
|
||||
- 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: true
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { os: ubuntu-latest , features: unix }
|
||||
|
||||
@@ -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 }}
|
||||
Generated
+401
@@ -2,18 +2,183 @@
|
||||
# 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]]
|
||||
@@ -26,6 +191,242 @@ dependencies = [
|
||||
"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"
|
||||
|
||||
@@ -16,6 +16,12 @@ 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"
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
The goal of this package is to be a dropped in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust.
|
||||
[](https://crates.io/crates/diffutils)
|
||||
[](https://discord.gg/wQVJbvJ)
|
||||
[](https://github.com/uutils/diffutils/blob/main/LICENSE)
|
||||
[](https://deps.rs/repo/github/uutils/diffutils)
|
||||
|
||||
[](https://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
|
||||
```
|
||||
~/diffutils$ cargo run -- diff -u3 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/diff -u3 Cargo.lock Cargo.toml`
|
||||
--- Cargo.lock
|
||||
+++ Cargo.toml
|
||||
@@ -1,39 +1,7 @@
|
||||
-# This file is automatically @generated by Cargo.
|
||||
-# It is not intended for manual editing.
|
||||
-version = 3
|
||||
-
|
||||
-[[package]]
|
||||
-name = "context-diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "diff 0.1.12",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "context-diff",
|
||||
- "normal-diff",
|
||||
- "unified-diff",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "diff"
|
||||
-version = "0.1.12"
|
||||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
-
|
||||
-[[package]]
|
||||
-name = "normal-diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "diff 0.1.12",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "unified-diff"
|
||||
-version = "0.3.0"
|
||||
-dependencies = [
|
||||
- "diff 0.1.12",
|
||||
+[workspace]
|
||||
+members = [
|
||||
+ "lib/unified-diff",
|
||||
+ "lib/context-diff",
|
||||
+ "lib/normal-diff",
|
||||
+ "bin/diff",
|
||||
]
|
||||
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
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ edition = "2018"
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.3"
|
||||
libfuzzer-sys = "0.4"
|
||||
diffutils = { path = "../" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{ed_diff, normal_diff, unified_diff};
|
||||
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');
|
||||
@@ -30,7 +38,7 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let diff = ed_diff::diff_w(&from, &to, "target/fuzz.file").unwrap();
|
||||
let diff = diff_w(&from, &to, "target/fuzz.file").unwrap();
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{normal_diff, unified_diff};
|
||||
use diffutilslib::normal_diff;
|
||||
use diffutilslib::params::Params;
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
@@ -21,7 +22,7 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||
} else {
|
||||
return
|
||||
}*/
|
||||
let diff = normal_diff::diff(&from, &to);
|
||||
let diff = normal_diff::diff(&from, &to, &Params::default());
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{normal_diff, unified_diff};
|
||||
use diffutilslib::params::Params;
|
||||
use diffutilslib::unified_diff;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
@@ -22,10 +23,13 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
||||
}*/
|
||||
let diff = unified_diff::diff(
|
||||
&from,
|
||||
"a/fuzz.file",
|
||||
&to,
|
||||
"target/fuzz.file",
|
||||
context as usize,
|
||||
&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()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
+151
-29
@@ -6,6 +6,9 @@
|
||||
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>),
|
||||
@@ -41,7 +44,12 @@ impl Mismatch {
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
||||
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);
|
||||
@@ -191,6 +199,10 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatc
|
||||
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);
|
||||
@@ -254,18 +266,20 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatc
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
actual: &[u8],
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size);
|
||||
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;
|
||||
@@ -301,17 +315,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).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");
|
||||
output.write_all(&e).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");
|
||||
output.write_all(&e).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();
|
||||
}
|
||||
}
|
||||
@@ -328,17 +345,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).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");
|
||||
output.write_all(&e).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");
|
||||
output.write_all(&e).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();
|
||||
}
|
||||
}
|
||||
@@ -404,8 +424,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2);
|
||||
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)
|
||||
@@ -422,7 +450,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -477,8 +505,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2);
|
||||
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)
|
||||
@@ -495,7 +531,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -553,8 +589,16 @@ mod tests {
|
||||
};
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2);
|
||||
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)
|
||||
@@ -571,7 +615,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abx.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -632,8 +676,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2);
|
||||
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)
|
||||
@@ -650,7 +702,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -662,4 +714,74 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
+65
-8
@@ -5,6 +5,9 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
@@ -42,7 +45,7 @@ impl Mismatch {
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8]) -> Result<Vec<Mismatch>, DiffError> {
|
||||
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();
|
||||
@@ -94,6 +97,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result<Vec<Mismatch>, DiffError>
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
@@ -103,9 +110,13 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result<Vec<Mismatch>, DiffError>
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn diff(expected: &[u8], actual: &[u8]) -> Result<Vec<u8>, DiffError> {
|
||||
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)?;
|
||||
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;
|
||||
@@ -122,6 +133,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result<Vec<u8>, DiffError> {
|
||||
expected_count + line_number_expected - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(),
|
||||
_ => writeln!(
|
||||
&mut output,
|
||||
"{},{}c",
|
||||
@@ -136,7 +148,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result<Vec<u8>, DiffError> {
|
||||
if actual == b"." {
|
||||
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
|
||||
} else {
|
||||
output.write_all(actual).unwrap();
|
||||
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -151,11 +163,20 @@ 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)?;
|
||||
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/";
|
||||
@@ -220,7 +241,7 @@ mod tests {
|
||||
.stdin(File::open("target/ab.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -291,7 +312,7 @@ mod tests {
|
||||
.stdin(File::open("target/ab_.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -368,7 +389,7 @@ mod tests {
|
||||
.stdin(File::open("target/abr.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -380,4 +401,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod context_diff;
|
||||
pub mod ed_diff;
|
||||
pub mod normal_diff;
|
||||
pub mod params;
|
||||
pub mod unified_diff;
|
||||
pub mod utils;
|
||||
|
||||
// Re-export the public functions/types you need
|
||||
pub use context_diff::diff as context_diff;
|
||||
|
||||
+59
-34
@@ -3,58 +3,83 @@
|
||||
// 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, Params};
|
||||
use crate::params::{parse_params, Format};
|
||||
use std::env;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::process::{exit, ExitCode};
|
||||
|
||||
mod context_diff;
|
||||
mod ed_diff;
|
||||
mod normal_diff;
|
||||
mod params;
|
||||
mod unified_diff;
|
||||
mod utils;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
// 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 {
|
||||
from,
|
||||
to,
|
||||
context_count,
|
||||
format,
|
||||
} = parse_params(opts)?;
|
||||
// read files
|
||||
let from_content = match fs::read(&from) {
|
||||
Ok(from_content) => from_content,
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to read from-file: {e}"));
|
||||
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(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let to_content = match fs::read(&to) {
|
||||
if same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) {
|
||||
maybe_report_identical_files();
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
// read files
|
||||
let from_content = match fs::read(¶ms.from) {
|
||||
Ok(from_content) => from_content,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read from-file: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let to_content = match fs::read(¶ms.to) {
|
||||
Ok(to_content) => to_content,
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to read from-file: {e}"));
|
||||
eprintln!("Failed to read to-file: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
// run diff
|
||||
let result: Vec<u8> = match format {
|
||||
Format::Normal => normal_diff::diff(&from_content, &to_content),
|
||||
Format::Unified => unified_diff::diff(
|
||||
&from_content,
|
||||
&from.to_string_lossy(),
|
||||
&to_content,
|
||||
&to.to_string_lossy(),
|
||||
context_count,
|
||||
),
|
||||
Format::Context => context_diff::diff(
|
||||
&from_content,
|
||||
&from.to_string_lossy(),
|
||||
&to_content,
|
||||
&to.to_string_lossy(),
|
||||
context_count,
|
||||
),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content)?,
|
||||
let result: Vec<u8> = match params.format {
|
||||
Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Context => context_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
}),
|
||||
};
|
||||
io::stdout().write_all(&result).unwrap();
|
||||
Ok(())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+100
-13
@@ -5,6 +5,9 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
@@ -29,7 +32,7 @@ impl Mismatch {
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8]) -> Vec<Mismatch> {
|
||||
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();
|
||||
@@ -100,6 +103,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec<Mismatch> {
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
@@ -110,9 +117,15 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec<Mismatch> {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
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);
|
||||
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;
|
||||
@@ -121,6 +134,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
match (expected_count, actual_count) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => writeln!(
|
||||
// 'a' stands for "Add lines"
|
||||
&mut output,
|
||||
"{}a{},{}",
|
||||
line_number_expected - 1,
|
||||
@@ -129,6 +143,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
)
|
||||
.unwrap(),
|
||||
(_, 0) => writeln!(
|
||||
// 'd' stands for "Delete lines"
|
||||
&mut output,
|
||||
"{},{}d{}",
|
||||
line_number_expected,
|
||||
@@ -136,7 +151,33 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
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,
|
||||
@@ -148,7 +189,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
}
|
||||
for expected in &result.expected {
|
||||
write!(&mut output, "< ").unwrap();
|
||||
output.write_all(expected).unwrap();
|
||||
do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
@@ -159,7 +200,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
}
|
||||
for actual in &result.actual {
|
||||
write!(&mut output, "> ").unwrap();
|
||||
output.write_all(actual).unwrap();
|
||||
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
@@ -173,6 +214,18 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
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/";
|
||||
@@ -221,7 +274,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -238,7 +291,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -313,7 +366,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(&format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -331,7 +384,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abn.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -387,7 +440,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -404,7 +457,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -465,7 +518,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -482,7 +535,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -494,4 +547,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
+277
-20
@@ -1,7 +1,10 @@
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Normal,
|
||||
Unified,
|
||||
Context,
|
||||
@@ -25,6 +28,25 @@ pub struct Params {
|
||||
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> {
|
||||
@@ -34,10 +56,11 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
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 mut context_count = 3;
|
||||
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
|
||||
while let Some(param) = opts.next() {
|
||||
if param == "--" {
|
||||
break;
|
||||
@@ -52,6 +75,34 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
}
|
||||
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(¶m);
|
||||
if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') {
|
||||
let mut bit = p[1..].iter().copied().peekable();
|
||||
@@ -60,10 +111,10 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
while let Some(b) = bit.next() {
|
||||
match b {
|
||||
b'0'..=b'9' => {
|
||||
context_count = (b - b'0') as usize;
|
||||
params.context_count = (b - b'0') as usize;
|
||||
while let Some(b'0'..=b'9') = bit.peek() {
|
||||
context_count *= 10;
|
||||
context_count += (bit.next().unwrap() - b'0') as usize;
|
||||
params.context_count *= 10;
|
||||
params.context_count += (bit.next().unwrap() - b'0') as usize;
|
||||
}
|
||||
}
|
||||
b'c' => {
|
||||
@@ -97,7 +148,7 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
if let Some(context_count_maybe) =
|
||||
context_count_maybe.and_then(|x| x.parse().ok())
|
||||
{
|
||||
context_count = context_count_maybe;
|
||||
params.context_count = context_count_maybe;
|
||||
break;
|
||||
}
|
||||
return Err("Invalid context count".to_string());
|
||||
@@ -113,27 +164,22 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
let from = if let Some(from) = from {
|
||||
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()));
|
||||
};
|
||||
let to = if let Some(to) = to {
|
||||
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()));
|
||||
};
|
||||
let format = format.unwrap_or(Format::Normal);
|
||||
Ok(Params {
|
||||
from,
|
||||
to,
|
||||
format,
|
||||
context_count,
|
||||
})
|
||||
params.format = format.unwrap_or(Format::default());
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -148,8 +194,7 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -161,7 +206,7 @@ mod tests {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Ed,
|
||||
context_count: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -174,6 +219,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-u54"), os("foo"), os("bar")]
|
||||
@@ -187,6 +233,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U54"), os("foo"), os("bar")]
|
||||
@@ -200,6 +247,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
|
||||
@@ -213,6 +261,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-c54"), os("foo"), os("bar")]
|
||||
@@ -222,18 +271,226 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[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"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
..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()
|
||||
|
||||
+152
-29
@@ -6,6 +6,9 @@
|
||||
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>),
|
||||
@@ -32,7 +35,12 @@ impl Mismatch {
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
||||
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);
|
||||
@@ -180,6 +188,10 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatc
|
||||
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);
|
||||
@@ -225,18 +237,20 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatc
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
actual: &[u8],
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size);
|
||||
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;
|
||||
@@ -358,17 +372,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Expected(e) => {
|
||||
write!(output, "-").expect("write to Vec is infallible");
|
||||
output.write_all(&e).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");
|
||||
output.write_all(&c).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");
|
||||
output.write_all(&r).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 => {
|
||||
@@ -434,8 +451,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2);
|
||||
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)
|
||||
@@ -469,7 +494,7 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
let alef = fs::read(&format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
@@ -542,8 +567,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefn", &bet, &format!("{target}/alefn"), 2);
|
||||
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)
|
||||
@@ -559,7 +592,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abn.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -630,8 +663,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2);
|
||||
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)
|
||||
@@ -647,7 +688,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -703,8 +744,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2);
|
||||
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)
|
||||
@@ -720,7 +769,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abx.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -781,8 +830,16 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2);
|
||||
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)
|
||||
@@ -798,7 +855,7 @@ mod tests {
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
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();
|
||||
@@ -810,4 +867,70 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Print the test results written to a JSON file by run-upstream-testsuite.sh
|
||||
# in a markdown format. The printout includes the name of the test, the result,
|
||||
# the URL to the test script and the contents of stdout and stderr.
|
||||
# It can be used verbatim as the description when filing an issue for a test
|
||||
# with an unexpected result.
|
||||
|
||||
json="test-results.json"
|
||||
[[ -n $1 ]] && json="$1"
|
||||
|
||||
codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; }
|
||||
|
||||
jq -c '.tests[]' "$json" | while read -r test
|
||||
do
|
||||
name=$(echo "$test" | jq -r '.test')
|
||||
echo "# test: $name"
|
||||
result=$(echo "$test" | jq -r '.result')
|
||||
echo "result: $result"
|
||||
url=$(echo "$test" | jq -r '.url')
|
||||
echo "url: $url"
|
||||
if [[ "$result" != "SKIP" ]]
|
||||
then
|
||||
stdout=$(echo "$test" | jq -r '.stdout' | base64 -d)
|
||||
if [[ -n "$stdout" ]]
|
||||
then
|
||||
echo "## stdout"
|
||||
codeblock "$stdout"
|
||||
fi
|
||||
stderr=$(echo "$test" | jq -r '.stderr' | base64 -d)
|
||||
if [[ -n "$stderr" ]]
|
||||
then
|
||||
echo "## stderr"
|
||||
codeblock "$stderr"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
Executable
+141
@@ -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
|
||||
Reference in New Issue
Block a user