Compare commits

42 Commits

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

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

* Unit tests for the stop_early parameter

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

* fuzz dependency change: diffutils -> diffutilslib

* fix fuzz build.
2024-03-18 21:50:33 +01:00
Olivier Tilloy c68d386170 Implement -s/--report-identical-files option (fixes #23) 2024-03-01 14:45:41 +01:00
Olivier Tilloy a89f30afa0 Ed diff: compact ranges of lines where possible (fixes #21) 2024-02-24 11:55:54 +01:00
Olivier Tilloy 0a67bf9fb8 Add integration tests that check the exit codes and stdout/stderr 2024-02-22 17:26:24 +01:00
Olivier Tilloy 1241db4806 Match GNU diff's implementation for exit codes (fixes #17) 2024-02-22 17:26:24 +01:00
renovate[bot] 3bc8668f78 fix(deps): update rust crate libfuzzer-sys to 0.4 (#5)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-17 18:19:02 +01:00
Sylvestre Ledru c90eee442f Add the example section 2024-02-17 17:07:31 +01:00
Sylvestre Ledru 6c29f02527 improve the readme and example 2024-02-17 17:06:14 +01:00
Olivier Tilloy 790ef1e633 Run the GNU test suite in CI (fixes #8) (#13) 2024-02-17 15:27:52 +01:00
Sylvestre Ledru 4c1a752f11 Merge pull request #12 from oSoMoN/trivial-typo-error-message
Fix trivial typo in error message
2024-02-09 21:21:43 +01:00
Olivier Tilloy 54a5407bec Fix trivial typo in error message 2024-02-09 17:51:50 +01:00
Sylvestre Ledru 02632e915c Merge pull request #11 from oSoMoN/compact-range-of-lines
Normal diff: compact ranges of lines where possible (fixes #10)
2024-02-09 13:57:51 +01:00
Olivier Tilloy 3f9556aa05 Add comments to ease readability and maintainability 2024-02-09 10:28:39 +01:00
Olivier Tilloy a94c6a60cf Normal diff: compact ranges of lines where possible (fixes #10) 2024-02-09 10:28:39 +01:00
Sylvestre Ledru c28973c019 coverage: remove the fail fast to continue for linux even if windows fails 2024-02-09 10:28:27 +01:00
Sylvestre Ledru a660f7440c Merge pull request #1 from uutils/renovate/configure
Configure Renovate
2024-01-27 09:54:27 +01:00
Sylvestre Ledru 6a69a39852 add badges 2024-01-27 09:51:04 +01:00
renovate[bot] b55cbf2ca2 Add renovate.json 2024-01-22 18:51:27 +01:00
21 changed files with 1885 additions and 194 deletions
+21 -1
View File
@@ -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 }
+72
View File
@@ -0,0 +1,72 @@
name: Fuzzing
# spell-checker:ignore fuzzer
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
fuzz-build:
name: Build the fuzzers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target"
- name: Run `cargo-fuzz build`
run: cargo +nightly fuzz build
fuzz-run:
needs: fuzz-build
name: Run the fuzzers
runs-on: ubuntu-latest
timeout-minutes: 5
env:
RUN_FOR: 60
strategy:
matrix:
test-target:
- { name: fuzz_ed, should_pass: true }
- { name: fuzz_normal, should_pass: true }
- { name: fuzz_patch, should_pass: true }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target"
- name: Restore Cached Corpus
uses: actions/cache/restore@v4
with:
key: corpus-cache-${{ matrix.test-target.name }}
path: |
fuzz/corpus/${{ matrix.test-target.name }}
- name: Run ${{ matrix.test-target.name }} for XX seconds
shell: bash
continue-on-error: ${{ !matrix.test-target.name.should_pass }}
run: |
cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Save Corpus Cache
uses: actions/cache/save@v4
with:
key: corpus-cache-${{ matrix.test-target.name }}
path: |
fuzz/corpus/${{ matrix.test-target.name }}
Generated
+401
View File
@@ -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"
+6
View File
@@ -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"
+49 -51
View File
@@ -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.
[![Crates.io](https://img.shields.io/crates/v/diffutils.svg)](https://crates.io/crates/diffutils)
[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE)
[![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils)
[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils)
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust.
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools.
## Installation
Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
Clone the repository and build the project using Cargo:
```bash
git clone https://github.com/uutils/diffutils.git
cd diffutils
cargo build --release
```
~/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
View File
@@ -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
+10 -2
View File
@@ -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)
+3 -2
View File
@@ -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)
+8 -4
View File
@@ -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()
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+151 -29
View File
@@ -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
View File
@@ -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());
}
}
+2
View File
@@ -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
View File
@@ -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(&params.from, &params.to).unwrap_or(false) {
maybe_report_identical_files();
return ExitCode::SUCCESS;
}
// read files
let from_content = match fs::read(&params.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(&params.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, &params),
Format::Unified => unified_diff::diff(&from_content, &to_content, &params),
Format::Context => context_diff::diff(&from_content, &to_content, &params),
Format::Ed => ed_diff::diff(&from_content, &to_content, &params).unwrap_or_else(|error| {
eprintln!("{error}");
exit(2);
}),
};
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
View File
@@ -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
View File
@@ -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(&param);
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
View File
@@ -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
View File
@@ -0,0 +1,118 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::io::Write;
use unicode_width::UnicodeWidthStr;
/// Replace tabs by spaces in the input line.
/// Correctly handle multi-bytes characters.
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
#[must_use]
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
let tab = b'\t';
let ntabs = line.iter().filter(|c| **c == tab).count();
if ntabs == 0 {
return line.to_vec();
}
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
let mut offset = 0;
let mut iter = line.split(|c| *c == tab).peekable();
while let Some(chunk) = iter.next() {
match String::from_utf8(chunk.to_vec()) {
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
Err(_) => offset += chunk.len(),
}
result.extend_from_slice(chunk);
if iter.peek().is_some() {
result.resize(result.len() + tabsize - offset % tabsize, b' ');
offset = 0;
}
}
result
}
/// Write a single line to an output stream, expanding tabs to space if necessary.
/// This assumes that line does not contain any line breaks
/// (if it does and tabs are to be expanded to spaces, the result is undefined).
pub fn do_write_line(
output: &mut Vec<u8>,
line: &[u8],
expand_tabs: bool,
tabsize: usize,
) -> std::io::Result<()> {
if expand_tabs {
output.write_all(do_expand_tabs(line, tabsize).as_slice())
} else {
output.write_all(line)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod expand_tabs {
use super::*;
use pretty_assertions::assert_eq;
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
assert_eq!(
do_expand_tabs(line.as_bytes(), tabsize),
expected.as_bytes()
);
}
#[test]
fn basics() {
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
}
#[test]
fn multibyte_chars() {
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
// Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
// it is displayed as a single emoji and should have a print size of 2 columns,
// but terminal emulators tend to not support this, and display the two emojis
// side by side, thus accounting for a print size of 4 columns.
assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz");
}
#[test]
fn invalid_utf8() {
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
assert_eq!(
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
);
}
}
mod write_line {
use super::*;
use pretty_assertions::assert_eq;
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
let mut output: Vec<u8> = Vec::new();
assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok());
assert_eq!(output, expected.as_bytes());
}
#[test]
fn basics() {
assert_line_written("foo bar baz", false, 8, "foo bar baz");
assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz");
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
}
}
}
+205
View File
@@ -0,0 +1,205 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use assert_cmd::cmd::Command;
use predicates::prelude::*;
use std::io::Write;
use tempfile::NamedTempFile;
// Integration tests for the diffutils command
#[test]
fn unknown_param() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("--foobar");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Usage: "));
Ok(())
}
#[test]
fn cannot_read_from_file() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("foo.txt").arg("bar.txt");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Failed to read from-file"));
Ok(())
}
#[test]
fn cannot_read_to_file() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg(file.path()).arg("bar.txt");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Failed to read to-file"));
Ok(())
}
#[test]
fn no_differences() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg(file.path()).arg(file.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty());
}
Ok(())
}
#[test]
fn no_differences_report_identical_files() -> Result<(), Box<dyn std::error::Error>> {
// same file
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-s").arg(file1.path()).arg(file1.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::eq(format!(
"Files {} and {} are identical\n",
file1.path().to_string_lossy(),
file1.path().to_string_lossy(),
)));
}
// two files with the same content
let mut file2 = NamedTempFile::new()?;
file2.write_all("foo\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-s").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::eq(format!(
"Files {} and {} are identical\n",
file1.path().to_string_lossy(),
file2.path().to_string_lossy(),
)));
}
Ok(())
}
#[test]
fn differences() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::is_empty().not());
}
Ok(())
}
#[test]
fn differences_brief() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = Command::cargo_bin("diffutils")?;
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-q").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"Files {} and {} differ\n",
file1.path().to_string_lossy(),
file2.path().to_string_lossy()
)));
}
Ok(())
}
#[test]
fn missing_newline() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar".as_bytes())?;
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-e").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("No newline at end of file"));
Ok(())
}
#[test]
fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-u")
.arg(file1.path())
.arg("-")
.write_stdin("bar\n");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n",
file1.path().to_string_lossy()
)));
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-u")
.arg("-")
.arg(file2.path())
.write_stdin("foo\n");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"--- /dev/stdin\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n",
file2.path().to_string_lossy()
)));
let mut cmd = Command::cargo_bin("diffutils")?;
cmd.arg("-u").arg("-").arg("-").write_stdin("foo\n");
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty());
Ok(())
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Print the test results written to a JSON file by run-upstream-testsuite.sh
# in a markdown format. The printout includes the name of the test, the result,
# the URL to the test script and the contents of stdout and stderr.
# It can be used verbatim as the description when filing an issue for a test
# with an unexpected result.
json="test-results.json"
[[ -n $1 ]] && json="$1"
codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; }
jq -c '.tests[]' "$json" | while read -r test
do
name=$(echo "$test" | jq -r '.test')
echo "# test: $name"
result=$(echo "$test" | jq -r '.result')
echo "result: $result"
url=$(echo "$test" | jq -r '.url')
echo "url: $url"
if [[ "$result" != "SKIP" ]]
then
stdout=$(echo "$test" | jq -r '.stdout' | base64 -d)
if [[ -n "$stdout" ]]
then
echo "## stdout"
codeblock "$stdout"
fi
stderr=$(echo "$test" | jq -r '.stderr' | base64 -d)
if [[ -n "$stderr" ]]
then
echo "## stderr"
codeblock "$stderr"
fi
fi
echo ""
done
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# Run the GNU upstream test suite for diffutils against a local build of the
# Rust implementation, print out a summary of the test results, and writes a
# JSON file ('test-results.json') containing detailed information about the
# test run.
# The JSON file contains metadata about the test run, and for each test the
# result as well as the contents of stdout, stderr, and of all the files
# written by the test script, if any (excluding subdirectories).
# The script takes a shortcut to fetch only the test suite from the upstream
# repository and carefully avoids running the autotools machinery which is
# time-consuming and resource-intensive, and doesn't offer the option to not
# build the upstream binaries. As a consequence, the environment in which the
# tests are run might not match exactly that used when the upstream tests are
# run through the autotools.
# By default it expects a release build of the diffutils binary, but a
# different build profile can be specified as an argument
# (e.g. 'dev' or 'test').
# Unless overriden by the $TESTS environment variable, all tests in the test
# suite will be run. Tests targeting a command that is not yet implemented
# (e.g. cmp, diff3 or sdiff) are skipped.
scriptpath=$(dirname "$(readlink -f "$0")")
rev=$(git rev-parse HEAD)
# Allow passing a specific profile as parameter (default to "release")
profile="release"
[[ -n $1 ]] && profile="$1"
# Verify that the diffutils binary was built for the requested profile
binary="$scriptpath/../target/$profile/diffutils"
if [[ ! -x "$binary" ]]
then
echo "Missing build for profile $profile"
exit 1
fi
# Work in a temporary directory
tempdir=$(mktemp -d)
cd "$tempdir"
# Check out the upstream test suite
gitserver="https://git.savannah.gnu.org"
testsuite="$gitserver/git/diffutils.git"
echo "Fetching upstream test suite from $testsuite"
git clone -n --depth=1 --filter=tree:0 "$testsuite" &> /dev/null
cd diffutils
git sparse-checkout set --no-cone tests &> /dev/null
git checkout &> /dev/null
upstreamrev=$(git rev-parse HEAD)
# Ensure that calling `diff` invokes the built `diffutils` binary instead of
# the upstream `diff` binary that is most likely installed on the system
mkdir src
cd src
ln -s "$binary" diff
cd ../tests
if [[ -n "$TESTS" ]]
then
tests="$TESTS"
else
# Get a list of all upstream tests (default if $TESTS isn't set)
echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am
tests=$(make -f Makefile.am printtests)
fi
total=$(echo "$tests" | wc -w)
echo "Running $total tests"
export LC_ALL=C
export KEEP=yes
exitcode=0
timestamp=$(date -Iseconds)
urlroot="$gitserver/cgit/diffutils.git/tree/tests/"
passed=0
failed=0
skipped=0
normal="$(tput sgr0)"
for test in $tests
do
result="FAIL"
url="$urlroot$test?id=$upstreamrev"
# Run only the tests that invoke `diff`,
# because other binaries aren't implemented yet
if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test"
then
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1
json+="{\"test\":\"$test\",\"result\":\"$result\","
json+="\"url\":\"$url\","
json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\","
json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\","
json+="\"files\":{"
cd gt-$test.*
# Note: this doesn't include the contents of subdirectories,
# but there isn't much value added in doing so
for file in *
do
[[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\","
done
json="${json%,}}},"
cd - > /dev/null
[[ "$result" = "PASS" ]] && (( passed++ ))
[[ "$result" = "FAIL" ]] && (( failed++ ))
else
result="SKIP"
(( skipped++ ))
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
fi
color=2 # green
[[ "$result" = "FAIL" ]] && color=1 # red
[[ "$result" = "SKIP" ]] && color=3 # yellow
printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test"
done
echo ""
echo -n "Summary: TOTAL: $total / "
echo -n "$(tput setaf 2)PASS$normal: $passed / "
echo -n "$(tput setaf 1)FAIL$normal: $failed / "
echo "$(tput setaf 3)SKIP$normal: $skipped"
echo ""
json="\"tests\":[${json%,}]"
metadata="\"timestamp\":\"$timestamp\","
metadata+="\"revision\":\"$rev\","
metadata+="\"upstream-revision\":\"$upstreamrev\","
if [[ -n "$GITHUB_ACTIONS" ]]
then
metadata+="\"branch\":\"$GITHUB_REF\","
fi
json="{$metadata $json}"
# Clean up
cd "$scriptpath"
rm -rf "$tempdir"
resultsfile="test-results.json"
echo "$json" | jq > "$resultsfile"
echo "Results written to $scriptpath/$resultsfile"
exit $exitcode