Skip to content
Merged
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
37 changes: 36 additions & 1 deletion tests/compat/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub struct Case<'a> {
expect_success: bool,
/// Whether diffy and external tool should agree on success/failure (default: true)
expect_compat: bool,
/// Inline snapshot for diffy's error message on failure.
expect_diffy_error: Option<snapbox::Data>,
/// Inline snapshot for external tool's stderr on failure.
expect_external_error: Option<snapbox::Data>,
}

impl<'a> Case<'a> {
Expand All @@ -40,6 +44,8 @@ impl<'a> Case<'a> {
strip_level: 0,
expect_success: true,
expect_compat: true,
expect_diffy_error: None,
expect_external_error: None,
}
}

Expand All @@ -51,6 +57,8 @@ impl<'a> Case<'a> {
strip_level: 0,
expect_success: true,
expect_compat: true,
expect_diffy_error: None,
expect_external_error: None,
}
}

Expand Down Expand Up @@ -81,6 +89,20 @@ impl<'a> Case<'a> {
self
}

/// Assert diffy's error message matches an inline snapshot.
/// Use with [`snapbox::str!`].
pub fn expect_diffy_error(mut self, expected: impl Into<snapbox::Data>) -> Self {
self.expect_diffy_error = Some(expected.into());
self
}

/// Assert external tool's stderr matches an inline snapshot.
/// Use with [`snapbox::str!`].
pub fn expect_external_error(mut self, expected: impl Into<snapbox::Data>) -> Self {
self.expect_external_error = Some(expected.into());
self
}

