Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.

use crate::params::{parse_params, Format};
use crate::params::{parse_params, Format, MissingFile};
use crate::utils::report_failure_to_read_input_file;
use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff};
use std::env::ArgsOs;
Expand Down Expand Up @@ -52,6 +52,11 @@ pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
let mut io_error = false;
let from_content = match read_file_contents(&params.from) {
Ok(from_content) => from_content,
Err(e)
if e.kind() == io::ErrorKind::NotFound && params.missing_file != MissingFile::Error =>
{
vec![]
}
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.from, &e);
io_error = true;
Expand All @@ -60,6 +65,11 @@ pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
};
let to_content = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e)
if e.kind() == io::ErrorKind::NotFound && params.missing_file == MissingFile::Empty =>
{
vec![]
}
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.to, &e);
io_error = true;
Expand Down
7 changes: 7 additions & 0 deletions src/ed_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result<Vec<u8>,
match (expected_count, actual_count) {
(0, 0) => unreachable!(),
(0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(),
(1, 0) => writeln!(&mut output, "{line_number_expected}d").unwrap(),
(_, 0) => writeln!(
&mut output,
"{},{}d",
Expand Down Expand Up @@ -180,6 +181,12 @@ mod tests {
assert_eq!(diff, expected.as_bytes());
}

#[test]
fn test_single_line_deleted() {
let diff = diff(b"a\nb\n", b"a\n", &Params::default()).unwrap();
assert_eq!(diff, b"2d\n");
}

#[test]
fn test_permutations() {
let target = "target/ed-diff/";
Expand Down
23 changes: 23 additions & 0 deletions src/normal_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
let actual_count = result.actual.len();
match (expected_count, actual_count) {
(0, 0) => unreachable!(),
(0, 1) => writeln!(
// exactly one line added
&mut output,
"{}a{line_number_actual}",
line_number_expected - 1
)
.unwrap(),
(0, _) => writeln!(
// 'a' stands for "Add lines"
&mut output,
Expand All @@ -142,6 +149,13 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
line_number_actual + actual_count - 1
)
.unwrap(),
(1, 0) => writeln!(
// exactly one line deleted
&mut output,
"{line_number_expected}d{}",
line_number_actual - 1
)
.unwrap(),
(_, 0) => writeln!(
// 'd' stands for "Delete lines"
&mut output,
Expand Down Expand Up @@ -228,6 +242,15 @@ mod tests {
assert_eq!(diff, expected);
}

#[test]
fn test_single_line_added_or_deleted() {
// A one-line range is written as a single number, as for c.
let added = diff(b"a\n", b"a\nb\n", &Params::default());
assert_eq!(added, b"1a2\n> b\n".to_vec());
let deleted = diff(b"a\nb\n", b"a\n", &Params::default());
assert_eq!(deleted, b"2d1\n< b\n".to_vec());
}

#[test]
fn test_permutations() {
let target = "target/normal-diff/";
Expand Down
25 changes: 25 additions & 0 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ pub enum Format {
SideBySide,
}

/// How to handle an operand that does not exist (`-N` and
/// `--unidirectional-new-file`).
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MissingFile {
/// A missing operand is an error.
#[default]
Error,
/// A missing operand is treated as an empty file.
Empty,
/// Only a missing first operand is treated as an empty file.
EmptyIfFirst,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Params {
pub executable: OsString,
Expand All @@ -26,6 +39,7 @@ pub struct Params {
pub expand_tabs: bool,
pub tabsize: usize,
pub width: usize,
pub missing_file: MissingFile,
}

impl Default for Params {
Expand All @@ -41,6 +55,7 @@ impl Default for Params {
expand_tabs: false,
tabsize: 8,
width: 130,
missing_file: MissingFile::default(),
}
}
}
Expand Down Expand Up @@ -87,6 +102,16 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
params.brief = true;
continue;
}
if param == "-N" || param == "--new-file" {
params.missing_file = MissingFile::Empty;
continue;
}
if param == "--unidirectional-new-file" {
if params.missing_file != MissingFile::Empty {
params.missing_file = MissingFile::EmptyIfFirst;
}
continue;
}
if param == "-t" || param == "--expand-tabs" {
params.expand_tabs = true;
continue;
Expand Down
86 changes: 86 additions & 0 deletions tests/by-util/test_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,44 @@ fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

#[test]
fn missing_file_as_empty() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let present = tmp_dir.path().join("present");
let absent = tmp_dir.path().join("absent");
std::fs::write(&present, "kiwi\nmango\n")?;

for option in ["-N", "--new-file", "--unidirectional-new-file"] {
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg(option).arg(&absent).arg(&present);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout("0a1,2\n> kiwi\n> mango\n");
}

let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg("-N").arg(&present).arg(&absent);
cmd.assert()
.code(predicate::eq(1))
.failure()
.stdout("1,2d0\n< kiwi\n< mango\n");

// Only a missing first operand is accepted here.
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff")
.arg("--unidirectional-new-file")
.arg(&present)
.arg(&absent);
cmd.assert().code(predicate::eq(2)).failure();

// Without the option, a missing file is trouble.
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg(&absent).arg(&present);
cmd.assert().code(predicate::eq(2)).failure();
Ok(())
}

#[test]
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
Expand Down Expand Up @@ -288,3 +326,51 @@ fn sdiff_gutter_marker_column() -> Result<(), Box<dyn std::error::Error>> {

Ok(())
}

#[test]
fn normal_format_single_line_ranges() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let old = tmp_dir.path().join("old");
let new = tmp_dir.path().join("new");
std::fs::write(&old, "north\neast\nsouth\nwest\n")?;
std::fs::write(&new, "north\nsouth\nwest\nup\ndown\n")?;

let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg(&old).arg(&new);
cmd.assert()
.code(predicate::eq(1))
.stdout("2d1\n< east\n4a4,5\n> up\n> down\n");

let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg("/dev/null").arg(&old);
if cfg!(unix) {
cmd.assert()
.code(predicate::eq(1))
.stdout("0a1,4\n> north\n> east\n> south\n> west\n");
}

std::fs::write(&new, "north\neast\nsouth\nwest\nup\n")?;
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg(&old).arg(&new);
cmd.assert().code(predicate::eq(1)).stdout("4a5\n> up\n");
Ok(())
}

#[test]
fn ed_format_single_line_deletions() -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = tempdir()?;
let old = tmp_dir.path().join("old");
let new = tmp_dir.path().join("new");
std::fs::write(&old, "red\norange\nyellow\ngreen\nblue\n")?;
std::fs::write(&new, "red\norange\nyellow\nblue\n")?;

let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg("-e").arg(&old).arg(&new);
cmd.assert().code(predicate::eq(1)).stdout("4d\n");

std::fs::write(&new, "red\nblue\n")?;
let mut cmd = cargo_bin_cmd!("diffutils");
cmd.arg("diff").arg("-e").arg(&old).arg(&new);
cmd.assert().code(predicate::eq(1)).stdout("2,4d\n");
Ok(())
}
Loading