From 0fb54ee98be9cba01f0adbfa0e370259c5951728 Mon Sep 17 00:00:00 2001 From: Sichen Date: Sat, 26 Sep 2026 05:53:07 +0000 Subject: [PATCH 1/2] diff: return the difference status in side-by-side mode --- src/diff.rs | 9 +++-- tests/by-util/test_diff.rs | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index f4c0614c..47deb30e 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -70,6 +70,9 @@ pub fn main(opts: Peekable) -> ExitCode { return ExitCode::from(2); } + // Side-by-side writes its output directly, so its result cannot indicate differences. + let side_by_side_differs = params.format == Format::SideBySide && from_content != to_content; + // run diff let result: Vec = match params.format { Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), @@ -79,12 +82,14 @@ pub fn main(opts: Peekable) -> ExitCode { eprintln!("{error}"); exit(2); }), + Format::SideBySide if params.brief => Vec::new(), Format::SideBySide => { let mut output = stdout().lock(); side_diff::diff(&from_content, &to_content, &mut output, ¶ms) } }; - if params.brief && !result.is_empty() { + let differs = side_by_side_differs || !result.is_empty(); + if params.brief && differs { println!( "Files {} and {} differ", params.from.to_string_lossy(), @@ -93,7 +98,7 @@ pub fn main(opts: Peekable) -> ExitCode { } else { io::stdout().write_all(&result).unwrap(); } - if result.is_empty() { + if !differs { maybe_report_identical_files(); ExitCode::SUCCESS } else { diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index fbdd4940..bf549e80 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -29,6 +29,76 @@ fn no_differences() -> Result<(), Box> { Ok(()) } +#[test] +fn side_by_side_exit_status() -> Result<(), Box> { + let dir = tempdir()?; + let left = dir.path().join("left"); + let right = dir.path().join("right"); + + for (left_content, right_content, code) in [ + (b"alpha\n".as_slice(), b"alpha\n".as_slice(), 0), + (b"alpha\n", b"alpha\nbeta\n", 1), + (b"alpha\n", b"bravo\n", 1), + (b"alpha", b"alpha\n", 1), + (b"alpha", b"alpha", 0), + (b"\xff\n", b"\xfe\n", 1), + (b"", b"alpha\n", 1), + (b"alpha\n", b"", 1), + (b"", b"", 0), + ] { + std::fs::write(&left, left_content)?; + std::fs::write(&right, right_content)?; + for options in [ + &["-y"][..], + &["-y", "-q"], + &["-y", "-s"], + &["-y", "--width=40"], + ] { + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").args(options).arg(&left).arg(&right); + let assertion = cmd + .assert() + .code(predicate::eq(code)) + .stderr(predicate::str::is_empty()); + if options == ["-y", "-q"] { + let expected = if code == 1 { + format!("Files {} and {} differ\n", left.display(), right.display()) + } else { + String::new() + }; + assertion.stdout(predicate::eq(expected)); + } + } + } + + std::fs::write(&left, b"alpha\n")?; + for (right_content, identical) in [(b"alpha\n".as_slice(), true), (b"bravo\n", false)] { + std::fs::write(&right, right_content)?; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-y").arg("-s").arg(&left).arg(&right); + let assertion = cmd.assert(); + if identical { + assertion.stdout(predicate::str::contains(format!( + "Files {} and {} are identical\n", + left.display(), + right.display() + ))); + } else { + assertion.stdout(predicate::str::contains("are identical").not()); + } + } + + std::fs::remove_file(&right)?; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-y").arg(&left).arg(&right); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("right")); + Ok(()) +} + #[test] fn no_differences_report_identical_files() -> Result<(), Box> { // same file From 6fc1ac537eae3862f73cc24d740488b8461e4d7a Mon Sep 17 00:00:00 2001 From: Sichen Date: Sat, 24 Oct 2026 14:07:43 -0400 Subject: [PATCH 2/2] diff: let side_diff::diff report whether the files differ Return whether `side_diff::diff` saw a changed line and use that value for the exit status instead of comparing the inputs again. With `-y -q`, keep running the renderer but send its output to `io::sink()`, so only the brief message is printed. --- src/diff.rs | 14 +++-- src/side_diff.rs | 22 ++++--- tests/by-util/test_diff.rs | 121 +++++++++++++++++++++++++------------ 3 files changed, 101 insertions(+), 56 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index 47deb30e..3069139c 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -70,10 +70,8 @@ pub fn main(opts: Peekable) -> ExitCode { return ExitCode::from(2); } - // Side-by-side writes its output directly, so its result cannot indicate differences. - let side_by_side_differs = params.format == Format::SideBySide && from_content != to_content; - // run diff + let mut side_by_side_differs = false; let result: Vec = match params.format { Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), @@ -82,10 +80,14 @@ pub fn main(opts: Peekable) -> ExitCode { eprintln!("{error}"); exit(2); }), - Format::SideBySide if params.brief => Vec::new(), Format::SideBySide => { - let mut output = stdout().lock(); - side_diff::diff(&from_content, &to_content, &mut output, ¶ms) + side_by_side_differs = if params.brief { + side_diff::diff(&from_content, &to_content, &mut io::sink(), ¶ms) + } else { + let mut output = stdout().lock(); + side_diff::diff(&from_content, &to_content, &mut output, ¶ms) + }; + Vec::new() } }; let differs = side_by_side_differs || !result.is_empty(); diff --git a/src/side_diff.rs b/src/side_diff.rs index 1128e2e7..015bde77 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -5,7 +5,7 @@ use core::cmp::{max, min}; use diff::Result; -use std::{io::Write, vec}; +use std::io::Write; use unicode_width::UnicodeWidthStr; use crate::params::Params; @@ -308,12 +308,7 @@ fn push_output( Ok(()) } -pub fn diff( - from_file: &[u8], - to_file: &[u8], - output: &mut T, - params: &Params, -) -> Vec { +pub fn diff(from_file: &[u8], to_file: &[u8], output: &mut T, params: &Params) -> bool { // ^ The left file ^ The right file let mut left_lines: Vec<&[u8]> = from_file.split_inclusive(|&c| c == b'\n').collect(); @@ -350,17 +345,24 @@ pub fn diff( More studies are needed to cover GNU diff side by side with 100% accuracy, which is one of the goals of this project : ) */ + let mut differs = false; for result in diff::slice(&left_lines, &right_lines) { match result { - Result::Left(left_ln) => push_output(left_ln, b"", b'<', output, &config).unwrap(), - Result::Right(right_ln) => push_output(b"", right_ln, b'>', output, &config).unwrap(), + Result::Left(left_ln) => { + differs = true; + push_output(left_ln, b"", b'<', output, &config).unwrap() + } + Result::Right(right_ln) => { + differs = true; + push_output(b"", right_ln, b'>', output, &config).unwrap() + } Result::Both(left_ln, right_ln) => { push_output(left_ln, right_ln, b' ', output, &config).unwrap() } } } - vec![] + differs } #[cfg(test)] diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index bf549e80..ec8b0166 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -29,73 +29,114 @@ fn no_differences() -> Result<(), Box> { Ok(()) } +const SIDE_BY_SIDE_CASES: [(&[u8], &[u8], i32); 9] = [ + (b"alpha\n", b"alpha\n", 0), + (b"alpha\n", b"alpha\nbeta\n", 1), + (b"alpha\n", b"bravo\n", 1), + (b"alpha", b"alpha\n", 1), + (b"alpha", b"alpha", 0), + (b"\xff\n", b"\xfe\n", 1), + (b"", b"alpha\n", 1), + (b"alpha\n", b"", 1), + (b"", b"", 0), +]; + #[test] fn side_by_side_exit_status() -> Result<(), Box> { let dir = tempdir()?; let left = dir.path().join("left"); let right = dir.path().join("right"); - - for (left_content, right_content, code) in [ - (b"alpha\n".as_slice(), b"alpha\n".as_slice(), 0), - (b"alpha\n", b"alpha\nbeta\n", 1), - (b"alpha\n", b"bravo\n", 1), - (b"alpha", b"alpha\n", 1), - (b"alpha", b"alpha", 0), - (b"\xff\n", b"\xfe\n", 1), - (b"", b"alpha\n", 1), - (b"alpha\n", b"", 1), - (b"", b"", 0), - ] { + for (left_content, right_content, code) in SIDE_BY_SIDE_CASES { std::fs::write(&left, left_content)?; std::fs::write(&right, right_content)?; - for options in [ - &["-y"][..], - &["-y", "-q"], - &["-y", "-s"], - &["-y", "--width=40"], - ] { + for options in [&["-y"][..], &["-y", "--width=40"]] { let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff").args(options).arg(&left).arg(&right); - let assertion = cmd - .assert() + cmd.assert() .code(predicate::eq(code)) .stderr(predicate::str::is_empty()); - if options == ["-y", "-q"] { - let expected = if code == 1 { - format!("Files {} and {} differ\n", left.display(), right.display()) - } else { - String::new() - }; - assertion.stdout(predicate::eq(expected)); - } } } + Ok(()) +} - std::fs::write(&left, b"alpha\n")?; - for (right_content, identical) in [(b"alpha\n".as_slice(), true), (b"bravo\n", false)] { +#[test] +fn side_by_side_brief() -> Result<(), Box> { + let dir = tempdir()?; + let left = dir.path().join("left"); + let right = dir.path().join("right"); + for (left_content, right_content, code) in SIDE_BY_SIDE_CASES { + std::fs::write(&left, left_content)?; std::fs::write(&right, right_content)?; + let expected = if code == 1 { + format!("Files {} and {} differ\n", left.display(), right.display()) + } else { + String::new() + }; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-y").arg("-q").arg(&left).arg(&right); + cmd.assert() + .code(predicate::eq(code)) + .stdout(predicate::eq(expected)) + .stderr(predicate::str::is_empty()); + } + Ok(()) +} + +#[test] +fn side_by_side_report_identical_files() -> Result<(), Box> { + let dir = tempdir()?; + let left = dir.path().join("left"); + let right = dir.path().join("right"); + for (left_content, right_content, code) in SIDE_BY_SIDE_CASES { + std::fs::write(&left, left_content)?; + std::fs::write(&right, right_content)?; + let identical = format!( + "Files {} and {} are identical\n", + left.display(), + right.display() + ); let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff").arg("-y").arg("-s").arg(&left).arg(&right); - let assertion = cmd.assert(); - if identical { - assertion.stdout(predicate::str::contains(format!( - "Files {} and {} are identical\n", - left.display(), - right.display() - ))); + let assertion = cmd + .assert() + .code(predicate::eq(code)) + .stderr(predicate::str::is_empty()); + if code == 0 { + assertion.stdout(predicate::str::ends_with(identical)); } else { - assertion.stdout(predicate::str::contains("are identical").not()); + assertion.stdout(predicate::function(|stdout: &[u8]| { + !stdout + .windows(identical.len()) + .any(|window| window == identical.as_bytes()) + })); } } + Ok(()) +} + +#[test] +fn side_by_side_missing_file() -> Result<(), Box> { + let dir = tempdir()?; + let left = dir.path().join("left"); + let right = dir.path().join("right"); + std::fs::write(&left, b"alpha\n")?; + + #[cfg(not(windows))] + let error_message = "No such file or directory"; + #[cfg(windows)] + let error_message = "The system cannot find the file specified."; - std::fs::remove_file(&right)?; let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff").arg("-y").arg(&left).arg(&right); cmd.assert() .code(predicate::eq(2)) .failure() .stdout(predicate::str::is_empty()) - .stderr(predicate::str::contains("right")); + .stderr(predicate::eq(format!( + "diff: {}: {error_message}\n", + right.display() + ))); Ok(()) }