/// Run the test case.
pub fn run(self) {
let case_dir = self.case_dir();
Expand Down Expand Up @@ -111,7 +133,12 @@ impl<'a> Case<'a> {
if self.expect_success {
diffy_result.as_ref().expect("diffy should succeed");
} else {
diffy_result.as_ref().expect_err("diffy should fail");
let err = diffy_result.as_ref().expect_err("diffy should fail");
let expected = self
.expect_diffy_error
.as_ref()
.expect("expect_diffy_error is required when expect_success(false)");
snapbox::assert_data_eq!(err.to_string(), expected.clone());
}

// In CI mode, also verify external tool behavior
Expand All @@ -130,6 +157,14 @@ impl<'a> Case<'a> {
}
};

if let Err(stderr) = &external_result {
let expected = self
.expect_external_error
.as_ref()
.expect("`expect_external_error` is required when the external tool fails");
snapbox::assert_data_eq!(stderr.as_str(), expected.clone());
}

// For success cases where both succeed and are expected to be compatible,
// verify outputs match
if diffy_result.is_ok() && external_result.is_ok() && self.expect_compat {
Expand Down
31 changes: 21 additions & 10 deletions tests/compat/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,31 @@ fn path_ambiguous_suffix() {
// split at 3: a/x vs b/x c/x → suffix `x` (len 1)
// split at 7: a/x b/x vs c/x → suffix `x` (len 1)
//
// - git apply: rejects: "git diff header lacks filename information when
// removing 1 leading pathname component")
// - diffy: succeeds, picks first (leftmost) split
// diffy succeeds (picks first/leftmost split); git apply rejects.
#[test]
fn fail_ambiguous_suffix_tie() {
Case::git("fail_ambiguous_suffix_tie")
.strip(1)
.expect_success(true)
.expect_compat(false)
.expect_external_error(snapbox::str![[r#"
error: git diff header lacks filename information when removing 1 leading pathname component (line 4)

"#]])
.run();
}

// Both --- and +++ point to /dev/null.
// git apply rejects: "dev/null: No such file or directory"
#[test]
fn fail_both_devnull() {
Case::git("fail_both_devnull")
.strip(1)
.expect_success(false)
.expect_diffy_error(snapbox::str!["parse error: error parsing patches at byte 0: patch has both original and modified as /dev/null"])
.expect_external_error(snapbox::str![[r#"
error: dev/null: No such file or directory

"#]])
.run();
}

Expand All @@ -172,15 +178,17 @@ fn path_custom_prefix() {
// Custom prefix without slash (e.g. `--src-prefix=foo --dst-prefix=bar`).
//
// Produces paths like `fooold.txt` / `barold.txt` with no `/` separator,
// making strip impossible. Both git apply and diffy fail:
// - git apply: "git diff header lacks filename information when removing 1
// leading pathname component"
// - diffy: paths don't match any input file
// making strip impossible. Both git apply and diffy fail.
#[test]
fn fail_prefix_no_slash() {
Case::git("fail_prefix_no_slash")
.strip(1)
.expect_success(false)
.expect_diffy_error(snapbox::str!["io error: No such file or directory (os error 2)"])
.expect_external_error(snapbox::str![[r#"
error: git diff header lacks filename information when removing 1 leading pathname component (line 5)

"#]])
.run();
}

Expand All @@ -193,13 +201,16 @@ fn non_utf8_hunk_content() {

// Single-file patch with junk between hunks.
//
// - git apply: errors ("patch fragment without header")
// - diffy: succeeds, ignores trailing junk (matches GNU patch behavior)
// diffy succeeds (ignores trailing junk, matches GNU patch); git apply rejects.
#[test]
fn junk_between_hunks() {
Case::git("junk_between_hunks")
.strip(1)
.expect_compat(false)
.expect_external_error(snapbox::str![[r#"
error: patch fragment without header at line 11: @@ -7,3 +7,3 @@

"#]])
.run();
}

Expand Down
35 changes: 28 additions & 7 deletions tests/compat/gnu_patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ fn missing_minus_header() {

// Empty file creation using unified diff format with empty hunk.
//
// Platform compatibility:
// - Apple patch 2.0 (macOS/BSD): ✅ Accepts, creates empty file (0 bytes)
// - GNU patch 2.8 (Linux): ❌ Rejects as "malformed patch at line 3"
// - diffy: ✅ Accepts (with our current implementation)
// diffy accepts (creates empty file); GNU patch rejects.
// Apple patch 2.0 (macOS/BSD) also accepts.
#[test]
fn create_empty_file_unidiff() {
Case::gnu_patch("create_empty_file_unidiff")
.expect_compat(false)
.expect_external_error(snapbox::str![[r#"
GNU patch failed with status exit status: 2: patch: **** Only garbage was found in the patch input.

"#]])
.run();
}

Expand All @@ -77,6 +79,9 @@ fn create_empty_file_gitdiff() {
.strip(1)
.expect_success(false)
.expect_compat(false)
.expect_diffy_error(snapbox::str![
"parse error: error parsing patches at byte 0: no valid patches found"
])
.run();
}

Expand Down Expand Up @@ -171,34 +176,48 @@ fn format_patch_mbox() {
fn fail_context_mismatch() {
Case::gnu_patch("fail_context_mismatch")
.expect_success(false)
.expect_diffy_error(snapbox::str!["apply error: error applying hunk #1"])
.expect_external_error(snapbox::str![
"GNU patch failed with status exit status: 1: "
])
.run();
}

#[test]
fn fail_hunk_not_found() {
Case::gnu_patch("fail_hunk_not_found")
.expect_success(false)
.expect_diffy_error(snapbox::str!["apply error: error applying hunk #1"])
.expect_external_error(snapbox::str![
"GNU patch failed with status exit status: 1: "
])
.run();
}

#[test]
fn fail_truncated_file() {
Case::gnu_patch("fail_truncated_file")
.expect_success(false)
.expect_diffy_error(snapbox::str!["apply error: error applying hunk #1"])
.expect_external_error(snapbox::str![
"GNU patch failed with status exit status: 1: "
])
.run();
}

// Patch with ---/+++ headers but no @@ hunks.
//
// - GNU patch: rejects ("Only garbage was found in the patch input")
// - diffy: succeeds, parses as 1 patch with 0 hunks
//
// diffy succeeds (parses as 1 patch with 0 hunks); GNU patch rejects.
// diffy allows 0-hunk patches for GitDiff mode where empty/binary files have no hunks.
#[test]
fn no_hunk() {
Case::gnu_patch("no_hunk")
.expect_success(true)
.expect_compat(false)
.expect_external_error(snapbox::str![[r#"
GNU patch failed with status exit status: 2: patch: **** Only garbage was found in the patch input.

"#]])
.run();
}

Expand All @@ -208,5 +227,7 @@ fn no_hunk() {
fn fail_both_devnull() {
Case::gnu_patch("fail_both_devnull")
.expect_success(false)
.expect_diffy_error(snapbox::str!["parse error: error parsing patches at byte 0: patch has both original and modified as /dev/null"])
.expect_external_error(snapbox::str!["GNU patch failed with status exit status: 1: "])
.run();
}