Compare commits

1 Commits

Author SHA1 Message Date
Sylvestre Ledru fe605ff853 ed_diff::tests::test_permutations: disable temporary tests on Windows 2024-04-21 15:52:52 +02:00
31 changed files with 706 additions and 6171 deletions
+25 -42
View File
@@ -4,7 +4,6 @@ name: Basic CI
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs: jobs:
check: check:
@@ -16,6 +15,7 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check - run: cargo check
test: test:
@@ -27,11 +27,7 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install GNU patch on MacOS - uses: dtolnay/rust-toolchain@stable
if: runner.os == 'macOS'
run: |
brew install gpatch
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
- name: set up PATH on Windows - name: set up PATH on Windows
# Needed to use GNU's patch.exe instead of Strawberry Perl patch # Needed to use GNU's patch.exe instead of Strawberry Perl patch
if: runner.os == 'Windows' if: runner.os == 'Windows'
@@ -43,6 +39,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check - run: cargo fmt --all -- --check
clippy: clippy:
@@ -54,18 +52,17 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- run: cargo clippy -- -D warnings - run: cargo clippy -- -D warnings
gnu-testsuite: gnu-testsuite:
permissions:
contents: write # Publish diffutils instead of discarding
name: GNU test suite name: GNU test suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: | - uses: dtolnay/rust-toolchain@stable
cargo build --config=profile.release.strip=true --profile=release #-fast - run: cargo build --release
zstd -19 target/release/diffutils -o diffutils-x86_64-unknown-linux-gnu.zst
# do not fail, the report is merely informative (at least until all tests pass reliably) # do not fail, the report is merely informative (at least until all tests pass reliably)
- run: ./tests/run-upstream-testsuite.sh release || true - run: ./tests/run-upstream-testsuite.sh release || true
env: env:
@@ -75,17 +72,6 @@ jobs:
name: test-results.json name: test-results.json
path: tests/test-results.json path: tests/test-results.json
- run: ./tests/print-test-results.sh tests/test-results.json - run: ./tests/print-test-results.sh tests/test-results.json
- name: Publish latest commit
uses: softprops/action-gh-release@v2
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
tag_name: latest-commit
draft: false
prerelease: true
files: |
diffutils-x86_64-unknown-linux-gnu.zst
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
coverage: coverage:
name: Code Coverage name: Code Coverage
@@ -100,42 +86,39 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Initialize workflow variables - name: Initialize workflow variables
env:
# Use -Z
RUSTC_BOOTSTRAP: 1
id: vars id: vars
shell: bash shell: bash
run: | run: |
## VARs setup ## VARs setup
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } 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 # target-specific options
# * CARGO_FEATURES_OPTION
CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage
# * CODECOV_FLAGS # * CODECOV_FLAGS
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS outputs CODECOV_FLAGS
- name: install GNU patch on MacOS - name: rust toolchain ~ install
if: runner.os == 'macOS' uses: dtolnay/rust-toolchain@nightly
run: |
brew install gpatch
echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH"
- name: set up PATH on Windows - name: set up PATH on Windows
# Needed to use GNU's patch.exe instead of Strawberry Perl patch # Needed to use GNU's patch.exe instead of Strawberry Perl patch
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
- name: Test - name: Test
run: cargo test --all-features --no-fail-fast run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast
env: env:
CARGO_INCREMENTAL: "0" CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: "" RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw"
# Use -Z
RUSTC_BOOTSTRAP: 1
- name: "`grcov` ~ install" - name: "`grcov` ~ install"
env:
# Use -Z
RUSTC_BOOTSTRAP: 1
id: build_grcov id: build_grcov
shell: bash shell: bash
run: | run: |
@@ -162,15 +145,15 @@ jobs:
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
mkdir -p "${COVERAGE_REPORT_DIR}" mkdir -p "${COVERAGE_REPORT_DIR}"
# display coverage files # display coverage files
grcov . --output-type files --binary-path "${COVERAGE_REPORT_DIR}" | sort --unique grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report # generate coverage report
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io) - name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }} file: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella name: codecov-umbrella
-37
View File
@@ -1,37 +0,0 @@
name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@v0
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Build the benchmark target(s)
run: cargo codspeed build -m simulation
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: cargo codspeed run
+8 -15
View File
@@ -21,17 +21,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz` - name: Install `cargo-fuzz`
run: | run: cargo install cargo-fuzz
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
echo "CARGO_INCREMENTAL=0" >> "${GITHUB_ENV}"
cargo install cargo-fuzz --locked
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
shared-key: "cargo-fuzz-cache-key" shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target" cache-directories: "fuzz/target"
- name: Run `cargo-fuzz build` - name: Run `cargo-fuzz build`
run: cargo fuzz build run: cargo +nightly fuzz build
fuzz-run: fuzz-run:
needs: fuzz-build needs: fuzz-build
@@ -43,25 +41,20 @@ jobs:
strategy: strategy:
matrix: matrix:
test-target: test-target:
- { name: fuzz_cmp, should_pass: true }
- { name: fuzz_cmp_args, should_pass: true }
- { name: fuzz_ed, should_pass: true } - { name: fuzz_ed, should_pass: true }
- { name: fuzz_normal, should_pass: true } - { name: fuzz_normal, should_pass: true }
- { name: fuzz_patch, should_pass: true } - { name: fuzz_patch, should_pass: true }
- { name: fuzz_side, should_pass: true }
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz` - name: Install `cargo-fuzz`
run: | run: cargo install cargo-fuzz
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
echo "CARGO_INCREMENTAL=0" >> "${GITHUB_ENV}"
cargo install cargo-fuzz --locked
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
shared-key: "cargo-fuzz-cache-key" shared-key: "cargo-fuzz-cache-key"
cache-directories: "fuzz/target" cache-directories: "fuzz/target"
- name: Restore Cached Corpus - name: Restore Cached Corpus
uses: actions/cache/restore@v5 uses: actions/cache/restore@v4
with: with:
key: corpus-cache-${{ matrix.test-target.name }} key: corpus-cache-${{ matrix.test-target.name }}
path: | path: |
@@ -70,9 +63,9 @@ jobs:
shell: bash shell: bash
continue-on-error: ${{ !matrix.test-target.name.should_pass }} continue-on-error: ${{ !matrix.test-target.name.should_pass }}
run: | run: |
cargo fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Save Corpus Cache - name: Save Corpus Cache
uses: actions/cache/save@v5 uses: actions/cache/save@v4
with: with:
key: corpus-cache-${{ matrix.test-target.name }} key: corpus-cache-${{ matrix.test-target.name }}
path: | path: |
+64 -94
View File
@@ -1,21 +1,20 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # Copyright 2022-2023, axodotdev
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0 # SPDX-License-Identifier: MIT or Apache-2.0
# #
# CI that: # CI that:
# #
# * checks for a Git Tag that looks like a release # * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes) # * builds artifacts with cargo-dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip # * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release # * on success, uploads the artifacts to a Github Release
# #
# Note that the GitHub Release will be created with a generated # Note that the Github Release will be created with a generated
# title/body based on your changelogs. # title/body based on your changelogs.
name: Release name: Release
permissions: permissions:
"contents": "write" contents: write
# This task will run whenever you push a git tag that looks like a version # 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. # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
@@ -24,30 +23,30 @@ permissions:
# must be a Cargo-style SemVer Version (must have at least major.minor.patch). # 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 # 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). # package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
# #
# If PACKAGE_NAME isn't specified, then the announcement will be for all # 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 # (cargo-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 # intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep). # packages versioned/released in lockstep).
# #
# If you push multiple tags at once, separate instances of this workflow will # If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub # 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 # will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake. # mistake.
# #
# If there's a prerelease-style suffix to the version, then the release(s) # If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease. # will be marked as a prerelease.
on: on:
pull_request:
push: push:
tags: tags:
- '**[0-9]+.[0-9]+.[0-9]+*' - '**[0-9]+.[0-9]+.[0-9]+*'
pull_request:
jobs: jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do # Run 'cargo dist plan' (or host) to determine what tasks we need to do
plan: plan:
runs-on: "ubuntu-22.04" runs-on: ubuntu-latest
outputs: outputs:
val: ${{ steps.plan.outputs.manifest }} val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }} tag: ${{ !github.event.pull_request && github.ref_name || '' }}
@@ -58,18 +57,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: recursive submodules: recursive
- name: Install dist - name: Install cargo-dist
# we specify bash to get pipefail; it guards against the `curl` command # we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0 # failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash 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" run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/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... # 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 # 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. # functionality based on whether this is a pull_request, and whether it's from a fork.
@@ -77,8 +70,8 @@ jobs:
# but also really annoying to build CI around when it needs secrets to work right.) # but also really annoying to build CI around when it needs secrets to work right.)
- id: plan - id: plan
run: | run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json cargo 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" echo "cargo dist ran successfully"
cat plan-dist-manifest.json cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json" - name: "Upload dist-manifest.json"
@@ -96,39 +89,28 @@ jobs:
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') }} 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: strategy:
fail-fast: false fail-fast: false
# Target platforms/runners are computed by dist in create-release. # Target platforms/runners are computed by cargo-dist in create-release.
# Each member of the matrix has the following arguments: # Each member of the matrix has the following arguments:
# #
# - runner: the github runner # - runner: the github runner
# - dist-args: cli flags to pass to dist # - dist-args: cli flags to pass to cargo dist
# - install-dist: expression to run to install dist on the runner # - install-dist: expression to run to install cargo-dist on the runner
# #
# Typically there will be: # Typically there will be:
# - 1 "global" task that builds universal installers # - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers # - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps: steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: recursive submodules: recursive
- name: Install Rust non-interactively if not already installed - uses: swatinem/rust-cache@v2
if: ${{ matrix.container }} - name: Install cargo-dist
run: | run: ${{ matrix.install_dist }}
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 # Get the dist-manifest
- name: Fetch local artifacts - name: Fetch local artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -142,8 +124,8 @@ jobs:
- name: Build artifacts - name: Build artifacts
run: | run: |
# Actually do builds and make zips and whatnot # 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 cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully" echo "cargo dist ran successfully"
- id: cargo-dist - id: cargo-dist
name: Post-build name: Post-build
# We force bash here just because github makes it really hard to get values up # We force bash here just because github makes it really hard to get values up
@@ -153,7 +135,7 @@ jobs:
run: | run: |
# Parse out what we just built and upload it to scratch storage # Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT" echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME" cp dist-manifest.json "$BUILD_MANIFEST_NAME"
@@ -170,21 +152,17 @@ jobs:
needs: needs:
- plan - plan
- build-local-artifacts - build-local-artifacts
runs-on: "ubuntu-22.04" runs-on: "ubuntu-20.04"
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: recursive submodules: recursive
- name: Install cached dist - name: Install cargo-dist
uses: actions/download-artifact@v4 shell: bash
with: run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
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) # Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts - name: Fetch local artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -195,12 +173,12 @@ jobs:
- id: cargo-dist - id: cargo-dist
shell: bash shell: bash
run: | run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully" echo "cargo dist ran successfully"
# Parse out what we just built and upload it to scratch storage # Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT" echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME" cp dist-manifest.json "$BUILD_MANIFEST_NAME"
@@ -217,24 +195,19 @@ jobs:
- plan - plan
- build-local-artifacts - build-local-artifacts
- build-global-artifacts - build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) # Only run if we're "publishing", and only if 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') }} if: ${{ always() && 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: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04" runs-on: "ubuntu-20.04"
outputs: outputs:
val: ${{ steps.host.outputs.manifest }} val: ${{ steps.host.outputs.manifest }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: recursive submodules: recursive
- name: Install cached dist - name: Install cargo-dist
uses: actions/download-artifact@v4 run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage # Fetch artifacts from scratch-storage
- name: Fetch artifacts - name: Fetch artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -242,10 +215,11 @@ jobs:
pattern: artifacts-* pattern: artifacts-*
path: target/distrib/ path: target/distrib/
merge-multiple: true merge-multiple: true
# This is a harmless no-op for Github Releases, hosting for that happens in "announce"
- id: host - id: host
shell: bash shell: bash
run: | run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully" echo "artifacts uploaded and released successfully"
cat dist-manifest.json cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
@@ -255,8 +229,24 @@ jobs:
# Overwrite the previous copy # Overwrite the previous copy
name: artifacts-dist-manifest name: artifacts-dist-manifest
path: dist-manifest.json path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts" # Create a Github Release while uploading all files to it
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-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: "Download Github Artifacts"
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
pattern: artifacts-* pattern: artifacts-*
@@ -266,31 +256,11 @@ jobs:
run: | run: |
# Remove the granular manifests # Remove the granular manifests
rm -f artifacts/*-dist-manifest.json rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release - name: Create Github Release
env: uses: ncipollo/release-action@v1
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: with:
persist-credentials: false tag: ${{ needs.plan.outputs.tag }}
submodules: recursive name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
artifacts: "artifacts/*"
Generated
+87 -751
View File
File diff suppressed because it is too large Load Diff
+17 -27
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "diffutils" name = "diffutils"
version = "0.5.0" version = "0.4.0"
edition = "2021" edition = "2021"
description = "A CLI app for generating diff files" description = "A CLI app for generating diff files"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -17,40 +17,30 @@ path = "src/main.rs"
[dependencies] [dependencies]
chrono = "0.4.38" chrono = "0.4.38"
diff = "0.1.13" diff = "0.1.13"
itoa = "1.0.11"
regex = "1.10.4" regex = "1.10.4"
same-file = "1.0.6" same-file = "1.0.6"
unicode-width = "0.2.0" unicode-width = "0.1.11"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1"
assert_cmd = "2.0.14" assert_cmd = "2.0.14"
divan = { version = "4.3.0", package = "codspeed-divan-compat" }
pretty_assertions = "1.4.0"
predicates = "3.1.0" predicates = "3.1.0"
rand = "0.10.0" tempfile = "3.10.1"
tempfile = "3.26.0"
[profile.release] # The profile that 'cargo dist' will build with
lto = "thin"
codegen-units = 1
[profile.release-fast]
inherits = "release"
panic = "abort"
# The profile that 'dist' will build with
[profile.dist] [profile.dist]
inherits = "release" inherits = "release"
lto = "thin" lto = "thin"
[[bench]] # Config for 'cargo dist'
name = "bench_diffutils" [workspace.metadata.dist]
path = "benches/bench-diffutils.rs" # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
harness = false cargo-dist-version = "0.12.0"
# CI backends to support
[features] ci = ["github"]
# default = ["feat_bench_not_diff"] # The installers to generate for each app
# Turn bench for diffutils cmp off installers = []
feat_bench_not_cmp = [] # Target platforms to build apps for (Rust target-triple syntax)
# Turn bench for diffutils diff off targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
feat_bench_not_diff = [] # Publish jobs to run in CI
pr-run-mode = "plan"
-3
View File
@@ -1,6 +1,3 @@
Copyright (c) Michael Howell
Copyright (c) uutils developers
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
-3
View File
@@ -1,6 +1,3 @@
Copyright (c) Michael Howell
Copyright (c) uutils developers
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the documentation files (the "Software"), to deal in the
+1 -2
View File
@@ -2,11 +2,10 @@
[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) [![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) [![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) [![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/uutils/diffutils?utm_source=badge)
[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) [![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils)
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) (diff, cmp, diff3, sdiff) in Rust. The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust.
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. 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.
-377
View File
@@ -1,377 +0,0 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
//! Benches for all utils in diffutils.
//!
//! There is a file generator included to create files of different sizes for comparison. \
//! Set the TEMP_DIR const to keep the files. df_to_ files have small changes in them, search for '#'. \
//! File generation up to 1 GB is really fast, Benchmarking above 100 MB takes very long.
/// Generate test files with these sizes in KB.
const FILE_SIZE_KILO_BYTES: [u64; 4] = [100, 1 * MB, 10 * MB, 25 * MB];
// const FILE_SIZE_KILO_BYTES: [u64; 3] = [100, 1 * MB, 5 * MB];
// Empty String to use TempDir (files will be removed after test) or specify dir to keep generated files
const TEMP_DIR: &str = "";
const NUM_DIFF: u64 = 4;
// just for FILE_SIZE_KILO_BYTES
const MB: u64 = 1_000;
const CHANGE_CHAR: u8 = b'#';
#[cfg(not(feature = "feat_bench_not_cmp"))]
mod diffutils_cmp {
use std::hint::black_box;
use diffutilslib::cmp;
use divan::Bencher;
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmp_compare_files_equal(bencher: Bencher, kb: u64) {
let (from, to) = get_context().get_test_files_equal(kb);
let cmd = format!("cmp {from} {to}");
let opts = str_to_options(&cmd).into_iter().peekable();
let params = cmp::parse_params(opts).unwrap();
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| params.clone())
.bench_refs(|params| black_box(cmp::cmp(&params).unwrap()));
}
// bench the actual compare; cmp exits on first difference
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmp_compare_files_different(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_different(bytes);
let cmd = format!("cmp {from} {to} -s");
let opts = str_to_options(&cmd).into_iter().peekable();
let params = cmp::parse_params(opts).unwrap();
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| params.clone())
.bench_refs(|params| black_box(cmp::cmp(&params).unwrap()));
}
// bench original GNU cmp
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_cmp_gnu_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("{from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("cmp", cmd_args));
}
// bench the compiled release version
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_cmp_release_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("cmp {from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
}
}
#[cfg(not(feature = "feat_bench_not_diff"))]
mod diffutils_diff {
// use std::hint::black_box;
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
// use diffutilslib::params;
use divan::Bencher;
// bench the actual compare
// TODO diff does not have a diff function
// #[divan::bench(args = [100_000,10_000])]
// fn diff_compare_files(bencher: Bencher, bytes: u64) {
// let (from, to) = gen_testfiles(lines, 0, "id");
// let cmd = format!("cmp {from} {to}");
// let opts = str_to_options(&cmd).into_iter().peekable();
// let params = params::parse_params(opts).unwrap();
//
// bencher
// // .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
// .with_inputs(|| params.clone())
// .bench_refs(|params| diff::diff(&params).unwrap());
// }
// bench original GNU diff
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_diff_gnu_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("{from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("diff", cmd_args));
}
// bench the compiled release version
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
fn cmd_diff_release_equal(bencher: Bencher, bytes: u64) {
let (from, to) = get_context().get_test_files_equal(bytes);
let args_str = format!("diff {from} {to}");
bencher
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
.with_inputs(|| args_str.clone())
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
}
}
mod parser {
use std::hint::black_box;
use diffutilslib::{cmp, params};
use divan::Bencher;
use crate::prepare::str_to_options;
// bench the time it takes to parse the command line arguments
#[divan::bench]
fn cmp_parser(bencher: Bencher) {
let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
let args = str_to_options(&cmd).into_iter().peekable();
bencher
.with_inputs(|| args.clone())
.bench_values(|data| black_box(cmp::parse_params(data)));
}
// // test the impact on the benchmark if not converting the cmd to Vec<OsString> (doubles for parse)
// #[divan::bench]
// fn cmp_parser_no_prepare() {
// let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
// let args = str_to_options(&cmd).into_iter().peekable();
// let _ = cmp::parse_params(args);
// }
// bench the time it takes to parse the command line arguments
#[divan::bench]
fn diff_parser(bencher: Bencher) {
let cmd = "diff file_1.txt file_2.txt -s --brief --expand-tabs --width=100";
let args = str_to_options(&cmd).into_iter().peekable();
bencher
.with_inputs(|| args.clone())
.bench_values(|data| black_box(params::parse_params(data)));
}
}
mod prepare {
use std::{
ffi::OsString,
fs::{self, File},
io::{BufWriter, Write},
path::Path,
sync::OnceLock,
};
use rand::RngExt;
use tempfile::TempDir;
use crate::{CHANGE_CHAR, FILE_SIZE_KILO_BYTES, NUM_DIFF, TEMP_DIR};
// file lines and .txt will be added
const FROM_FILE: &str = "from_file";
const TO_FILE: &str = "to_file";
const LINE_LENGTH: usize = 60;
/// Contains test data (file names) which only needs to be created once.
#[derive(Debug, Default)]
pub struct BenchContext {
pub tmp_dir: Option<TempDir>,
pub dir: String,
pub files_equal: Vec<(String, String)>,
pub files_different: Vec<(String, String)>,
}
impl BenchContext {
pub fn get_path(&self) -> &Path {
match &self.tmp_dir {
Some(tmp) => tmp.path(),
None => Path::new(&self.dir),
}
}
pub fn get_test_files_equal(&self, kb: u64) -> &(String, String) {
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
&self.files_equal[p]
}
#[allow(unused)]
pub fn get_test_files_different(&self, kb: u64) -> &(String, String) {
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
&self.files_different[p]
}
}
// Since each bench function is separate in Divan it is more difficult to dynamically create test data.
// This keeps the TempDir alive until the program exits and generates the files only once.
static SHARED_CONTEXT: OnceLock<BenchContext> = OnceLock::new();
/// Creates the test files once and provides them to all tests.
pub fn get_context() -> &'static BenchContext {
SHARED_CONTEXT.get_or_init(|| {
let mut ctx = BenchContext::default();
if TEMP_DIR.is_empty() {
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
ctx.tmp_dir = Some(tmp_dir);
} else {
// uses current directory, the generated files are kept
let path = Path::new(TEMP_DIR);
if !path.exists() {
fs::create_dir_all(path).expect("Path {path} could not be created");
}
ctx.dir = TEMP_DIR.to_string();
};
// generate test bytes
for kb in FILE_SIZE_KILO_BYTES {
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, 0, "eq")
.expect("generate_test_files failed");
ctx.files_equal.push(f);
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, NUM_DIFF, "df")
.expect("generate_test_files failed");
ctx.files_different.push(f);
}
ctx
})
}
pub fn str_to_options(opt: &str) -> Vec<OsString> {
let s: Vec<OsString> = opt
.split(" ")
.into_iter()
.filter(|s| !s.is_empty())
.map(|s| OsString::from(s))
.collect();
s
}
/// Generates two test files for comparison with <bytes> size.
///
/// Each line consists of 10 words with 5 letters, giving a line length of 60 bytes.
/// If num_differences is set, '#' will be inserted between the first two words of a line,
/// evenly spaced in the file. 1 will add the change in the last line, so the comparison takes longest.
fn generate_test_files_bytes(
dir: &Path,
bytes: u64,
num_differences: u64,
id: &str,
) -> std::io::Result<(String, String)> {
let id = if id.is_empty() {
"".to_string()
} else {
format!("{id}_")
};
let f1 = format!("{id}{FROM_FILE}_{bytes}.txt");
let f2 = format!("{id}{TO_FILE}_{bytes}.txt");
let from_path = dir.join(f1);
let to_path = dir.join(f2);
generate_file_bytes(&from_path, &to_path, bytes, num_differences)?;
Ok((
from_path.to_string_lossy().to_string(),
to_path.to_string_lossy().to_string(),
))
}
fn generate_file_bytes(
from_name: &Path,
to_name: &Path,
bytes: u64,
num_differences: u64,
) -> std::io::Result<()> {
let file_from = File::create(from_name)?;
let file_to = File::create(to_name)?;
// for int division, lines will be smaller than requested bytes
let n_lines = bytes / LINE_LENGTH as u64;
let change_every_n_lines = if num_differences == 0 {
0
} else {
let c = n_lines / num_differences;
if c == 0 {
1
} else {
c
}
};
// Use a larger 128KB buffer for massive files
let mut writer_from = BufWriter::with_capacity(128 * 1024, file_from);
let mut writer_to = BufWriter::with_capacity(128 * 1024, file_to);
let mut rng = rand::rng();
// Each line: (5 chars * 10 words) + 9 spaces + 1 newline = 60 bytes
let mut line_buffer = [b' '; 60];
line_buffer[59] = b'\n'; // Set the newline once at the end
for i in (0..n_lines).rev() {
// Fill only the letter positions, skipping spaces and the newline
for word_idx in 0..10 {
let start = word_idx * 6; // Each word + space block is 6 bytes
for i in 0..5 {
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
}
}
// Write the raw bytes directly to both files
writer_from.write_all(&line_buffer)?;
// make changes in the file
if num_differences == 0 {
writer_to.write_all(&line_buffer)?;
} else {
if i % change_every_n_lines == 0 && n_lines - i > 2 {
line_buffer[5] = CHANGE_CHAR;
}
writer_to.write_all(&line_buffer)?;
line_buffer[5] = b' ';
}
}
// create last line
let missing = (bytes - n_lines as u64 * LINE_LENGTH as u64) as usize;
if missing > 0 {
for word_idx in 0..10 {
let start = word_idx * 6; // Each word + space block is 6 bytes
for i in 0..5 {
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
}
}
line_buffer[missing - 1] = b'\n';
writer_from.write_all(&line_buffer[0..missing])?;
writer_to.write_all(&line_buffer[0..missing])?;
}
writer_from.flush()?;
writer_to.flush()?;
Ok(())
}
}
mod binary {
use std::process::Command;
use crate::prepare::str_to_options;
pub fn bench_binary(program: &str, cmd_args: &str) -> std::process::ExitStatus {
let args = str_to_options(cmd_args);
Command::new(program)
.args(args)
.status()
.expect("Failed to execute binary")
}
}
fn main() {
// Run registered benchmarks.
divan::main();
}
-13
View File
@@ -1,13 +0,0 @@
[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"]
-474
View File
@@ -1,474 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cc"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "const_format"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "diffutils"
version = "0.5.0"
dependencies = [
"chrono",
"const_format",
"diff",
"itoa",
"regex",
"same-file",
"unicode-width",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unified-diff-fuzz"
version = "0.0.0"
dependencies = [
"diffutils",
"libfuzzer-sys",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+2 -18
View File
@@ -9,25 +9,13 @@ edition = "2018"
cargo-fuzz = true cargo-fuzz = true
[dependencies] [dependencies]
libfuzzer-sys = "0.4.7" libfuzzer-sys = "0.4"
diffutils = { path = "../" } diffutils = { path = "../" }
# Prevent this from interfering with workspaces # Prevent this from interfering with workspaces
[workspace] [workspace]
members = ["."] members = ["."]
[[bin]]
name = "fuzz_cmp"
path = "fuzz_targets/fuzz_cmp.rs"
test = false
doc = false
[[bin]]
name = "fuzz_cmp_args"
path = "fuzz_targets/fuzz_cmp_args.rs"
test = false
doc = false
[[bin]] [[bin]]
name = "fuzz_patch" name = "fuzz_patch"
path = "fuzz_targets/fuzz_patch.rs" path = "fuzz_targets/fuzz_patch.rs"
@@ -47,8 +35,4 @@ path = "fuzz_targets/fuzz_ed.rs"
test = false test = false
doc = false doc = false
[[bin]]
name = "fuzz_side"
path = "fuzz_targets/fuzz_side.rs"
test = false
doc = false
-36
View File
@@ -1,36 +0,0 @@
"-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
@@ -1,51 +0,0 @@
#![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
@@ -1,23 +0,0 @@
#![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
});
-42
View File
@@ -1,42 +0,0 @@
#![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();
});
-1211
View File
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -439,26 +439,26 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/ab.diff")) File::create(&format!("{target}/ab.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg("--context") .arg("--context")
.stdin(File::open(format!("{target}/ab.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef")).unwrap(); let alef = fs::read(&format!("{target}/alef")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -520,26 +520,26 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/ab_.diff")) File::create(&format!("{target}/ab_.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg("--context") .arg("--context")
.stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap(); let alef = fs::read(&format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -604,26 +604,26 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/abx.diff")) File::create(&format!("{target}/abx.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefx")).unwrap(); let mut fa = File::create(&format!("{target}/alefx")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betx")).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg("--context") .arg("--context")
.stdin(File::open(format!("{target}/abx.diff")).unwrap()) .stdin(File::open(&format!("{target}/abx.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefx")).unwrap(); let alef = fs::read(&format!("{target}/alefx")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -691,26 +691,26 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/abr.diff")) File::create(&format!("{target}/abr.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg("--context") .arg("--context")
.stdin(File::open(format!("{target}/abr.diff")).unwrap()) .stdin(File::open(&format!("{target}/abr.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap(); let alef = fs::read(&format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
-102
View File
@@ -1,102 +0,0 @@
// 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)
}
}
+21 -18
View File
@@ -178,6 +178,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_permutations() { fn test_permutations() {
let target = "target/ed-diff/"; let target = "target/ed-diff/";
// test all possible six-line files. // test all possible six-line files.
@@ -225,13 +226,13 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap(); let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap();
File::create(format!("{target}/ab.ed")) File::create(&format!("{target}/ab.ed"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
@@ -239,14 +240,14 @@ mod tests {
{ {
use std::process::Command; use std::process::Command;
let output = Command::new("ed") let output = Command::new("ed")
.arg(format!("{target}/alef")) .arg(&format!("{target}/alef"))
.stdin(File::open(format!("{target}/ab.ed")).unwrap()) .stdin(File::open(&format!("{target}/ab.ed")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alef")).unwrap(); let alef = std::fs::read(&format!("{target}/alef")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -258,6 +259,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_permutations_empty_lines() { fn test_permutations_empty_lines() {
let target = "target/ed-diff/"; let target = "target/ed-diff/";
// test all possible six-line files with missing newlines. // test all possible six-line files with missing newlines.
@@ -299,13 +301,13 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap(); let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap();
File::create(format!("{target}/ab_.ed")) File::create(&format!("{target}/ab_.ed"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
@@ -313,14 +315,14 @@ mod tests {
{ {
use std::process::Command; use std::process::Command;
let output = Command::new("ed") let output = Command::new("ed")
.arg(format!("{target}/alef_")) .arg(&format!("{target}/alef_"))
.stdin(File::open(format!("{target}/ab_.ed")).unwrap()) .stdin(File::open(&format!("{target}/ab_.ed")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alef_")).unwrap(); let alef = std::fs::read(&format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -332,6 +334,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(not(windows))]
fn test_permutations_reverse() { fn test_permutations_reverse() {
let target = "target/ed-diff/"; let target = "target/ed-diff/";
// test all possible six-line files. // test all possible six-line files.
@@ -379,13 +382,13 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap(); let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
File::create(format!("{target}/abr.ed")) File::create(&format!("{target}/abr.ed"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
@@ -393,14 +396,14 @@ mod tests {
{ {
use std::process::Command; use std::process::Command;
let output = Command::new("ed") let output = Command::new("ed")
.arg(format!("{target}/alefr")) .arg(&format!("{target}/alefr"))
.stdin(File::open(format!("{target}/abr.ed")).unwrap()) .stdin(File::open(&format!("{target}/abr.ed")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = std::fs::read(format!("{target}/alefr")).unwrap(); let alef = std::fs::read(&format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
-3
View File
@@ -1,10 +1,8 @@
pub mod cmp;
pub mod context_diff; pub mod context_diff;
pub mod ed_diff; pub mod ed_diff;
pub mod macros; pub mod macros;
pub mod normal_diff; pub mod normal_diff;
pub mod params; pub mod params;
pub mod side_diff;
pub mod unified_diff; pub mod unified_diff;
pub mod utils; pub mod utils;
@@ -12,5 +10,4 @@ pub mod utils;
pub use context_diff::diff as context_diff; pub use context_diff::diff as context_diff;
pub use ed_diff::diff as ed_diff; pub use ed_diff::diff as ed_diff;
pub use normal_diff::diff as normal_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; pub use unified_diff::diff as unified_diff;
+1 -1
View File
@@ -2,7 +2,7 @@
// considering datetime varitations // considering datetime varitations
// //
// It replaces the modification time in the actual diff // It replaces the modification time in the actual diff
// with placeholder "TIMESTAMP" and then asserts the equality // with placeholer "TIMESTAMP" and then asserts the equality
// //
// For eg. // For eg.
// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n // let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n
+78 -63
View File
@@ -3,79 +3,94 @@
// For the full copyright and license information, please view the LICENSE-* // For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code. // files that was distributed with this source code.
use std::{ use crate::params::{parse_params, Format};
env::ArgsOs, use std::env;
ffi::{OsStr, OsString}, use std::ffi::OsString;
iter::Peekable, use std::fs;
path::{Path, PathBuf}, use std::io::{self, Read, Write};
process::ExitCode, use std::process::{exit, ExitCode};
};
mod cmp;
mod context_diff; mod context_diff;
mod diff;
mod ed_diff; mod ed_diff;
mod macros; mod macros;
mod normal_diff; mod normal_diff;
mod params; mod params;
mod side_diff;
mod unified_diff; mod unified_diff;
mod utils; mod utils;
/// # Panics // Exit codes are documented at
/// Panics if the binary path cannot be determined // https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
fn binary_path(args: &mut Peekable<ArgsOs>) -> PathBuf { // An exit status of 0 means no differences were found,
match args.peek() { // 1 means some differences were found,
Some(ref s) if !s.is_empty() => PathBuf::from(s), // and 2 means trouble.
_ => 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 { fn main() -> ExitCode {
let mut args = std::env::args_os().peekable(); let opts = env::args_os();
let params = parse_params(opts).unwrap_or_else(|error| {
let exe_path = binary_path(&mut args); eprintln!("{error}");
let exe_name = name(&exe_path); exit(2);
});
let util_name = if exe_name == "diffutils" { // if from and to are the same file, no need to perform any comparison
// Discard the item we peeked. let maybe_report_identical_files = || {
let _ = args.next(); if params.report_identical_files {
println!(
args.peek() "Files {} and {} are identical",
.cloned() params.from.to_string_lossy(),
.unwrap_or_else(|| second_arg_error(exe_name)) params.to.to_string_lossy(),
} 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), };
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 from_content = match read_file_contents(&params.from) {
Ok(from_content) => from_content,
Err(e) => {
eprintln!("Failed to read from-file: {e}");
return ExitCode::from(2);
}
};
let to_content = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e) => {
eprintln!("Failed to read to-file: {e}");
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);
}),
};
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)
} }
} }
+24 -24
View File
@@ -275,26 +275,26 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default()); let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/ab.diff")) File::create(&format!("{target}/ab.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg(format!("{target}/alef")) .arg(&format!("{target}/alef"))
.stdin(File::open(format!("{target}/ab.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef")).unwrap(); let alef = fs::read(&format!("{target}/alef")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -367,27 +367,27 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default()); let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/abn.diff")) File::create(&format!("{target}/abn.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefn")).unwrap(); let mut fa = File::create(&format!("{target}/alefn")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betn")).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg("--normal") .arg("--normal")
.arg(format!("{target}/alefn")) .arg(&format!("{target}/alefn"))
.stdin(File::open(format!("{target}/abn.diff")).unwrap()) .stdin(File::open(&format!("{target}/abn.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefn")).unwrap(); let alef = fs::read(&format!("{target}/alefn")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -441,26 +441,26 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default()); let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/ab_.diff")) File::create(&format!("{target}/ab_.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg(format!("{target}/alef_")) .arg(&format!("{target}/alef_"))
.stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap(); let alef = fs::read(&format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -519,26 +519,26 @@ mod tests {
// This test diff is intentionally reversed. // This test diff is intentionally reversed.
// We want it to turn the alef into bet. // We want it to turn the alef into bet.
let diff = diff(&alef, &bet, &Params::default()); let diff = diff(&alef, &bet, &Params::default());
File::create(format!("{target}/abr.diff")) File::create(&format!("{target}/abr.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.arg(format!("{target}/alefr")) .arg(&format!("{target}/alefr"))
.stdin(File::open(format!("{target}/abr.diff")).unwrap()) .stdin(File::open(&format!("{target}/abr.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap(); let alef = fs::read(&format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
+102 -505
View File
@@ -1,6 +1,4 @@
use std::ffi::OsString; use std::ffi::{OsStr, OsString};
use std::iter::Peekable;
use std::path::PathBuf;
use regex::Regex; use regex::Regex;
@@ -11,12 +9,21 @@ pub enum Format {
Unified, Unified,
Context, Context,
Ed, Ed,
SideBySide, }
#[cfg(unix)]
fn osstr_bytes(osstr: &OsStr) -> &[u8] {
use std::os::unix::ffi::OsStrExt;
osstr.as_bytes()
}
#[cfg(not(unix))]
fn osstr_bytes(osstr: &OsStr) -> Vec<u8> {
osstr.to_string_lossy().bytes().collect()
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Params { pub struct Params {
pub executable: OsString,
pub from: OsString, pub from: OsString,
pub to: OsString, pub to: OsString,
pub format: Format, pub format: Format,
@@ -25,13 +32,11 @@ pub struct Params {
pub brief: bool, pub brief: bool,
pub expand_tabs: bool, pub expand_tabs: bool,
pub tabsize: usize, pub tabsize: usize,
pub width: usize,
} }
impl Default for Params { impl Default for Params {
fn default() -> Self { fn default() -> Self {
Self { Self {
executable: OsString::default(),
from: OsString::default(), from: OsString::default(),
to: OsString::default(), to: OsString::default(),
format: Format::default(), format: Format::default(),
@@ -40,29 +45,23 @@ impl Default for Params {
brief: false, brief: false,
expand_tabs: false, expand_tabs: false,
tabsize: 8, tabsize: 8,
width: 130,
} }
} }
} }
pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> { pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
let mut opts = opts.into_iter();
// parse CLI // parse CLI
let Some(executable) = opts.next() else { let Some(exe) = opts.next() else {
return Err("Usage: <exe> <from> <to>".to_string()); return Err("Usage: <exe> <from> <to>".to_string());
}; };
let mut params = Params { let mut params = Params::default();
executable,
..Default::default()
};
let mut from = None; let mut from = None;
let mut to = None; let mut to = None;
let mut format = None; let mut format = None;
let mut context = None;
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap(); 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() { while let Some(param) = opts.next() {
let next_param = opts.peek();
if param == "--" { if param == "--" {
break; break;
} }
@@ -72,10 +71,7 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
} else if to.is_none() { } else if to.is_none() {
to = Some(param); to = Some(param);
} else { } else {
return Err(format!( return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
} }
continue; continue;
} }
@@ -91,48 +87,6 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
params.expand_tabs = true; params.expand_tabs = true;
continue; 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()) { if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
// Because param matches the regular expression, // Because param matches the regular expression,
// it is safe to assume it is valid UTF-8. // it is safe to assume it is valid UTF-8.
@@ -144,68 +98,70 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
.unwrap() .unwrap()
.as_str(); .as_str();
params.tabsize = match tabsize_str.parse::<usize>() { params.tabsize = match tabsize_str.parse::<usize>() {
Ok(num) => { Ok(num) => num,
if num == 0 {
return Err("invalid tabsize «0»".to_string());
}
num
}
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")), Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
}; };
continue; continue;
} }
match match_context_diff_params(&param, next_param, format) { let p = osstr_bytes(&param);
Ok(DiffStyleMatch { if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') {
is_match, let mut bit = p[1..].iter().copied().peekable();
context_count, // Can't use a for loop because `diff -30u` is supposed to make a diff
next_param_consumed, // with 30 lines of context.
}) => { while let Some(b) = bit.next() {
if is_match { match b {
format = Some(Format::Context); b'0'..=b'9' => {
if context_count.is_some() { params.context_count = (b - b'0') as usize;
context = context_count; while let Some(b'0'..=b'9') = bit.peek() {
params.context_count *= 10;
params.context_count += (bit.next().unwrap() - b'0') as usize;
}
} }
if next_param_consumed { b'c' => {
opts.next(); if format.is_some() && format != Some(Format::Context) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Context);
} }
continue; b'e' => {
if format.is_some() && format != Some(Format::Ed) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Ed);
}
b'u' => {
if format.is_some() && format != Some(Format::Unified) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Unified);
}
b'U' => {
if format.is_some() && format != Some(Format::Unified) {
return Err("Conflicting output style options".to_string());
}
format = Some(Format::Unified);
let context_count_maybe = if bit.peek().is_some() {
String::from_utf8(bit.collect::<Vec<u8>>()).ok()
} else {
opts.next().map(|x| x.to_string_lossy().into_owned())
};
if let Some(context_count_maybe) =
context_count_maybe.and_then(|x| x.parse().ok())
{
params.context_count = context_count_maybe;
break;
}
return Err("Invalid context count".to_string());
}
_ => return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))),
} }
} }
Err(error) => return Err(error), } else if from.is_none() {
}
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!("unrecognized option: {param:?}"));
}
if from.is_none() {
from = Some(param); from = Some(param);
} else if to.is_none() { } else if to.is_none() {
to = Some(param); to = Some(param);
} else { } else {
return Err(format!( return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
} }
} }
params.from = if let Some(from) = from { params.from = if let Some(from) = from {
@@ -213,136 +169,19 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
} else if let Some(param) = opts.next() { } else if let Some(param) = opts.next() {
param param
} else { } else {
return Err(format!( return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
"Usage: {} <from> <to>",
params.executable.to_string_lossy()
));
}; };
params.to = if let Some(to) = to { params.to = if let Some(to) = to {
to to
} else if let Some(param) = opts.next() { } else if let Some(param) = opts.next() {
param param
} else { } else {
return Err(format!( return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
"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()); params.format = format.unwrap_or(Format::default());
if let Some(context_count) = context {
params.context_count = context_count;
}
Ok(params) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -353,176 +192,29 @@ mod tests {
fn basics() { fn basics() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
[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] #[test]
fn basics_ed() { fn basics_ed() {
for arg in ["-e", "--ed"] { assert_eq!(
assert_eq!( Ok(Params {
Ok(Params { from: os("foo"),
executable: os("diff"), to: os("bar"),
from: os("foo"), format: Format::Ed,
to: os("bar"), ..Default::default()
format: Format::Ed, }),
..Default::default() parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
}), );
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] #[test]
fn context_count() { fn context_count() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
format: Format::Unified, format: Format::Unified,
@@ -533,12 +225,10 @@ mod tests {
[os("diff"), os("-u54"), os("foo"), os("bar")] [os("diff"), os("-u54"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
format: Format::Unified, format: Format::Unified,
@@ -549,12 +239,10 @@ mod tests {
[os("diff"), os("-U54"), os("foo"), os("bar")] [os("diff"), os("-U54"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
format: Format::Unified, format: Format::Unified,
@@ -565,12 +253,10 @@ mod tests {
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")] [os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
format: Format::Context, format: Format::Context,
@@ -581,7 +267,6 @@ mod tests {
[os("diff"), os("-c54"), os("foo"), os("bar")] [os("diff"), os("-c54"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
} }
@@ -589,36 +274,23 @@ mod tests {
fn report_identical_files() { fn report_identical_files() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
report_identical_files: true, report_identical_files: true,
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("-s"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
report_identical_files: true, report_identical_files: true,
@@ -633,7 +305,6 @@ mod tests {
] ]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
} }
@@ -641,36 +312,23 @@ mod tests {
fn brief() { fn brief() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
brief: true, brief: true,
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("-q"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
brief: true, brief: true,
@@ -680,7 +338,6 @@ mod tests {
[os("diff"), os("--brief"), os("foo"), os("bar"),] [os("diff"), os("--brief"), os("foo"), os("bar"),]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
} }
@@ -688,22 +345,15 @@ mod tests {
fn expand_tabs() { fn expand_tabs() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
for option in ["-t", "--expand-tabs"] { for option in ["-t", "--expand-tabs"] {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
expand_tabs: true, expand_tabs: true,
@@ -713,7 +363,6 @@ mod tests {
[os("diff"), os(option), os("foo"), os("bar")] [os("diff"), os(option), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
} }
@@ -722,36 +371,27 @@ mod tests {
fn tabsize() { fn tabsize() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
[os("diff"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
tabsize: 1, tabsize: 0,
..Default::default() ..Default::default()
}), }),
parse_params( parse_params(
[os("diff"), os("--tabsize=1"), os("foo"), os("bar")] [os("diff"), os("--tabsize=0"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("bar"), to: os("bar"),
tabsize: 42, tabsize: 42,
@@ -761,42 +401,36 @@ mod tests {
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")] [os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
); );
assert!(parse_params( assert!(parse_params(
[os("diff"), os("--tabsize"), os("foo"), os("bar")] [os("diff"), os("--tabsize"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
assert!(parse_params( assert!(parse_params(
[os("diff"), os("--tabsize="), os("foo"), os("bar")] [os("diff"), os("--tabsize="), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
assert!(parse_params( assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
assert!(parse_params( assert!(parse_params(
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")] [os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
assert!(parse_params( assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
assert!(parse_params( assert!(parse_params(
@@ -808,7 +442,6 @@ mod tests {
] ]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
} }
@@ -816,102 +449,66 @@ mod tests {
fn double_dash() { fn double_dash() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("-g"), from: os("-g"),
to: os("-h"), to: os("-h"),
..Default::default() ..Default::default()
}), }),
parse_params( parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned())
[os("diff"), os("--"), os("-g"), os("-h")]
.iter()
.cloned()
.peekable()
)
); );
} }
#[test] #[test]
fn default_to_stdin() { fn default_to_stdin() {
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("foo"), from: os("foo"),
to: os("-"), to: os("-"),
..Default::default() ..Default::default()
}), }),
parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable()) parse_params([os("diff"), os("foo"), os("-")].iter().cloned())
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("-"), from: os("-"),
to: os("bar"), to: os("bar"),
..Default::default() ..Default::default()
}), }),
parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable()) parse_params([os("diff"), os("-"), os("bar")].iter().cloned())
); );
assert_eq!( assert_eq!(
Ok(Params { Ok(Params {
executable: os("diff"),
from: os("-"), from: os("-"),
to: os("-"), to: os("-"),
..Default::default() ..Default::default()
}), }),
parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable()) parse_params([os("diff"), os("-"), os("-")].iter().cloned())
); );
assert!(parse_params( assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err());
[os("diff"), os("foo"), os("bar"), os("-")] assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err());
.iter()
.cloned()
.peekable()
)
.is_err());
assert!(parse_params(
[os("diff"), os("-"), os("-"), os("-")]
.iter()
.cloned()
.peekable()
)
.is_err());
} }
#[test] #[test]
fn missing_arguments() { fn missing_arguments() {
assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err()); assert!(parse_params([os("diff")].iter().cloned()).is_err());
assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err()); assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err());
} }
#[test] #[test]
fn unknown_argument() { fn unknown_argument() {
assert!(parse_params(
[os("diff"), os("-g"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
)
.is_err());
assert!( assert!(
parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err() parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err()
); );
assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err()); assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err());
assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err());
} }
#[test] #[test]
fn empty() { fn empty() {
assert!(parse_params([].iter().cloned().peekable()).is_err()); assert!(parse_params([].iter().cloned()).is_err());
} }
#[test] #[test]
fn conflicting_output_styles() { fn conflicting_output_styles() {
for (arg1, arg2) in [ for (arg1, arg2) in [("-u", "-c"), ("-u", "-e"), ("-c", "-u"), ("-c", "-U42")] {
("-u", "-c"),
("-u", "-e"),
("-c", "-u"),
("-c", "-U42"),
("-u", "--normal"),
("--normal", "-e"),
("--context", "--normal"),
] {
assert!(parse_params( assert!(parse_params(
[os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")]
.iter() .iter()
.cloned() .cloned()
.peekable()
) )
.is_err()); .is_err());
} }
-1263
View File
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -466,13 +466,13 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/ab.diff")) File::create(&format!("{target}/ab.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
@@ -494,13 +494,13 @@ mod tests {
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.stdin(File::open(format!("{target}/ab.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
println!("{}", String::from_utf8_lossy(&output.stdout)); println!("{}", String::from_utf8_lossy(&output.stdout));
println!("{}", String::from_utf8_lossy(&output.stderr)); println!("{}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
let alef = fs::read(format!("{target}/alef")).unwrap(); let alef = fs::read(&format!("{target}/alef")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -582,25 +582,25 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/abn.diff")) File::create(&format!("{target}/abn.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefn")).unwrap(); let mut fa = File::create(&format!("{target}/alefn")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betn")).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.stdin(File::open(format!("{target}/abn.diff")).unwrap()) .stdin(File::open(&format!("{target}/abn.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefn")).unwrap(); let alef = fs::read(&format!("{target}/alefn")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -678,25 +678,25 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/ab_.diff")) File::create(&format!("{target}/ab_.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alef_")).unwrap(); let alef = fs::read(&format!("{target}/alef_")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -759,25 +759,25 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/abx.diff")) File::create(&format!("{target}/abx.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefx")).unwrap(); let mut fa = File::create(&format!("{target}/alefx")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betx")).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.stdin(File::open(format!("{target}/abx.diff")).unwrap()) .stdin(File::open(&format!("{target}/abx.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefx")).unwrap(); let alef = fs::read(&format!("{target}/alefx")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
@@ -845,25 +845,25 @@ mod tests {
..Default::default() ..Default::default()
}, },
); );
File::create(format!("{target}/abr.diff")) File::create(&format!("{target}/abr.diff"))
.unwrap() .unwrap()
.write_all(&diff) .write_all(&diff)
.unwrap(); .unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap(); fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap(); fb.write_all(&bet[..]).unwrap();
let _ = fa; let _ = fa;
let _ = fb; let _ = fb;
let output = Command::new("patch") let output = Command::new("patch")
.arg("-p0") .arg("-p0")
.stdin(File::open(format!("{target}/abr.diff")).unwrap()) .stdin(File::open(&format!("{target}/abr.diff")).unwrap())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success(), "{output:?}"); assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr)); //println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap(); let alef = fs::read(&format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet); assert_eq!(alef, bet);
} }
} }
+6 -34
View File
@@ -3,8 +3,8 @@
// For the full copyright and license information, please view the LICENSE-* // For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code. // files that was distributed with this source code.
use regex::Regex; use std::io::Write;
use std::{ffi::OsString, io::Write};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
/// Replace tabs by spaces in the input line. /// Replace tabs by spaces in the input line.
@@ -71,33 +71,6 @@ pub fn get_modification_time(file_path: &str) -> String {
modification_time 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -128,11 +101,10 @@ mod tests {
// Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining // Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms // 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. // it is displayed as a single emoji and should have a print size of 2 columns,
// Terminal emulators tend to not support this, and display the two emojis // but terminal emulators tend to not support this, and display the two emojis
// side by side, thus accounting for a print size of 4 columns, but the // side by side, thus accounting for a print size of 4 columns.
// unicode_width crate reports a correct size of 2. assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz");
assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz");
} }
#[test] #[test]
+199 -855
View File
File diff suppressed because it is too large Load Diff
+26 -39
View File
@@ -19,9 +19,9 @@
# By default it expects a release build of the diffutils binary, but a # By default it expects a release build of the diffutils binary, but a
# different build profile can be specified as an argument # different build profile can be specified as an argument
# (e.g. 'dev' or 'test'). # (e.g. 'dev' or 'test').
# Unless overridden by the $TESTS environment variable, all tests in the test # Unless overriden by the $TESTS environment variable, all tests in the test
# suite will be run. Tests targeting a command that is not yet implemented # suite will be run. Tests targeting a command that is not yet implemented
# (e.g. diff3 or sdiff) are skipped. # (e.g. cmp, diff3 or sdiff) are skipped.
scriptpath=$(dirname "$(readlink -f "$0")") scriptpath=$(dirname "$(readlink -f "$0")")
rev=$(git rev-parse HEAD) rev=$(git rev-parse HEAD)
@@ -57,13 +57,8 @@ upstreamrev=$(git rev-parse HEAD)
mkdir src mkdir src
cd src cd src
ln -s "$binary" diff ln -s "$binary" diff
ln -s "$binary" cmp
cd ../tests 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" ]] if [[ -n "$TESTS" ]]
then then
tests="$TESTS" tests="$TESTS"
@@ -76,6 +71,7 @@ total=$(echo "$tests" | wc -w)
echo "Running $total tests" echo "Running $total tests"
export LC_ALL=C export LC_ALL=C
export KEEP=yes export KEEP=yes
exitcode=0
timestamp=$(date -Iseconds) timestamp=$(date -Iseconds)
urlroot="$gitserver/cgit/diffutils.git/tree/tests/" urlroot="$gitserver/cgit/diffutils.git/tree/tests/"
passed=0 passed=0
@@ -86,43 +82,35 @@ for test in $tests
do do
result="FAIL" result="FAIL"
url="$urlroot$test?id=$upstreamrev" url="$urlroot$test?id=$upstreamrev"
# Run only the tests that invoke `diff` or `cmp`, # Run only the tests that invoke `diff`,
# because other binaries aren't implemented yet # because other binaries aren't implemented yet
if ! grep -E -s -q "(diff3|sdiff)" "$test" if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test"
then then
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1
if [[ $? = 77 ]] json+="{\"test\":\"$test\",\"result\":\"$result\","
then json+="\"url\":\"$url\","
result="SKIP" json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\","
else json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\","
json+="{\"test\":\"$test\",\"result\":\"$result\"," json+="\"files\":{"
json+="\"url\":\"$url\"," cd gt-$test.*
json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\"," # Note: this doesn't include the contents of subdirectories,
json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\"," # but there isn't much value added in doing so
json+="\"files\":{" for file in *
cd gt-$test.* do
# Note: this doesn't include the contents of subdirectories, [[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\","
# but there isn't much value added in doing so done
for file in * json="${json%,}}},"
do cd - > /dev/null
[[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\"," [[ "$result" = "PASS" ]] && (( passed++ ))
done [[ "$result" = "FAIL" ]] && (( failed++ ))
json="${json%,}}},"
cd - > /dev/null
[[ "$result" = "PASS" ]] && (( passed++ ))
[[ "$result" = "FAIL" ]] && (( failed++ ))
fi
else else
result="SKIP" result="SKIP"
(( skipped++ ))
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
fi fi
color=2 # green color=2 # green
[[ "$result" = "FAIL" ]] && color=1 # red [[ "$result" = "FAIL" ]] && color=1 # red
if [[ $result = "SKIP" ]] [[ "$result" = "SKIP" ]] && color=3 # yellow
then
(( skipped++ ))
json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"},"
color=3 # yellow
fi
printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test" printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test"
done done
echo "" echo ""
@@ -150,5 +138,4 @@ resultsfile="test-results.json"
echo "$json" | jq > "$resultsfile" echo "$json" | jq > "$resultsfile"
echo "Results written to $scriptpath/$resultsfile" echo "Results written to $scriptpath/$resultsfile"
(( failed > 0 )) && exit 1 exit $exitcode
exit 0