// 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), Expected(Vec), Actual(Vec), MissingNL, } #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: u32, pub line_number_actual: u32, pub lines: Vec, } 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 { 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 { let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); let to_modified_time = get_modification_time(¶ms.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(), ¶ms); 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(), ¶ms); 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(), ¶ms); assert_diff_eq!(output, expected); } }