mirror of
https://github.com/uutils/diffutils.git
synced 2026-06-28 22:58:30 -04:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf13d528be | |||
| 329a7e1f4a | |||
| e5de3cd93e | |||
| 0e14b37e38 | |||
| 3c1176082e | |||
| 865f97c78d | |||
| f5b65a5720 | |||
| 1372c5386c | |||
| d891e1034d | |||
| fdc35f6b8e | |||
| 6648963df1 | |||
| a10ef621c8 | |||
| 7939749338 | |||
| 7b3001f1ff | |||
| 416a4be06c | |||
| 9084134f04 | |||
| f42fc82f18 | |||
| 0b2505d249 | |||
| 045435b803 | |||
| 12f3f16792 | |||
| e9e69b86db | |||
| 029d747e14 | |||
| e38055e5b2 | |||
| 61cfe6eec4 | |||
| ea9376aaaf | |||
| 582259a867 | |||
| da05a5254b | |||
| a4f7642d7a | |||
| e72ea046b7 | |||
| 2e84164d2f |
@@ -0,0 +1,134 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Basic CI
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: cargo check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
name: cargo test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy -- -D warnings
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- { os: ubuntu-latest , features: unix }
|
||||
- { os: macos-latest , features: macos }
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
## VARs setup
|
||||
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
|
||||
# toolchain
|
||||
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
|
||||
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
|
||||
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
|
||||
# * use requested TOOLCHAIN if specified
|
||||
if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi
|
||||
outputs TOOLCHAIN
|
||||
# target-specific options
|
||||
# * CARGO_FEATURES_OPTION
|
||||
CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage
|
||||
# * CODECOV_FLAGS
|
||||
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
||||
outputs CODECOV_FLAGS
|
||||
|
||||
- name: rust toolchain ~ install
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Test
|
||||
run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: ""
|
||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||
- name: "`grcov` ~ install"
|
||||
id: build_grcov
|
||||
shell: bash
|
||||
run: |
|
||||
git clone https://github.com/mozilla/grcov.git ~/grcov/
|
||||
cd ~/grcov
|
||||
# Hardcode the version of crossbeam-epoch. See
|
||||
# https://github.com/uutils/coreutils/issues/3680
|
||||
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
|
||||
cargo install --path .
|
||||
cd -
|
||||
# Uncomment when the upstream issue
|
||||
# https://github.com/mozilla/grcov/issues/849 is fixed
|
||||
# uses: actions-rs/install@v0.1
|
||||
# with:
|
||||
# crate: grcov
|
||||
# version: latest
|
||||
# use-tool-cache: false
|
||||
- name: Generate coverage data (via `grcov`)
|
||||
id: coverage
|
||||
shell: bash
|
||||
run: |
|
||||
## Generate coverage data
|
||||
COVERAGE_REPORT_DIR="target/debug"
|
||||
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
|
||||
mkdir -p "${COVERAGE_REPORT_DIR}"
|
||||
# display coverage files
|
||||
grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
|
||||
# generate coverage report
|
||||
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
|
||||
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||
- name: Upload coverage results (to Codecov.io)
|
||||
uses: codecov/codecov-action@v3
|
||||
# if: steps.vars.outputs.HAS_CODECOV_TOKEN
|
||||
with:
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
*.swp
|
||||
|
||||
Generated
+33
@@ -0,0 +1,33 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "diffutils"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
+14
-11
@@ -1,18 +1,21 @@
|
||||
[package]
|
||||
name = "unified-diff"
|
||||
version = "0.2.1"
|
||||
authors = [
|
||||
"Michael Howell <michael@notriddle.com>",
|
||||
"The Rust Project Developers"
|
||||
]
|
||||
edition = "2018"
|
||||
description = "An implementation of the GNU unified diff format"
|
||||
name = "diffutils"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
description = "A CLI app for generating diff files"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/notriddle/rust-unified-diff"
|
||||
exclude = [ "fuzz" ]
|
||||
repository = "https://github.com/uutils/diffutils"
|
||||
|
||||
[lib]
|
||||
name = "diffutilslib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "unified-diff"
|
||||
name = "diffutils"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
diff = "0.1.10"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
A GNU unified diff generator. Oracle tested against GNU patch 2.7.6
|
||||
The goal of this package is to be a dropped 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,
|
||||
but it implements a different format.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
~/unified-diff$ cargo run Cargo.lock Cargo.toml
|
||||
~/diffutils$ cargo run -- diff -u3 Cargo.lock Cargo.toml
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
|
||||
Running `target/debug/unified-diff Cargo.lock Cargo.toml`
|
||||
--- Cargo.lock
|
||||
+++ Cargo.toml
|
||||
@@ -1,14 +1,14 @@
|
||||
Running `target/debug/diff -u3 Cargo.lock Cargo.toml`
|
||||
--- Cargo.lock
|
||||
+++ Cargo.toml
|
||||
@@ -1,39 +1,7 @@
|
||||
-# This file is automatically @generated by Cargo.
|
||||
-# It is not intended for manual editing.
|
||||
-version = 3
|
||||
-
|
||||
-[[package]]
|
||||
-name = "context-diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "diff 0.1.12",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "context-diff",
|
||||
- "normal-diff",
|
||||
- "unified-diff",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "diff"
|
||||
-version = "0.1.12"
|
||||
@@ -19,23 +37,22 @@ but it implements a different format.
|
||||
-checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
-
|
||||
-[[package]]
|
||||
+[package]
|
||||
name = "unified-diff"
|
||||
version = "0.1.0"
|
||||
-name = "normal-diff"
|
||||
-version = "0.1.0"
|
||||
-dependencies = [
|
||||
- "diff",
|
||||
+authors = [
|
||||
+ "Michael Howell <michael@notriddle.com>",
|
||||
+ "The Rust Project Developers"
|
||||
- "diff 0.1.12",
|
||||
-]
|
||||
-
|
||||
-[[package]]
|
||||
-name = "unified-diff"
|
||||
-version = "0.3.0"
|
||||
-dependencies = [
|
||||
- "diff 0.1.12",
|
||||
+[workspace]
|
||||
+members = [
|
||||
+ "lib/unified-diff",
|
||||
+ "lib/context-diff",
|
||||
+ "lib/normal-diff",
|
||||
+ "bin/diff",
|
||||
]
|
||||
+edition = "2018"
|
||||
+
|
||||
+[[bin]]
|
||||
+name = "unified-diff"
|
||||
+
|
||||
+[dependencies]
|
||||
+diff = "0.1.10"
|
||||
~/unified-diff$ rustup override set nightly
|
||||
~/unified-diff$ cargo fuzz run fuzz_patch
|
||||
```
|
||||
|
||||
|
||||
+15
-4
@@ -2,7 +2,6 @@
|
||||
[package]
|
||||
name = "unified-diff-fuzz"
|
||||
version = "0.0.0"
|
||||
authors = ["Automatically generated"]
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
@@ -11,9 +10,7 @@ cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.3"
|
||||
|
||||
[dependencies.unified-diff]
|
||||
path = ".."
|
||||
diffutils = { path = "../" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
@@ -25,3 +22,17 @@ path = "fuzz_targets/fuzz_patch.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_normal"
|
||||
path = "fuzz_targets/fuzz_normal.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_ed"
|
||||
path = "fuzz_targets/fuzz_ed.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{ed_diff, normal_diff, unified_diff};
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||
let (mut from, mut to) = x;
|
||||
from.push(b'\n');
|
||||
to.push(b'\n');
|
||||
if let Ok(s) = String::from_utf8(from.clone()) {
|
||||
if !s.is_ascii() {
|
||||
return;
|
||||
}
|
||||
if s.find(|x| x < ' ' && x != '\n').is_some() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if let Ok(s) = String::from_utf8(to.clone()) {
|
||||
if !s.is_ascii() {
|
||||
return;
|
||||
}
|
||||
if s.find(|x| x < ' ' && x != '\n').is_some() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let diff = ed_diff::diff_w(&from, &to, "target/fuzz.file").unwrap();
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.file.expected")
|
||||
.unwrap()
|
||||
.write_all(&to)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.file")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.ed")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let output = Command::new("ed")
|
||||
.arg("target/fuzz.file")
|
||||
.stdin(File::open("target/fuzz.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let result = fs::read("target/fuzz.file").unwrap();
|
||||
if result != to {
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{normal_diff, unified_diff};
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||
let (from, to) = x;
|
||||
/*if let Ok(s) = String::from_utf8(from.clone()) {
|
||||
if !s.is_ascii() { return }
|
||||
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if let Ok(s) = String::from_utf8(to.clone()) {
|
||||
if !s.is_ascii() { return }
|
||||
if s.find(|x| x < ' ' && x != '\n').is_some() { return }
|
||||
} else {
|
||||
return
|
||||
}*/
|
||||
let diff = normal_diff::diff(&from, &to);
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.file.expected")
|
||||
.unwrap()
|
||||
.write_all(&to)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.file")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
.unwrap();
|
||||
File::create("target/fuzz.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--binary")
|
||||
.arg("--fuzz=0")
|
||||
.arg("--normal")
|
||||
.arg("target/fuzz.file")
|
||||
.stdin(File::open("target/fuzz.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let result = fs::read("target/fuzz.file").unwrap();
|
||||
if result != to {
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
#![no_main]
|
||||
#[macro_use] extern crate libfuzzer_sys;
|
||||
extern crate unified_diff;
|
||||
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
use diffutils::{normal_diff, unified_diff};
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
@@ -20,7 +20,13 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
||||
} else {
|
||||
return
|
||||
}*/
|
||||
let diff = unified_diff::diff(&from, "a/fuzz.file", &to, "target/fuzz.file", context as usize);
|
||||
let diff = unified_diff::diff(
|
||||
&from,
|
||||
"a/fuzz.file",
|
||||
&to,
|
||||
"target/fuzz.file",
|
||||
context as usize,
|
||||
);
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
@@ -45,11 +51,18 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let result = fs::read("target/fuzz.file").unwrap();
|
||||
if result != to {
|
||||
panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
||||
panic!(
|
||||
"STDOUT:\n{}\nSTDERR:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,665 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
Change(Vec<u8>),
|
||||
Add(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<DiffLine>,
|
||||
pub actual: Vec<DiffLine>,
|
||||
pub expected_missing_nl: bool,
|
||||
pub actual_missing_nl: bool,
|
||||
pub expected_all_context: bool,
|
||||
pub actual_all_context: bool,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
expected_missing_nl: false,
|
||||
actual_missing_nl: false,
|
||||
expected_all_context: false,
|
||||
actual_all_context: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||
let mut lines_since_mismatch = context_size + 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(0, 0);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() - 1;
|
||||
let actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
// Rust only allows allocations to grow to isize::MAX, and this is bigger than that.
|
||||
let mut expected_lines_change_idx: usize = !0;
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
expected_lines_change_idx = mismatch.expected.len();
|
||||
mismatch.expected.push(DiffLine::Add(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.expected_missing_nl = true;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
expected_lines_change_idx = !0;
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
if let Some(DiffLine::Add(content)) =
|
||||
mismatch.expected.get_mut(expected_lines_change_idx)
|
||||
{
|
||||
let content = std::mem::take(content);
|
||||
mismatch.expected[expected_lines_change_idx] = DiffLine::Change(content);
|
||||
expected_lines_change_idx = expected_lines_change_idx.wrapping_sub(1); // if 0, becomes !0
|
||||
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||
} else {
|
||||
mismatch.actual.push(DiffLine::Add(str.to_vec()));
|
||||
}
|
||||
if line_number_actual > actual_lines_count {
|
||||
mismatch.actual_missing_nl = true;
|
||||
}
|
||||
line_number_actual += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
expected_lines_change_idx = !0;
|
||||
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||
if (line_number_actual > actual_lines_count)
|
||||
&& (line_number_expected > expected_lines_count)
|
||||
{
|
||||
if context_queue.len() < context_size {
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual_missing_nl = true;
|
||||
}
|
||||
}
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_actual > actual_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.actual_missing_nl = true;
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_expected > expected_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||
lines_since_mismatch = 0;
|
||||
} else {
|
||||
debug_assert!(context_queue.len() <= context_size);
|
||||
if context_queue.len() >= context_size {
|
||||
let _ = context_queue.pop_front();
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||
} else if context_size > 0 {
|
||||
context_queue.push_back(str);
|
||||
}
|
||||
lines_since_mismatch += 1;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push(mismatch);
|
||||
results.remove(0);
|
||||
|
||||
if results.is_empty() && expected_lines_count != actual_lines_count {
|
||||
let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len());
|
||||
// empty diff and only expected lines has a missing line at end
|
||||
if expected_lines_count != expected_lines.len() {
|
||||
mismatch.expected.push(DiffLine::Change(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(DiffLine::Change(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
results.push(mismatch);
|
||||
} else if actual_lines_count != actual_lines.len() {
|
||||
mismatch.expected.push(DiffLine::Change(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.actual.push(DiffLine::Change(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.actual_missing_nl = true;
|
||||
results.push(mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
// hunks with pure context lines get truncated to empty
|
||||
for mismatch in &mut results {
|
||||
if !mismatch
|
||||
.expected
|
||||
.iter()
|
||||
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||
{
|
||||
mismatch.expected_all_context = true;
|
||||
}
|
||||
if !mismatch
|
||||
.actual
|
||||
.iter()
|
||||
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||
{
|
||||
mismatch.actual_all_context = true;
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
actual: &[u8],
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size);
|
||||
if diff_results.is_empty() {
|
||||
return Vec::new();
|
||||
};
|
||||
for result in diff_results {
|
||||
let mut line_number_expected = result.line_number_expected;
|
||||
let mut line_number_actual = result.line_number_actual;
|
||||
let mut expected_count = result.expected.len();
|
||||
let mut actual_count = result.actual.len();
|
||||
if expected_count == 0 {
|
||||
line_number_expected -= 1;
|
||||
expected_count = 1;
|
||||
}
|
||||
if actual_count == 0 {
|
||||
line_number_actual -= 1;
|
||||
actual_count = 1;
|
||||
}
|
||||
let end_line_number_expected = expected_count + line_number_expected - 1;
|
||||
let end_line_number_actual = actual_count + line_number_actual - 1;
|
||||
let exp_start = if end_line_number_expected == line_number_expected {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{line_number_expected},")
|
||||
};
|
||||
let act_start = if end_line_number_actual == line_number_actual {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{line_number_actual},")
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"***************\n*** {exp_start}{end_line_number_expected} ****"
|
||||
)
|
||||
.expect("write to Vec is infallible");
|
||||
if !result.expected_all_context {
|
||||
for line in result.expected {
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "- ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
writeln!(output, "--- {act_start}{end_line_number_actual} ----")
|
||||
.expect("write to Vec is infallible");
|
||||
if !result.actual_all_context {
|
||||
for line in result.actual {
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "+ ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
// test all possible six-line files.
|
||||
let target = "target/context-diff/";
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(&format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_lines() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
if alef.is_empty() && bet.is_empty() {
|
||||
continue;
|
||||
};
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2);
|
||||
File::create(&format!("{target}/abx.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefx")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betx")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(&format!("{target}/abx.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefx")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<Vec<u8>>,
|
||||
pub actual: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum DiffError {
|
||||
MissingNL,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DiffError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
std::fmt::Display::fmt("No newline at end of file", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiffError> for String {
|
||||
fn from(_: DiffError) -> String {
|
||||
"No newline at end of file".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8]) -> Result<Vec<Mismatch>, DiffError> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let _expected_lines_count = expected_lines.len() - 1;
|
||||
let _actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
} else {
|
||||
return Err(DiffError::MissingNL);
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
} else {
|
||||
return Err(DiffError::MissingNL);
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if !mismatch.actual.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
}
|
||||
mismatch.expected.push(str.to_vec());
|
||||
line_number_expected += 1;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
mismatch.actual.push(str.to_vec());
|
||||
line_number_actual += 1;
|
||||
}
|
||||
diff::Result::Both(_str, _) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
} else {
|
||||
mismatch.line_number_expected = line_number_expected;
|
||||
mismatch.line_number_actual = line_number_actual;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn diff(expected: &[u8], actual: &[u8]) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = Vec::new();
|
||||
let diff_results = make_diff(expected, actual)?;
|
||||
let mut lines_offset = 0;
|
||||
for result in diff_results {
|
||||
let line_number_expected: isize = result.line_number_expected as isize + lines_offset;
|
||||
let _line_number_actual: isize = result.line_number_actual as isize + lines_offset;
|
||||
let expected_count: isize = result.expected.len() as isize;
|
||||
let actual_count: isize = result.actual.len() as isize;
|
||||
match (expected_count, actual_count) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(),
|
||||
(_, 0) => writeln!(
|
||||
&mut output,
|
||||
"{},{}d",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1
|
||||
)
|
||||
.unwrap(),
|
||||
_ => writeln!(
|
||||
&mut output,
|
||||
"{},{}c",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
lines_offset += actual_count - expected_count;
|
||||
if actual_count != 0 {
|
||||
for actual in &result.actual {
|
||||
if actual == b"." {
|
||||
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
|
||||
} else {
|
||||
output.write_all(actual).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(&mut output, ".").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = diff(expected, actual)?;
|
||||
writeln!(&mut output, "w {filename}").unwrap();
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap();
|
||||
File::create("target/ab.ed")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("ed")
|
||||
.arg(&format!("{target}/alef"))
|
||||
.stdin(File::open("target/ab.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, "target/alef_").unwrap();
|
||||
File::create("target/ab_.ed")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alef_").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("ed")
|
||||
.arg("target/alef_")
|
||||
.stdin(File::open("target/ab_.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alef_").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
|
||||
File::create("target/abr.ed")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("ed")
|
||||
.arg(&format!("{target}/alefr"))
|
||||
.stdin(File::open("target/abr.ed").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-781
@@ -1,782 +1,10 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
pub mod context_diff;
|
||||
pub mod ed_diff;
|
||||
pub mod normal_diff;
|
||||
pub mod unified_diff;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
Expected(Vec<u8>),
|
||||
Actual(Vec<u8>),
|
||||
MissingNL,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: u32,
|
||||
pub line_number_actual: u32,
|
||||
pub lines: Vec<DiffLine>,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||
let mut lines_since_mismatch = context_size + 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(0, 0);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() as u32 - 1;
|
||||
let actual_lines_count = actual_lines.len() as u32 - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
|
||||
mismatch.lines.pop();
|
||||
match mismatch.lines.pop() {
|
||||
Some(DiffLine::Actual(res)) => {
|
||||
// We have to make sure that Actual (the + lines)
|
||||
// always come after Expected (the - lines)
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL)
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Actual(res));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
|
||||
}
|
||||
} else {
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL)
|
||||
}
|
||||
}
|
||||
line_number_expected += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
if line_number_actual > actual_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL)
|
||||
}
|
||||
line_number_actual += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||
if (line_number_actual > actual_lines_count)
|
||||
&& (line_number_expected > expected_lines_count)
|
||||
{
|
||||
if context_queue.len() < context_size {
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
}
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_actual > actual_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_expected > expected_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
lines_since_mismatch = 0;
|
||||
} else {
|
||||
debug_assert!(context_queue.len() <= context_size);
|
||||
if context_queue.len() >= context_size {
|
||||
let _ = context_queue.pop_front();
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||
} else if context_size > 0 {
|
||||
context_queue.push_back(str);
|
||||
}
|
||||
lines_since_mismatch += 1;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push(mismatch);
|
||||
results.remove(0);
|
||||
|
||||
if results.len() == 0 && expected_lines_count != actual_lines_count {
|
||||
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
|
||||
// empty diff and only expected lines has a missing line at end
|
||||
if expected_lines_count != expected_lines.len() as u32 {
|
||||
mismatch.lines.push(DiffLine::Expected(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
mismatch.lines.push(DiffLine::Actual(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
results.push(mismatch);
|
||||
} else if actual_lines_count != actual_lines.len() as u32 {
|
||||
mismatch.lines.push(DiffLine::Expected(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::Actual(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
results.push(mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
actual: &[u8],
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output =
|
||||
format!("--- {}\t\n+++ {}\t\n", expected_filename, actual_filename).into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size);
|
||||
if diff_results.len() == 0 {
|
||||
return Vec::new();
|
||||
};
|
||||
for result in diff_results {
|
||||
let mut line_number_expected = result.line_number_expected;
|
||||
let mut line_number_actual = result.line_number_actual;
|
||||
let mut expected_count = 0;
|
||||
let mut actual_count = 0;
|
||||
for line in &result.lines {
|
||||
match line {
|
||||
DiffLine::Expected(_) => {
|
||||
expected_count += 1;
|
||||
}
|
||||
DiffLine::Context(_) => {
|
||||
expected_count += 1;
|
||||
actual_count += 1;
|
||||
}
|
||||
DiffLine::Actual(_) => {
|
||||
actual_count += 1;
|
||||
}
|
||||
DiffLine::MissingNL => {}
|
||||
}
|
||||
}
|
||||
// Let's imagine this diff file
|
||||
//
|
||||
// --- a/something
|
||||
// +++ b/something
|
||||
// @@ -2,0 +3,1 @@
|
||||
// + x
|
||||
//
|
||||
// In the unified diff format as implemented by GNU diff and patch,
|
||||
// this is an instruction to insert the x *after* the preexisting line 2,
|
||||
// not before. You can demonstrate it this way:
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// Notice how the x winds up at line 3, not line 2. This requires contortions to
|
||||
// work with our diffing algorithm, which keeps track of the "intended destination line",
|
||||
// not a line that things are supposed to be placed after. It's changing the first number,
|
||||
// not the second, that actually affects where the x goes.
|
||||
//
|
||||
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// c
|
||||
// x
|
||||
// d
|
||||
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
|
||||
// # actually being read
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// Now watch what happens if I add a context line:
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// Hunk #1 succeeded at 3 (offset 1 line).
|
||||
//
|
||||
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
|
||||
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
|
||||
// line that the x will wind up on, and not the line immediately before.
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// I made this comment because this stuff is not obvious from GNU's
|
||||
// documentation on the format at all.
|
||||
if expected_count == 0 {
|
||||
line_number_expected -= 1
|
||||
}
|
||||
if actual_count == 0 {
|
||||
line_number_actual -= 1
|
||||
}
|
||||
let exp_ct = if expected_count == 1 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(",{}", expected_count)
|
||||
};
|
||||
let act_ct = if actual_count == 1 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(",{}", actual_count)
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"@@ -{}{} +{}{} @@",
|
||||
line_number_expected, exp_ct, line_number_actual, act_ct
|
||||
)
|
||||
.expect("write to Vec is infallible");
|
||||
for line in result.lines {
|
||||
match line {
|
||||
DiffLine::Expected(e) => {
|
||||
write!(output, "-").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Context(c) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&c).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Actual(r) => {
|
||||
write!(output, "+",).expect("write to Vec is infallible");
|
||||
output.write_all(&r).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::MissingNL => {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
// test all possible six-line files.
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, "a/alef", &bet, "target/alef", 2);
|
||||
File::create("target/ab.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alef").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create("target/bet").unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open("target/ab.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
}
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alef").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_line_ending() {
|
||||
// test all possible six-line files with missing newlines.
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, "a/alefn", &bet, "target/alefn", 2);
|
||||
File::create("target/abn.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alefn").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create("target/betn").unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open("target/abn.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
}
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alefn").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
// test all possible six-line files with missing newlines.
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2, 3] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
3 => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, "a/alef_", &bet, "target/alef_", 2);
|
||||
File::create("target/ab_.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alef_").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create("target/bet_").unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open("target/ab_.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
}
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alef_").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_lines() {
|
||||
// test all possible six-line files.
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, "a/alefx", &bet, "target/alefx", 2);
|
||||
File::create("target/abx.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alefx").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create("target/betx").unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open("target/abx.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
}
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alefx").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
// test all possible six-line files.
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, "a/alefr", &bet, "target/alefr", 2);
|
||||
File::create("target/abr.diff")
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create("target/alefr").unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create("target/betr").unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open("target/abr.diff").unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
}
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read("target/alefr").unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Re-export the public functions/types you need
|
||||
pub use context_diff::diff as context_diff;
|
||||
pub use ed_diff::diff as ed_diff;
|
||||
pub use normal_diff::diff as normal_diff;
|
||||
pub use unified_diff::diff as unified_diff;
|
||||
|
||||
+40
-35
@@ -1,55 +1,60 @@
|
||||
// Sample program. Do not use.
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use crate::params::{parse_params, Format, Params};
|
||||
use std::env;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::process;
|
||||
fn main() {
|
||||
let mut o = env::args_os();
|
||||
// parse CLI
|
||||
let exe = match o.next() {
|
||||
Some(from) => from,
|
||||
None => {
|
||||
eprintln!("Usage: [exe] [from] [to]");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let from = match o.next() {
|
||||
Some(from) => from,
|
||||
None => {
|
||||
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let to = match o.next() {
|
||||
Some(from) => from,
|
||||
None => {
|
||||
eprintln!("Usage: {} [from] [to]", exe.to_string_lossy());
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
mod context_diff;
|
||||
mod ed_diff;
|
||||
mod normal_diff;
|
||||
mod params;
|
||||
mod unified_diff;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let opts = env::args_os();
|
||||
let Params {
|
||||
from,
|
||||
to,
|
||||
context_count,
|
||||
format,
|
||||
} = parse_params(opts)?;
|
||||
// read files
|
||||
let from_content = match fs::read(&from) {
|
||||
Ok(from_content) => from_content,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read from-file: {}", e);
|
||||
process::exit(2);
|
||||
return Err(format!("Failed to read from-file: {e}"));
|
||||
}
|
||||
};
|
||||
let to_content = match fs::read(&to) {
|
||||
Ok(to_content) => to_content,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read to-file: {}", e);
|
||||
process::exit(2);
|
||||
return Err(format!("Failed to read from-file: {e}"));
|
||||
}
|
||||
};
|
||||
// run diff
|
||||
io::stdout()
|
||||
.write_all(&unified_diff::diff(
|
||||
let result: Vec<u8> = match format {
|
||||
Format::Normal => normal_diff::diff(&from_content, &to_content),
|
||||
Format::Unified => unified_diff::diff(
|
||||
&from_content,
|
||||
&from.to_string_lossy(),
|
||||
&to_content,
|
||||
&to.to_string_lossy(),
|
||||
1,
|
||||
))
|
||||
.unwrap();
|
||||
context_count,
|
||||
),
|
||||
Format::Context => context_diff::diff(
|
||||
&from_content,
|
||||
&from.to_string_lossy(),
|
||||
&to_content,
|
||||
&to.to_string_lossy(),
|
||||
context_count,
|
||||
),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content)?,
|
||||
};
|
||||
io::stdout().write_all(&result).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<Vec<u8>>,
|
||||
pub actual: Vec<Vec<u8>>,
|
||||
pub expected_missing_nl: bool,
|
||||
pub actual_missing_nl: bool,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
expected_missing_nl: false,
|
||||
actual_missing_nl: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8]) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() - 1;
|
||||
let actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
}
|
||||
mismatch.expected.push(str.to_vec());
|
||||
mismatch.expected_missing_nl = line_number_expected > expected_lines_count;
|
||||
line_number_expected += 1;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
mismatch.actual.push(str.to_vec());
|
||||
mismatch.actual_missing_nl = line_number_actual > actual_lines_count;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
match (
|
||||
line_number_expected > expected_lines_count,
|
||||
line_number_actual > actual_lines_count,
|
||||
) {
|
||||
(true, false) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
mismatch.expected.push(str.to_vec());
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(str.to_vec());
|
||||
}
|
||||
(false, true) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
mismatch.actual.push(str.to_vec());
|
||||
mismatch.actual_missing_nl = true;
|
||||
mismatch.expected.push(str.to_vec());
|
||||
}
|
||||
(true, true) | (false, false) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
} else {
|
||||
mismatch.line_number_expected = line_number_expected;
|
||||
mismatch.line_number_actual = line_number_actual;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8]) -> Vec<u8> {
|
||||
let mut output = Vec::new();
|
||||
let diff_results = make_diff(expected, actual);
|
||||
for result in diff_results {
|
||||
let line_number_expected = result.line_number_expected;
|
||||
let line_number_actual = result.line_number_actual;
|
||||
let expected_count = result.expected.len();
|
||||
let actual_count = result.actual.len();
|
||||
match (expected_count, actual_count) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => writeln!(
|
||||
&mut output,
|
||||
"{}a{},{}",
|
||||
line_number_expected - 1,
|
||||
line_number_actual,
|
||||
line_number_actual + actual_count - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(_, 0) => writeln!(
|
||||
&mut output,
|
||||
"{},{}d{}",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1,
|
||||
line_number_actual - 1
|
||||
)
|
||||
.unwrap(),
|
||||
_ => writeln!(
|
||||
&mut output,
|
||||
"{},{}c{},{}",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1,
|
||||
line_number_actual,
|
||||
actual_count + line_number_actual - 1
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
for expected in &result.expected {
|
||||
write!(&mut output, "< ").unwrap();
|
||||
output.write_all(expected).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||
}
|
||||
if expected_count != 0 && actual_count != 0 {
|
||||
writeln!(&mut output, "---").unwrap();
|
||||
}
|
||||
for actual in &result.actual {
|
||||
write!(&mut output, "> ").unwrap();
|
||||
output.write_all(actual).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg(&format!("{target}/alef"))
|
||||
.stdin(File::open(&format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_line_ending() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
File::create(&format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefn")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betn")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg("--normal")
|
||||
.arg(&format!("{target}/alefn"))
|
||||
.stdin(File::open(&format!("{target}/abn.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefn")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg(&format!("{target}/alef_"))
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.arg(&format!("{target}/alefr"))
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Format {
|
||||
Normal,
|
||||
Unified,
|
||||
Context,
|
||||
Ed,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn osstr_bytes(osstr: &OsStr) -> &[u8] {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
osstr.as_bytes()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn osstr_bytes(osstr: &OsStr) -> Vec<u8> {
|
||||
osstr.to_string_lossy().bytes().collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Params {
|
||||
pub from: OsString,
|
||||
pub to: OsString,
|
||||
pub format: Format,
|
||||
pub context_count: usize,
|
||||
}
|
||||
|
||||
pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
|
||||
let mut opts = opts.into_iter();
|
||||
// parse CLI
|
||||
|
||||
let Some(exe) = opts.next() else {
|
||||
return Err("Usage: <exe> <from> <to>".to_string());
|
||||
};
|
||||
let mut from = None;
|
||||
let mut to = None;
|
||||
let mut format = None;
|
||||
let mut context_count = 3;
|
||||
while let Some(param) = opts.next() {
|
||||
if param == "--" {
|
||||
break;
|
||||
}
|
||||
if param == "-" {
|
||||
if from.is_none() {
|
||||
from = Some(OsString::from("/dev/stdin"));
|
||||
} else if to.is_none() {
|
||||
to = Some(OsString::from("/dev/stdin"));
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let p = osstr_bytes(¶m);
|
||||
if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') {
|
||||
let mut bit = p[1..].iter().copied().peekable();
|
||||
// Can't use a for loop because `diff -30u` is supposed to make a diff
|
||||
// with 30 lines of context.
|
||||
while let Some(b) = bit.next() {
|
||||
match b {
|
||||
b'0'..=b'9' => {
|
||||
context_count = (b - b'0') as usize;
|
||||
while let Some(b'0'..=b'9') = bit.peek() {
|
||||
context_count *= 10;
|
||||
context_count += (bit.next().unwrap() - b'0') as usize;
|
||||
}
|
||||
}
|
||||
b'c' => {
|
||||
if format.is_some() && format != Some(Format::Context) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Context);
|
||||
}
|
||||
b'e' => {
|
||||
if format.is_some() && format != Some(Format::Ed) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Ed);
|
||||
}
|
||||
b'u' => {
|
||||
if format.is_some() && format != Some(Format::Unified) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Unified);
|
||||
}
|
||||
b'U' => {
|
||||
if format.is_some() && format != Some(Format::Unified) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Unified);
|
||||
let context_count_maybe = if bit.peek().is_some() {
|
||||
String::from_utf8(bit.collect::<Vec<u8>>()).ok()
|
||||
} else {
|
||||
opts.next().map(|x| x.to_string_lossy().into_owned())
|
||||
};
|
||||
if let Some(context_count_maybe) =
|
||||
context_count_maybe.and_then(|x| x.parse().ok())
|
||||
{
|
||||
context_count = context_count_maybe;
|
||||
break;
|
||||
}
|
||||
return Err("Invalid context count".to_string());
|
||||
}
|
||||
_ => return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))),
|
||||
}
|
||||
}
|
||||
} else if from.is_none() {
|
||||
from = Some(param);
|
||||
} else if to.is_none() {
|
||||
to = Some(param);
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
let from = if let Some(from) = from {
|
||||
from
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
};
|
||||
let to = if let Some(to) = to {
|
||||
to
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
};
|
||||
let format = format.unwrap_or(Format::Normal);
|
||||
Ok(Params {
|
||||
from,
|
||||
to,
|
||||
format,
|
||||
context_count,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn os(s: &str) -> OsString {
|
||||
OsString::from(s)
|
||||
}
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn basics_ed() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Ed,
|
||||
context_count: 3,
|
||||
}),
|
||||
parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn context_count() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-u54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
context_count: 54,
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-c54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn double_dash() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("-g"),
|
||||
to: os("-h"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
}),
|
||||
parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn unknown_argument() {
|
||||
assert!(
|
||||
parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err()
|
||||
);
|
||||
assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err());
|
||||
assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert!(parse_params([].iter().cloned()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
Expected(Vec<u8>),
|
||||
Actual(Vec<u8>),
|
||||
MissingNL,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: u32,
|
||||
pub line_number_actual: u32,
|
||||
pub lines: Vec<DiffLine>,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||
let mut lines_since_mismatch = context_size + 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(0, 0);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() as u32 - 1;
|
||||
let actual_lines_count = actual_lines.len() as u32 - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
|
||||
mismatch.lines.pop();
|
||||
match mismatch.lines.pop() {
|
||||
Some(DiffLine::Actual(res)) => {
|
||||
// We have to make sure that Actual (the + lines)
|
||||
// always come after Expected (the - lines)
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Actual(res));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
_ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
|
||||
}
|
||||
} else {
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
}
|
||||
line_number_expected += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
if line_number_actual > actual_lines_count {
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
line_number_actual += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||
if (line_number_actual > actual_lines_count)
|
||||
&& (line_number_expected > expected_lines_count)
|
||||
{
|
||||
if context_queue.len() < context_size {
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
}
|
||||
}
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_actual > actual_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_expected > expected_lines_count {
|
||||
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len() as u32,
|
||||
line_number_actual - context_queue.len() as u32,
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
|
||||
mismatch.lines.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
|
||||
lines_since_mismatch = 0;
|
||||
} else {
|
||||
debug_assert!(context_queue.len() <= context_size);
|
||||
if context_queue.len() >= context_size {
|
||||
let _ = context_queue.pop_front();
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.lines.push(DiffLine::Context(str.to_vec()));
|
||||
} else if context_size > 0 {
|
||||
context_queue.push_back(str);
|
||||
}
|
||||
lines_since_mismatch += 1;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push(mismatch);
|
||||
results.remove(0);
|
||||
|
||||
if results.is_empty() && expected_lines_count != actual_lines_count {
|
||||
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
|
||||
// empty diff and only expected lines has a missing line at end
|
||||
if expected_lines_count != expected_lines.len() as u32 {
|
||||
mismatch.lines.push(DiffLine::Expected(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
mismatch.lines.push(DiffLine::Actual(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
results.push(mismatch);
|
||||
} else if actual_lines_count != actual_lines.len() as u32 {
|
||||
mismatch.lines.push(DiffLine::Expected(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::Actual(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.lines.push(DiffLine::MissingNL);
|
||||
results.push(mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
actual: &[u8],
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size);
|
||||
if diff_results.is_empty() {
|
||||
return Vec::new();
|
||||
};
|
||||
for result in diff_results {
|
||||
let mut line_number_expected = result.line_number_expected;
|
||||
let mut line_number_actual = result.line_number_actual;
|
||||
let mut expected_count = 0;
|
||||
let mut actual_count = 0;
|
||||
for line in &result.lines {
|
||||
match line {
|
||||
DiffLine::Expected(_) => {
|
||||
expected_count += 1;
|
||||
}
|
||||
DiffLine::Context(_) => {
|
||||
expected_count += 1;
|
||||
actual_count += 1;
|
||||
}
|
||||
DiffLine::Actual(_) => {
|
||||
actual_count += 1;
|
||||
}
|
||||
DiffLine::MissingNL => {}
|
||||
}
|
||||
}
|
||||
// Let's imagine this diff file
|
||||
//
|
||||
// --- a/something
|
||||
// +++ b/something
|
||||
// @@ -2,0 +3,1 @@
|
||||
// + x
|
||||
//
|
||||
// In the unified diff format as implemented by GNU diff and patch,
|
||||
// this is an instruction to insert the x *after* the preexisting line 2,
|
||||
// not before. You can demonstrate it this way:
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// Notice how the x winds up at line 3, not line 2. This requires contortions to
|
||||
// work with our diffing algorithm, which keeps track of the "intended destination line",
|
||||
// not a line that things are supposed to be placed after. It's changing the first number,
|
||||
// not the second, that actually affects where the x goes.
|
||||
//
|
||||
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// c
|
||||
// x
|
||||
// d
|
||||
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
|
||||
// # actually being read
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// Now watch what happens if I add a context line:
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// Hunk #1 succeeded at 3 (offset 1 line).
|
||||
//
|
||||
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
|
||||
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
|
||||
// line that the x will wind up on, and not the line immediately before.
|
||||
//
|
||||
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
|
||||
// $ echo -ne 'a\nb\nc\nd\n' > something
|
||||
// $ patch -p1 < diff
|
||||
// patching file something
|
||||
// $ cat something
|
||||
// a
|
||||
// b
|
||||
// x
|
||||
// c
|
||||
// d
|
||||
//
|
||||
// I made this comment because this stuff is not obvious from GNU's
|
||||
// documentation on the format at all.
|
||||
if expected_count == 0 {
|
||||
line_number_expected -= 1;
|
||||
}
|
||||
if actual_count == 0 {
|
||||
line_number_actual -= 1;
|
||||
}
|
||||
let exp_ct = if expected_count == 1 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(",{expected_count}")
|
||||
};
|
||||
let act_ct = if actual_count == 1 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(",{actual_count}")
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@"
|
||||
)
|
||||
.expect("write to Vec is infallible");
|
||||
for line in result.lines {
|
||||
match line {
|
||||
DiffLine::Expected(e) => {
|
||||
write!(output, "-").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Context(c) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&c).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Actual(r) => {
|
||||
write!(output, "+",).expect("write to Vec is infallible");
|
||||
output.write_all(&r).expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::MissingNL => {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
let target = "target/unified-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
println!(
|
||||
"diff: {:?}",
|
||||
String::from_utf8(diff.clone())
|
||||
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||
);
|
||||
println!(
|
||||
"alef: {:?}",
|
||||
String::from_utf8(alef.clone())
|
||||
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||
);
|
||||
println!(
|
||||
"bet: {:?}",
|
||||
String::from_utf8(bet.clone())
|
||||
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
|
||||
);
|
||||
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open(&format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let alef = fs::read(&format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_line_ending() {
|
||||
let target = "target/unified-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefn", &bet, &format!("{target}/alefn"), 2);
|
||||
File::create(&format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefn")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betn")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open(&format!("{target}/abn.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefn")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/unified-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2, 3] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
3 => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open(&format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_lines() {
|
||||
let target = "target/unified-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2);
|
||||
File::create(&format!("{target}/abx.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefx")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betx")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open(&format!("{target}/abx.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefx")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/unified-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff =
|
||||
diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(&format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(&format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = Command::new("patch")
|
||||
.arg("-p0")
|
||||
.stdin(File::open(&format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(&format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user