Compare commits

288 Commits

Author SHA1 Message Date
Sylvestre Ledru c38fe5f2e5 Merge pull request #158 from uutils/cargo-dist-3
cargo-dist: remove duplicate info
2026-01-02 10:51:12 +01:00
Sylvestre Ledru 44565de705 cargo-dist: refresh & remove duplicate info 2026-01-02 10:43:28 +01:00
Sylvestre Ledru 8c8c1db5c6 Merge pull request #157 from uutils/cargo-dist-2
update cargo dist conf
2026-01-01 20:33:26 +01:00
Sylvestre Ledru 3e02493701 ride along: ship fuzz/Cargo.lock 2026-01-01 20:03:49 +01:00
Sylvestre Ledru c6e8b46d21 update cargo dist 2026-01-01 20:03:47 +01:00
Sylvestre Ledru 125fc298c5 Merge pull request #156 from uutils/0.5.0
prepare version 0.5.0
2026-01-01 19:36:05 +01:00
Sylvestre Ledru 16673cf466 prepare version 0.5.0 2026-01-01 18:40:17 +01:00
Sylvestre Ledru f66ad85757 Merge pull request #152 from kov/cmp_fast_path
cmp_fast_path test improvements
2026-01-01 18:32:30 +01:00
Sylvestre Ledru de9bf94d01 Merge pull request #154 from uutils/renovate/itoa-1.x-lockfile
chore(deps): update rust crate itoa to v1.0.17
2025-12-27 11:17:32 +01:00
renovate[bot] 940a0e00b6 chore(deps): update rust crate itoa to v1.0.17 2025-12-27 09:24:33 +00:00
Gustavo Noronha Silva 7ddc6c6c4b Make cmp_fast_path more robust
On my Mac I see this test fail quite consistently. This change makes it
more resilient in systems with slower startup times, while still
allowing faster systems to finish as soon as possible.
2025-12-26 14:52:16 -03:00
Gustavo Noronha Silva 8997ac06b8 Use specific locale for cmp_fast_path test
The test was failing in the regular MacOS terminal due to it defaulting
to LC_ALL=C. Best to standardize like the other tests that check for
locale-dependent output.
2025-12-26 14:52:12 -03:00
Daniel Hofstetter df90e37566 Merge pull request #151 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.24.0
2025-12-24 07:13:44 +01:00
renovate[bot] f25cad8497 chore(deps): update rust crate tempfile to v3.24.0 2025-12-24 01:55:24 +00:00
LunarEclipse a09dcac41d Fix compilation for 32-bit targets 2025-12-22 00:04:48 +01:00
Daniel Hofstetter 98bf765a98 Merge pull request #150 from uutils/renovate/itoa-1.x-lockfile
chore(deps): update rust crate itoa to v1.0.16
2025-12-21 10:14:13 +01:00
renovate[bot] 1e1e968027 chore(deps): update rust crate itoa to v1.0.16 2025-12-21 05:08:26 +00:00
Daniel Hofstetter 4eee9cefa0 Merge pull request #147 from uutils/renovate/actions-cache-5.x
chore(deps): update actions/cache action to v5
2025-12-12 07:14:43 +01:00
renovate[bot] 67589b9331 chore(deps): update actions/cache action to v5 2025-12-12 02:51:06 +00:00
Daniel Hofstetter 83f6d2db7c tests: fix deprecation warnings from assert_cmd 2025-11-02 23:28:41 +01:00
Daniel Hofstetter b193ea0c43 Merge pull request #141 from uutils/renovate/assert_cmd-2.x-lockfile
chore(deps): update rust crate assert_cmd to v2.1.1
2025-10-29 16:52:48 +01:00
renovate[bot] 5f2ba7a84c chore(deps): update rust crate assert_cmd to v2.1.1 2025-10-29 15:19:26 +00:00
E 15473edcd7 Cargo.toml sync 2 profiles with other uutils 2025-10-29 16:18:18 +01:00
renovate[bot] 30b6bd2523 chore(deps): update rust crate assert_cmd to v2.1.0 2025-10-29 10:21:47 +01:00
renovate[bot] 590a4b405e chore(deps): update rust crate regex to v1.12.2 2025-10-13 21:31:41 +02:00
Daniel Hofstetter 418596138e Merge pull request #135 from uutils/renovate/regex-1.x-lockfile
chore(deps): update rust crate regex to v1.12.1
2025-10-11 07:17:39 +02:00
renovate[bot] eadc8c3dc5 chore(deps): update rust crate regex to v1.12.1 2025-10-11 00:35:41 +00:00
renovate[bot] 2806ec2029 chore(deps): update rust crate unicode-width to v0.2.2 2025-10-09 13:25:15 +02:00
renovate[bot] dbd60416e6 chore(deps): update rust crate regex to v1.11.3 2025-10-09 13:13:45 +02:00
Olivier Tilloy 392b8fa07b cargo dist: update to a recent release 2025-09-24 19:18:39 +02:00
Olivier Tilloy 44645e5428 Merge pull request #131 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.23.0
2025-09-23 12:55:07 +02:00
renovate[bot] 644a794067 chore(deps): update rust crate tempfile to v3.23.0 2025-09-23 10:46:28 +00:00
Olivier Tilloy 19b79efd76 Merge pull request #130 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.22.0
2025-09-09 21:57:23 +02:00
renovate[bot] 3380bab935 chore(deps): update rust crate tempfile to v3.22.0 2025-09-09 17:31:06 +00:00
Olivier Tilloy a0a05eeba9 Merge pull request #129 from uutils/renovate/chrono-0.x-lockfile
fix(deps): update rust crate chrono to v0.4.42
2025-09-08 12:56:03 +02:00
renovate[bot] 611e380266 fix(deps): update rust crate chrono to v0.4.42 2025-09-08 10:46:32 +00:00
Daniel Hofstetter af0dc993b8 Merge pull request #128 from uutils/renovate/regex-1.x-lockfile
fix(deps): update rust crate regex to v1.11.2
2025-08-24 18:49:09 +02:00
renovate[bot] a680c4f467 fix(deps): update rust crate regex to v1.11.2 2025-08-24 16:38:04 +00:00
Daniel Hofstetter 0b604f67aa Merge pull request #127 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.21.0
2025-08-20 08:00:37 +02:00
renovate[bot] cc67cbcc59 chore(deps): update rust crate tempfile to v3.21.0 2025-08-20 01:43:49 +00:00
Olivier Tilloy a95ca0062f Merge pull request #125 from cakebaker/clippy_fix_warnings
clippy: fix warnings from `unnecessary_unwrap` lint
2025-08-11 13:09:06 +02:00
Daniel Hofstetter b59d9be943 clippy: fix warnings from unnecessary_unwrap lint 2025-08-08 11:48:58 +02:00
Olivier Tilloy 3654b82a6d Merge pull request #123 from cakebaker/clippy_fix_warnings
clippy: fix warnings
2025-06-27 11:28:09 +02:00
Daniel Hofstetter 7df02399ba clippy: fix warning from ptr_arg lint 2025-06-27 10:52:37 +02:00
Daniel Hofstetter 03fe614087 clippy: fix warnings from useless_format lint 2025-06-27 10:50:06 +02:00
Daniel Hofstetter 8261d790f4 clippy: fix warnings from uninlined_format_args 2025-06-27 10:45:40 +02:00
Olivier Tilloy 168dae3aee Merge pull request #122 from uutils/renovate/unicode-width-0.x-lockfile
fix(deps): update rust crate unicode-width to v0.2.1
2025-06-10 06:41:24 +02:00
renovate[bot] c7d4140fa3 fix(deps): update rust crate unicode-width to v0.2.1 2025-06-09 23:03:42 +00:00
Sylvestre Ledru dee3bc1d66 Merge pull request #117 from sami-daniel/main
Create the side-by-side option (-y) feature for the diff command (Incomplete)
2025-06-03 14:01:38 +02:00
Sami Daniel fce0881e27 Merge branch 'main' into main 2025-06-02 22:37:12 -03:00
Sami Daniel (Tsoi) 45b3072534 Configure CI fuzzer for fuzz_side
Configuring CI to run fuzz from fuzz_side
2025-06-02 22:33:21 -03:00
renovate[bot] a3e57c950e chore(deps): update rust crate tempfile to v3.20.0 2025-06-02 22:33:21 -03:00
Sami Daniel (Tsoi) 1ef6923b7d Add side by side diff (partial)
Create the diff -y utility, this time introducing tests and changes focused
    mainly on the construction of the utility and issues related to alignment
    and response tabulation. New parameters were introduced such as the size
    of the total width of the output in the parameters. A new calculation was
    introduced to determine the size of the output columns and the maximum
    total column size. The tab and spacing mechanism has the same behavior
     as the original diff, with tabs and spaces formatted in the same way.

    - Introducing tests for the diff 'main' function
    - Introducing fuzzing for side diff utility
    - Introducing tests for internal mechanisms
    - Modular functions that allow consistent changes across the entire project
2025-06-02 22:33:04 -03:00
renovate[bot] dff98a2969 fix(deps): update rust crate chrono to v0.4.41 2025-06-02 22:33:04 -03:00
Sami Daniel 8105420bb4 Create the side-by-side option (-y) feature for the diff command (Incomplete).
- Create the function, in the utils package, limited_string that allows you to truncate a string based on a
delimiter (May break the encoding of the character where it was cut)

- Create tests for limited_string function

- Add support for -y and --side-by-side flags that enables diff output for side-by-side mode

- Create implementation of the diff -y (SideBySide) command, base command for sdiff, using the crate
diff as engine. Currently it does not fully represent GNU diff -y, some flags (|, (, ), , /) could
not be developed due to the limitation of the engine we currently use (crate diff), which did not
allow perform logic around it. Only the use of '<' and '>' were enabled.

- Create tests for SideBySide implementation
2025-06-02 22:32:11 -03:00
Daniel Hofstetter 5b791e8bf6 Merge pull request #119 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.20.0
2025-05-12 07:17:30 +02:00
renovate[bot] b31df0b5e8 chore(deps): update rust crate tempfile to v3.20.0 2025-05-11 22:12:26 +00:00
Olivier Tilloy c02273c827 Merge pull request #118 from uutils/renovate/chrono-0.x-lockfile
fix(deps): update rust crate chrono to v0.4.41
2025-04-29 17:58:44 +02:00
renovate[bot] 199c7f169c fix(deps): update rust crate chrono to v0.4.41 2025-04-29 15:45:04 +00:00
Olivier Tilloy 978390c14d Merge pull request #116 from uutils/renovate/assert_cmd-2.x-lockfile
chore(deps): update rust crate assert_cmd to v2.0.17
2025-04-16 22:04:28 +02:00
renovate[bot] 87ccc8e4c2 chore(deps): update rust crate assert_cmd to v2.0.17 2025-04-16 19:55:52 +00:00
Olivier Tilloy 9bc53486df Merge pull request #115 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.19.1
2025-03-20 06:47:26 +01:00
renovate[bot] 0d7e4d82ae chore(deps): update rust crate tempfile to v3.19.1 2025-03-19 22:53:36 +00:00
Olivier Tilloy 360bff50ed Merge pull request #114 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.19.0
2025-03-14 06:28:33 +01:00
renovate[bot] 26ee98dfaa chore(deps): update rust crate tempfile to v3.19.0 2025-03-14 02:47:05 +00:00
Olivier Tilloy 009d64acd2 Merge pull request #113 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.18.0
2025-03-07 06:42:32 +01:00
renovate[bot] b53d4f427c chore(deps): update rust crate tempfile to v3.18.0 2025-03-06 23:24:15 +00:00
Olivier Tilloy 8448fd8068 Merge pull request #111 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.17.1
2025-03-04 12:08:10 +01:00
renovate[bot] d573c3ae1d chore(deps): update rust crate tempfile to v3.17.1 2025-03-04 10:51:49 +00:00
Olivier Tilloy ca1c4c3618 Merge pull request #110 from uutils/renovate/predicates-3.x-lockfile
chore(deps): update rust crate predicates to v3.1.3
2025-03-04 11:50:11 +01:00
renovate[bot] ba1cac3c20 chore(deps): update rust crate predicates to v3.1.3 2025-03-04 10:40:57 +00:00
Olivier Tilloy 949cccebd4 Merge pull request #109 from uutils/renovate/chrono-0.x-lockfile
fix(deps): update rust crate chrono to v0.4.40
2025-03-04 11:39:52 +01:00
renovate[bot] bbdfa1b765 fix(deps): update rust crate chrono to v0.4.40 2025-03-04 10:29:12 +00:00
Olivier Tilloy 2f1a89173a Merge pull request #108 from uutils/renovate/itoa-1.x-lockfile
fix(deps): update rust crate itoa to v1.0.15
2025-03-04 11:27:34 +01:00
renovate[bot] f9553984f4 fix(deps): update rust crate itoa to v1.0.15 2025-03-04 10:09:06 +00:00
Olivier Tilloy 59920040f6 Merge pull request #112 from oSoMoN/ci-macos-use-gpatch
ci: make sure gpatch is actually being used for tests on MacOS
2025-03-04 11:07:02 +01:00
Olivier Tilloy 44c195c0b2 ci: make sure gpatch is actually being used for tests on MacOS 2025-03-04 10:58:14 +01:00
Daniel Hofstetter fdc69921e6 Merge pull request #107 from uutils/renovate/codecov-codecov-action-5.x
chore(deps): update codecov/codecov-action action to v5
2024-11-16 09:52:40 +01:00
Daniel Hofstetter 4ff2d6b182 ci: fix deprecated codecov argument 2024-11-16 09:42:50 +01:00
renovate[bot] 39e092488b chore(deps): update codecov/codecov-action action to v5 2024-11-16 07:49:05 +00:00
Olivier Tilloy dcd3dfd6e0 Merge pull request #106 from cakebaker/ci_fix_cargo_features_option
ci: remove `CARGO_FEATURES_OPTION`
2024-11-08 16:50:55 +01:00
Daniel Hofstetter 90bed40046 ci: remove CARGO_FEATURES_OPTION 2024-11-08 09:48:11 +01:00
Sylvestre Ledru 1575aec22c Merge pull request #105 from cakebaker/fix_unused_import_on_windows
Fix "unused import" warning on Windows
2024-11-08 09:36:06 +01:00
Daniel Hofstetter 4f2f869021 Fix "unused import" warning on Windows 2024-11-08 09:06:33 +01:00
Daniel Hofstetter 3101aa1aff Merge pull request #104 from uutils/renovate/tempfile-3.x-lockfile
chore(deps): update rust crate tempfile to v3.14.0
2024-11-08 07:09:59 +01:00
renovate[bot] 14b062251f chore(deps): update rust crate tempfile to v3.14.0 2024-11-08 03:47:37 +00:00
Daniel Hofstetter 0e11811ce1 Merge pull request #102 from uutils/renovate/regex-1.x-lockfile
fix(deps): update rust crate regex to v1.11.1
2024-10-24 17:14:14 +02:00
renovate[bot] 3de1930bbe fix(deps): update rust crate regex to v1.11.1 2024-10-24 15:05:12 +00:00
Sylvestre Ledru 889e7bb7cc Merge pull request #101 from cakebaker/fix_clippy_warnings
Fix clippy warnings
2024-10-18 09:53:12 +02:00
Daniel Hofstetter 1910cbfe58 Fix warnings from needless_borrow lint 2024-10-18 09:10:03 +02:00
Daniel Hofstetter c70cc1921c Fix warnings from write_with_newline lint 2024-10-18 09:08:07 +02:00
Olivier Tilloy 933230e103 Merge pull request #100 from kov/cmp-mem
cmp: print verbose diffs as we find them
2024-10-08 14:07:56 +02:00
Gustavo Noronha Silva a316262603 cmp: print verbose diffs as we find them
Before this change, we would first find all changes so we could obtain
the largest offset we will report and use that to set up the padding.

Now we use the file sizes to estimate the largest possible offset.
Not only does this allow us to print earlier, reduces memory usage, as
we do not store diffs to report later, but it also fixes a case in
which our output was different to GNU cmp's - because it also seems
to estimate based on size.

Memory usage drops by a factor of 1000(!), without losing performance
while comparing 2 binaries of hundreds of MBs:

Before:

 Maximum resident set size (kbytes): 2489260

 Benchmark 1: ../target/release/diffutils \
 cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so
   Time (mean ± σ):     14.466 s ±  0.166 s    [User: 12.367 s, System: 2.012 s]
   Range (min … max):   14.350 s … 14.914 s    10 runs

After:

 Maximum resident set size (kbytes): 2636

 Benchmark 1: ../target/release/diffutils \
 cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so
   Time (mean ± σ):     13.724 s ±  0.038 s    [User: 12.263 s, System: 1.372 s]
   Range (min … max):   13.667 s … 13.793 s    10 runs
