From 1ab40065c7459cd1336e5af362623cd574b92284 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 18 Apr 2026 12:21:43 -0400 Subject: [PATCH 1/2] test(compat): mandatory error snapshot assertion This ensures we see the expected error, not unrelated random one. --- tests/compat/common.rs | 37 ++++++++++++++++++++++++++++++++++- tests/compat/git/mod.rs | 18 +++++++++++++++++ tests/compat/gnu_patch/mod.rs | 25 +++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/tests/compat/common.rs b/tests/compat/common.rs index 0993e2a7..1c8c8c2e 100644 --- a/tests/compat/common.rs +++ b/tests/compat/common.rs @@ -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, + /// Inline snapshot for external tool's stderr on failure. + expect_external_error: Option, } impl<'a> Case<'a> { @@ -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, } } @@ -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, } } @@ -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) -> 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) -> Self { + self.expect_external_error = Some(expected.into()); + self + } + /// Run the test case. pub fn run(self) { let case_dir = self.case_dir(); @@ -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 @@ -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 { diff --git a/tests/compat/git/mod.rs b/tests/compat/git/mod.rs index 041edaf3..9e27511e 100644 --- a/tests/compat/git/mod.rs +++ b/tests/compat/git/mod.rs @@ -136,6 +136,10 @@ fn 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(); } @@ -146,6 +150,11 @@ 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(); } @@ -181,6 +190,11 @@ 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(); } @@ -200,6 +214,10 @@ 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(); } diff --git a/tests/compat/gnu_patch/mod.rs b/tests/compat/gnu_patch/mod.rs index 3931a414..11454f47 100644 --- a/tests/compat/gnu_patch/mod.rs +++ b/tests/compat/gnu_patch/mod.rs @@ -64,6 +64,10 @@ fn missing_minus_header() { 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(); } @@ -77,6 +81,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(); } @@ -171,6 +178,10 @@ 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(); } @@ -178,6 +189,10 @@ fn fail_context_mismatch() { 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(); } @@ -185,6 +200,10 @@ fn fail_hunk_not_found() { 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(); } @@ -199,6 +218,10 @@ 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(); } @@ -208,5 +231,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(); } From 0c89a752a10718a273b30b0588697c54aafe29fb Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 18 Apr 2026 12:44:45 -0400 Subject: [PATCH 2/2] test(compat): update/remote redundant comments --- tests/compat/git/mod.rs | 13 +++---------- tests/compat/gnu_patch/mod.rs | 10 +++------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/tests/compat/git/mod.rs b/tests/compat/git/mod.rs index 9e27511e..b294cb11 100644 --- a/tests/compat/git/mod.rs +++ b/tests/compat/git/mod.rs @@ -127,9 +127,7 @@ 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") @@ -144,7 +142,6 @@ error: git diff header lacks filename information when removing 1 leading pathna } // 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") @@ -181,10 +178,7 @@ 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") @@ -207,8 +201,7 @@ 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") diff --git a/tests/compat/gnu_patch/mod.rs b/tests/compat/gnu_patch/mod.rs index 11454f47..f57c1603 100644 --- a/tests/compat/gnu_patch/mod.rs +++ b/tests/compat/gnu_patch/mod.rs @@ -56,10 +56,8 @@ 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") @@ -209,9 +207,7 @@ fn fail_truncated_file() { // 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() {