diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 81ddfb4..0f1ce69 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -123,24 +123,21 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result, 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(), - (1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(), - _ => writeln!( - &mut output, - "{},{}c", - line_number_expected, - expected_count + line_number_expected - 1 - ) - .unwrap(), + if expected_count == 0 { + writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(); + } else { + let command = if actual_count == 0 { 'd' } else { 'c' }; + if expected_count > 1 { + write!( + &mut output, + "{line_number_expected},{}", + expected_count + line_number_expected - 1 + ) + .unwrap(); + } else { + write!(&mut output, "{line_number_expected}").unwrap(); + } + writeln!(&mut output, "{command}").unwrap(); } lines_offset += actual_count - expected_count; if actual_count != 0 { @@ -180,6 +177,47 @@ mod tests { assert_eq!(diff, expected.as_bytes()); } + #[test] + fn test_single_line_delete() { + let params = Params::default(); + assert_eq!( + diff( + b"owl +", b"", ¶ms + ) + .unwrap(), + b"1d +" + ); + assert_eq!( + diff( + b"owl +emu +", + b"owl +", + ¶ms + ) + .unwrap(), + b"2d +" + ); + assert_eq!( + diff( + b"owl +emu +yak +", + b"yak +", + ¶ms + ) + .unwrap(), + b"1,2d +" + ); + } + #[test] fn test_permutations() { let target = "target/ed-diff/"; diff --git a/src/normal_diff.rs b/src/normal_diff.rs index 652dc84..2bbe1bf 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -116,6 +116,15 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec results } +/// Writes a line range as `start,end`, or just `start` when it spans at most one line. +fn write_range(output: &mut Vec, start: usize, count: usize) { + if count > 1 { + write!(output, "{start},{}", start + count - 1).unwrap(); + } else { + write!(output, "{start}").unwrap(); + } +} + #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html @@ -131,62 +140,21 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { 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!( - // 'a' stands for "Add lines" - &mut output, - "{}a{},{}", - line_number_expected - 1, - line_number_actual, - line_number_actual + actual_count - 1 - ) - .unwrap(), - (_, 0) => writeln!( - // 'd' stands for "Delete lines" - &mut output, - "{},{}d{}", - line_number_expected, - expected_count + line_number_expected - 1, - line_number_actual - 1 - ) - .unwrap(), - (1, 1) => writeln!( - // 'c' stands for "Change lines" - // exactly one line replaced by one line - &mut output, - "{line_number_expected}c{line_number_actual}" - ) - .unwrap(), - (1, _) => writeln!( - // one line replaced by multiple lines - &mut output, - "{}c{},{}", - line_number_expected, - line_number_actual, - actual_count + line_number_actual - 1 - ) - .unwrap(), - (_, 1) => writeln!( - // multiple lines replaced by one line - &mut output, - "{},{}c{}", - line_number_expected, - expected_count + line_number_expected - 1, - line_number_actual - ) - .unwrap(), - _ => writeln!( - // general case: multiple lines replaced by multiple lines - &mut output, - "{},{}c{},{}", - line_number_expected, - expected_count + line_number_expected - 1, - line_number_actual, - actual_count + line_number_actual - 1 - ) - .unwrap(), - } + // 'a' stands for "Add lines", 'd' for "Delete lines", 'c' for "Change lines" + let (command, expected_start) = match (expected_count, actual_count) { + (0, _) => ('a', line_number_expected - 1), + (_, 0) => ('d', line_number_expected), + _ => ('c', line_number_expected), + }; + let actual_start = if actual_count == 0 { + line_number_actual - 1 + } else { + line_number_actual + }; + write_range(&mut output, expected_start, expected_count); + write!(&mut output, "{command}").unwrap(); + write_range(&mut output, actual_start, actual_count); + writeln!(&mut output).unwrap(); for expected in &result.expected { write!(&mut output, "< ").unwrap(); do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap(); @@ -228,6 +196,18 @@ mod tests { assert_eq!(diff, expected); } + #[test] + fn test_single_line_ranges() { + let params = Params::default(); + assert_eq!(diff(b"", b"pear\n", ¶ms), b"0a1\n> pear\n"); + assert_eq!(diff(b"plum\n", b"", ¶ms), b"1d0\n< plum\n"); + assert_eq!(diff(b"fig\nlime\n", b"fig\n", ¶ms), b"2d1\n< lime\n"); + assert_eq!( + diff(b"fig\n", b"fig\nlime\nsloe\n", ¶ms), + b"1a2,3\n> lime\n> sloe\n" + ); + } + #[test] fn test_permutations() { let target = "target/normal-diff/"; diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index fbdd494..bd408c9 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -288,3 +288,51 @@ fn sdiff_gutter_marker_column() -> Result<(), Box> { Ok(()) } + +#[test] +fn normal_format_single_line_ranges() -> Result<(), Box> { + 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> { + 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(()) +}