Files
sausage/userland/diffutils/src/unified_diff.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs
(ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r),
fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like
client of the process interface. search-test.nu: 42 cases match native tools.
2026-11-06 17:21:51 +00:00

1033 lines
45 KiB
Rust

// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::collections::VecDeque;
use std::io::Write;
use crate::params::Params;
use crate::utils::do_write_line;
use crate::utils::get_modification_time;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
Expected(Vec<u8>),
Actual(Vec<u8>),
MissingNL,
}
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: u32,
pub line_number_actual: u32,
pub lines: Vec<DiffLine>,
}
impl Mismatch {
fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
Mismatch {
line_number_expected,
line_number_actual,
lines: Vec::new(),
}
}
}
// Produces a diff between the expected output and actual output.
fn make_diff(
expected: &[u8],
actual: &[u8],
context_size: usize,
stop_early: bool,
) -> Vec<Mismatch> {
let mut line_number_expected = 1;
let mut line_number_actual = 1;
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
// Like GNU diff, keep changes in one hunk while at most twice the context
// size of unchanged lines separates them.
let max_gap = 2 * context_size;
let mut lines_since_mismatch = max_gap + 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 > max_gap {
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 > max_gap {
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 > max_gap {
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 > max_gap {
results.push(mismatch);
mismatch = Mismatch::new(
line_number_expected - context_queue.len() as u32,
line_number_actual - context_queue.len() as u32,
);
}
while let Some(line) = context_queue.pop_front() {
debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
mismatch.lines.push(DiffLine::Context(line.to_vec()));
}
mismatch.lines.push(DiffLine::Expected(str.to_vec()));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(str.to_vec()));
lines_since_mismatch = 0;
} else {
debug_assert!(context_queue.len() <= context_size);
if context_queue.len() >= context_size {
let _ = context_queue.pop_front();
}
if lines_since_mismatch < context_size {
mismatch.lines.push(DiffLine::Context(str.to_vec()));
} else if context_size > 0 {
context_queue.push_back(str);
}
lines_since_mismatch += 1;
}
line_number_expected += 1;
line_number_actual += 1;
}
}
if stop_early && !results.is_empty() {
// Optimization: stop analyzing the files as soon as there are any differences
return results;
}
}
results.push(mismatch);
results.remove(0);
if results.is_empty() && expected_lines_count != actual_lines_count {
let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
// empty diff and only expected lines has a missing line at end
if expected_lines_count != expected_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
results.push(mismatch);
} else if actual_lines_count != actual_lines.len() as u32 {
mismatch.lines.push(DiffLine::Expected(
expected_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::Actual(
actual_lines
.pop()
.expect("can't be empty; produced by split()")
.to_vec(),
));
mismatch.lines.push(DiffLine::MissingNL);
results.push(mismatch);
}
}
results
}
#[must_use]
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
let from_modified_time = get_modification_time(&params.from.to_string_lossy());
let to_modified_time = get_modification_time(&params.to.to_string_lossy());
let mut output = format!(
"--- {0}\t{1}\n+++ {2}\t{3}\n",
params.from.to_string_lossy(),
from_modified_time,
params.to.to_string_lossy(),
to_modified_time
)
.into_bytes();
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
if diff_results.is_empty() {
return Vec::new();
}
if params.brief {
return output;
}
for result in diff_results {
let mut line_number_expected = result.line_number_expected;
let mut line_number_actual = result.line_number_actual;
let mut expected_count = 0;
let mut actual_count = 0;
for line in &result.lines {
match line {
DiffLine::Expected(_) => {
expected_count += 1;
}
DiffLine::Context(_) => {
expected_count += 1;
actual_count += 1;
}
DiffLine::Actual(_) => {
actual_count += 1;
}
DiffLine::MissingNL => {}
}
}
// Let's imagine this diff file
//
// --- a/something
// +++ b/something
// @@ -2,0 +3,1 @@
// + x
//
// In the unified diff format as implemented by GNU diff and patch,
// this is an instruction to insert the x *after* the preexisting line 2,
// not before. You can demonstrate it this way:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Notice how the x winds up at line 3, not line 2. This requires contortions to
// work with our diffing algorithm, which keeps track of the "intended destination line",
// not a line that things are supposed to be placed after. It's changing the first number,
// not the second, that actually affects where the x goes.
//
// # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// c
// x
// d
// # change the third number from 3 to 1000, and it's obvious that it's the first number that's
// # actually being read
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// Now watch what happens if I add a context line:
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// Hunk #1 succeeded at 3 (offset 1 line).
//
// It technically "succeeded", but this is a warning. We want to produce clean diffs.
// Now that I have a context line, I'm supposed to say what line it's actually on, which is the
// line that the x will wind up on, and not the line immediately before.
//
// $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
// $ echo -ne 'a\nb\nc\nd\n' > something
// $ patch -p1 < diff
// patching file something
// $ cat something
// a
// b
// x
// c
// d
//
// I made this comment because this stuff is not obvious from GNU's
// documentation on the format at all.
if expected_count == 0 {
line_number_expected -= 1;
}
if actual_count == 0 {
line_number_actual -= 1;
}
let exp_ct = if expected_count == 1 {
String::new()
} else {
format!(",{expected_count}")
};
let act_ct = if actual_count == 1 {
String::new()
} else {
format!(",{actual_count}")
};
writeln!(
output,
"@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@"
)
.expect("write to Vec is infallible");
for line in result.lines {
match line {
DiffLine::Expected(e) => {
write!(output, "-").expect("write to Vec is infallible");
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Context(c) => {
write!(output, " ").expect("write to Vec is infallible");
do_write_line(&mut output, &c, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::Actual(r) => {
write!(output, "+",).expect("write to Vec is infallible");
do_write_line(&mut output, &r, params.expand_tabs, params.tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::MissingNL => {
writeln!(output, r"\ No newline at end of file")
.expect("write to Vec is infallible");
}
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use crate::utils::testcmds::PATCH_CMD;
#[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;
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 patched = &format!("{target}/alef");
let diff = diff(
&alef,
&bet,
&Params {
from: patched.into(),
to: patched.into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
println!(
"diff: {:?}",
String::from_utf8(diff.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
println!(
"alef: {:?}",
String::from_utf8(alef.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
println!(
"bet: {:?}",
String::from_utf8(bet.clone())
.unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
);
use crate::utils::testcmds::PATCH_CMD;
let output = PATCH_CMD
.new()
.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;
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 patched = &format!("{target}/alefn");
let diff = diff(
&alef,
&bet,
&Params {
from: patched.into(),
to: patched.into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abn.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefn")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betn")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = PATCH_CMD
.new()
.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;
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 patched = &format!("{target}/alef_");
let diff = diff(
&alef,
&bet,
&Params {
from: patched.into(),
to: patched.into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/ab_.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alef_")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/bet_")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = PATCH_CMD
.new()
.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;
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 patched = &format!("{target}/alefx");
let diff = diff(
&alef,
&bet,
&Params {
from: patched.into(),
to: patched.into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abx.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefx")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betx")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = PATCH_CMD
.new()
.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;
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 patched = &format!("{target}/alefr");
let diff = diff(
&alef,
&bet,
&Params {
from: patched.into(),
to: patched.into(),
context_count: 2,
..Default::default()
},
);
File::create(format!("{target}/abr.diff"))
.unwrap()
.write_all(&diff)
.unwrap();
let mut fa = File::create(format!("{target}/alefr")).unwrap();
fa.write_all(&alef[..]).unwrap();
let mut fb = File::create(format!("{target}/betr")).unwrap();
fb.write_all(&bet[..]).unwrap();
let _ = fa;
let _ = fb;
let output = PATCH_CMD
.new()
.arg("-p0")
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
//println!("{}", String::from_utf8_lossy(&output.stdout));
//println!("{}", String::from_utf8_lossy(&output.stderr));
let alef = fs::read(format!("{target}/alefr")).unwrap();
assert_eq!(alef, bet);
}
}
}
}
}
}
}
#[test]
fn test_stop_early() {
use crate::assert_diff_eq;
let from_filename = "foo";
let from = ["a", "b", "c", ""].join("\n");
let to_filename = "bar";
let to = ["a", "d", "c", ""].join("\n");
let diff_full = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
let expected_full = [
"--- foo\tTIMESTAMP",
"+++ bar\tTIMESTAMP",
"@@ -1,3 +1,3 @@",
" a",
"-b",
"+d",
" c",
"",
]
.join("\n");
assert_diff_eq!(diff_full, expected_full);
let diff_brief = diff(
from.as_bytes(),
to.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n");
assert_diff_eq!(diff_brief, expected_brief);
let nodiff_full = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
..Default::default()
},
);
assert!(nodiff_full.is_empty());
let nodiff_brief = diff(
from.as_bytes(),
from.as_bytes(),
&Params {
from: from_filename.into(),
to: to_filename.into(),
brief: true,
..Default::default()
},
);
assert!(nodiff_brief.is_empty());
}
#[test]
fn test_hunk_merging() {
use crate::assert_diff_eq;
let params = Params {
from: "foo".into(),
to: "bar".into(),
context_count: 1,
..Default::default()
};
// Changes separated by up to twice the context size share one hunk.
let from = ["a", "b", "c", "d", "e", "f", ""].join("\n");
let to = ["a", "B", "c", "d", "E", "f", ""].join("\n");
let expected = [
"--- foo\tTIMESTAMP",
"+++ bar\tTIMESTAMP",
"@@ -1,6 +1,6 @@",
" a",
"-b",
"+B",
" c",
" d",
"-e",
"+E",
" f",
"",
]
.join("\n");
let output = diff(from.as_bytes(), to.as_bytes(), &params);
assert_diff_eq!(output, expected);
// One more unchanged line between them splits the hunk.
let from = ["a", "b", "c", "d", "e", "f", "g", ""].join("\n");
let to = ["a", "B", "c", "d", "e", "F", "g", ""].join("\n");
let expected = [
"--- foo\tTIMESTAMP",
"+++ bar\tTIMESTAMP",
"@@ -1,3 +1,3 @@",
" a",
"-b",
"+B",
" c",
"@@ -5,3 +5,3 @@",
" e",
"-f",
"+F",
" g",
"",
]
.join("\n");
let output = diff(from.as_bytes(), to.as_bytes(), &params);
assert_diff_eq!(output, expected);
// An insertion before a nearby change gives the two sides different
// line counts; the hunks must still be merged (issue #268).
let from = ["milk", "eggs", "bread", "butter", "apples", ""].join("\n");
let to = ["milk", "eggs", "yogurt", "bread", "honey", "apples", ""].join("\n");
let expected = [
"--- foo\tTIMESTAMP",
"+++ bar\tTIMESTAMP",
"@@ -2,4 +2,5 @@",
" eggs",
"+yogurt",
" bread",
"-butter",
"+honey",
" apples",
"",
]
.join("\n");
let output = diff(from.as_bytes(), to.as_bytes(), &params);
assert_diff_eq!(output, expected);
}
}