2024-10-08 08:28:06 -03:00
Sylvestre Ledru 0bf04b4395 README.md: be explicit with the list of tools (#99) 2024-10-02 12:00:43 +00:00
Sylvestre Ledru 7480068e7d Merge pull request #93 from oSoMoN/upstream-test-suite-skipped-tests-issue92
Upstream test suite: correctly handle tests that are skipped (fixes #92)
2024-10-02 13:55:53 +02:00
Olivier Tilloy 763074a804 Merge pull request #88 from kov/cmp
Add cmp utility (fixes #14)
2024-10-01 18:59:15 +02:00
Gustavo Noronha Silva fac8dab182 cmp: completely avoid Rust fmt in verbose mode
This makes the code less readable, but gets us a massive improvement
to performance. Comparing ~36M completely different files now takes
~40% of the time. Compared to GNU cmp, we now run the same comparison
in ~26% of the time.

This also improves comparing binary files. A comparison of chromium
and libxul now takes ~60% of the time. We also beat GNU cmpi by about
the same margin.

Before:

 > hyperfine --warmup 1 -i --output=pipe \
     '../target/release/diffutils cmp -l huge huge.3'
 Benchmark 1: ../target/release/diffutils cmp -l huge huge.3
   Time (mean ± σ):      2.000 s ±  0.016 s    [User: 1.603 s, System: 0.392 s]
   Range (min … max):    1.989 s …  2.043 s    10 runs

   Warning: Ignoring non-zero exit code.

 > hyperfine --warmup 1 -i --output=pipe \
     '../target/release/diffutils cmp -l -b \
     /usr/lib64/chromium-browser/chromium-browser \
     /usr/lib64/firefox/libxul.so'
 Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so
   Time (mean ± σ):     24.704 s ±  0.162 s    [User: 21.948 s, System: 2.700 s]
   Range (min … max):   24.359 s … 24.889 s    10 runs

   Warning: Ignoring non-zero exit code.

After:

 > hyperfine --warmup 1 -i --output=pipe \
     '../target/release/diffutils cmp -l huge huge.3'
 Benchmark 1: ../target/release/diffutils cmp -l huge huge.3
   Time (mean ± σ):     849.5 ms ±   6.2 ms    [User: 538.3 ms, System: 306.8 ms]
   Range (min … max):   839.4 ms … 857.7 ms    10 runs

   Warning: Ignoring non-zero exit code.

 > hyperfine --warmup 1 -i --output=pipe \
     '../target/release/diffutils cmp -l -b \
     /usr/lib64/chromium-browser/chromium-browser \
     /usr/lib64/firefox/libxul.so'
 Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so
   Time (mean ± σ):     14.646 s ±  0.040 s    [User: 12.328 s, System: 2.286 s]
   Range (min … max):   14.585 s … 14.702 s    10 runs

   Warning: Ignoring non-zero exit code.
2024-10-01 13:30:57 -03:00
Gustavo Noronha Silva 2e681301b4 cmp: avoid using advanced rust formatting for -l
Octal conversion and simple integer to string both show up in profiling.
This change improves comparing ~36M completely different files wth both
-l and -b by ~11-13%.
2024-10-01 13:30:57 -03:00
Gustavo Noronha Silva 50057412bd Add cmp utility
The utility should support all the arguments supported by GNU cmp and
perform slightly better.

On a "bad" scenario, ~36M files which are completely different, our
version runs in ~72% of the time of the original on my M1 Max:

 > hyperfine --warmup 1 -i --output=pipe \
     'cmp -l huge huge.3'
 Benchmark 1: cmp -l huge huge.3
   Time (mean ± σ):      3.237 s ±  0.014 s    [User: 2.891 s, System: 0.341 s]
   Range (min … max):    3.221 s …  3.271 s    10 runs

   Warning: Ignoring non-zero exit code.

 > hyperfine --warmup 1 -i --output=pipe \
     '../target/release/diffutils cmp -l huge huge.3'
 Benchmark 1: ../target/release/diffutils cmp -l huge huge.3
   Time (mean ± σ):      2.392 s ±  0.009 s    [User: 1.978 s, System: 0.406 s]
   Range (min … max):    2.378 s …  2.406 s    10 runs

   Warning: Ignoring non-zero exit code.

Our cmp runs in ~116% of the time when comparing libxul.so to the
chromium-browser binary with -l and -b. In a best case scenario of
comparing 2 files which are the same except for the last byte, our
tool is slightly faster.
2024-10-01 13:30:57 -03:00
Daniel Hofstetter 68292b370d Merge pull request #95 from uutils/renovate/regex-1.x-lockfile
Update Rust crate regex to v1.11.0
2024-09-29 17:43:59 +02:00
renovate[bot] 26bcc102c0 Update Rust crate regex to v1.11.0 2024-09-29 15:37:34 +00:00
Olivier Tilloy 50198ef2c1 Merge pull request #94 from uutils/renovate/tempfile-3.x-lockfile
Update Rust crate tempfile to v3.13.0
2024-09-28 23:45:47 +02:00
renovate[bot] bfdbf6d7b2 Update Rust crate tempfile to v3.13.0 2024-09-28 19:37:34 +00:00
Olivier Tilloy f75c187971 Upstream test suite: correctly handle tests that are skipped (fixes #92) 2024-09-27 19:45:34 +02:00
Sylvestre Ledru d07c0438b5 Merge pull request #91 from oSoMoN/upstream-test-suite-fetch-init-issue90
When running the upstream test suite, fetch missing tests/init.sh (fixes #90)
2024-09-27 07:57:43 +02:00
Gustavo Noronha Silva 72c7802f06 Take utility name as first parameter on diffutils
This is in preparation for adding the other diffutils commands, cmp,
diff3, sdiff.

We use a similar strategy to uutils/coreutils, with the single binary
acting as one of the supported tools if called through a symlink with
the appropriate name. When using the multi-tool binary directly, the
utility needds to be the first parameter.
2024-09-26 21:22:24 -03:00
Olivier Tilloy c1b66e4a47 When running the upstream test suite, fetch missing tests/init.sh (fixes #90) 2024-09-26 22:44:56 +02:00
Daniel Hofstetter 9103365691 Merge pull request #87 from uutils/renovate/unicode-width-0.x
Update Rust crate unicode-width to 0.2.0
2024-09-20 07:24:05 +02:00
renovate[bot] 7574243de1 Update Rust crate unicode-width to 0.2.0 2024-09-20 05:14:22 +00:00
Olivier Tilloy 6f3834d69d Merge pull request #86 from uutils/renovate/unicode-width-0.x-lockfile
Update Rust crate unicode-width to v0.1.14
2024-09-19 22:57:18 +02:00
Olivier Tilloy d8b91fd60e Update unit test expectation 2024-09-19 22:33:33 +02:00
renovate[bot] 7c9c2a1ab2 Update Rust crate unicode-width to v0.1.14 2024-09-19 20:09:56 +00:00
Daniel Hofstetter 63d51bcd69 Merge pull request #85 from uutils/renovate/pretty_assertions-1.x-lockfile
Update Rust crate pretty_assertions to v1.4.1
2024-09-16 13:30:18 +02:00
renovate[bot] d5bce65a29 Update Rust crate pretty_assertions to v1.4.1 2024-09-16 10:41:54 +00:00
Sylvestre Ledru 9db1eab1d0 Revert "cargo-dist: generate more targets"
This reverts commit 80b993141b.
2024-09-12 10:10:47 +02:00
Sylvestre Ledru 2392acfad1 Merge pull request #84 from cakebaker/fix_clippy_warnings
Fix clippy warnings in tests
2024-09-06 10:05:13 +02:00
Daniel Hofstetter 2a899a9fc7 Fix clippy warnings in tests
from needless_borrows_for_generic_args lint
2024-09-06 09:27:53 +02:00
Sylvestre Ledru 6ec8370b4b Merge pull request #70 from oSoMoN/grcov-instrumentation-based-coverage
Use the instrumentation-based code coverage implementation
2024-09-06 08:54:32 +02:00
Olivier Tilloy dbabf399d5 Use the instrumentation-based code coverage implementation 2024-08-16 00:35:46 +02:00
Daniel Hofstetter b815162b80 Merge pull request #83 from uutils/renovate/assert_cmd-2.x-lockfile
Update Rust crate assert_cmd to v2.0.16
2024-08-09 08:08:54 +02:00
renovate[bot] 12b205e655 Update Rust crate assert_cmd to v2.0.16 2024-08-09 03:18:54 +00:00
Daniel Hofstetter 8a6504dd83 Merge pull request #82 from uutils/renovate/tempfile-3.x-lockfile
Update Rust crate tempfile to v3.12.0
2024-08-07 08:08:29 +02:00
renovate[bot] 67ef43083a Update Rust crate tempfile to v3.12.0 2024-08-06 23:23:06 +00:00
Daniel Hofstetter b1738538a8 Merge pull request #81 from uutils/renovate/tempfile-3.x-lockfile
Update Rust crate tempfile to v3.11.0
2024-08-03 07:01:16 +02:00
renovate[bot] eea6b62b20 Update Rust crate tempfile to v3.11.0 2024-08-02 22:18:35 +00:00
Daniel Hofstetter f08a3bf512 Merge pull request #80 from uutils/renovate/regex-1.x-lockfile
Update Rust crate regex to v1.10.6
2024-08-02 18:41:22 +02:00
renovate[bot] e55ee893dd Update Rust crate regex to v1.10.6 2024-08-02 16:27:14 +00:00
Olivier Tilloy 24245ee098 Merge pull request #78 from uutils/renovate/assert_cmd-2.x-lockfile
Update Rust crate assert_cmd to v2.0.15
2024-07-28 15:12:16 +00:00
Olivier Tilloy 11f815a7c2 Merge pull request #79 from uutils/renovate/predicates-3.x-lockfile
Update Rust crate predicates to v3.1.2
2024-07-28 15:11:13 +00:00
renovate[bot] e9a8141618 Update Rust crate predicates to v3.1.2 2024-07-25 17:59:30 +00:00
renovate[bot] c9a756eb43 Update Rust crate assert_cmd to v2.0.15 2024-07-25 14:28:54 +00:00
Sylvestre Ledru 468c4bf934 Merge pull request #63 from oSoMoN/io-error-msg
Make error message consistent with GNU diff's implementation when failing to read input file(s)
2024-06-27 14:16:10 +02:00
Daniel Hofstetter 1e8fdd58d9 Merge pull request #77 from uutils/renovate/regex-1.x-lockfile
Update Rust crate regex to v1.10.5
2024-06-09 15:59:55 +02:00
renovate[bot] e98b5e179e Update Rust crate regex to v1.10.5 2024-06-09 13:30:51 +00:00
Sylvestre Ledru 1901982375 Merge pull request #76 from uutils/renovate/unicode-width-0.x-lockfile
Update Rust crate unicode-width to v0.1.13
2024-06-04 20:40:56 +02:00
renovate[bot] eee6f49920 Update Rust crate unicode-width to v0.1.13 2024-06-04 17:06:50 +00:00
Olivier Tilloy 8a3a977d2c Update the expected error message for Windows 2024-06-04 14:57:50 +02:00
Olivier Tilloy fa4e0c6097 Make error message consistent with GNU diff's implementation when failing to read input file(s) 2024-06-04 14:57:50 +02:00
Sylvestre Ledru d362046ae5 release v0.4.2 2024-05-19 19:09:05 +02:00
Daniel Hofstetter 7964afa336 Merge pull request #74 from uutils/cargo-dist
cargo-dist: generate more targets
2024-05-19 12:40:07 +02:00
Sylvestre Ledru 80b993141b cargo-dist: generate more targets 2024-05-19 11:59:26 +02:00
Daniel Hofstetter d922313c8c Merge pull request #71 from uutils/renovate/libfuzzer-sys-0.x
Update Rust crate libfuzzer-sys to 0.4.7
2024-05-01 13:37:01 +02:00
Daniel Hofstetter 3e246ab36c Merge pull request #72 from uutils/renovate/pretty_assertions-1.x
Update Rust crate pretty_assertions to 1.4.0
2024-05-01 13:29:09 +02:00
renovate[bot] 4b70969ff1 Update Rust crate pretty_assertions to 1.4.0 2024-05-01 09:56:55 +00:00
renovate[bot] 767c6f6c4a Update Rust crate libfuzzer-sys to 0.4.7 2024-05-01 09:56:50 +00:00
Sylvestre Ledru 1f896ca1ac Merge pull request #69 from oSoMoN/cargo-dist-version-0.13.3
CI: Update 'cargo dist' to version 0.13.3
2024-05-01 08:31:36 +02:00
Olivier Tilloy 713bd210ab CI: Update 'cargo dist' to version 0.13.3 2024-04-30 23:55:49 +02:00
Olivier Tilloy 61314eaf4e Merge pull request #65 from uutils/renovate/unicode-width-0.x
Update Rust crate unicode-width to 0.1.12
2024-04-30 18:47:39 +02:00
renovate[bot] bf9147733d Update Rust crate unicode-width to 0.1.12 2024-04-30 05:30:32 +00:00
Daniel Hofstetter ce8457cbdb Merge pull request #67 from oSoMoN/macos-ci-install-gpatch
CI: install GNU patch on MacOS (fixes #66)
2024-04-30 07:14:02 +02:00
Olivier Tilloy df778c610b CI: install GNU patch on MacOS (fixes #66) 2024-04-29 22:55:08 +02:00
Sylvestre Ledru d92132e721 version 0.4.1 2024-04-27 13:12:58 +02:00
Sylvestre Ledru 99d4d02985 add missing copyright 2024-04-27 13:12:16 +02:00
Olivier Tilloy e7dc6558c6 Merge pull request #56 from TanmayPatil105/handle-directory-input
Handle directory-file and file-directory comparisons in the diff
2024-04-23 22:40:28 +02:00
Tanmay Patil 8c6a648aef Merge branch 'main' into handle-directory-input 2024-04-23 23:11:31 +05:30
Tanmay Patil 0304391bc5 Create test files in temporary directory 2024-04-23 22:44:06 +05:30
Sylvestre Ledru 8de0ca60d1 Merge pull request #52 from oSoMoN/long-options
Handle long option names for the supported output styles…
2024-04-23 18:44:47 +02:00
Sylvestre Ledru 43b9c524d9 Merge pull request #62 from oSoMoN/integration-no-hardcoded-filename
Un-hardcode a test filename in an integration test (fixes #61)
2024-04-23 18:36:33 +02:00
Olivier Tilloy 3dc3fdf5cd Un-hardcode a test filename in an integration test (fixes #61) 2024-04-23 18:00:56 +02:00
Olivier Tilloy b7261a43f4 Break out the logic to match context/unified diff params into separate functions, for improved readability 2024-04-22 18:01:00 +02:00
Olivier Tilloy 37fe1ae808 Handle --normal, -e and --ed options 2024-04-22 18:01:00 +02:00
Olivier Tilloy 22d973fce6 Parse all valid arguments accepted by GNU diff to request a regular context (with an optional number of lines) 2024-04-22 18:01:00 +02:00
Olivier Tilloy fe28610f21 Parse all valid arguments accepted by GNU diff to request a unified context (with an optional number of lines) 2024-04-22 18:01:00 +02:00
Sylvestre Ledru 3a8eddfe2c Fix typos 2024-04-21 16:07:01 +02:00
Tanmay Patil 476e69ee20 Windows: Fix tests 2024-04-21 18:06:15 +05:30
Tanmay Patil 65993d6a13 Add tests for diff FILE DIRECTORY 2024-04-21 16:10:48 +05:30
Tanmay Patil 39d2ece187 Handle directory-file and file-directory comparisons in the diff
GNU diff treats `diff DIRECTORY FILE` as `diff DIRECTORY/FILE FILE`
2024-04-21 16:10:48 +05:30
Sylvestre Ledru 46a26e896b Merge pull request #58 from oSoMoN/ed-diff-tests-fix-path
Move test assertions in the cfg block where they belong (fixes #3)
2024-04-21 09:37:58 +02:00
Olivier Tilloy 14799eea89 Move test assertions in the cfg block where they belong 2024-04-21 00:13:52 +02:00
Olivier Tilloy 831348d1fc Fix file path in ed diff tests 2024-04-21 00:12:43 +02:00
Sylvestre Ledru 00a5c0ba44 Merge pull request #57 from oSoMoN/windows-fix-path-for-ci
CI: On Windows, use GNU's patch.exe instead of Strawberry Perl patch
2024-04-20 19:44:40 +02:00
Olivier Tilloy bf104648c1 CI: On Windows, use GNU's patch.exe instead of Strawberry Perl patch 2024-04-20 19:30:34 +02:00
Olivier Tilloy 5669f164b3 Merge pull request #34 from uutils/renovate/regex-1.x
Update Rust crate regex to 1.10.4
2024-04-17 19:19:00 +02:00
Sylvestre Ledru 11bf271666 Merge pull request #9 from uutils/renovate/codecov-codecov-action-4.x
Update codecov/codecov-action action to v4
2024-04-16 22:18:53 +02:00
renovate[bot] 674974d5e6 Update Rust crate regex to 1.10.4 2024-04-16 19:54:53 +00:00
Sylvestre Ledru 2ba35db431 Merge pull request #4 from uutils/renovate/diff-0.x
Update Rust crate diff to 0.1.13
2024-04-16 21:53:45 +02:00
renovate[bot] fcec7277c9 Update codecov/codecov-action action to v4 2024-04-16 19:35:04 +00:00
renovate[bot] b8efad6b90 Update Rust crate diff to 0.1.13 2024-04-16 19:35:01 +00:00
Sylvestre Ledru 68e2f51983 Merge pull request #54 from uutils/renovate/chrono-0.x
Update Rust crate chrono to 0.4.38
2024-04-16 21:34:16 +02:00
Sylvestre Ledru 4edaee190f Merge pull request #55 from oSoMoN/use-codecov-token
Use the private Codecov token stored as a secret,
2024-04-16 21:34:01 +02:00
Olivier Tilloy 7f7821f558 Use the private Codecov token stored as a secret,
to work around rate-limiting issues like https://github.com/codecov/codecov-action/issues/557
2024-04-16 18:37:59 +02:00
renovate[bot] 1149a247dd Update Rust crate chrono to 0.4.38 2024-04-16 16:20:52 +00:00
Olivier Tilloy 1b311c6673 Merge pull request #33 from TanmayPatil105/context-diff-modification-time
Display modification times of input files in context and unified diff
2024-04-16 18:20:00 +02:00
Tanmay Patil aedd0684d1 Replace only the first two occurences of timestamp regex 2024-04-16 10:41:38 +05:30
Tanmay Patil 54c02bdf0b Use NamedTempFile instead of manually creating files 2024-04-16 10:17:09 +05:30
Tanmay Patil ba7cb0aef9 Do not create dummy files
Since we now returning SystemTime::now() for invalid file input,
there is no need to crate dummy files
2024-04-14 22:56:37 +05:30
Tanmay Patil 33783d094e Improve tests 2024-04-14 17:16:53 +05:30
Tanmay Patil 900e1c3a68 Tests: Replace modification time in diff with "TIMESTAMP" placeholder 2024-04-14 13:43:30 +05:30
Tanmay Patil 0a77fe12b9 Add tests for get_modification_time function 2024-04-13 21:31:13 +05:30
Tanmay Patil 86bd05c739 Merge branch 'context-diff-modification-time' of github.com:TanmayPatil105/diffutils into context-diff-modification-time 2024-04-10 22:31:09 +05:30
Tanmay Patil 00e18a6b0c Define assert_diff_eq macro for context&unified diff comparison 2024-04-10 22:20:48 +05:30
Tanmay f6eb0835b0 Merge branch 'main' into context-diff-modification-time 2024-04-10 22:13:18 +05:30
Sylvestre Ledru be66ff3299 Merge pull request #47 from oSoMoN/handle-stdin-filename
Handle the rewrite of "-" to "/dev/stdin" in main to leave the filenames unchanged (fixes #46)
2024-04-09 09:53:53 +02:00
Olivier Tilloy e1c319f96b Add an integration test for reading from "/dev/stdin" on unix-like systems 2024-04-08 22:36:14 +02:00
Olivier Tilloy 84ad116845 Use io::stdin() to read from standard input in a portable manner 2024-04-08 20:21:24 +02:00
Olivier Tilloy 6dc34fed44 Handle the rewrite of "-" to "/dev/stdin" in main to leave the filenames unchanged (fixes #46) 2024-04-08 20:21:24 +02:00
Sylvestre Ledru 9507ca28d7 Merge pull request #51 from oSoMoN/unit-tests-for-conflicting-output-style
Unit test to verify that conflicting output styles result in an error
2024-04-06 08:47:53 +02:00
Olivier Tilloy c325291696 Unit test to verify that conflicting output styles result in an error 2024-04-05 23:22:26 +02:00
Olivier Tilloy c08e0b6e1f Merge pull request #25 from uutils/renovate/tempfile-3.x
chore(deps): update rust crate tempfile to 3.10.1
2024-04-04 22:50:50 +02:00
Tanmay Patil 72da7fca40 Show current time if fs::metadata errors 2024-04-04 20:01:11 +05:30
Tanmay 61fb0657c1 Merge branch 'main' into context-diff-modification-time 2024-04-04 19:56:13 +05:30
Sylvestre Ledru 096aa1dad9 Merge pull request #50 from cakebaker/disable_tests_using_ed_on_windows
Disable tests on Windows that use `ed`
2024-04-04 08:48:41 +02:00
Daniel Hofstetter 2d9e625a5b Disable tests on Windows that use ed 2024-04-04 08:30:54 +02:00
Daniel Hofstetter d863fe443a Merge pull request #48 from uutils/clip
Run clippy pedantic fixes
2024-04-04 07:45:18 +02:00
renovate[bot] 6be94d8683 chore(deps): update rust crate tempfile to 3.10.1 2024-04-03 22:31:44 +00:00
Sylvestre Ledru 44ef772e4a release: version 0.4.0 2024-04-04 00:30:46 +02:00
Sylvestre Ledru bbfca84e17 chore: wow shiny new cargo-dist CI 2024-04-04 00:29:50 +02:00
Sylvestre Ledru 281098d751 Run clippy pedantic fixes 2024-04-03 23:25:48 +02:00
Tanmay Patil a3a372ff36 Display modification times of input files in unified diff 2024-04-04 00:13:41 +05:30
Tanmay Patil 5b814f8530 Fix tests 2024-04-03 10:50:52 +05:30
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
Tanmay Patil 88a7568b52 Merge branch 'main' into context-diff-modification-time 2024-04-01 13:05:37 +05:30
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
Tanmay Patil 80c9944bf7 Create foo/bar in target/context-diff 2024-03-31 22:57:51 +05:30
Sylvestre Ledru 043c5f9493 Merge branch 'main' into context-diff-modification-time 2024-03-31 16:17:23 +02:00
Tanmay Patil 9ff8f89626 Fix tests 2024-03-31 16:14:44 +05:30
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
Tanmay Patil 42eb15b87a Display modification times of input files in context diff
Fixes #31
2024-03-27 22:46:23 +05:30
Sylvestre Ledru a304ac0a68 Merge pull request #30 from cakebaker/fix_clippy_warnings
clippy: fix warnings from useless_vec lint
2024-03-24 19:28:03 +01:00
Daniel Hofstetter f916f1ce86 clippy: fix warnings from useless_vec lint 2024-03-24 14:05:44 +01:00
Olivier Tilloy 4ed7ea1553 Implement -q/--brief option (fixes #19) (#20)
* Implement -q/--brief option

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

* Unit tests for the stop_early parameter

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

* fuzz dependency change: diffutils -> diffutilslib

* fix fuzz build.
2024-03-18 21:50:33 +01:00
Olivier Tilloy c68d386170 Implement -s/--report-identical-files option (fixes #23) 2024-03-01 14:45:41 +01:00
Olivier Tilloy a89f30afa0 Ed diff: compact ranges of lines where possible (fixes #21) 2024-02-24 11:55:54 +01:00
Olivier Tilloy 0a67bf9fb8 Add integration tests that check the exit codes and stdout/stderr 2024-02-22 17:26:24 +01:00
Olivier Tilloy 1241db4806 Match GNU diff's implementation for exit codes (fixes #17) 2024-02-22 17:26:24 +01:00
renovate[bot] 3bc8668f78 fix(deps): update rust crate libfuzzer-sys to 0.4 (#5)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-17 18:19:02 +01:00
Sylvestre Ledru c90eee442f Add the example section 2024-02-17 17:07:31 +01:00
Sylvestre Ledru 6c29f02527 improve the readme and example 2024-02-17 17:06:14 +01:00
Olivier Tilloy 790ef1e633 Run the GNU test suite in CI (fixes #8) (#13) 2024-02-17 15:27:52 +01:00
Sylvestre Ledru 4c1a752f11 Merge pull request #12 from oSoMoN/trivial-typo-error-message
Fix trivial typo in error message
2024-02-09 21:21:43 +01:00
Olivier Tilloy 54a5407bec Fix trivial typo in error message 2024-02-09 17:51:50 +01:00
Sylvestre Ledru 02632e915c Merge pull request #11 from oSoMoN/compact-range-of-lines
Normal diff: compact ranges of lines where possible (fixes #10)
2024-02-09 13:57:51 +01:00
Olivier Tilloy 3f9556aa05 Add comments to ease readability and maintainability 2024-02-09 10:28:39 +01:00
Olivier Tilloy a94c6a60cf Normal diff: compact ranges of lines where possible (fixes #10) 2024-02-09 10:28:39 +01:00
Sylvestre Ledru c28973c019 coverage: remove the fail fast to continue for linux even if windows fails 2024-02-09 10:28:27 +01:00
Sylvestre Ledru a660f7440c Merge pull request #1 from uutils/renovate/configure
Configure Renovate
2024-01-27 09:54:27 +01:00
Sylvestre Ledru 6a69a39852 add badges 2024-01-27 09:51:04 +01:00
Sylvestre Ledru bf13d528be Merge pull request #2 from cakebaker/ci_replace_actions_rs
ci: replace unmaintained `actions-rs` actions
2024-01-26 17:25:19 +01:00
Daniel Hofstetter 329a7e1f4a ci: use dtolnay/rust-toolchain instead of
unmaintained actions-rs/toolchain
2024-01-26 16:29:22 +01:00
Daniel Hofstetter e5de3cd93e ci: remove actions-rs/cargo & use cargo directly 2024-01-26 16:23:16 +01:00
Sylvestre Ledru 0e14b37e38 refresh the gitignore 2024-01-24 09:31:09 +01:00
Sylvestre Ledru 3c1176082e Ship with Cargo.lock 2024-01-24 09:30:47 +01:00
Sylvestre Ledru 865f97c78d upgrade the rust edition 2024-01-24 09:30:11 +01:00
Sylvestre Ledru f5b65a5720 add debug info 2024-01-23 12:49:20 +01:00
Sylvestre Ledru 1372c5386c use pretty assertions to help with Windows debug 2024-01-23 12:36:57 +01:00
Sylvestre Ledru d891e1034d run clippy pedantic
$ cargo +nightly clippy --allow-dirty --fix -- -W clippy::pedantic
2024-01-23 12:36:38 +01:00
Sylvestre Ledru fdc35f6b8e use mod tests + bring back the function used only for tests 2024-01-23 10:12:08 +01:00
Sylvestre Ledru 6648963df1 remove the unused function diff_w 2024-01-23 10:08:08 +01:00
Sylvestre Ledru a10ef621c8 Ajust the test paths 2024-01-23 10:06:02 +01:00
Sylvestre Ledru 7939749338 remove old config 2024-01-22 22:33:47 +01:00
Sylvestre Ledru 7b3001f1ff adjust the rule 2024-01-22 22:33:29 +01:00
Sylvestre Ledru 416a4be06c remove old files 2024-01-22 22:32:49 +01:00
Sylvestre Ledru 9084134f04 rustfmt the code 2024-01-22 22:32:32 +01:00
Sylvestre Ledru f42fc82f18 adjust the fuzzers to use the new structure 2024-01-22 22:32:11 +01:00
Sylvestre Ledru 0b2505d249 simplify the structure of the crate 2024-01-22 22:01:07 +01:00
Sylvestre Ledru 045435b803 add missing license headers 2024-01-22 21:49:07 +01:00
renovate[bot] b55cbf2ca2 Add renovate.json 2024-01-22 18:51:27 +01:00
Sylvestre Ledru 12f3f16792 add github action 2024-01-22 18:50:30 +01:00
Sylvestre Ledru e9e69b86db update of the README 2024-01-22 18:47:53 +01:00
Sylvestre Ledru 029d747e14 remove trailing spaces 2024-01-22 18:45:41 +01:00
Sylvestre Ledru e38055e5b2 rustfmt the code 2024-01-22 18:41:33 +01:00
Sylvestre Ledru 61cfe6eec4 Fix some clippy warnings 2024-01-22 18:41:01 +01:00
Michael Howell ea9376aaaf Add ed-formatted diff 2021-09-15 19:06:14 -07:00
Michael Howell 582259a867 Refactor into separate bin and lib folders 2021-07-21 13:40:52 -07:00
Michael Howell da05a5254b Fix missingnl behavior on normal-diff 2021-04-03 09:42:35 -07:00
Michael Howell a4f7642d7a Get rid of missing line ending tests
These don't work right with GNU patch. I get assertion errors.
2021-03-09 16:37:42 -07:00
Michael Howell e72ea046b7 Add context and normal diff modes 2021-03-09 16:34:57 -07:00
Michael Howell 2e84164d2f Get rid of sample binary 2021-03-09 11:07:44 -07:00
Michael Howell aa339419e3 Exclude "fuzz" folder 2021-02-25 09:07:43 -07:00
Michael Howell feb8b18ea0 v0.2.0 2021-02-25 09:06:32 -07:00
35 changed files with 9731 additions and 894 deletions
+171
View File
@@ -0,0 +1,171 @@
on: [push, pull_request]
name: Basic CI
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: cargo check
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
test:
name: cargo test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: install GNU patch on MacOS
if: runner.os == 'macOS'
run: |
brew install gpatch
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
- name: set up PATH on Windows
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
if: runner.os == 'Windows'
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
- run: cargo test
fmt:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
clippy:
name: cargo clippy -- -D warnings
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- run: cargo clippy -- -D warnings
gnu-testsuite:
name: GNU test suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release
# do not fail, the report is merely informative (at least until all tests pass reliably)
- run: ./tests/run-upstream-testsuite.sh release || true
env:
TERM: xterm
- uses: actions/upload-artifact@v4
with:
name: test-results.json
path: tests/test-results.json
- run: ./tests/print-test-results.sh tests/test-results.json
coverage:
name: Code Coverage
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- { os: ubuntu-latest , features: unix }
- { os: macos-latest , features: macos }
- { os: windows-latest , features: windows }
steps:
- uses: actions/checkout@v4
- name: Initialize workflow variables
id: vars
shell: bash
run: |
## VARs setup
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
# toolchain
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
# * use requested TOOLCHAIN if specified
if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi
outputs TOOLCHAIN
# target-specific options
# * CODECOV_FLAGS
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS
- name: rust toolchain ~ install
uses: dtolnay/rust-toolchain@nightly
- run: rustup component add llvm-tools-preview
- name: install GNU patch on MacOS
if: runner.os == 'macOS'
run: |
brew install gpatch
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
- name: set up PATH on Windows
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
if: runner.os == 'Windows'
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
- name: Test
run: cargo test --all-features --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw"
- name: "`grcov` ~ install"
id: build_grcov
shell: bash
run: |
git clone https://github.com/mozilla/grcov.git ~/grcov/
cd ~/grcov
# Hardcode the version of crossbeam-epoch. See
# https://github.com/uutils/coreutils/issues/3680
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
cargo install --path .
cd -
# Uncomment when the upstream issue
# https://github.com/mozilla/grcov/issues/849 is fixed
# uses: actions-rs/install@v0.1
# with:
# crate: grcov
# version: latest
# use-tool-cache: false
- name: Generate coverage data (via `grcov`)
id: coverage
shell: bash
run: |
## Generate coverage data
COVERAGE_REPORT_DIR="target/debug"
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
mkdir -p "${COVERAGE_REPORT_DIR}"
# display coverage files
grcov . --output-type files --binary-path "${COVERAGE_REPORT_DIR}" | sort --unique
# generate coverage report
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
+75
View File
@@ -0,0 +1,75 @@
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_cmp, should_pass: true }
- { name: fuzz_cmp_args, should_pass: true }
- { name: fuzz_ed, should_pass: true }
- { name: fuzz_normal, should_pass: true }
- { name: fuzz_patch, should_pass: true }
- { name: fuzz_side, should_pass: true }
steps:
- uses: actions/checkout@v4
- 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@v5
with:
key: corpus-cache-${{ matrix.test-target.name }}
path: |
fuzz/corpus/${{ matrix.test-target.name }}
- name: Run ${{ matrix.test-target.name }} for XX seconds
shell: bash
continue-on-error: ${{ !matrix.test-target.name.should_pass }}
run: |
cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Save Corpus Cache
uses: actions/cache/save@v5
with:
key: corpus-cache-${{ matrix.test-target.name }}
path: |
fuzz/corpus/${{ matrix.test-target.name }}
+296
View File
@@ -0,0 +1,296 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
-1
View File
@@ -1,3 +1,2 @@
/target
Cargo.lock
*.swp
Generated
+627
View File
@@ -0,0 +1,627 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "assert_cmd"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85"
dependencies = [
"anstyle",
"bstr",
"libc",
"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 = "bumpalo"
version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "cc"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[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.5.0"
dependencies = [
"assert_cmd",
"chrono",
"diff",
"itoa",
"predicates",
"pretty_assertions",
"regex",
"same-file",
"tempfile",
"unicode-width",
]
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi",
"windows-targets",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[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 = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
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.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"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.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[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-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+36 -11
View File
@@ -1,17 +1,42 @@
[package]
name = "unified-diff"
version = "0.1.2"
authors = [
"Michael Howell <michael@notriddle.com>",
"The Rust Project Developers"
]
edition = "2018"
description = "An implementation of the GNU unified diff format"
name = "diffutils"
version = "0.5.0"
edition = "2021"
description = "A CLI app for generating diff files"
license = "MIT OR Apache-2.0"
repository = "https://github.com/notriddle/rust-unified-diff"
repository = "https://github.com/uutils/diffutils"
[lib]
name = "diffutilslib"
path = "src/lib.rs"
[[bin]]
name = "unified-diff"
name = "diffutils"
path = "src/main.rs"
[dependencies]
diff = "0.1.10"
chrono = "0.4.38"
diff = "0.1.13"
itoa = "1.0.11"
regex = "1.10.4"
same-file = "1.0.6"
unicode-width = "0.2.0"
[dev-dependencies]
pretty_assertions = "1.4.0"
assert_cmd = "2.0.14"
predicates = "3.1.0"
tempfile = "3.10.1"
[profile.release]
lto = "thin"
codegen-units = 1
[profile.release-fast]
inherits = "release"
panic = "abort"
# The profile that 'dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"
+3
View File
@@ -1,3 +1,6 @@
Copyright (c) Michael Howell
Copyright (c) uutils developers
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
+3
View File
@@ -1,3 +1,6 @@
Copyright (c) Michael Howell
Copyright (c) uutils developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
+50 -35
View File
@@ -1,41 +1,56 @@
A GNU unified diff generator. Oracle tested against GNU patch 2.7.6
[![Crates.io](https://img.shields.io/crates/v/diffutils.svg)](https://crates.io/crates/diffutils)
[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE)
[![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils)
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs,
but it implements a different format.
[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils)
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) (diff, cmp, diff3, sdiff) in Rust.
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools.
## Installation
Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
Clone the repository and build the project using Cargo:
```bash
git clone https://github.com/uutils/diffutils.git
cd diffutils
cargo build --release
```
~/unified-diff$ cargo run Cargo.lock Cargo.toml
## Example
```bash
cat <<EOF >fruits_old.txt
Apple
Banana
Cherry
EOF
cat <<EOF >fruits_new.txt
Apple
Fig
Cherry
EOF
$ cargo run -- -u fruits_old.txt fruits_new.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/unified-diff Cargo.lock Cargo.toml`
--- Cargo.lock
+++ Cargo.toml
@@ -1,14 +1,14 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-[[package]]
-name = "diff"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
-
-[[package]]
+[package]
name = "unified-diff"
version = "0.1.0"
-dependencies = [
- "diff",
+authors = [
+ "Michael Howell <michael@notriddle.com>",
+ "The Rust Project Developers"
]
+edition = "2018"
+
+[[bin]]
+name = "unified-diff"
+
+[dependencies]
+diff = "0.1.10"
~/unified-diff$ rustup override set nightly
~/unified-diff$ cargo fuzz run fuzz_patch
Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt`
--- fruits_old.txt
+++ fruits_new.txt
@@ -1,3 +1,3 @@
Apple
-Banana
+Fig
Cherry
```
## License
diffutils is licensed under the MIT and Apache Licenses - see the `LICENSE-MIT` or `LICENSE-APACHE` files for details
+13
View File
@@ -0,0 +1,13 @@
[workspace]
members = ["cargo:."]
# Config for 'dist'
[dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.30.3"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = []
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
+447
View File
@@ -0,0 +1,447 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cc"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "diffutils"
version = "0.5.0"
dependencies = [
"chrono",
"diff",
"itoa",
"regex",
"same-file",
"unicode-width",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unified-diff-fuzz"
version = "0.0.0"
dependencies = [
"diffutils",
"libfuzzer-sys",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+32 -5
View File
@@ -2,7 +2,6 @@
[package]
name = "unified-diff-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
@@ -10,18 +9,46 @@ edition = "2018"
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.unified-diff]
path = ".."
libfuzzer-sys = "0.4.7"
diffutils = { path = "../" }
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "fuzz_cmp"
path = "fuzz_targets/fuzz_cmp.rs"
test = false
doc = false
[[bin]]
name = "fuzz_cmp_args"
path = "fuzz_targets/fuzz_cmp_args.rs"
test = false
doc = false
[[bin]]
name = "fuzz_patch"
path = "fuzz_targets/fuzz_patch.rs"
test = false
doc = false
[[bin]]
name = "fuzz_normal"
path = "fuzz_targets/fuzz_normal.rs"
test = false
doc = false
[[bin]]
name = "fuzz_ed"
path = "fuzz_targets/fuzz_ed.rs"
test = false
doc = false
[[bin]]
name = "fuzz_side"
path = "fuzz_targets/fuzz_side.rs"
test = false
doc = false
+36
View File
@@ -0,0 +1,36 @@
"-l"
"--verbose"
"-b"
"--print-bytes"
"-lb"
"-bl"
"-n"
"--bytes"
"--bytes="
"--bytes=1024"
"--bytes=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"
"-i"
"--ignore-initial"
"--ignore-initial="
"--ignore-initial=1024"
"--ignore-initial=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"
"-s"
"-q"
"--quiet"
"--silent"
"-"
"--"
"1kB"
"1G"
"1GB"
"1T"
"1TB"
"1P"
"1PB"
"1Z"
"1ZB"
"1Y"
"1YB"
"1Y"
"0"
"1:2"
+51
View File
@@ -0,0 +1,51 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::cmp::{self, Cmp};
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
fn os(s: &str) -> OsString {
OsString::from(s)
}
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
let args = vec!["cmp", "-l", "-b", "target/fuzz.cmp.a", "target/fuzz.cmp.b"]
.into_iter()
.map(|s| os(s))
.peekable();
let (from, to) = x;
File::create("target/fuzz.cmp.a")
.unwrap()
.write_all(&from)
.unwrap();
File::create("target/fuzz.cmp.b")
.unwrap()
.write_all(&to)
.unwrap();
let params =
cmp::parse_params(args).unwrap_or_else(|e| panic!("Failed to parse params: {}", e));
let ret = cmp::cmp(&params);
if from == to && !matches!(ret, Ok(Cmp::Equal)) {
panic!(
"target/fuzz.cmp.a and target/fuzz.cmp.b are equal, but cmp returned {:?}.",
ret
);
} else if from != to && !matches!(ret, Ok(Cmp::Different)) {
panic!(
"target/fuzz.cmp.a and target/fuzz.cmp.b are different, but cmp returned {:?}.",
ret
);
} else if ret.is_err() {
panic!(
"target/fuzz.cmp.a and target/fuzz.cmp.b caused cmp to error ({:?}).",
ret
);
}
});
+23
View File
@@ -0,0 +1,23 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::cmp;
use libfuzzer_sys::Corpus;
use std::ffi::OsString;
fn os(s: &str) -> OsString {
OsString::from(s)
}
fuzz_target!(|x: Vec<OsString>| -> Corpus {
if x.len() > 6 {
// Make sure we try to parse an option when we get longer args. x[0] will be
// the executable name.
if ![os("-l"), os("-b"), os("-s"), os("-n"), os("-i")].contains(&x[1]) {
return Corpus::Reject;
}
}
let _ = cmp::parse_params(x.into_iter().peekable());
Corpus::Keep
});
+78
View File
@@ -0,0 +1,78 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::ed_diff;
use diffutilslib::ed_diff::DiffError;
use diffutilslib::params::Params;
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
let mut output = ed_diff::diff(expected, actual, &Params::default())?;
writeln!(&mut output, "w {filename}").unwrap();
Ok(output)
}
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
let (mut from, mut to) = x;
from.push(b'\n');
to.push(b'\n');
if let Ok(s) = String::from_utf8(from.clone()) {
if !s.is_ascii() {
return;
}
if s.find(|x| x < ' ' && x != '\n').is_some() {
return;
}
} else {
return;
}
if let Ok(s) = String::from_utf8(to.clone()) {
if !s.is_ascii() {
return;
}
if s.find(|x| x < ' ' && x != '\n').is_some() {
return;
}
} else {
return;
}
let diff = diff_w(&from, &to, "target/fuzz.file").unwrap();
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&from)
.unwrap();
File::create("target/fuzz.file.expected")
.unwrap()
.write_all(&to)
.unwrap();
File::create("target/fuzz.file")
.unwrap()
.write_all(&from)
.unwrap();
File::create("target/fuzz.ed")
.unwrap()
.write_all(&diff)
.unwrap();
let output = Command::new("ed")
.arg("target/fuzz.file")
.stdin(File::open("target/fuzz.ed").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let result = fs::read("target/fuzz.file").unwrap();
if result != to {
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
});
+66
View File
@@ -0,0 +1,66 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::normal_diff;
use diffutilslib::params::Params;
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
let (from, to) = x;
/*if let Ok(s) = String::from_utf8(from.clone()) {
if !s.is_ascii() { return }
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
} else {
return
}
if let Ok(s) = String::from_utf8(to.clone()) {
if !s.is_ascii() { return }
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
} else {
return
}*/
let diff = normal_diff::diff(&from, &to, &Params::default());
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&from)
.unwrap();
File::create("target/fuzz.file.expected")
.unwrap()
.write_all(&to)
.unwrap();
File::create("target/fuzz.file")
.unwrap()
.write_all(&from)
.unwrap();
File::create("target/fuzz.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let output = Command::new("patch")
.arg("-p0")
.arg("--binary")
.arg("--fuzz=0")
.arg("--normal")
.arg("target/fuzz.file")
.stdin(File::open("target/fuzz.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let result = fs::read("target/fuzz.file").unwrap();
if result != to {
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
});
+24 -7
View File
@@ -1,7 +1,8 @@
#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate unified_diff;
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::params::Params;
use diffutilslib::unified_diff;
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
@@ -20,7 +21,16 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
} else {
return
}*/
let diff = unified_diff::diff(&from, "a/fuzz.file", &to, "target/fuzz.file", context as usize);
let diff = unified_diff::diff(
&from,
&to,
&Params {
from: "a/fuzz.file".into(),
to: "target/fuzz.file".into(),
context_count: context as usize,
..Default::default()
}
);
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&from)
@@ -45,11 +55,18 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
.output()
.unwrap();
if !output.status.success() {
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let result = fs::read("target/fuzz.file").unwrap();
if result != to {
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
panic!(
"STDOUT:\n{}\nSTDERR:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
});
+42
View File
@@ -0,0 +1,42 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::side_diff;
use std::fs::File;
use std::io::Write;
use diffutilslib::params::Params;
fuzz_target!(|x: (Vec<u8>, Vec<u8>, /* usize, usize */ bool)| {
let (original, new, /* width, tabsize, */ expand) = x;
// if width == 0 || tabsize == 0 {
// return;
// }
let params = Params {
// width,
// tabsize,
expand_tabs: expand,
..Default::default()
};
let mut output_buf = vec![];
side_diff::diff(&original, &new, &mut output_buf, &params);
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&original)
.unwrap();
File::create("target/fuzz.file.new")
.unwrap()
.write_all(&new)
.unwrap();
File::create("target/fuzz.file")
.unwrap()
.write_all(&original)
.unwrap();
File::create("target/fuzz.diff")
.unwrap()
.write_all(&output_buf)
.unwrap();
});
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+1185
View File
File diff suppressed because it is too large Load Diff
+796
View File
@@ -0,0 +1,796 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::collections::VecDeque;
use std::io::Write;
use crate::params::Params;
use crate::utils::do_write_line;
use crate::utils::get_modification_time;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
Change(Vec<u8>),
Add(Vec<u8>),
}
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: usize,
pub line_number_actual: usize,
pub expected: Vec<DiffLine>,
pub actual: Vec<DiffLine>,
pub expected_missing_nl: bool,
pub actual_missing_nl: bool,
pub expected_all_context: bool,
pub actual_all_context: bool,
}
impl Mismatch {
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
expected: Vec::new(),
actual: Vec::new(),
expected_missing_nl: false,
actual_missing_nl: false,
expected_all_context: false,
actual_all_context: false,
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(
expected: &[u8],
actual: &[u8],
context_size: usize,
stop_early: bool,
) -> Vec<Mismatch> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
let mut lines_since_mismatch = context_size + 1;
let mut results = Vec::new();
let mut mismatch = Mismatch::new(0, 0);
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
// ^ means that underflow here is impossible
let expected_lines_count = expected_lines.len() - 1;
let actual_lines_count = actual_lines.len() - 1;
if expected_lines.last() == Some(&&b""[..]) {
expected_lines.pop();
}
if actual_lines.last() == Some(&&b""[..]) {
actual_lines.pop();
}
// Rust only allows allocations to grow to isize::MAX, and this is bigger than that.
let mut expected_lines_change_idx: usize = !0;
for result in diff::slice(&expected_lines, &actual_lines) {
match result {
diff::Result::Left(str) => {
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len(),
line_number_actual - context_queue.len(),
);
}
while let Some(line) = context_queue.pop_front() {
mismatch.expected.push(DiffLine::Context(line.to_vec()));
mismatch.actual.push(DiffLine::Context(line.to_vec()));
}
expected_lines_change_idx = mismatch.expected.len();
mismatch.expected.push(DiffLine::Add(str.to_vec()));
if line_number_expected > expected_lines_count {
mismatch.expected_missing_nl = true;
}
line_number_expected += 1;
lines_since_mismatch = 0;
}
diff::Result::Right(str) => {
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len(),
line_number_actual - context_queue.len(),
);
expected_lines_change_idx = !0;
}
while let Some(line) = context_queue.pop_front() {
mismatch.expected.push(DiffLine::Context(line.to_vec()));
mismatch.actual.push(DiffLine::Context(line.to_vec()));
}
if let Some(DiffLine::Add(content)) =
mismatch.expected.get_mut(expected_lines_change_idx)
{
let content = std::mem::take(content);
mismatch.expected[expected_lines_change_idx] = DiffLine::Change(content);
expected_lines_change_idx = expected_lines_change_idx.wrapping_sub(1); // if 0, becomes !0
mismatch.actual.push(DiffLine::Change(str.to_vec()));
} else {
mismatch.actual.push(DiffLine::Add(str.to_vec()));
}
if line_number_actual > actual_lines_count {
mismatch.actual_missing_nl = true;
}
line_number_actual += 1;
lines_since_mismatch = 0;
}
diff::Result::Both(str, _) => {
expected_lines_change_idx = !0;
// if one of them is missing a newline and the other isn't, then they don't actually match
if (line_number_actual > actual_lines_count)
&& (line_number_expected > expected_lines_count)
{
if context_queue.len() < context_size {
while let Some(line) = context_queue.pop_front() {
mismatch.expected.push(DiffLine::Context(line.to_vec()));
mismatch.actual.push(DiffLine::Context(line.to_vec()));
}
if lines_since_mismatch < context_size {
mismatch.expected.push(DiffLine::Context(str.to_vec()));
mismatch.actual.push(DiffLine::Context(str.to_vec()));
mismatch.expected_missing_nl = true;
mismatch.actual_missing_nl = true;
}
}
lines_since_mismatch = 0;
} else if line_number_actual > actual_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len(),
line_number_actual - context_queue.len(),
);
}
while let Some(line) = context_queue.pop_front() {
mismatch.expected.push(DiffLine::Context(line.to_vec()));
mismatch.actual.push(DiffLine::Context(line.to_vec()));
}
mismatch.expected.push(DiffLine::Change(str.to_vec()));
mismatch.actual.push(DiffLine::Change(str.to_vec()));
mismatch.actual_missing_nl = true;
lines_since_mismatch = 0;
} else if line_number_expected > expected_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len(),
line_number_actual - context_queue.len(),
);
}
while let Some(line) = context_queue.pop_front() {
mismatch.expected.push(DiffLine::Context(line.to_vec()));
mismatch.actual.push(DiffLine::Context(line.to_vec()));
}
mismatch.expected.push(DiffLine::Change(str.to_vec()));
mismatch.expected_missing_nl = true;
mismatch.actual.push(DiffLine::Change(str.to_vec()));
lines_since_mismatch = 0;
} else {
debug_assert!(context_queue.len() <= context_size);
if context_queue.len() >= context_size {
let _ = context_queue.pop_front();
}
if lines_since_mismatch < context_size {
mismatch.expected.push(DiffLine::Context(str.to_vec()));
mismatch.actual.push(DiffLine::Context(str.to_vec()));
} else if context_size > 0 {
context_queue.push_back(str);
}
lines_since_mismatch += 1;
}
line_number_expected += 1;
line_number_actual += 1;
}
}
if stop_early && !results.is_empty() {
// Optimization: stop analyzing the files as soon as there are any differences
return results;
}
}
results.push(mismatch);
results.remove(0);
if results.is_empty() && expected_lines_count != actual_lines_count {
let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len());
// empty diff and only expected lines has a missing line at end
if expected_lines_count != expected_lines.len() {
mismatch.expected.push(DiffLine::Change(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.expected_missing_nl = true;
mismatch.actual.push(DiffLine::Change(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
results.push(mismatch);
} else if actual_lines_count != actual_lines.len() {
mismatch.expected.push(DiffLine::Change(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.actual.push(DiffLine::Change(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.actual_missing_nl = true;
results.push(mismatch);
}
}
// hunks with pure context lines get truncated to empty
for mismatch in &mut results {
if !mismatch
.expected
.iter()
.any(|x| !matches!(&x, DiffLine::Context(_)))
{
mismatch.expected_all_context = true;
}
if !mismatch
.actual
.iter()
.any(|x| !matches!(&x, DiffLine::Context(_)))
{
mismatch.actual_all_context = true;
}
}
results
}
#[must_use]
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
let from_modified_time = get_modification_time(&params.from.to_string_lossy());
let to_modified_time = get_modification_time(&params.to.to_string_lossy());
let mut output = format!(
"*** {0}\t{1}\n--- {2}\t{3}\n",
params.from.to_string_lossy(),
from_modified_time,
params.to.to_string_lossy(),
to_modified_time
)
.into_bytes();
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
if diff_results.is_empty() {
return Vec::new();
}
if params.brief {
return output;
}
for result in diff_results {
let mut line_number_expected = result.line_number_expected;
let mut line_number_actual = result.line_number_actual;
let mut expected_count = result.expected.len();
let mut actual_count = result.actual.len();
if expected_count == 0 {
line_number_expected -= 1;
expected_count = 1;
}
if actual_count == 0 {
line_number_actual -= 1;
actual_count = 1;
}
let end_line_number_expected = expected_count + line_number_expected - 1;
let end_line_number_actual = actual_count + line_number_actual - 1;
let exp_start = if end_line_number_expected == line_number_expected {
String::new()
} else {
format!("{line_number_expected},")
};
let act_start = if end_line_number_actual == line_number_actual {
String::new()
} else {
format!("{line_number_actual},")
};
writeln!(
output,
"***************\n*** {exp_start}{end_line_number_expected} ****"
)
.expect("write to Vec is infallible");
if !result.expected_all_context {
for line in result.expected {
match line {
DiffLine::Context(e) => {
write!(output, " ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Change(e) => {
write!(output, "! ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Add(e) => {
write!(output, "- ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
}
}
if result.expected_missing_nl {
writeln!(output, r"\ No newline at end of file")
.expect("write to Vec is infallible");
}
}
writeln!(output, "--- {act_start}{end_line_number_actual} ----")
.expect("write to Vec is infallible");
if !result.actual_all_context {
for line in result.actual {
match line {
DiffLine::Context(e) => {
write!(output, " ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Change(e) => {
write!(output, "! ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Add(e) => {
write!(output, "+ ").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
}
}
if result.actual_missing_nl {
writeln!(output, r"\ No newline at end of file")
.expect("write to Vec is infallible");
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_permutations() {
// test all possible six-line files.
let target = "target/context-diff/";
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alef".into(),
to: (&format!("{target}/alef")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg("--context")
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_empty_lines() {
let target = "target/context-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alef_".into(),
to: (&format!("{target}/alef_")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab_.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg("--context")
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_lines() {
let target = "target/context-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
if alef.is_empty() && bet.is_empty() {
continue;
};
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alefx".into(),
to: (&format!("{target}/alefx")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abx.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefx")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betx")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg("--context")
.stdin(File::open(format!("{target}/abx.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefx")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_reverse() {
let target = "target/context-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
.unwrap();
if a != 2 {
bet.write_all(b"a\n").unwrap();
}
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
.unwrap();
if b != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if c != 2 {
bet.write_all(b"c\n").unwrap();
}
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
.unwrap();
if d != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
.unwrap();
if e != 2 {
bet.write_all(b"e\n").unwrap();
}
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
.unwrap();
if f != 2 {
bet.write_all(b"f\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alefr".into(),
to: (&format!("{target}/alefr")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abr.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg("--context")
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_stop_early() {
use crate::assert_diff_eq;
let from_filename = "foo";
let from = ["a", "b", "c", ""].join("\n");
let to_filename = "bar";
let to = ["a", "d", "c", ""].join("\n");
let diff_full = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
let expected_full = [
"*** foo\tTIMESTAMP",
"--- bar\tTIMESTAMP",
"***************",
"*** 1,3 ****",
" a",
"! b",
" c",
"--- 1,3 ----",
" a",
"! d",
" c",
"",
]
.join("\n");
assert_diff_eq!(diff_full, expected_full);
let diff_brief = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n");
assert_diff_eq!(diff_brief, expected_brief);
let nodiff_full = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
assert!(nodiff_full.is_empty());
let nodiff_brief = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
assert!(nodiff_brief.is_empty());
}
}
+102
View File
@@ -0,0 +1,102 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use crate::params::{parse_params, Format};
use crate::utils::report_failure_to_read_input_file;
use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff};
use std::env::ArgsOs;
use std::ffi::OsString;
use std::fs;
use std::io::{self, stdout, Read, Write};
use std::iter::Peekable;
use std::process::{exit, ExitCode};
// Exit codes are documented at
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
// An exit status of 0 means no differences were found,
// 1 means some differences were found,
// and 2 means trouble.
pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
let params = parse_params(opts).unwrap_or_else(|error| {
eprintln!("{error}");
exit(2);
});
// if from and to are the same file, no need to perform any comparison
let maybe_report_identical_files = || {
if params.report_identical_files {
println!(
"Files {} and {} are identical",
params.from.to_string_lossy(),
params.to.to_string_lossy(),
);
}
};
if params.from == "-" && params.to == "-"
|| same_file::is_same_file(&params.from, &params.to).unwrap_or(false)
{
maybe_report_identical_files();
return ExitCode::SUCCESS;
}
// read files
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
if filepath == "-" {
let mut content = Vec::new();
io::stdin().read_to_end(&mut content).and(Ok(content))
} else {
fs::read(filepath)
}
}
let mut io_error = false;
let from_content = match read_file_contents(&params.from) {
Ok(from_content) => from_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.from, &e);
io_error = true;
vec![]
}
};
let to_content = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.to, &e);
io_error = true;
vec![]
}
};
if io_error {
return ExitCode::from(2);
}
// run diff
let result: Vec<u8> = match params.format {
Format::Normal => normal_diff::diff(&from_content, &to_content, &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);
}),
Format::SideBySide => {
let mut output = stdout().lock();
side_diff::diff(&from_content, &to_content, &mut output, &params)
}
};
if params.brief && !result.is_empty() {
println!(
"Files {} and {} differ",
params.from.to_string_lossy(),
params.to.to_string_lossy()
);
} else {
io::stdout().write_all(&result).unwrap();
}
if result.is_empty() {
maybe_report_identical_files();
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
+449
View File
@@ -0,0 +1,449 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::io::Write;
use crate::params::Params;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: usize,
pub line_number_actual: usize,
pub expected: Vec<Vec<u8>>,
pub actual: Vec<Vec<u8>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum DiffError {
MissingNL,
}
impl std::fmt::Display for DiffError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
std::fmt::Display::fmt("No newline at end of file", f)
}
}
impl From<DiffError> for String {
fn from(_: DiffError) -> String {
"No newline at end of file".into()
}
}
impl Mismatch {
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
expected: Vec::new(),
actual: Vec::new(),
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<Mismatch>, DiffError> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut results = Vec::new();
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
// ^ means that underflow here is impossible
let _expected_lines_count = expected_lines.len() - 1;
let _actual_lines_count = actual_lines.len() - 1;
if expected_lines.last() == Some(&&b""[..]) {
expected_lines.pop();
} else {
return Err(DiffError::MissingNL);
}
if actual_lines.last() == Some(&&b""[..]) {
actual_lines.pop();
} else {
return Err(DiffError::MissingNL);
}
for result in diff::slice(&expected_lines, &actual_lines) {
match result {
diff::Result::Left(str) => {
if !mismatch.actual.is_empty() {
results.push(mismatch);
mismatch = Mismatch::new(line_number_expected, line_number_actual);
}
mismatch.expected.push(str.to_vec());
line_number_expected += 1;
}
diff::Result::Right(str) => {
mismatch.actual.push(str.to_vec());
line_number_actual += 1;
}
diff::Result::Both(_str, _) => {
line_number_expected += 1;
line_number_actual += 1;
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
results.push(mismatch);
mismatch = Mismatch::new(line_number_expected, line_number_actual);
} else {
mismatch.line_number_expected = line_number_expected;
mismatch.line_number_actual = line_number_actual;
}
}
}
if stop_early && !results.is_empty() {
// Optimization: stop analyzing the files as soon as there are any differences
return Ok(results);
}
}
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
results.push(mismatch);
}
Ok(results)
}
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result<Vec<u8>, DiffError> {
let mut output = Vec::new();
let diff_results = make_diff(expected, actual, params.brief)?;
if params.brief && !diff_results.is_empty() {
write!(&mut output, "\0").unwrap();
return Ok(output);
}
let mut lines_offset = 0;
for result in diff_results {
let line_number_expected: isize = result.line_number_expected as isize + lines_offset;
let _line_number_actual: isize = result.line_number_actual as isize + lines_offset;
let expected_count: isize = result.expected.len() as isize;
let actual_count: isize = result.actual.len() as isize;
match (expected_count, actual_count) {
(0, 0) => unreachable!(),
(0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(),
(_, 0) => writeln!(
&mut output,
"{},{}d",
line_number_expected,
expected_count + line_number_expected - 1
)
.unwrap(),
(1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(),
_ => writeln!(
&mut output,
"{},{}c",
line_number_expected,
expected_count + line_number_expected - 1
)
.unwrap(),
}
lines_offset += actual_count - expected_count;
if actual_count != 0 {
for actual in &result.actual {
if actual == b"." {
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
} else {
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
writeln!(&mut output).unwrap();
}
}
writeln!(&mut output, ".").unwrap();
}
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
let mut output = diff(expected, actual, &Params::default())?;
writeln!(&mut output, "w {filename}").unwrap();
Ok(output)
}
#[test]
fn test_basic() {
let from = b"a\n";
let to = b"b\n";
let diff = diff(from, to, &Params::default()).unwrap();
let expected = ["1c", "b", ".", ""].join("\n");
assert_eq!(diff, expected.as_bytes());
}
#[test]
fn test_permutations() {
let target = "target/ed-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::File;
use std::io::Write;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap();
File::create(format!("{target}/ab.ed"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
#[cfg(not(windows))] // there's no ed on windows
{
use std::process::Command;
let output = Command::new("ed")
.arg(format!("{target}/alef"))
.stdin(File::open(format!("{target}/ab.ed")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alef")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_empty_lines() {
let target = "target/ed-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::File;
use std::io::Write;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap();
File::create(format!("{target}/ab_.ed"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
#[cfg(not(windows))] // there's no ed on windows
{
use std::process::Command;
let output = Command::new("ed")
.arg(format!("{target}/alef_"))
.stdin(File::open(format!("{target}/ab_.ed")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_reverse() {
let target = "target/ed-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::File;
use std::io::Write;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
.unwrap();
if a != 2 {
bet.write_all(b"a\n").unwrap();
}
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
.unwrap();
if b != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if c != 2 {
bet.write_all(b"c\n").unwrap();
}
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
.unwrap();
if d != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
.unwrap();
if e != 2 {
bet.write_all(b"e\n").unwrap();
}
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
.unwrap();
if f != 2 {
bet.write_all(b"f\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
File::create(format!("{target}/abr.ed"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
#[cfg(not(windows))] // there's no ed on windows
{
use std::process::Command;
let output = Command::new("ed")
.arg(format!("{target}/alefr"))
.stdin(File::open(format!("{target}/abr.ed")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_stop_early() {
let from = ["a", "b", "c", ""].join("\n");
let to = ["a", "d", "c", ""].join("\n");
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()).unwrap();
let expected_full = ["2c", "d", ".", ""].join("\n");
assert_eq!(diff_full, expected_full.as_bytes());
let diff_brief = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
brief: true,
..Default::default()
},
)
.unwrap();
let expected_brief = "\0".as_bytes();
assert_eq!(diff_brief, expected_brief);
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()).unwrap();
assert!(nodiff_full.is_empty());
let nodiff_brief = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
brief: true,
..Default::default()
},
)
.unwrap();
assert!(nodiff_brief.is_empty());
}
}
+15 -781
View File
@@ -1,782 +1,16 @@
use std::collections::VecDeque;
use std::io::Write;
pub mod cmp;
pub mod context_diff;
pub mod ed_diff;
pub mod macros;
pub mod normal_diff;
pub mod params;
pub mod side_diff;
pub mod unified_diff;
pub mod utils;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
Expected(Vec<u8>),
Actual(Vec<u8>),
MissingNL,
}
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: u32,
pub line_number_actual: u32,
pub lines: Vec<DiffLine>,
}
impl Mismatch {
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
lines: Vec::new(),
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
let mut lines_since_mismatch = context_size + 1;
let mut results = Vec::new();
let mut mismatch = Mismatch::new(0, 0);
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
// ^ means that underflow here is impossible
let expected_lines_count = expected_lines.len() as u32 - 1;
let actual_lines_count = actual_lines.len() as u32 - 1;
if expected_lines.last() == Some(&&b""[..]) {
expected_lines.pop();
}
if actual_lines.last() == Some(&&b""[..]) {
actual_lines.pop();
}
for result in diff::slice(&expected_lines, &actual_lines) {
match result {
diff::Result::Left(str) => {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
mismatch.lines.pop();
match mismatch.lines.pop() {
Some(DiffLine::Actual(res)) => {
// We have to make sure that Actual (the + lines)
// always come after Expected (the - lines)
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
if line_number_expected > expected_lines_count {
mismatch.lines.push(DiffLine::MissingNL)
}
mismatch.lines.push(DiffLine::Actual(res));
mismatch.lines.push(DiffLine::MissingNL);
}
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
}
} else {
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
if line_number_expected > expected_lines_count {
mismatch.lines.push(DiffLine::MissingNL)
}
}
line_number_expected += 1;
lines_since_mismatch = 0;
}
diff::Result::Right(str) => {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
if line_number_actual > actual_lines_count {
mismatch.lines.push(DiffLine::MissingNL)
}
line_number_actual += 1;
lines_since_mismatch = 0;
}
diff::Result::Both(str, _) => {
// if one of them is missing a newline and the other isn't, then they don't actually match
if (line_number_actual > actual_lines_count)
&& (line_number_expected > expected_lines_count)
{
if context_queue.len() < context_size {
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
if lines_since_mismatch < context_size {
mismatch.lines.push(DiffLine::Context(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
}
}
lines_since_mismatch = 0;
} else if line_number_actual > actual_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
lines_since_mismatch = 0;
} else if line_number_expected > expected_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
lines_since_mismatch = 0;
} else {
debug_assert!(context_queue.len() <= context_size);
if context_queue.len() >= context_size {
let _ = context_queue.pop_front();
}
if lines_since_mismatch < context_size {
mismatch.lines.push(DiffLine::Context(str.to_vec()));
} else if context_size > 0 {
context_queue.push_back(str);
}
lines_since_mismatch += 1;
}
line_number_expected += 1;
line_number_actual += 1;
}
}
}
results.push(mismatch);
results.remove(0);
if results.len() == 0 && expected_lines_count != actual_lines_count {
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
// empty diff and only expected lines has a missing line at end
if expected_lines_count != expected_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
results.push(mismatch);
} else if actual_lines_count != actual_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
results.push(mismatch);
}
}
results
}
pub fn diff(
expected: &[u8],
expected_filename: &str,
actual: &[u8],
actual_filename: &str,
context_size: usize,
) -> Vec<u8> {
let mut output =
format!("--- {}\t\n+++ {}\t\n", expected_filename, actual_filename).into_bytes();
let diff_results = make_diff(expected, actual, context_size);
if diff_results.len() == 0 {
return Vec::new();
};
for result in diff_results {
let mut line_number_expected = result.line_number_expected;
let mut line_number_actual = result.line_number_actual;
let mut expected_count = 0;
let mut actual_count = 0;
for line in &result.lines {
match line {
DiffLine::Expected(_) => {
expected_count += 1;
}
DiffLine::Context(_) => {
expected_count += 1;
actual_count += 1;
}
DiffLine::Actual(_) => {
actual_count += 1;
}
DiffLine::MissingNL => {}
}
}
// Let's imagine this diff file
//
// --- a/something
// +++ b/something
// @@ -2,0 +3,1 @@
// + x
//
// In the unified diff format as implemented by GNU diff and patch,
// this is an instruction to insert the x *after* the preexisting line 2,
// not before. You can demonstrate it this way:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Notice how the x winds up at line 3, not line 2. This requires contortions to
// work with our diffing algorithm, which keeps track of the "intended destination line",
// not a line that things are supposed to be placed after. It's changing the first number,
// not the second, that actually affects where the x goes.
//
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// c
// x
// d
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
// # actually being read
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Now watch what happens if I add a context line:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// Hunk #1 succeeded at 3 (offset 1 line).
//
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
// line that the x will wind up on, and not the line immediately before.
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// I made this comment because this stuff is not obvious from GNU's
// documentation on the format at all.
if expected_count == 0 {
line_number_expected -= 1
}
if actual_count == 0 {
line_number_actual -= 1
}
let exp_ct = if expected_count == 1 {
String::new()
} else {
format!(",{}", expected_count)
};
let act_ct = if actual_count == 1 {
String::new()
} else {
format!(",{}", actual_count)
};
writeln!(
output,
"@@ -{}{} +{}{} @@",
line_number_expected, exp_ct, line_number_actual, act_ct
)
.expect("write to Vec is infallible");
for line in result.lines {
match line {
DiffLine::Expected(e) => {
write!(output, "-").expect("write to Vec is infallible");
output.write_all(&e).expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Context(c) => {
write!(output, " ").expect("write to Vec is infallible");
output.write_all(&c).expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Actual(r) => {
write!(output, "+",).expect("write to Vec is infallible");
output.write_all(&r).expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::MissingNL => {
writeln!(output, r"\ No newline at end of file")
.expect("write to Vec is infallible");
}
}
}
}
output
}
#[test]
fn test_permutations() {
// test all possible six-line files.
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, "a/alef", &bet, "target/alef", 2);
File::create("target/ab.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("target/alef").unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create("target/bet").unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open("target/ab.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!("{:?}", output);
}
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read("target/alef").unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_line_ending() {
// test all possible six-line files with missing newlines.
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
for &g in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
match g {
0 => {
alef.pop();
}
1 => {
bet.pop();
}
2 => {
alef.pop();
bet.pop();
}
_ => unreachable!(),
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, "a/alefn", &bet, "target/alefn", 2);
File::create("target/abn.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("target/alefn").unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create("target/betn").unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open("target/abn.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!("{:?}", output);
}
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read("target/alefn").unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_empty_lines() {
// test all possible six-line files with missing newlines.
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
for &g in &[0, 1, 2, 3] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
match g {
0 => {
alef.pop();
}
1 => {
bet.pop();
}
2 => {
alef.pop();
bet.pop();
}
3 => {}
_ => unreachable!(),
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, "a/alef_", &bet, "target/alef_", 2);
File::create("target/ab_.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("target/alef_").unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create("target/bet_").unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open("target/ab_.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!("{:?}", output);
}
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read("target/alef_").unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_lines() {
// test all possible six-line files.
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, "a/alefx", &bet, "target/alefx", 2);
File::create("target/abx.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("target/alefx").unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create("target/betx").unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open("target/abx.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!("{:?}", output);
}
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read("target/alefx").unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_reverse() {
// test all possible six-line files.
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
.unwrap();
if a != 2 {
bet.write_all(b"a\n").unwrap();
}
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
.unwrap();
if b != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if c != 2 {
bet.write_all(b"c\n").unwrap();
}
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
.unwrap();
if d != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
.unwrap();
if e != 2 {
bet.write_all(b"e\n").unwrap();
}
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
.unwrap();
if f != 2 {
bet.write_all(b"f\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, "a/alefr", &bet, "target/alefr", 2);
File::create("target/abr.diff")
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create("target/alefr").unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create("target/betr").unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open("target/abr.diff").unwrap())
.output()
.unwrap();
if !output.status.success() {
panic!("{:?}", output);
}
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read("target/alefr").unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
// Re-export the public functions/types you need
pub use context_diff::diff as context_diff;
pub use ed_diff::diff as ed_diff;
pub use normal_diff::diff as normal_diff;
pub use side_diff::diff as side_by_side_diff;
pub use unified_diff::diff as unified_diff;
+25
View File
@@ -0,0 +1,25 @@
// asserts equality of the actual diff and expected diff
// considering datetime varitations
//
// It replaces the modification time in the actual diff
// with placeholder "TIMESTAMP" and then asserts the equality
//
// For eg.
// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n
// --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n";
//
// replaced = "*** fruits_old.txt\tTIMESTAMP\n
// --- fruits_new.txt\tTIMESTAMP\n";
#[macro_export]
macro_rules! assert_diff_eq {
($actual:expr, $expected:expr) => {{
use regex::Regex;
use std::str;
let diff = str::from_utf8(&$actual).unwrap();
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
let actual = re.replacen(diff, 2, "TIMESTAMP");
assert_eq!(actual, $expected);
}};
}
+80 -54
View File
@@ -1,55 +1,81 @@
// Sample program. Do not use.
use std::env;
use std::fs;
use std::io::{self, Write};
use std::process;
fn main() {
let mut o = env::args_os();
// parse CLI
let exe = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: [exe] [from] [to]");
process::exit(1);
}
};
let from = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
process::exit(1);
}
};
let to = match o.next() {
Some(from) => from,
None => {
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
process::exit(1);
}
};
// read files
let from_content = match fs::read(&from) {
Ok(from_content) => from_content,
Err(e) => {
eprintln!("Failed to read from-file: {}", e);
process::exit(2);
}
};
let to_content = match fs::read(&to) {
Ok(to_content) => to_content,
Err(e) => {
eprintln!("Failed to read to-file: {}", e);
process::exit(2);
}
};
// run diff
io::stdout()
.write_all(&unified_diff::diff(
&from_content,
&from.to_string_lossy(),
&to_content,
&to.to_string_lossy(),
1,
))
.unwrap();
// 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::{
env::ArgsOs,
ffi::{OsStr, OsString},
iter::Peekable,
path::{Path, PathBuf},
process::ExitCode,
};
mod cmp;
mod context_diff;
mod diff;
mod ed_diff;
mod macros;
mod normal_diff;
mod params;
mod side_diff;
mod unified_diff;
mod utils;
/// # Panics
/// Panics if the binary path cannot be determined
fn binary_path(args: &mut Peekable<ArgsOs>) -> PathBuf {
match args.peek() {
Some(ref s) if !s.is_empty() => PathBuf::from(s),
_ => std::env::current_exe().unwrap(),
}
}
/// #Panics
/// Panics if path has no UTF-8 valid name
fn name(binary_path: &Path) -> &OsStr {
binary_path.file_stem().unwrap()
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn usage(name: &str) {
println!("{name} {VERSION} (multi-call binary)\n");
println!("Usage: {name} [function [arguments...]]\n");
println!("Currently defined functions:\n");
println!(" cmp, diff\n");
}
fn second_arg_error(name: &OsStr) -> ! {
eprintln!("Expected utility name as second argument, got nothing.");
usage(&name.to_string_lossy());
std::process::exit(0);
}
fn main() -> ExitCode {
let mut args = std::env::args_os().peekable();
let exe_path = binary_path(&mut args);
let exe_name = name(&exe_path);
let util_name = if exe_name == "diffutils" {
// Discard the item we peeked.
let _ = args.next();
args.peek()
.cloned()
.unwrap_or_else(|| second_arg_error(exe_name))
} else {
OsString::from(exe_name)
};
match util_name.to_str() {
Some("diff") => diff::main(args),
Some("cmp") => cmp::main(args),
Some(name) => {
eprintln!("{name}: utility not supported");
ExitCode::from(2)
}
None => second_arg_error(exe_name),
}
}
+584
View File
@@ -0,0 +1,584 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::io::Write;
use crate::params::Params;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: usize,
pub line_number_actual: usize,
pub expected: Vec<Vec<u8>>,
pub actual: Vec<Vec<u8>>,
pub expected_missing_nl: bool,
pub actual_missing_nl: bool,
}
impl Mismatch {
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
expected: Vec::new(),
actual: Vec::new(),
expected_missing_nl: false,
actual_missing_nl: false,
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<Mismatch> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut results = Vec::new();
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
// ^ means that underflow here is impossible
let expected_lines_count = expected_lines.len() - 1;
let actual_lines_count = actual_lines.len() - 1;
if expected_lines.last() == Some(&&b""[..]) {
expected_lines.pop();
}
if actual_lines.last() == Some(&&b""[..]) {
actual_lines.pop();
}
for result in diff::slice(&expected_lines, &actual_lines) {
match result {
diff::Result::Left(str) => {
if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl {
results.push(mismatch);
mismatch = Mismatch::new(line_number_expected, line_number_actual);
}
mismatch.expected.push(str.to_vec());
mismatch.expected_missing_nl = line_number_expected > expected_lines_count;
line_number_expected += 1;
}
diff::Result::Right(str) => {
mismatch.actual.push(str.to_vec());
mismatch.actual_missing_nl = line_number_actual > actual_lines_count;
line_number_actual += 1;
}
diff::Result::Both(str, _) => {
match (
line_number_expected > expected_lines_count,
line_number_actual > actual_lines_count,
) {
(true, false) => {
line_number_expected += 1;
line_number_actual += 1;
mismatch.expected.push(str.to_vec());
mismatch.expected_missing_nl = true;
mismatch.actual.push(str.to_vec());
}
(false, true) => {
line_number_expected += 1;
line_number_actual += 1;
mismatch.actual.push(str.to_vec());
mismatch.actual_missing_nl = true;
mismatch.expected.push(str.to_vec());
}
(true, true) | (false, false) => {
line_number_expected += 1;
line_number_actual += 1;
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
results.push(mismatch);
mismatch = Mismatch::new(line_number_expected, line_number_actual);
} else {
mismatch.line_number_expected = line_number_expected;
mismatch.line_number_actual = line_number_actual;
}
}
}
}
}
if stop_early && !results.is_empty() {
// Optimization: stop analyzing the files as soon as there are any differences
return results;
}
}
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
results.push(mismatch);
}
results
}
#[must_use]
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
// See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html
// for details on the syntax of the normal format.
let mut output = Vec::new();
let diff_results = make_diff(expected, actual, params.brief);
if params.brief && !diff_results.is_empty() {
write!(&mut output, "\0").unwrap();
return output;
}
for result in diff_results {
let line_number_expected = result.line_number_expected;
let line_number_actual = result.line_number_actual;
let expected_count = result.expected.len();
let actual_count = result.actual.len();
match (expected_count, actual_count) {
(0, 0) => unreachable!(),
(0, _) => writeln!(
// 'a' stands for "Add lines"
&mut output,
"{}a{},{}",
line_number_expected - 1,
line_number_actual,
line_number_actual + actual_count - 1
)
.unwrap(),
(_, 0) => writeln!(
// 'd' stands for "Delete lines"
&mut output,
"{},{}d{}",
line_number_expected,
expected_count + line_number_expected - 1,
line_number_actual - 1
)
.unwrap(),
(1, 1) => writeln!(
// 'c' stands for "Change lines"
// exactly one line replaced by one line
&mut output,
"{line_number_expected}c{line_number_actual}"
)
.unwrap(),
(1, _) => writeln!(
// one line replaced by multiple lines
&mut output,
"{}c{},{}",
line_number_expected,
line_number_actual,
actual_count + line_number_actual - 1
)
.unwrap(),
(_, 1) => writeln!(
// multiple lines replaced by one line
&mut output,
"{},{}c{}",
line_number_expected,
expected_count + line_number_expected - 1,
line_number_actual
)
.unwrap(),
_ => writeln!(
// general case: multiple lines replaced by multiple lines
&mut output,
"{},{}c{},{}",
line_number_expected,
expected_count + line_number_expected - 1,
line_number_actual,
actual_count + line_number_actual - 1
)
.unwrap(),
}
for expected in &result.expected {
write!(&mut output, "< ").unwrap();
do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap();
writeln!(&mut output).unwrap();
}
if result.expected_missing_nl {
writeln!(&mut output, r"\ No newline at end of file").unwrap();
}
if expected_count != 0 && actual_count != 0 {
writeln!(&mut output, "---").unwrap();
}
for actual in &result.actual {
write!(&mut output, "> ").unwrap();
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
writeln!(&mut output).unwrap();
}
if result.actual_missing_nl {
writeln!(&mut output, r"\ No newline at end of file").unwrap();
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_basic() {
let mut a = Vec::new();
a.write_all(b"a\n").unwrap();
let mut b = Vec::new();
b.write_all(b"b\n").unwrap();
let diff = diff(&a, &b, &Params::default());
let expected = b"1c1\n< a\n---\n> b\n".to_vec();
assert_eq!(diff, expected);
}
#[test]
fn test_permutations() {
let target = "target/normal-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/ab.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg(format!("{target}/alef"))
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_line_ending() {
let target = "target/normal-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
for &g in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
match g {
0 => {
alef.pop();
}
1 => {
bet.pop();
}
2 => {
alef.pop();
bet.pop();
}
_ => unreachable!(),
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/abn.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefn")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betn")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg("--normal")
.arg(format!("{target}/alefn"))
.stdin(File::open(format!("{target}/abn.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefn")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_empty_lines() {
let target = "target/normal-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/ab_.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg(format!("{target}/alef_"))
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_reverse() {
let target = "target/normal-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
.unwrap();
if a != 2 {
bet.write_all(b"a\n").unwrap();
}
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
.unwrap();
if b != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if c != 2 {
bet.write_all(b"c\n").unwrap();
}
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
.unwrap();
if d != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
.unwrap();
if e != 2 {
bet.write_all(b"e\n").unwrap();
}
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
.unwrap();
if f != 2 {
bet.write_all(b"f\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/abr.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.arg(format!("{target}/alefr"))
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_stop_early() {
let from = ["a", "b", "c"].join("\n");
let to = ["a", "d", "c"].join("\n");
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default());
let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n");
assert_eq!(diff_full, expected_full.as_bytes());
let diff_brief = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
brief: true,
..Default::default()
},
);
let expected_brief = "\0".as_bytes();
assert_eq!(diff_brief, expected_brief);
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default());
assert!(nodiff_full.is_empty());
let nodiff_brief = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
brief: true,
..Default::default()
},
);
assert!(nodiff_brief.is_empty());
}
}
+919
View File
@@ -0,0 +1,919 @@
use std::ffi::OsString;
use std::iter::Peekable;
use std::path::PathBuf;
use regex::Regex;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Format {
#[default]
Normal,
Unified,
Context,
Ed,
SideBySide,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Params {
pub executable: OsString,
pub from: OsString,
pub to: OsString,
pub format: Format,
pub context_count: usize,
pub report_identical_files: bool,
pub brief: bool,
pub expand_tabs: bool,
pub tabsize: usize,
pub width: usize,
}
impl Default for Params {
fn default() -> Self {
Self {
executable: OsString::default(),
from: OsString::default(),
to: OsString::default(),
format: Format::default(),
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: 8,
width: 130,
}
}
}
pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
// parse CLI
let Some(executable) = opts.next() else {
return Err("Usage: <exe> <from> <to>".to_string());
};
let mut params = Params {
executable,
..Default::default()
};
let mut from = None;
let mut to = None;
let mut format = None;
let mut context = None;
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
let width_re = Regex::new(r"--width=(?P<long>\d+)$").unwrap();
while let Some(param) = opts.next() {
let next_param = opts.peek();
if param == "--" {
break;
}
if param == "-" {
if from.is_none() {
from = Some(param);
} else if to.is_none() {
to = Some(param);
} else {
return Err(format!(
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
}
continue;
}
if param == "-s" || param == "--report-identical-files" {
params.report_identical_files = true;
continue;
}
if param == "-q" || param == "--brief" {
params.brief = true;
continue;
}
if param == "-t" || param == "--expand-tabs" {
params.expand_tabs = true;
continue;
}
if param == "--normal" {
if format.is_some() && format != Some(Format::Normal) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Normal);
continue;
}
if param == "-e" || param == "--ed" {
if format.is_some() && format != Some(Format::Ed) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Ed);
continue;
}
if param == "-y" || param == "--side-by-side" {
if format.is_some() && format != Some(Format::SideBySide) {
return Err("Conflicting output style option".to_string());
}
format = Some(Format::SideBySide);
continue;
}
if width_re.is_match(param.to_string_lossy().as_ref()) {
let param = param.into_string().unwrap();
let width_str: &str = width_re
.captures(param.as_str())
.unwrap()
.name("long")
.unwrap()
.as_str();
params.width = match width_str.parse::<usize>() {
Ok(num) => {
if num == 0 {
return Err("invalid width «0»".to_string());
}
num
}
Err(_) => return Err(format!("invalid width «{width_str}»")),
};
continue;
}
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
// Because param matches the regular expression,
// it is safe to assume it is valid UTF-8.
let param = param.into_string().unwrap();
let tabsize_str = tabsize_re
.captures(param.as_str())
.unwrap()
.name("num")
.unwrap()
.as_str();
params.tabsize = match tabsize_str.parse::<usize>() {
Ok(num) => {
if num == 0 {
return Err("invalid tabsize «0»".to_string());
}
num
}
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
};
continue;
}
match match_context_diff_params(&param, next_param, format) {
Ok(DiffStyleMatch {
is_match,
context_count,
next_param_consumed,
}) => {
if is_match {
format = Some(Format::Context);
if context_count.is_some() {
context = context_count;
}
if next_param_consumed {
opts.next();
}
continue;
}
}
Err(error) => return Err(error),
}
match match_unified_diff_params(&param, next_param, format) {
Ok(DiffStyleMatch {
is_match,
context_count,
next_param_consumed,
}) => {
if is_match {
format = Some(Format::Unified);
if context_count.is_some() {
context = context_count;
}
if next_param_consumed {
opts.next();
}
continue;
}
}
Err(error) => return Err(error),
}
if param.to_string_lossy().starts_with('-') {
return Err(format!("Unknown option: {param:?}"));
}
if from.is_none() {
from = Some(param);
} else if to.is_none() {
to = Some(param);
} else {
return Err(format!(
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
}
}
params.from = if let Some(from) = from {
from
} else if let Some(param) = opts.next() {
param
} else {
return Err(format!(
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
};
params.to = if let Some(to) = to {
to
} else if let Some(param) = opts.next() {
param
} else {
return Err(format!(
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
};
// diff DIRECTORY FILE => diff DIRECTORY/FILE FILE
// diff FILE DIRECTORY => diff FILE DIRECTORY/FILE
let mut from_path: PathBuf = PathBuf::from(&params.from);
let mut to_path: PathBuf = PathBuf::from(&params.to);
if from_path.is_dir() && to_path.is_file() {
from_path.push(to_path.file_name().unwrap());
params.from = from_path.into_os_string();
} else if from_path.is_file() && to_path.is_dir() {
to_path.push(from_path.file_name().unwrap());
params.to = to_path.into_os_string();
}
params.format = format.unwrap_or(Format::default());
if let Some(context_count) = context {
params.context_count = context_count;
}
Ok(params)
}
struct DiffStyleMatch {
is_match: bool,
context_count: Option<usize>,
next_param_consumed: bool,
}
fn match_context_diff_params(
param: &OsString,
next_param: Option<&OsString>,
format: Option<Format>,
) -> Result<DiffStyleMatch, String> {
const CONTEXT_RE: &str = r"^(-[cC](?<num1>\d*)|--context(=(?<num2>\d*))?|-(?<num3>\d+)c)$";
let regex = Regex::new(CONTEXT_RE).unwrap();
let is_match = regex.is_match(param.to_string_lossy().as_ref());
let mut context_count = None;
let mut next_param_consumed = false;
if is_match {
if format.is_some() && format != Some(Format::Context) {
return Err("Conflicting output style options".to_string());
}
let captures = regex.captures(param.to_str().unwrap()).unwrap();
let num = captures
.name("num1")
.or(captures.name("num2"))
.or(captures.name("num3"));
if let Some(numvalue) = num {
if !numvalue.as_str().is_empty() {
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
}
}
if param == "-C" {
if let Some(p) = next_param {
let size_str = p.to_string_lossy();
match size_str.parse::<usize>() {
Ok(context_size) => {
context_count = Some(context_size);
next_param_consumed = true;
}
Err(_) => return Err(format!("invalid context length '{size_str}'")),
}
}
}
}
Ok(DiffStyleMatch {
is_match,
context_count,
next_param_consumed,
})
}
fn match_unified_diff_params(
param: &OsString,
next_param: Option<&OsString>,
format: Option<Format>,
) -> Result<DiffStyleMatch, String> {
const UNIFIED_RE: &str = r"^(-[uU](?<num1>\d*)|--unified(=(?<num2>\d*))?|-(?<num3>\d+)u)$";
let regex = Regex::new(UNIFIED_RE).unwrap();
let is_match = regex.is_match(param.to_string_lossy().as_ref());
let mut context_count = None;
let mut next_param_consumed = false;
if is_match {
if format.is_some() && format != Some(Format::Unified) {
return Err("Conflicting output style options".to_string());
}
let captures = regex.captures(param.to_str().unwrap()).unwrap();
let num = captures
.name("num1")
.or(captures.name("num2"))
.or(captures.name("num3"));
if let Some(numvalue) = num {
if !numvalue.as_str().is_empty() {
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
}
}
if param == "-U" {
if let Some(p) = next_param {
let size_str = p.to_string_lossy();
match size_str.parse::<usize>() {
Ok(context_size) => {
context_count = Some(context_size);
next_param_consumed = true;
}
Err(_) => return Err(format!("invalid context length '{size_str}'")),
}
}
}
}
Ok(DiffStyleMatch {
is_match,
context_count,
next_param_consumed,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn os(s: &str) -> OsString {
OsString::from(s)
}
#[test]
fn basics() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("--normal"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
}
#[test]
fn basics_ed() {
for arg in ["-e", "--ed"] {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Ed,
..Default::default()
}),
parse_params(
[os("diff"), os(arg), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
}
}
#[test]
fn context_valid() {
for args in [vec!["-c"], vec!["--context"], vec!["--context="]] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Context,
..Default::default()
}),
parse_params(params.iter().map(|x| os(x)).peekable())
);
}
for args in [
vec!["-c42"],
vec!["-C42"],
vec!["-C", "42"],
vec!["--context=42"],
vec!["-42c"],
] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Context,
context_count: 42,
..Default::default()
}),
parse_params(params.iter().map(|x| os(x)).peekable())
);
}
}
#[test]
fn context_invalid() {
for args in [
vec!["-c", "42"],
vec!["-c=42"],
vec!["-c="],
vec!["-C"],
vec!["-C=42"],
vec!["-C="],
vec!["--context42"],
vec!["--context", "42"],
vec!["-42C"],
] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
}
}
#[test]
fn unified_valid() {
for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Unified,
..Default::default()
}),
parse_params(params.iter().map(|x| os(x)).peekable())
);
}
for args in [
vec!["-u42"],
vec!["-U42"],
vec!["-U", "42"],
vec!["--unified=42"],
vec!["-42u"],
] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Unified,
context_count: 42,
..Default::default()
}),
parse_params(params.iter().map(|x| os(x)).peekable())
);
}
}
#[test]
fn unified_invalid() {
for args in [
vec!["-u", "42"],
vec!["-u=42"],
vec!["-u="],
vec!["-U"],
vec!["-U=42"],
vec!["-U="],
vec!["--unified42"],
vec!["--unified", "42"],
vec!["-42U"],
] {
let mut params = vec!["diff"];
params.extend(args);
params.extend(["foo", "bar"]);
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
}
}
#[test]
fn context_count() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Unified,
context_count: 54,
..Default::default()
}),
parse_params(
[os("diff"), os("-u54"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Unified,
context_count: 54,
..Default::default()
}),
parse_params(
[os("diff"), os("-U54"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Unified,
context_count: 54,
..Default::default()
}),
parse_params(
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
format: Format::Context,
context_count: 54,
..Default::default()
}),
parse_params(
[os("diff"), os("-c54"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
}
#[test]
fn report_identical_files() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
report_identical_files: true,
..Default::default()
}),
parse_params(
[os("diff"), os("-s"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
report_identical_files: true,
..Default::default()
}),
parse_params(
[
os("diff"),
os("--report-identical-files"),
os("foo"),
os("bar"),
]
.iter()
.cloned()
.peekable()
)
);
}
#[test]
fn brief() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
brief: true,
..Default::default()
}),
parse_params(
[os("diff"), os("-q"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
brief: true,
..Default::default()
}),
parse_params(
[os("diff"), os("--brief"), os("foo"), os("bar"),]
.iter()
.cloned()
.peekable()
)
);
}
#[test]
fn expand_tabs() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
for option in ["-t", "--expand-tabs"] {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
expand_tabs: true,
..Default::default()
}),
parse_params(
[os("diff"), os(option), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
}
}
#[test]
fn tabsize() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
..Default::default()
}),
parse_params(
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
tabsize: 1,
..Default::default()
}),
parse_params(
[os("diff"), os("--tabsize=1"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
tabsize: 42,
..Default::default()
}),
parse_params(
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
);
assert!(parse_params(
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[
os("diff"),
os("--tabsize=92233720368547758088"),
os("foo"),
os("bar")
]
.iter()
.cloned()
.peekable()
)
.is_err());
}
#[test]
fn double_dash() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("-g"),
to: os("-h"),
..Default::default()
}),
parse_params(
[os("diff"), os("--"), os("-g"), os("-h")]
.iter()
.cloned()
.peekable()
)
);
}
#[test]
fn default_to_stdin() {
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("foo"),
to: os("-"),
..Default::default()
}),
parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable())
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("-"),
to: os("bar"),
..Default::default()
}),
parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable())
);
assert_eq!(
Ok(Params {
executable: os("diff"),
from: os("-"),
to: os("-"),
..Default::default()
}),
parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable())
);
assert!(parse_params(
[os("diff"), os("foo"), os("bar"), os("-")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("-"), os("-"), os("-")]
.iter()
.cloned()
.peekable()
)
.is_err());
}
#[test]
fn missing_arguments() {
assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err());
assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err());
}
#[test]
fn unknown_argument() {
assert!(parse_params(
[os("diff"), os("-g"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(
parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err()
);
assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err());
}
#[test]
fn empty() {
assert!(parse_params([].iter().cloned().peekable()).is_err());
}
#[test]
fn conflicting_output_styles() {
for (arg1, arg2) in [
("-u", "-c"),
("-u", "-e"),
("-c", "-u"),
("-c", "-U42"),
("-u", "--normal"),
("--normal", "-e"),
("--context", "--normal"),
] {
assert!(parse_params(
[os("diff"), os(arg1), os(arg2), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
}
}
}
+1263
View File
File diff suppressed because it is too large Load Diff
+945
View File
@@ -0,0 +1,945 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::collections::VecDeque;
use std::io::Write;
use crate::params::Params;
use crate::utils::do_write_line;
use crate::utils::get_modification_time;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
Expected(Vec<u8>),
Actual(Vec<u8>),
MissingNL,
}
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: u32,
pub line_number_actual: u32,
pub lines: Vec<DiffLine>,
}
impl Mismatch {
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
lines: Vec::new(),
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(
expected: &[u8],
actual: &[u8],
context_size: usize,
stop_early: bool,
) -> Vec<Mismatch> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
let mut lines_since_mismatch = context_size + 1;
let mut results = Vec::new();
let mut mismatch = Mismatch::new(0, 0);
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
// ^ means that underflow here is impossible
let expected_lines_count = expected_lines.len() as u32 - 1;
let actual_lines_count = actual_lines.len() as u32 - 1;
if expected_lines.last() == Some(&&b""[..]) {
expected_lines.pop();
}
if actual_lines.last() == Some(&&b""[..]) {
actual_lines.pop();
}
for result in diff::slice(&expected_lines, &actual_lines) {
match result {
diff::Result::Left(str) => {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
mismatch.lines.pop();
match mismatch.lines.pop() {
Some(DiffLine::Actual(res)) => {
// We have to make sure that Actual (the + lines)
// always come after Expected (the - lines)
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
if line_number_expected > expected_lines_count {
mismatch.lines.push(DiffLine::MissingNL);
}
mismatch.lines.push(DiffLine::Actual(res));
mismatch.lines.push(DiffLine::MissingNL);
}
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
}
} else {
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
if line_number_expected > expected_lines_count {
mismatch.lines.push(DiffLine::MissingNL);
}
}
line_number_expected += 1;
lines_since_mismatch = 0;
}
diff::Result::Right(str) => {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
if line_number_actual > actual_lines_count {
mismatch.lines.push(DiffLine::MissingNL);
}
line_number_actual += 1;
lines_since_mismatch = 0;
}
diff::Result::Both(str, _) => {
// if one of them is missing a newline and the other isn't, then they don't actually match
if (line_number_actual > actual_lines_count)
&& (line_number_expected > expected_lines_count)
{
if context_queue.len() < context_size {
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
if lines_since_mismatch < context_size {
mismatch.lines.push(DiffLine::Context(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
}
}
lines_since_mismatch = 0;
} else if line_number_actual > actual_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
lines_since_mismatch = 0;
} else if line_number_expected > expected_lines_count {
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
lines_since_mismatch = 0;
} else {
debug_assert!(context_queue.len() <= context_size);
if context_queue.len() >= context_size {
let _ = context_queue.pop_front();
}
if lines_since_mismatch < context_size {
mismatch.lines.push(DiffLine::Context(str.to_vec()));
} else if context_size > 0 {
context_queue.push_back(str);
}
lines_since_mismatch += 1;
}
line_number_expected += 1;
line_number_actual += 1;
}
}
if stop_early && !results.is_empty() {
// Optimization: stop analyzing the files as soon as there are any differences
return results;
}
}
results.push(mismatch);
results.remove(0);
if results.is_empty() && expected_lines_count != actual_lines_count {
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
// empty diff and only expected lines has a missing line at end
if expected_lines_count != expected_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
results.push(mismatch);
} else if actual_lines_count != actual_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
results.push(mismatch);
}
}
results
}
#[must_use]
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
let from_modified_time = get_modification_time(&params.from.to_string_lossy());
let to_modified_time = get_modification_time(&params.to.to_string_lossy());
let mut output = format!(
"--- {0}\t{1}\n+++ {2}\t{3}\n",
params.from.to_string_lossy(),
from_modified_time,
params.to.to_string_lossy(),
to_modified_time
)
.into_bytes();
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
if diff_results.is_empty() {
return Vec::new();
}
if params.brief {
return output;
}
for result in diff_results {
let mut line_number_expected = result.line_number_expected;
let mut line_number_actual = result.line_number_actual;
let mut expected_count = 0;
let mut actual_count = 0;
for line in &result.lines {
match line {
DiffLine::Expected(_) => {
expected_count += 1;
}
DiffLine::Context(_) => {
expected_count += 1;
actual_count += 1;
}
DiffLine::Actual(_) => {
actual_count += 1;
}
DiffLine::MissingNL => {}
}
}
// Let's imagine this diff file
//
// --- a/something
// +++ b/something
// @@ -2,0 +3,1 @@
// + x
//
// In the unified diff format as implemented by GNU diff and patch,
// this is an instruction to insert the x *after* the preexisting line 2,
// not before. You can demonstrate it this way:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Notice how the x winds up at line 3, not line 2. This requires contortions to
// work with our diffing algorithm, which keeps track of the "intended destination line",
// not a line that things are supposed to be placed after. It's changing the first number,
// not the second, that actually affects where the x goes.
//
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// c
// x
// d
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
// # actually being read
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Now watch what happens if I add a context line:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// Hunk #1 succeeded at 3 (offset 1 line).
//
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
// line that the x will wind up on, and not the line immediately before.
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// I made this comment because this stuff is not obvious from GNU's
// documentation on the format at all.
if expected_count == 0 {
line_number_expected -= 1;
}
if actual_count == 0 {
line_number_actual -= 1;
}
let exp_ct = if expected_count == 1 {
String::new()
} else {
format!(",{expected_count}")
};
let act_ct = if actual_count == 1 {
String::new()
} else {
format!(",{actual_count}")
};
writeln!(
output,
"@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@"
)
.expect("write to Vec is infallible");
for line in result.lines {
match line {
DiffLine::Expected(e) => {
write!(output, "-").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Context(c) => {
write!(output, " ").expect("write to Vec is infallible");
do_write_line(&mut output, &c, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Actual(r) => {
write!(output, "+",).expect("write to Vec is infallible");
do_write_line(&mut output, &r, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::MissingNL => {
writeln!(output, r"\ No newline at end of file")
.expect("write to Vec is infallible");
}
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_permutations() {
let target = "target/unified-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alef".into(),
to: (&format!("{target}/alef")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
println!(
"diff: {:?}",
String::from_utf8(diff.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
println!(
"alef: {:?}",
String::from_utf8(alef.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
println!(
"bet: {:?}",
String::from_utf8(bet.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
.output()
.unwrap();
println!("{}", String::from_utf8_lossy(&output.stdout));
println!("{}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success(), "{output:?}");
let alef = fs::read(format!("{target}/alef")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_line_ending() {
let target = "target/unified-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
for &g in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
.unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
.unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
.unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
.unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
.unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
match g {
0 => {
alef.pop();
}
1 => {
bet.pop();
}
2 => {
alef.pop();
bet.pop();
}
_ => unreachable!(),
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alefn".into(),
to: (&format!("{target}/alefn")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abn.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefn")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betn")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open(format!("{target}/abn.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefn")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_empty_lines() {
let target = "target/unified-diff/";
// test all possible six-line files with missing newlines.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
for &g in &[0, 1, 2, 3] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
match g {
0 => {
alef.pop();
}
1 => {
bet.pop();
}
2 => {
alef.pop();
bet.pop();
}
3 => {}
_ => unreachable!(),
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alef_".into(),
to: (&format!("{target}/alef_")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab_.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
}
#[test]
fn test_permutations_missing_lines() {
let target = "target/unified-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
if a != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
if b != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
if c != 2 {
bet.write_all(b"f\n").unwrap();
}
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
if d != 2 {
bet.write_all(b"h\n").unwrap();
}
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
if e != 2 {
bet.write_all(b"j\n").unwrap();
}
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
if f != 2 {
bet.write_all(b"l\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alefx".into(),
to: (&format!("{target}/alefx")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abx.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefx")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betx")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open(format!("{target}/abx.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefx")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_permutations_reverse() {
let target = "target/unified-diff/";
// test all possible six-line files.
let _ = std::fs::create_dir(target);
for &a in &[0, 1, 2] {
for &b in &[0, 1, 2] {
for &c in &[0, 1, 2] {
for &d in &[0, 1, 2] {
for &e in &[0, 1, 2] {
for &f in &[0, 1, 2] {
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
let mut alef = Vec::new();
let mut bet = Vec::new();
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
.unwrap();
if a != 2 {
bet.write_all(b"a\n").unwrap();
}
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
.unwrap();
if b != 2 {
bet.write_all(b"b\n").unwrap();
}
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
.unwrap();
if c != 2 {
bet.write_all(b"c\n").unwrap();
}
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
.unwrap();
if d != 2 {
bet.write_all(b"d\n").unwrap();
}
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
.unwrap();
if e != 2 {
bet.write_all(b"e\n").unwrap();
}
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
.unwrap();
if f != 2 {
bet.write_all(b"f\n").unwrap();
}
// This test diff is intentionally reversed.
// We want it to turn the alef into bet.
let diff = diff(
&alef,
&bet,
&Params {
from: "a/alefr".into(),
to: (&format!("{target}/alefr")).into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abr.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = Command::new("patch")
.arg("-p0")
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_stop_early() {
use crate::assert_diff_eq;
let from_filename = "foo";
let from = ["a", "b", "c", ""].join("\n");
let to_filename = "bar";
let to = ["a", "d", "c", ""].join("\n");
let diff_full = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
let expected_full = [
"--- foo\tTIMESTAMP",
"+++ bar\tTIMESTAMP",
"@@ -1,3 +1,3 @@",
" a",
"-b",
"+d",
" c",
"",
]
.join("\n");
assert_diff_eq!(diff_full, expected_full);
let diff_brief = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n");
assert_diff_eq!(diff_brief, expected_brief);
let nodiff_full = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
assert!(nodiff_full.is_empty());
let nodiff_brief = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
assert!(nodiff_brief.is_empty());
}
}
+207
View File
@@ -0,0 +1,207 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use regex::Regex;
use std::{ffi::OsString, io::Write};
use unicode_width::UnicodeWidthStr;
/// Replace tabs by spaces in the input line.
/// Correctly handle multi-bytes characters.
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
#[must_use]
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
let tab = b'\t';
let ntabs = line.iter().filter(|c| **c == tab).count();
if ntabs == 0 {
return line.to_vec();
}
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
let mut offset = 0;
let mut iter = line.split(|c| *c == tab).peekable();
while let Some(chunk) = iter.next() {
match String::from_utf8(chunk.to_vec()) {
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
Err(_) => offset += chunk.len(),
}
result.extend_from_slice(chunk);
if iter.peek().is_some() {
result.resize(result.len() + tabsize - offset % tabsize, b' ');
offset = 0;
}
}
result
}
/// Write a single line to an output stream, expanding tabs to space if necessary.
/// This assumes that line does not contain any line breaks
/// (if it does and tabs are to be expanded to spaces, the result is undefined).
pub fn do_write_line(
output: &mut Vec<u8>,
line: &[u8],
expand_tabs: bool,
tabsize: usize,
) -> std::io::Result<()> {
if expand_tabs {
output.write_all(do_expand_tabs(line, tabsize).as_slice())
} else {
output.write_all(line)
}
}
/// Retrieves the modification time of the input file specified by file path
/// If an error occurs, it returns the current system time
pub fn get_modification_time(file_path: &str) -> String {
use chrono::{DateTime, Local};
use std::fs;
use std::time::SystemTime;
let modification_time: SystemTime = fs::metadata(file_path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::now());
let modification_time: DateTime<Local> = modification_time.into();
let modification_time: String = modification_time
.format("%Y-%m-%d %H:%M:%S%.9f %z")
.to_string();
modification_time
}
pub fn format_failure_to_read_input_file(
executable: &OsString,
filepath: &OsString,
error: &std::io::Error,
) -> String {
// std::io::Error's display trait outputs "{detail} (os error {code})"
// but we want only the {detail} (error string) part
let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap();
format!(
"{}: {}: {}",
executable.to_string_lossy(),
filepath.to_string_lossy(),
error_code_re.replace(error.to_string().as_str(), ""),
)
}
pub fn report_failure_to_read_input_file(
executable: &OsString,
filepath: &OsString,
error: &std::io::Error,
) {
eprintln!(
"{}",
format_failure_to_read_input_file(executable, filepath, error)
);
}
#[cfg(test)]
mod tests {
use super::*;
mod expand_tabs {
use super::*;
use pretty_assertions::assert_eq;
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
assert_eq!(
do_expand_tabs(line.as_bytes(), tabsize),
expected.as_bytes()
);
}
#[test]
fn basics() {
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
}
#[test]
fn multibyte_chars() {
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
// Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
// it is displayed as a single emoji and has a print size of 2 columns.
// Terminal emulators tend to not support this, and display the two emojis
// side by side, thus accounting for a print size of 4 columns, but the
// unicode_width crate reports a correct size of 2.
assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz");
}
#[test]
fn invalid_utf8() {
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
assert_eq!(
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
);
}
}
mod write_line {
use super::*;
use pretty_assertions::assert_eq;
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
let mut output: Vec<u8> = Vec::new();
assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok());
assert_eq!(output, expected.as_bytes());
}
#[test]
fn basics() {
assert_line_written("foo bar baz", false, 8, "foo bar baz");
assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz");
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
}
}
mod modification_time {
use super::*;
#[test]
fn set_time() {
use chrono::{DateTime, Local};
use std::time::SystemTime;
use tempfile::NamedTempFile;
let temp = NamedTempFile::new().unwrap();
// set file modification time equal to current time
let current = SystemTime::now();
let _ = temp.as_file().set_modified(current);
// format current time
let current: DateTime<Local> = current.into();
let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string();
// verify
assert_eq!(
current,
get_modification_time(&temp.path().to_string_lossy())
);
}
#[test]
fn invalid_file() {
use chrono::{DateTime, Local};
use std::time::SystemTime;
let invalid_file = "target/utils/invalid-file";
// store current time before calling `get_modification_time`
// Because the file is invalid, it will return SystemTime::now()
// which will be greater than previously saved time
let current_time: DateTime<Local> = SystemTime::now().into();
let m_time: DateTime<Local> = get_modification_time(invalid_file).parse().unwrap();
assert!(m_time > current_time);
}
}
}
+890
View File
@@ -0,0 +1,890 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use std::fs::File;
#[cfg(not(windows))]
use std::fs::OpenOptions;
use std::io::Write;
use tempfile::{tempdir, NamedTempFile};
// Integration tests for the diffutils command
mod common {
use super::*;
#[test]
fn unknown_param() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("patch");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::eq("patch: utility not supported\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::starts_with(
"Expected utility name as second argument, got nothing.\n",
));
for subcmd in ["diff", "cmp"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg(subcmd);
cmd.arg("--foobar");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("Unknown option: \"--foobar\""));
}
Ok(())
}
#[test]
fn cannot_read_files() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
let nofile = NamedTempFile::new()?;
let nopath = nofile.into_temp_path();
std::fs::remove_file(&nopath)?;
#[cfg(not(windows))]
let error_message = "No such file or directory";
#[cfg(windows)]
let error_message = "The system cannot find the file specified.";
for subcmd in ["diff", "cmp"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg(subcmd);
cmd.arg(&nopath).arg(file.path());
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::ends_with(format!(
": {}: {error_message}\n",
&nopath.as_os_str().to_string_lossy()
)));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg(subcmd);
cmd.arg(file.path()).arg(&nopath);
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::ends_with(format!(
": {}: {error_message}\n",
&nopath.as_os_str().to_string_lossy()
)));
}
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg(&nopath).arg(&nopath);
cmd.assert().code(predicate::eq(2)).failure().stderr(
predicate::str::contains(format!(
": {}: {error_message}\n",
&nopath.as_os_str().to_string_lossy()
))
.count(2),
);
Ok(())
}
}
mod diff {
use diffutilslib::assert_diff_eq;
use super::*;
#[test]
fn no_differences() -> Result<(), Box<dyn std::error::Error>> {
let file = NamedTempFile::new()?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg(file.path()).arg(file.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty());
}
Ok(())
}
#[test]
fn no_differences_report_identical_files() -> Result<(), Box<dyn std::error::Error>> {
// same file
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-s").arg(file1.path()).arg(file1.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::eq(format!(
"Files {} and {} are identical\n",
file1.path().to_string_lossy(),
file1.path().to_string_lossy(),
)));
}
// two files with the same content
let mut file2 = NamedTempFile::new()?;
file2.write_all("foo\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-s").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::eq(format!(
"Files {} and {} are identical\n",
file1.path().to_string_lossy(),
file2.path().to_string_lossy(),
)));
}
Ok(())
}
#[test]
fn differences() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::is_empty().not());
}
Ok(())
}
#[test]
fn differences_brief() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
for option in ["", "-u", "-c", "-e"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
if !option.is_empty() {
cmd.arg(option);
}
cmd.arg("-q").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(format!(
"Files {} and {} differ\n",
file1.path().to_string_lossy(),
file2.path().to_string_lossy()
)));
}
Ok(())
}
#[test]
fn missing_newline() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar".as_bytes())?;
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-e").arg(file1.path()).arg(file2.path());
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::starts_with("No newline at end of file"));
Ok(())
}
#[test]
fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
let mut file1 = NamedTempFile::new()?;
file1.write_all("foo\n".as_bytes())?;
let mut file2 = NamedTempFile::new()?;
file2.write_all("bar\n".as_bytes())?;
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u")
.arg(file1.path())
.arg("-")
.write_stdin("bar\n");
cmd.assert().code(predicate::eq(1)).failure();
let output = cmd.output().unwrap().stdout;
assert_diff_eq!(
output,
format!(
"--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
file1.path().to_string_lossy()
)
);
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u")
.arg("-")
.arg(file2.path())
.write_stdin("foo\n");
cmd.assert().code(predicate::eq(1)).failure();
let output = cmd.output().unwrap().stdout;
assert_diff_eq!(
output,
format!(
"--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
file2.path().to_string_lossy()
)
);
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u").arg("-").arg("-");
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty());
#[cfg(unix)]
{
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u")
.arg(file1.path())
.arg("/dev/stdin")
.write_stdin("bar\n");
cmd.assert().code(predicate::eq(1)).failure();
let output = cmd.output().unwrap().stdout;
assert_diff_eq!(
output,
format!(
"--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
file1.path().to_string_lossy()
)
);
}
Ok(())
}
#[test]
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let directory = tmp_dir.path().join("d");
let _ = std::fs::create_dir(&directory);
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"a\n").unwrap();
let da_path = directory.join("a");
let mut da = File::create(&da_path).unwrap();
da.write_all(b"da\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u").arg(&directory).arg(&a_path);
cmd.assert().code(predicate::eq(1)).failure();
let output = cmd.output().unwrap().stdout;
assert_diff_eq!(
output,
format!(
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n",
da_path.display(),
a_path.display()
)
);
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff");
cmd.arg("-u").arg(&a_path).arg(&directory);
cmd.assert().code(predicate::eq(1)).failure();
let output = cmd.output().unwrap().stdout;
assert_diff_eq!(
output,
format!(
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n",
a_path.display(),
da_path.display()
)
);
Ok(())
}
}
mod cmp {
use super::*;
#[test]
fn cmp_incompatible_params() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-s");
cmd.arg("/etc/passwd").arg("/etc/group");
cmd.assert()
.code(predicate::eq(2))
.failure()
.stderr(predicate::str::ends_with(
": options -l and -s are incompatible\n",
));
Ok(())
}
#[test]
fn cmp_stdin() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"a\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg(&a_path);
cmd.write_stdin("a\n");
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg(&a_path);
cmd.write_stdin("b\n");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(" - differ: char 1, line 1\n"));
Ok(())
}
#[test]
fn cmp_equal_files() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"a\n").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"a\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
Ok(())
}
#[test]
fn cmp_one_file_empty() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"a\n").unwrap();
let b_path = tmp_dir.path().join("b");
let _ = File::create(&b_path).unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::contains(" EOF on "))
.stderr(predicate::str::ends_with(" which is empty\n"));
Ok(())
}
#[test]
fn cmp_immediate_difference() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"abc\n").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"bcd\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-b");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(
" differ: byte 1, line 1 is 141 a 142 b\n",
));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::eq("1 141 142\n2 142 143\n3 143 144\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::eq(
"1 141 a 142 b\n2 142 b 143 c\n3 143 c 144 d\n",
));
Ok(())
}
#[test]
fn cmp_newline_difference() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"abc\ndefg").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"abc\ndef\ng").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(" differ: char 8, line 2\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-b");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(
" differ: byte 8, line 2 is 147 g 12 ^J\n",
));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::starts_with("8 147 12\n"))
.stderr(predicate::str::contains(" EOF on"))
.stderr(predicate::str::ends_with(" after byte 8\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-b");
cmd.arg("-l");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::starts_with("8 147 g 12 ^J\n"))
.stderr(predicate::str::contains(" EOF on"))
.stderr(predicate::str::ends_with(" after byte 8\n"));
Ok(())
}
#[test]
fn cmp_max_bytes() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"abc efg ijkl\n").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"abcdefghijkl\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg("-n");
cmd.arg("3");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg("-n");
cmd.arg("4");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::eq("4 40 144 d\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg("-n");
cmd.arg("13");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::eq(" 4 40 144 d\n 8 40 150 h\n"));
Ok(())
}
#[test]
fn cmp_skip_args_parsing() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"---abc\n").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"###abc\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-i");
cmd.arg("3");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
// Positional skips should be ignored
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg("-i");
cmd.arg("3");
cmd.arg(&a_path).arg(&b_path);
cmd.arg("1").arg("1");
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
// Single positional argument should only affect first file.
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.arg("3");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.env("LC_ALL", "C");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.arg("3");
cmd.arg("3");
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
Ok(())
}
#[test]
fn cmp_skip_suffix_parsing() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
writeln!(a, "{}c", "a".repeat(1024)).unwrap();
a.flush().unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
writeln!(b, "{}c", "b".repeat(1024)).unwrap();
b.flush().unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("--ignore-initial=1K");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
Ok(())
}
#[test]
fn cmp_skip() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(b"abc efg ijkl\n").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(b"abcdefghijkl\n").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg("-i");
cmd.arg("8");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(0))
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::is_empty());
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-b");
cmd.arg("-i");
cmd.arg("4");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::ends_with(
" differ: byte 4, line 1 is 40 150 h\n",
));
Ok(())
}
#[test]
fn cmp_binary() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let mut bytes = vec![0, 15, 31, 32, 33, 40, 64, 126, 127, 128, 129, 200, 254, 255];
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(&bytes).unwrap();
bytes.reverse();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(&bytes).unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-l");
cmd.arg("-b");
cmd.arg(&a_path).arg(&b_path);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::eq(concat!(
" 1 0 ^@ 377 M-^?\n",
" 2 17 ^O 376 M-~\n",
" 3 37 ^_ 310 M-H\n",
" 4 40 201 M-^A\n",
" 5 41 ! 200 M-^@\n",
" 6 50 ( 177 ^?\n",
" 7 100 @ 176 ~\n",
" 8 176 ~ 100 @\n",
" 9 177 ^? 50 (\n",
"10 200 M-^@ 41 !\n",
"11 201 M-^A 40 \n",
"12 310 M-H 37 ^_\n",
"13 376 M-~ 17 ^O\n",
"14 377 M-^? 0 ^@\n"
)));
Ok(())
}
#[test]
#[cfg(not(windows))]
fn cmp_fast_paths() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
// This test mimics one found in the GNU cmp test suite. It is used for
// validating the /dev/null optimization.
let a_path = tmp_dir.path().join("a");
let a = File::create(&a_path).unwrap();
a.set_len(14 * 1024 * 1024 * 1024 * 1024).unwrap();
let b_path = tmp_dir.path().join("b");
let b = File::create(&b_path).unwrap();
b.set_len(15 * 1024 * 1024 * 1024 * 1024).unwrap();
let dev_null = OpenOptions::new().write(true).open("/dev/null").unwrap();
let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils"))
.arg("cmp")
.arg(&a_path)
.arg(&b_path)
.stdout(dev_null)
.spawn()
.unwrap();
// Bound the runtime to a very short time that still allows for some resource
// constraint to slow it down while also allowing very fast systems to exit as
// early as possible.
const MAX_TRIES: u8 = 50;
for tries in 0..=MAX_TRIES {
if tries == MAX_TRIES {
panic!("cmp took too long to run, /dev/null optimization probably not working")
}
match child.try_wait() {
Ok(Some(status)) => {
assert_eq!(status.code(), Some(1));
break;
}
Ok(None) => (),
Err(e) => panic!("{e:#?}"),
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
// Two stdins should be equal
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg("-");
cmd.arg("-");
cmd.assert()
.code(predicate::eq(0))
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::is_empty());
// Files with longer than block size equal segments should still report
// the correct line number for the difference. Assumes 8KB block size (see
// https://github.com/rust-lang/rust/blob/master/library/std/src/sys_common/io.rs),
// create a 24KB equality.
let mut bytes = " ".repeat(4095);
bytes.push('\n');
bytes.push_str(&" ".repeat(4096));
let bytes = bytes.repeat(3);
let bytes = bytes.as_bytes();
let a_path = tmp_dir.path().join("a");
let mut a = File::create(&a_path).unwrap();
a.write_all(bytes).unwrap();
a.write_all(b"A").unwrap();
let b_path = tmp_dir.path().join("b");
let mut b = File::create(&b_path).unwrap();
b.write_all(bytes).unwrap();
b.write_all(b"B").unwrap();
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("cmp");
cmd.arg(&a_path).arg(&b_path);
cmd.env("LC_ALL", "en_US");
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout(predicate::str::ends_with(" differ: byte 24577, line 4\n"));
Ok(())
}
}
+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
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
# Run the GNU upstream test suite for diffutils against a local build of the
# Rust implementation, print out a summary of the test results, and writes a
# JSON file ('test-results.json') containing detailed information about the
# test run.
# The JSON file contains metadata about the test run, and for each test the
# result as well as the contents of stdout, stderr, and of all the files
# written by the test script, if any (excluding subdirectories).
# The script takes a shortcut to fetch only the test suite from the upstream
# repository and carefully avoids running the autotools machinery which is
# time-consuming and resource-intensive, and doesn't offer the option to not
# build the upstream binaries. As a consequence, the environment in which the
# tests are run might not match exactly that used when the upstream tests are
# run through the autotools.
# By default it expects a release build of the diffutils binary, but a
# different build profile can be specified as an argument
# (e.g. 'dev' or 'test').
# Unless overridden by the $TESTS environment variable, all tests in the test
# suite will be run. Tests targeting a command that is not yet implemented
# (e.g. diff3 or sdiff) are skipped.
scriptpath=$(dirname "$(readlink -f "$0")")
rev=$(git rev-parse HEAD)
# Allow passing a specific profile as parameter (default to "release")
profile="release"
[[ -n $1 ]] && profile="$1"
# Verify that the diffutils binary was built for the requested profile
binary="$scriptpath/../target/$profile/diffutils"
if [[ ! -x "$binary" ]]
then
echo "Missing build for profile $profile"
exit 1
fi
# Work in a temporary directory
tempdir=$(mktemp -d)
cd "$tempdir"
# Check out the upstream test suite
gitserver="https://git.savannah.gnu.org"
testsuite="$gitserver/git/diffutils.git"
echo "Fetching upstream test suite from $testsuite"
git clone -n --depth=1 --filter=tree:0 "$testsuite" &> /dev/null
cd diffutils
git sparse-checkout set --no-cone tests &> /dev/null
git checkout &> /dev/null
upstreamrev=$(git rev-parse HEAD)
# Ensure that calling `diff` invokes the built `diffutils` binary instead of
# the upstream `diff` binary that is most likely installed on the system
mkdir src
cd src
ln -s "$binary" diff
ln -s "$binary" cmp
cd ../tests
# Fetch tests/init.sh from the gnulib repository (needed since
# https://git.savannah.gnu.org/cgit/diffutils.git/commit/tests?id=1d2456f539)
curl -s "$gitserver/gitweb/?p=gnulib.git;a=blob_plain;f=tests/init.sh;hb=HEAD" -o init.sh
if [[ -n "$TESTS" ]]
then
tests="$TESTS"
else
# Get a list of all upstream tests (default if $TESTS isn't set)
echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am
tests=$(make -f Makefile.am printtests)
fi
total=$(echo "$tests" | wc -w)
echo "Running $total tests"
export LC_ALL=C
export KEEP=yes
timestamp=$(date -Iseconds)
urlroot="$gitserver/cgit/diffutils.git/tree/tests/"
passed=0
failed=0
skipped=0
normal="$(tput sgr0)"
for test in $tests
do
result="FAIL"
url="$urlroot$test?id=$upstreamrev"
# Run only the tests that invoke `diff` or `cmp`,
# because other binaries aren't implemented yet
if ! grep -E -s -q "(diff3|sdiff)" "$test"
then
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS"
if [[ $? = 77 ]]
then
result="SKIP"
else
json+="{\"test\":\"$test\",\"result\":\"$result\","
json+="\"url\":\"$url\","
json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\","
json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\","
json+="\"files\":{"
cd gt-$test.*
# Note: this doesn't include the contents of subdirectories,
# but there isn't much value added in doing so
for file in *
do
[[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\","
done
json="${json%,}}},"
cd - > /dev/null
[[ "$result" = "PASS" ]] && (( passed++ ))
[[ "$result" = "FAIL" ]] && (( failed++ ))
fi
else
result="SKIP"
fi
color=2 # green
[[ "$result" = "FAIL" ]] && color=1 # red
if [[ $result = "SKIP" ]]
then
(( skipped++ ))
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
color=3 # yellow
fi
printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test"
done
echo ""
echo -n "Summary: TOTAL: $total / "
echo -n "$(tput setaf 2)PASS$normal: $passed / "
echo -n "$(tput setaf 1)FAIL$normal: $failed / "
echo "$(tput setaf 3)SKIP$normal: $skipped"
echo ""
json="\"tests\":[${json%,}]"
metadata="\"timestamp\":\"$timestamp\","
metadata+="\"revision\":\"$rev\","
metadata+="\"upstream-revision\":\"$upstreamrev\","
if [[ -n "$GITHUB_ACTIONS" ]]
then
metadata+="\"branch\":\"$GITHUB_REF\","
fi
json="{$metadata $json}"
# Clean up
cd "$scriptpath"
rm -rf "$tempdir"
resultsfile="test-results.json"
echo "$json" | jq > "$resultsfile"
echo "Results written to $scriptpath/$resultsfile"
(( failed > 0 )) && exit 1
exit 0