use indoc::indoc;
use mpatch::{
apply_hunk_to_lines, apply_patch_to_file, apply_patch_to_lines, apply_patches_to_dir,
detect_patch, find_hunk_location, find_hunk_location_in_lines, invert_patches, parse_auto,
parse_diffs, parse_patches, parse_patches_from_lines, patch_content_str,
try_apply_patch_to_content, try_apply_patch_to_file, try_apply_patch_to_lines, ApplyOptions,
DefaultHunkFinder, Hunk, HunkApplyError, HunkApplyStatus, HunkFinder, HunkLocation, MatchType,
ParseError, Patch, PatchError, PatchFormat, StrictApplyError,
};
use std::fs;
use tempfile::tempdir;
#[test]
fn test_parse_simple_diff() {
let diff = indoc! {"
Some text before.
```diff
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,5 @@
fn main() {
- println!(\"Hello, world!\");
+ println!(\"Hello, mpatch!\");
}
```
Some text after.
"};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "src/main.rs");
assert_eq!(patch.hunks.len(), 1);
assert!(patch.ends_with_newline);
let hunk = &patch.hunks[0];
assert_eq!(hunk.lines.len(), 4);
assert_eq!(
hunk.get_match_block(),
vec!["fn main() {", " println!(\"Hello, world!\");", "}"]
);
assert_eq!(
hunk.get_replace_block(),
vec!["fn main() {", " println!(\"Hello, mpatch!\");", "}"]
);
}
#[test]
fn test_parse_patch_block_header() {
let diff = indoc! {"
Some text before.
```patch
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,5 @@
fn main() {
- println!(\"Hello, world!\");
+ println!(\"Hello, mpatch!\");
}
```
Some text after.
"};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "src/main.rs");
assert_eq!(patch.hunks.len(), 1);
assert!(patch.ends_with_newline);
let hunk = &patch.hunks[0];
assert_eq!(hunk.lines.len(), 4);
assert_eq!(
hunk.get_match_block(),
vec!["fn main() {", " println!(\"Hello, world!\");", "}"]
);
assert_eq!(
hunk.get_replace_block(),
vec!["fn main() {", " println!(\"Hello, mpatch!\");", "}"]
);
}
#[test]
fn test_parse_flexible_diff_block_headers() {
let test_cases = vec![
"```diff,rust",
"```rust, diff",
"``` patch ",
"``` some info,patch,more info ",
"```diff", "```patch", "``` diff", "``` diff rust", ];
for header in test_cases {
let diff = format!(
"{}\n--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-a\n+b\n```",
header
);
let patches = parse_diffs(&diff).unwrap();
assert_eq!(patches.len(), 1, "Failed for header: {}", header);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
}
}
#[test]
fn test_parse_accepts_all_code_blocks() {
let test_cases = vec![
"```rust",
"```",
"``` dif", "``` patch-work", "```mydiff", "```different", "``` a,b,c", "```patchwork", ];
for header in test_cases {
let diff = format!(
"{}\n--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-a\n+b\n```",
header
);
let patches = parse_diffs(&diff).unwrap();
assert_eq!(
patches.len(),
1,
"Should have parsed block with header: {}",
header
);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
}
}
#[test]
fn test_parse_multiple_diff_blocks() {
let diff = indoc! {r#"
First change:
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
```
Second change:
```diff
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-baz
+qux
\ No newline at end of file
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(patches[0].hunks.len(), 1);
assert_eq!(patches[0].hunks[0].get_replace_block(), vec!["bar"]);
assert!(patches[0].ends_with_newline);
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2.txt");
assert_eq!(patches[1].hunks.len(), 1);
assert_eq!(patches[1].hunks[0].get_replace_block(), vec!["qux"]);
assert!(!patches[1].ends_with_newline);
}
#[test]
fn test_parse_multiple_files_in_one_block() {
let diff = indoc! {r#"
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-baz
+qux
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(patches[0].hunks.len(), 1);
assert_eq!(patches[0].hunks[0].get_replace_block(), vec!["bar"]);
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2.txt");
assert_eq!(patches[1].hunks.len(), 1);
assert_eq!(patches[1].hunks[0].get_replace_block(), vec!["qux"]);
}
#[test]
fn test_parse_multiple_sections_for_same_file_in_one_block() {
let diff = indoc! {r#"
```diff
--- a/same_file.txt
+++ b/same_file.txt
@@ -1 +1 @@
-hunk1
+hunk one
--- a/same_file.txt
+++ b/same_file.txt
@@ -10 +10 @@
-hunk2
+hunk two
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(
patches.len(),
1,
"Should produce a single patch for the same file"
);
assert_eq!(patches[0].file_path.to_str().unwrap(), "same_file.txt");
assert_eq!(patches[0].hunks.len(), 2, "Should contain two hunks");
assert_eq!(patches[0].hunks[0].get_replace_block(), vec!["hunk one"]);
assert_eq!(patches[0].hunks[1].get_replace_block(), vec!["hunk two"]);
}
#[test]
fn test_parse_file_creation_with_dev_null() {
let diff = indoc! {r#"
```diff
--- /dev/null
+++ b/new_from_null.txt
@@ -0,0 +1,2 @@
+hello
+world
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "new_from_null.txt");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].old_start_line, Some(0));
assert_eq!(patch.hunks[0].get_replace_block(), vec!["hello", "world"]);
assert!(patch.ends_with_newline);
}
#[test]
fn test_parse_file_creation_with_a_dev_null() {
let diff = indoc! {r#"
```diff
--- a/dev/null
+++ b/another_new.txt
@@ -0,0 +1 @@
+content
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "another_new.txt");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].old_start_line, Some(0));
assert_eq!(patch.hunks[0].get_replace_block(), vec!["content"]);
}
#[test]
fn test_parse_diff_without_ab_prefix() {
let diff = indoc! {r#"
```diff
--- path/to/file.txt
+++ path/to/file.txt
@@ -1 +1 @@
-old
+new
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "path/to/file.txt");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].old_start_line, Some(1));
assert_eq!(patch.hunks[0].get_replace_block(), vec!["new"]);
}
#[test]
fn test_parse_file_creation_without_b_prefix() {
let diff = indoc! {r#"
```diff
--- /dev/null
+++ new_file.txt
@@ -0,0 +1 @@
+content
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "new_file.txt");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].old_start_line, Some(0));
assert_eq!(patch.hunks[0].get_replace_block(), vec!["content"]);
}
#[test]
fn test_parse_error_on_missing_file_header() {
let diff = indoc! {"
Some text on line 1.
```diff
@@ -1,2 +1,2 @@
-foo
+bar
```
"};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_parse_patches_raw_diff() {
let raw_diff = indoc! {r#"
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(patches[0].hunks.len(), 1);
assert_eq!(patches[0].hunks[0].get_replace_block(), vec!["bar"]);
}
#[test]
fn test_parse_patches_multi_file_raw_diff() {
let raw_diff = indoc! {r#"
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-baz
+qux
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2.txt");
}
#[test]
fn test_parse_ignores_irrelevant_code_blocks() {
let content = indoc! {r#"
Here is some rust code that is not a patch:
```rust
fn main() {
println!("Not a patch");
}
```
Here is a list:
```text
- item 1
- item 2
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(
patches.is_empty(),
"Should not find patches in standard code blocks that lack diff signatures"
);
}
#[test]
fn test_parse_finds_patch_in_unlabeled_block() {
let content = indoc! {r#"
Here is a patch in a generic block:
```
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_parse_finds_patch_in_misleading_language_block() {
let content = indoc! {r#"
```python
--- a/script.py
+++ b/script.py
@@ -1 +1 @@
-print("old")
+print("new")
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "script.py");
}
#[test]
fn test_parse_mixed_content_robustness() {
let content = indoc! {r#"
Step 1: Update config
```toml
[package]
name = "demo"
```
Step 2: Apply this patch
```
--- a/src/main.rs
+++ b/src/main.rs
@@ -1 +1 @@
-fn main() {}
+fn main() { println!("hi"); }
```
Step 3: Run it
```bash
cargo run
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "src/main.rs");
}
#[test]
fn test_heuristic_skips_yaml_separators() {
let content = indoc! {r#"
```yaml
---
title: Not a diff
---
key: value
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_conflict_markers_in_rust_block() {
let content = indoc! {r#"
```rust
fn main() {
<<<<
old();
====
new();
>>>>
}
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].hunks[0].removed_lines(), vec![" old();"]);
assert_eq!(patches[0].hunks[0].added_lines(), vec![" new();"]);
}
#[test]
fn test_heuristic_trigger_but_invalid_diff_is_ignored() {
let content = indoc! {r#"
```
--- This looks like a header but isn't
Just some text
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_block_with_only_hunk_no_header_is_skipped() {
let content = indoc! {r#"
```
@@ -1 +1 @@
-foo
+bar
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_git_diff_header_triggers_parsing() {
let content = indoc! {r#"
```
diff --git a/file b/file
index 0000000..1111111
--- a/file
+++ b/file
@@ -1 +1 @@
-a
+b
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
}
#[test]
fn test_yaml_block_with_header_like_content_is_ignored() {
let content = indoc! {r#"
```yaml
--- title: Some YAML
key: value
---
other: value
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty(), "YAML block should not produce patches");
}
#[test]
fn test_crlf_line_endings() {
let content =
"```diff\r\n--- a/file.txt\r\n+++ b/file.txt\r\n@@ -1 +1 @@\r\n-old\r\n+new\r\n```";
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_heuristic_skips_indented_unified_headers() {
let content = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(
patches.is_empty(),
"Indented headers should be skipped by heuristic/parser"
);
}
#[test]
fn test_multiple_blocks_with_noise() {
let content = indoc! {r#"
# Documentation
Here is a config example (should be ignored):
```yaml
--- config
setting: true
```
Here is the actual fix (should be parsed):
```
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1 @@
-bug
+fix
```
Here is a comment about bitwise operators (should be ignored):
```rust
// We use << for shifting
let x = 1 << 4;
```
Here is a conflict marker block (should be parsed):
```
<<<<
old
====
new
>>>>
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "src/lib.rs");
assert_eq!(patches[0].hunks[0].added_lines(), vec!["fix"]);
assert_eq!(patches[1].file_path.to_str().unwrap(), "patch_target");
assert_eq!(patches[1].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_horizontal_rule_in_markdown_code_block() {
let content = indoc! {r#"
```markdown
Title
---
Content
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_diff_git_header_only_is_ignored() {
let content = indoc! {r#"
```
diff --git a/file b/file
index 123..456
(end of block, no changes)
```
"#};
let patches = parse_diffs(content).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_parse_patches_error_on_missing_header() {
let raw_diff = indoc! {r#"
@@ -1 +1 @@
-foo
+bar
"#};
assert!(matches!(
parse_patches(raw_diff),
Err(ParseError::MissingFileHeader { line: 1 })
));
}
#[test]
fn test_parse_patches_empty_input() {
let patches = parse_patches("").unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_parse_patches_whitespace_input() {
let patches = parse_patches(" \n\t\n ").unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_parse_patches_file_creation() {
let raw_diff = indoc! {r#"
--- /dev/null
+++ b/new_file.txt
@@ -0,0 +1,2 @@
+Hello
+World
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "new_file.txt");
assert!(patch.is_creation());
assert_eq!(patch.hunks[0].get_replace_block(), vec!["Hello", "World"]);
}
#[test]
fn test_parse_patches_file_deletion() {
let raw_diff = indoc! {r#"
--- a/old_file.txt
+++ b/old_file.txt
@@ -1,2 +0,0 @@
-Hello
-World
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "old_file.txt");
assert!(patch.is_deletion());
assert_eq!(patch.hunks[0].get_match_block(), vec!["Hello", "World"]);
assert!(patch.hunks[0].get_replace_block().is_empty());
}
#[test]
fn test_parse_patches_no_newline() {
let raw_diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-foo
+bar
\ No newline at end of file
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 1);
assert!(!patches[0].ends_with_newline);
}
#[test]
fn test_parse_patches_with_git_headers() {
let raw_diff = indoc! {r#"
diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1 +1 @@
-old
+new
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "src/main.rs");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].get_replace_block(), vec!["new"]);
}
#[test]
fn test_parse_patches_merges_sections_for_same_file() {
let raw_diff = indoc! {r#"
--- a/same_file.txt
+++ b/same_file.txt
@@ -1 +1 @@
-hunk1
+hunk one
--- a/same_file.txt
+++ b/same_file.txt
@@ -10 +10 @@
-hunk2
+hunk two
"#};
let patches = parse_patches(raw_diff).unwrap();
assert_eq!(
patches.len(),
1,
"Should merge sections into a single patch"
);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "same_file.txt");
assert_eq!(patch.hunks.len(), 2, "Should contain two hunks");
assert_eq!(patch.hunks[0].get_replace_block(), vec!["hunk one"]);
assert_eq!(patch.hunks[1].get_replace_block(), vec!["hunk two"]);
}
#[test]
fn test_parse_patches_from_lines() {
let raw_diff_lines = vec![
"--- a/src/main.rs",
"+++ b/src/main.rs",
"@@ -1,3 +1,3 @@",
" fn main() {",
"- println!(\"Hello, world!\");",
"+ println!(\"Hello, mpatch!\");",
" }",
];
let patches = parse_patches_from_lines(raw_diff_lines.into_iter()).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str(), Some("src/main.rs"));
assert_eq!(patches[0].hunks.len(), 1);
assert_eq!(
patches[0].hunks[0].added_lines(),
vec![" println!(\"Hello, mpatch!\");"]
);
}
#[test]
fn test_parse_patches_from_lines_error() {
let raw_diff_lines = vec![
"@@ -1,3 +1,3 @@",
"- println!(\"Hello, world!\");",
"+ println!(\"Hello, mpatch!\");",
];
let result = parse_patches_from_lines(raw_diff_lines.into_iter());
assert!(matches!(
result,
Err(ParseError::MissingFileHeader { line: 1 })
));
}
#[test]
fn test_apply_simple_patch() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "line one\nline two\nline three\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
line one
-line two
+line 2
line three
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line one\nline 2\nline three\n");
}
#[test]
fn test_apply_multiple_hunks_in_one_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("multi.txt");
let original_content = "Header\n\nunchanged line 1\n\nMiddle\n\nunchanged line 2\n\nFooter\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/multi.txt
+++ b/multi.txt
@@ -1,3 +1,3 @@
-Header
+New Header
unchanged line 1
@@ -7,3 +7,3 @@
unchanged line 2
-Footer
+New Footer
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
let expected_content =
"New Header\n\nunchanged line 1\n\nMiddle\n\nunchanged line 2\n\nNew Footer\n";
assert_eq!(content, expected_content);
}
#[test]
fn test_file_creation() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("new_file.txt");
let diff = indoc! {"
```diff
--- a/new_file.txt
+++ b/new_file.txt
@@ -0,0 +1,2 @@
+Hello
+New World
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "Hello\nNew World\n");
}
#[test]
fn test_patch_to_empty_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty.txt");
fs::write(&file_path, "").unwrap();
let diff = indoc! {"
```diff
--- a/empty.txt
+++ b/empty.txt
@@ -0,0 +1,2 @@
+line 1
+line 2
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line 1\nline 2\n");
}
#[test]
fn test_file_creation_in_subdirectory() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("src/new_file.txt");
let diff = indoc! {"
```diff
--- a/src/new_file.txt
+++ b/src/new_file.txt
@@ -0,0 +1 @@
+hello from subdir
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "hello from subdir\n");
}
#[test]
fn test_file_deletion_by_removing_all_content() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("delete_me.txt");
fs::write(&file_path, "line 1\nline 2\n").unwrap();
let diff = indoc! {"
```diff
--- a/delete_me.txt
+++ b/delete_me.txt
@@ -1,2 +0,0 @@
-line 1
-line 2
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
assert!(
!file_path.exists(),
"File should be deleted when content becomes empty"
);
}
#[test]
fn test_file_creation_empty_content_does_not_create_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty_create.txt");
let patch = Patch {
file_path: std::path::PathBuf::from("empty_create.txt"),
hunks: vec![Hunk {
lines: vec![], old_start_line: Some(0),
new_start_line: Some(0),
}],
ends_with_newline: false,
};
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(!file_path.exists(), "Empty file should not be created");
}
#[test]
fn test_dry_run_deletion_preserves_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("preserve_me.txt");
fs::write(&file_path, "content\n").unwrap();
let diff = indoc! {r#"
```diff
--- a/preserve_me.txt
+++ b/preserve_me.txt
@@ -1 +0,0 @@
-content
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::dry_run();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(file_path.exists(), "Dry run should not delete the file");
let content = fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "content\n");
}
#[test]
fn test_fuzzy_deletion_removes_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("fuzzy_delete.txt");
fs::write(&file_path, "content modified\n").unwrap();
let diff = indoc! {r#"
```diff
--- a/fuzzy_delete.txt
+++ b/fuzzy_delete.txt
@@ -1 +0,0 @@
-content original
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions {
dry_run: false,
fuzz_factor: 0.3,
};
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(
!file_path.exists(),
"File should be deleted even via fuzzy match if result is empty"
);
}
#[test]
fn test_creation_of_empty_file_is_skipped() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("ghost.txt");
let diff = indoc! {r#"
```diff
--- /dev/null
+++ b/ghost.txt
@@ -0,0 +0,0 @@
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(!file_path.exists());
}
#[test]
fn test_no_newline_at_end_of_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "line one\n").unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1 +1 @@
-line one
+line one no newline
\ No newline at end of file
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line one no newline");
}
#[test]
fn test_preserves_no_newline_when_patch_does_not_touch_eof() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("start_only.txt");
fs::write(&file_path, "line 1\nline 2").unwrap();
let diff = indoc! {r#"
```diff
--- a/start_only.txt
+++ b/start_only.txt
@@ -1 +1 @@
-line 1
+line one
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(
content, "line one\nline 2",
"Should preserve existing no-newline state if patch doesn't touch EOF"
);
}
#[test]
fn test_patch_content_str_preserves_no_newline_on_partial_patch() {
let original = "line 1\nline 2";
let diff = indoc! {r#"
```diff
--- a/file
+++ b/file
@@ -1 +1 @@
-line 1
+line one
```
"#};
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
assert_eq!(result, "line one\nline 2");
}
#[test]
fn test_fuzzy_match_succeeds() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "context A\nline two\ncontext C\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context one
-line two
+line 2
context three
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions {
dry_run: false,
fuzz_factor: 0.5,
};
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "context A\nline 2\ncontext C\n");
}
#[test]
fn test_fuzzy_match_with_internal_insertion() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(
&file_path,
"context A\ninserted line\nline to change\ncontext C\n",
)
.unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply by matching a slightly larger context block"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(
content,
"context A\ninserted line\nline was changed\ncontext C\n"
);
}
#[test]
fn test_match_with_different_trailing_whitespace() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("whitespace.txt");
fs::write(&file_path, "line one \nchange me\nline three\t\n").unwrap();
let diff = indoc! {"
```diff
--- a/whitespace.txt
+++ b/whitespace.txt
@@ -1,3 +1,3 @@
line one
-change me
+changed
line three
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply by ignoring trailing whitespace"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line one \nchanged\nline three\t\n");
}
#[test]
fn test_ambiguous_match_fails() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(
&file_path,
"header\nchange me\nfooter\n\nheader\nchange me\nfooter\n",
)
.unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -3,3 +3,3 @@
header
-change me
+changed
footer
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
!result.report.all_applied_cleanly(),
"Patch should have failed due to ambiguity"
);
assert!(matches!(
result.report.hunk_results[0],
HunkApplyStatus::Failed(HunkApplyError::AmbiguousExactMatch(_))
));
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(
content,
"header\nchange me\nfooter\n\nheader\nchange me\nfooter\n"
);
}
#[test]
fn test_ambiguous_fuzzy_match_fails() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content =
"section one\ncommon line\nDIFFERENT A\n\nsection two\ncommon line\nDIFFERENT B\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -3,3 +3,3 @@
section
-common line
+changed line
DIFFERENT
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions {
dry_run: false,
fuzz_factor: 0.5,
};
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
!result.report.all_applied_cleanly(),
"Patch should have failed due to fuzzy ambiguity"
);
assert!(matches!(
result.report.hunk_results[0],
HunkApplyStatus::Failed(HunkApplyError::AmbiguousFuzzyMatch(_))
));
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, original_content);
}
#[test]
fn test_dry_run() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "line one\nline two\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,2 @@
line one
-line two
+line 2
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::dry_run();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_some());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, original_content);
}
#[test]
fn test_path_traversal_is_blocked() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let diff = indoc! {"
```diff
--- a/../evil.txt
+++ b/../evil.txt
@@ -0,0 +1 @@
+hacked
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
let evil_path = dir.path().parent().unwrap().join("evil.txt");
assert!(!evil_path.exists());
}
#[test]
fn test_path_traversal_with_dot_is_blocked() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let diff = indoc! {"
```diff
--- a/./../evil.txt
+++ b/./../evil.txt
@@ -0,0 +1 @@
+hacked
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
let evil_path = dir.path().parent().unwrap().join("evil.txt");
assert!(!evil_path.exists());
}
#[test]
fn test_apply_to_nonexistent_file_fails_if_not_creation() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let diff = indoc! {"
```diff
--- a/missing.txt
+++ b/missing.txt
@@ -1 +1 @@
-foo
+bar
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(matches!(result, Err(PatchError::TargetNotFound(_))));
}
#[test]
fn test_partial_apply_fails_on_second_hunk() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("partial.txt");
let original_content = "line 1\nline 2\nline 3\n\nline 5\nline 6\nline 7\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/partial.txt
+++ b/partial.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
@@ -5,3 +5,3 @@
line 5
-line WRONG
+line six
line 7
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(!result.report.all_applied_cleanly());
let failures = result.report.failures();
assert_eq!(failures.len(), 1);
assert_eq!(failures[0].hunk_index, 2);
assert!(matches!(
failures[0].reason,
HunkApplyError::ContextNotFound
));
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(result.report.hunk_results.len(), 2);
assert!(
matches!(&result.report.hunk_results[0], HunkApplyStatus::Applied { replaced_lines, .. } if replaced_lines.as_slice() == ["line 1", "line 2", "line 3"])
);
assert!(matches!(
result.report.hunk_results[1],
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound)
));
let expected_content_after_first_hunk = "line 1\nline two\nline 3\n\nline 5\nline 6\nline 7\n";
assert_eq!(content, expected_content_after_first_hunk);
}
#[test]
fn test_creation_patch_fails_on_non_empty_file() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("exists.txt");
fs::write(&file_path, "I already exist.\n").unwrap();
let diff = indoc! {"
```diff
--- a/exists.txt
+++ b/exists.txt
@@ -0,0 +1 @@
+new content
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
!result.report.all_applied_cleanly(),
"Creation patch should fail on a non-empty file"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "I already exist.\n", "File should be unchanged");
}
#[test]
fn test_hunk_with_no_changes_is_skipped() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "line 1\nline 2\nline 3\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
line 1
line 2
line 3
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
assert!(!patch.hunks[0].has_changes());
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch with no changes should apply successfully"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, original_content, "File should be unchanged");
}
#[test]
fn test_parse_empty_diff_block() {
let diff = indoc! {"
Some text.
```diff
```
More text.
"};
let patches = parse_diffs(diff).unwrap();
assert!(
patches.is_empty(),
"Parsing an empty diff block should result in no patches"
);
}
#[test]
fn test_parse_diff_block_with_header_only() {
let diff = indoc! {"
```diff
--- a/some_file.txt
+++ b/some_file.txt
```
"};
let patches = parse_diffs(diff).unwrap();
assert!(
patches.is_empty(),
"Parsing a diff block with only a header should result in no patches"
);
}
#[test]
fn test_indented_diff_block_is_ignored() {
let diff = r#"
This should not be parsed.
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-a
+b
```
"#;
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty(), "Indented diff blocks should be ignored");
}
#[test]
fn test_find_hunk_location_in_lines() {
let original_lines = vec!["line 1", "line two", "line 3"];
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line two
+line 2
line 3
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = ApplyOptions::exact();
let (location, match_type) =
find_hunk_location_in_lines(hunk, &original_lines, &options).unwrap();
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 3
}
);
assert!(matches!(match_type, MatchType::Exact));
let original_lines_string: Vec<String> = original_lines.iter().map(|s| s.to_string()).collect();
let (location2, match_type2) =
find_hunk_location_in_lines(hunk, &original_lines_string, &options).unwrap();
assert_eq!(location, location2);
assert_eq!(match_type, match_type2);
}
#[test]
fn test_apply_patch_to_lines() {
let original_lines = vec!["Hello, world!"];
let diff_str = [
"```diff",
"--- a/hello.txt",
"+++ b/hello.txt",
"@@ -1 +1 @@",
"-Hello, world!",
"+Hello, mpatch!",
"```",
]
.join("\n");
let patches = parse_diffs(&diff_str).unwrap();
let patch = &patches[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_lines(patch, Some(&original_lines), &options);
assert_eq!(result.new_content, "Hello, mpatch!\n");
assert!(result.report.all_applied_cleanly());
}
#[test]
fn test_apply_hunk_to_lines_in_place() {
let mut original_lines = vec![
"line 1".to_string(),
"line two".to_string(),
"line 3".to_string(),
];
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line two
+line 2
line 3
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = ApplyOptions::exact();
let status = apply_hunk_to_lines(hunk, &mut original_lines, &options);
assert!(
matches!(status, HunkApplyStatus::Applied { replaced_lines, .. } if replaced_lines.as_slice() == ["line 1", "line two", "line 3"])
);
assert_eq!(original_lines, vec!["line 1", "line 2", "line 3"]);
let mut failing_lines = vec!["completely".to_string(), "different".to_string()];
let fail_status = apply_hunk_to_lines(hunk, &mut failing_lines, &options);
assert!(matches!(
fail_status,
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound)
));
assert_eq!(failing_lines, vec!["completely", "different"]);
}
#[test]
fn test_hunk_applier_iterator() {
let original_content = "line 1\nline 2\nline 3\n\nline 5\nline 6\nline 7\n";
let original_lines: Vec<_> = original_content.lines().collect();
let diff = indoc! {r#"
```diff
--- a/partial.txt
+++ b/partial.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
@@ -5,3 +5,3 @@
line 5
-line WRONG
+line six
line 7
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let mut applier = mpatch::HunkApplier::new(patch, Some(&original_lines), &options);
let status1 = applier.next().unwrap();
assert!(
matches!(status1, HunkApplyStatus::Applied { replaced_lines, .. } if replaced_lines.as_slice() == ["line 1", "line 2", "line 3"])
);
assert_eq!(
applier.current_lines(),
&["line 1", "line two", "line 3", "", "line 5", "line 6", "line 7"]
);
let status2 = applier.next().unwrap();
assert!(matches!(
status2,
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound)
));
assert_eq!(
applier.current_lines(),
&["line 1", "line two", "line 3", "", "line 5", "line 6", "line 7"]
);
assert!(applier.next().is_none());
let new_content = applier.into_content();
let expected_content = "line 1\nline two\nline 3\n\nline 5\nline 6\nline 7\n";
assert_eq!(new_content, expected_content);
}
#[test]
fn test_fuzzy_match_below_threshold_fails() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "completely different content\nthat has no resemblance\nto the patch\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context one
-line two
+line 2
context three
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions {
dry_run: false,
fuzz_factor: 0.9,
};
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
!result.report.all_applied_cleanly(),
"Patch should fail to apply as no hunk meets the fuzzy threshold"
);
assert!(matches!(
result.report.hunk_results[0],
HunkApplyStatus::Failed(HunkApplyError::FuzzyMatchBelowThreshold { .. })
));
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, original_content, "File should be unchanged");
}
#[test]
fn test_find_hunk_location_exact_match() {
let original_content = "line 1\nline two\nline 3\n";
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line two
+line 2
line 3
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = ApplyOptions::exact();
let (location, match_type) = find_hunk_location(hunk, original_content, &options).unwrap();
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 3
}
);
assert!(matches!(match_type, MatchType::Exact));
}
#[test]
fn test_find_hunk_location_fuzzy_match() {
let original_content = "context A\ninserted line\nline to change\ncontext C\n";
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = ApplyOptions::new();
let (location, match_type) = find_hunk_location(hunk, original_content, &options).unwrap();
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 4
}
);
assert!(matches!(match_type, MatchType::Fuzzy { .. }));
}
#[test]
fn test_find_hunk_location_not_found() {
let original_content = "completely different content\n";
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-foo
+bar
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = mpatch::ApplyOptions {
fuzz_factor: 0.9,
..Default::default()
};
let result = find_hunk_location(hunk, original_content, &options);
assert!(matches!(
result,
Err(HunkApplyError::FuzzyMatchBelowThreshold { .. })
));
}
#[test]
fn test_find_hunk_location_ambiguous() {
let original_content = "duplicate\n\nduplicate\n";
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -2,1 +2,1 @@
-duplicate
+changed
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
let options = ApplyOptions::exact();
let result = find_hunk_location(hunk, original_content, &options);
assert!(matches!(
result,
Err(HunkApplyError::AmbiguousExactMatch(_))
));
}
#[test]
#[cfg(unix)] fn test_apply_to_readonly_file_fails() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("readonly.txt");
let original_content = "don't change me\n";
fs::write(&file_path, original_content).unwrap();
let original_perms = fs::metadata(&file_path).unwrap().permissions();
let mut perms = original_perms.clone();
perms.set_readonly(true);
fs::set_permissions(&file_path, perms).unwrap();
let diff = indoc! {"
```diff
--- a/readonly.txt
+++ b/readonly.txt
@@ -1 +1 @@
-don't change me
+I tried to change you
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(
matches!(result, Err(PatchError::PermissionDenied { .. })),
"Applying patch to a read-only file should result in a PermissionDenied error"
);
fs::set_permissions(&file_path, original_perms).unwrap();
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(
content, original_content,
"Read-only file should not be changed"
);
}
#[test]
fn test_apply_to_path_that_is_a_directory() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let dir_as_file_path = dir.path().join("a_directory");
fs::create_dir(&dir_as_file_path).unwrap();
let diff = indoc! {"
```diff
--- a/a_directory
+++ b/a_directory
@@ -1 +1 @@
-foo
+bar
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(
matches!(result, Err(PatchError::TargetIsDirectory { .. })),
"Applying patch to a path that is a directory should fail with TargetIsDirectory"
);
}
#[test]
fn test_file_creation_with_spaces_in_path() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("a file with spaces.txt");
let diff = indoc! {"
```diff
--- a/a file with spaces.txt
+++ b/a file with spaces.txt
@@ -0,0 +1 @@
+content
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should be applied successfully"
);
assert!(result.diff.is_none());
assert!(
file_path.exists(),
"File with spaces in name should be created"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "content\n");
}
#[test]
fn test_apply_hunk_to_file_beginning() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "line 1\nline 2\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,3 @@
+new first line
line 1
line 2
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "new first line\nline 1\nline 2\n");
}
#[test]
fn test_apply_hunk_to_file_end() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "line 1\nline 2\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,3 @@
line 1
line 2
+new last line
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line 1\nline 2\nnew last line\n");
}
#[test]
fn test_parse_diff_with_git_headers() {
let diff = indoc! {r#"
```diff
diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
fn main() {
- println!("Hello, world!");
+ println!("Hello, mpatch!");
}
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "src/main.rs");
assert_eq!(patch.hunks.len(), 1);
assert_eq!(
patch.hunks[0].get_replace_block(),
vec!["fn main() {", " println!(\"Hello, mpatch!\");", "}"]
);
}
#[test]
fn test_path_normalization_within_project() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let src_dir = dir.path().join("src");
fs::create_dir(&src_dir).unwrap();
let file_path = dir.path().join("main.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let diff = indoc! {"
```diff
--- a/src/../main.rs
+++ b/src/../main.rs
@@ -1 +1 @@
-fn main() {}
+fn main() { /* changed */ }
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch with '..' that resolves inside the project should apply"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "fn main() { /* changed */ }\n");
}
#[test]
fn test_apply_hunk_with_single_line_match_block() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "unique_line\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,1 +1,1 @@
-unique_line
+changed_line
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
assert_eq!(patch.hunks[0].get_match_block(), vec!["unique_line"]);
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "changed_line\n");
}
#[test]
fn test_file_creation_with_unicode_path() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_name = "文件.txt";
let file_path = dir.path().join(file_name);
let diff = format!(
indoc! {r#"
```diff
--- a/{}
+++ b/{}
@@ -0,0 +1 @@
+内容
```
"#},
file_name, file_name
);
let patch = &parse_diffs(&diff).unwrap()[0];
assert_eq!(patch.file_path.to_str().unwrap(), file_name);
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should be applied successfully"
);
assert!(result.diff.is_none());
assert!(
file_path.exists(),
"File with unicode name should be created"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "内容\n");
}
#[test]
#[cfg(unix)] fn test_path_traversal_with_absolute_path_is_blocked() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let diff = indoc! {"
```diff
--- a//etc/evil.txt
+++ b//etc/evil.txt
@@ -0,0 +1 @@
+hacked
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options);
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
}
#[test]
fn test_apply_patch_where_file_is_prefix_of_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "line 1\nline 2\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
line 1
line 2
-line 3
+line three
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply via end-of-file fuzzy logic"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line 1\nline 2\nline three\n");
}
#[test]
fn test_apply_patch_at_end_of_file_with_fuzz_and_missing_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.rs");
fs::write(&file_path, "fn main() {\n println!(\"Hello\");\n").unwrap();
let diff = indoc! {r#"
```diff
--- a/test.rs
+++ b/test.rs
@@ -1,4 +1,5 @@
fn main() {
println!("Hello");
}
+ println!("World");
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply via end-of-file fuzzy logic"
);
let content = fs::read_to_string(file_path).unwrap();
let expected_content = "fn main() {\n println!(\"Hello\");\n}\n println!(\"World\");\n";
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_missing_line_in_patch_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "line A\nline B\nline C\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,2 @@
line A
-line C
+line changed
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "line A\nline B\nline changed\n";
if !result.report.all_applied_cleanly() || content != expected_content {
eprintln!(
"\n\n--- DIAGNOSTICS FOR `test_fuzzy_match_with_missing_line_in_patch_context` ---\n"
);
eprintln!("Original Content:\n```\n{}\n```", original_content);
eprintln!("Patch:\n```diff\n{}\n```", diff);
eprintln!("Apply Result: {:#?}", result.report);
eprintln!("Expected Content:\n```\n{}\n```", expected_content);
eprintln!("Actual Content:\n```\n{}\n```\n", content);
}
assert!(
result.report.all_applied_cleanly(),
"Patch should apply successfully despite a missing line in its context"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_finder_with_missing_line_in_patch_context() {
let _ = env_logger::builder().is_test(true).try_init();
let options = ApplyOptions::new();
let finder = DefaultHunkFinder::new(&options);
let hunk = parse_diffs(indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,2 @@
line A
-line C
+line changed
```
"})
.unwrap()
.remove(0)
.hunks
.remove(0);
let target_lines = vec!["line A", "line B", "line C"];
let (location, match_type) = finder.find_location(&hunk, &target_lines).unwrap();
assert!(matches!(match_type, MatchType::Fuzzy { .. }));
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 3
},
"Finder should have matched all three lines to account for the insertion"
);
}
#[test]
fn test_fuzzy_match_with_duplicated_context_line_insertion() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "line A\nline A\nline C\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,2 @@
line A
-line C
+line changed
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "line A\nline A\nline changed\n";
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly even with duplicated context lines"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_more_context_and_insertion() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "header\nline A\nline B\nline C\nfooter\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,4 +1,4 @@
header
line A
-line C
+line changed
footer
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "header\nline A\nline B\nline changed\nfooter\n";
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly with more context"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_insertion_at_hunk_start() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "extra line\ncontext A\nline to change\ncontext C\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "extra line\ncontext A\nline was changed\ncontext C\n";
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly with insertion at hunk start"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_smart_indentation_tabs_to_tabs_multiple_levels() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("tabs_multi.py");
let original_content = "def main():\n\tif True:\n\t\tprint(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/tabs_multi.py
+++ b/tabs_multi.py
@@ -1,3 +1,4 @@
def main():
if True:
print("hello")
+ print("world")
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n\tif True:\n\t\tprint(\"hello\")\n\t\tprint(\"world\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_after_empty_line() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty_line.py");
let original_content = "def main():\n\tprint(\"hello\")\n\n\tprint(\"world\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/empty_line.py
+++ b/empty_line.py
@@ -1,4 +1,5 @@
def main():
print("hello")
+ print("inserted")
print("world")
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n\tprint(\"hello\")\n\n\tprint(\"inserted\")\n\tprint(\"world\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_empty_lines_stripped() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty_lines.py");
let original_content = "def main():\n print(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = format!(
"```diff\n--- a/empty_lines.py\n+++ b/empty_lines.py\n@@ -1,2 +1,4 @@\n def main():\n print(\"hello\")\n+{spaces}\n+ print(\"world\")\n```",
spaces = " "
);
let patches = parse_diffs(&diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n print(\"hello\")\n\n print(\"world\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_nested_markdown_list_spaces_to_tabs() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("nested_tabs.py");
let original_content = "def main():\n\tprint(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = format!(
"```diff\n--- a/nested_tabs.py\n+++ b/nested_tabs.py\n@@ -1,2 +1,4 @@\n{s3}def main():\n{s7}print(\"hello\")\n+{s6}if True:\n+{s10}print(\"world\")\n```",
s3 = " ",
s7 = " ",
s6 = " ",
s10 = " "
);
let patches = parse_diffs(&diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n\tprint(\"hello\")\n\t if True:\n\t\t print(\"world\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_preserved_across_unindented_lines() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("unindented.py");
let original_content =
"class Foo:\n\tdef __init__(self):\n\t\tpass\n\ndef main():\n\tprint(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/unindented.py
+++ b/unindented.py
@@ -2,4 +2,5 @@
def __init__(self):
pass
def main():
+ print("setup")
print("hello")
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "class Foo:\n\tdef __init__(self):\n\t\tpass\n\ndef main():\n\tprint(\"setup\")\n\tprint(\"hello\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_mixed_style_fallback() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("mixed.py");
let original_content = "def main():\n\tprint(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = "```diff\n--- a/mixed.py\n+++ b/mixed.py\n@@ -1,2 +1,3 @@\n def main():\n \t print(\"hello\")\n+\t print(\"world\")\n```";
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n\tprint(\"hello\")\n\t print(\"world\")\n";
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_match_with_insertion_at_hunk_end() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "context A\nline to change\ncontext C\nextra line\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "context A\nline was changed\ncontext C\nextra line\n";
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly with insertion at hunk end"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_multiple_insertions() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "context A\nextra line 1\nline to change\nextra line 2\ncontext C\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = "context A\nextra line 1\nline was changed\nextra line 2\ncontext C\n";
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly with multiple insertions"
);
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_extra_line_in_patch_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "line A\nline C\n").unwrap();
let diff = indoc! {"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,2 @@
line A
line B
-line C
+line changed
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply successfully despite an extra line in its context"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line A\nline changed\n");
}
#[test]
fn test_fuzzy_match_preserves_different_file_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "context in file (A)\nline to change\ncontext in file (C)\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
context in patch (A)
-line to change
+line was changed
context in patch (C)
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly via fuzzy match"
);
let content = fs::read_to_string(file_path).unwrap();
let expected_content = "context in file (A)\nline was changed\ncontext in file (C)\n";
assert_eq!(content, expected_content);
}
#[test]
fn test_fuzzy_match_with_multiple_differences_preserves_context() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = indoc! {"
line A
line B (in file)
line C (to be changed)
line D (in file)
line E
"};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -1,4 +1,4 @@
line A
line B (in patch)
-line C (to be changed)
+line C (was changed)
line D (in patch)
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly via fuzzy match"
);
let content = fs::read_to_string(file_path).unwrap();
let expected_content = indoc! {"
line A
line B (in file)
line C (was changed)
line D (in file)
line E
"};
assert_eq!(content, expected_content);
}
#[test]
fn test_parse_hunk_header_line_number() {
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +2,3 @@
a
-b
+c
d
@@ -10,1 +12,1 @@
-x
+y
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.hunks.len(), 2);
assert_eq!(patch.hunks[0].old_start_line, Some(1));
assert_eq!(patch.hunks[0].new_start_line, Some(2));
assert_eq!(patch.hunks[1].old_start_line, Some(10));
assert_eq!(patch.hunks[1].new_start_line, Some(12));
}
#[test]
fn test_ambiguous_match_resolved_by_line_number() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = indoc! {"
// Block 1
fn duplicate() {
println!(\"hello\");
}
// Block 2
fn duplicate() {
println!(\"hello\");
}
"};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -7,3 +7,3 @@
fn duplicate() {
- println!("hello");
+ println!("world");
}
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
assert_eq!(patch.hunks[0].old_start_line, Some(7));
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should have applied successfully using line number hint"
);
let content = fs::read_to_string(file_path).unwrap();
let expected_content = indoc! {"
// Block 1
fn duplicate() {
println!(\"hello\");
}
// Block 2
fn duplicate() {
println!(\"world\");
}
"};
assert_eq!(content, expected_content);
}
#[test]
fn test_ambiguous_match_fails_with_equidistant_line_hint() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original_content = "duplicate\n\nduplicate\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/test.txt
+++ b/test.txt
@@ -2,1 +2,1 @@
-duplicate
+changed
```
"#};
let patch = &parse_diffs(diff).unwrap()[0];
assert_eq!(patch.hunks[0].old_start_line, Some(2));
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(
!result.report.all_applied_cleanly(),
"Patch should fail due to unresolved ambiguity"
);
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, original_content, "File should be unchanged");
}
#[test]
fn test_hunk_semantic_helpers() {
let hunk = mpatch::Hunk {
lines: vec![
" context 1".to_string(),
"-removed 1".to_string(),
"-removed 2".to_string(),
"+added 1".to_string(),
" context 2".to_string(),
],
old_start_line: Some(1),
new_start_line: Some(1),
};
assert_eq!(hunk.context_lines(), vec!["context 1", "context 2"]);
assert_eq!(hunk.added_lines(), vec!["added 1"]);
assert_eq!(hunk.removed_lines(), vec!["removed 1", "removed 2"]);
}
#[test]
fn test_patch_is_creation() {
let creation_diff = indoc! {r#"
```diff
--- a/new_file.txt
+++ b/new_file.txt
@@ -0,0 +1,2 @@
+Hello
+World
```
"#};
let patches = parse_diffs(creation_diff).unwrap();
assert!(patches[0].is_creation());
let modification_diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-foo
+bar
```
"#};
let patches = parse_diffs(modification_diff).unwrap();
assert!(!patches[0].is_creation());
}
#[test]
fn test_patch_is_deletion() {
let deletion_diff = indoc! {r#"
```diff
--- a/old_file.txt
+++ b/old_file.txt
@@ -1,2 +0,0 @@
-Hello
-World
```
"#};
let patches = parse_diffs(deletion_diff).unwrap();
assert!(patches[0].is_deletion());
let modification_diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-foo
+bar
```
"#};
let patches = parse_diffs(modification_diff).unwrap();
assert!(!patches[0].is_deletion());
let partial_removal_diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,1 @@
-foo
-bar
baz
```
"#};
let patches = parse_diffs(partial_removal_diff).unwrap();
assert!(!patches[0].is_deletion());
}
#[test]
fn test_apply_options_builder() {
let options = ApplyOptions::builder()
.dry_run(true)
.fuzz_factor(0.99)
.build();
assert!(options.dry_run);
assert_eq!(options.fuzz_factor, 0.99);
let default_options = ApplyOptions::builder().build();
assert!(!default_options.dry_run);
assert_eq!(default_options.fuzz_factor, 0.7);
}
#[test]
fn test_apply_options_convenience_constructors() {
let new_options = ApplyOptions::new();
assert!(!new_options.dry_run);
assert_eq!(new_options.fuzz_factor, 0.7);
let dry_run_options = ApplyOptions::dry_run();
assert!(dry_run_options.dry_run);
assert_eq!(dry_run_options.fuzz_factor, 0.7);
}
#[test]
fn test_apply_options_fluent_methods() {
let options = ApplyOptions::new().with_dry_run(true).with_fuzz_factor(0.9);
assert!(options.dry_run);
assert_eq!(options.fuzz_factor, 0.9);
let options2 = options.with_dry_run(false);
assert!(options.dry_run, "Original options should be unchanged");
assert!(
!options2.dry_run,
"New options should have dry_run set to false"
);
assert_eq!(
options2.fuzz_factor, 0.9,
"Other fields should be preserved"
);
let options3 = options2.with_fuzz_factor(0.1);
assert_eq!(
options2.fuzz_factor, 0.9,
"Original options should be unchanged"
);
assert_eq!(
options3.fuzz_factor, 0.1,
"New options should have new fuzz factor"
);
assert!(!options3.dry_run, "Other fields should be preserved");
}
#[test]
fn test_patch_from_texts() {
let old_text = "hello\nworld\n";
let new_text = "hello\nrust\n";
let patch = Patch::from_texts("file.txt", old_text, new_text, 3).unwrap();
assert_eq!(patch.file_path.to_str(), Some("file.txt"));
assert_eq!(patch.hunks.len(), 1);
let hunk = &patch.hunks[0];
assert_eq!(hunk.context_lines(), vec!["hello"]);
assert_eq!(hunk.removed_lines(), vec!["world"]);
assert_eq!(hunk.added_lines(), vec!["rust"]);
}
#[test]
fn test_patch_from_texts_no_change() {
let old_text = "hello\nworld\n";
let patch = Patch::from_texts("file.txt", old_text, old_text, 3).unwrap();
assert!(patch.hunks.is_empty());
}
#[test]
fn test_patch_inversion() {
let old_text = "line 1\nline 2\n";
let new_text = "line 1\nline two\n";
let patch = Patch::from_texts("file.txt", old_text, new_text, 3).unwrap();
let inverted_patch = patch.invert();
assert_eq!(inverted_patch.hunks.len(), 1);
let inverted_hunk = &inverted_patch.hunks[0];
assert_eq!(inverted_hunk.removed_lines(), vec!["line two"]);
assert_eq!(inverted_hunk.added_lines(), vec!["line 2"]);
let dir = tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs::write(&file_path, old_text).unwrap();
apply_patch_to_file(&patch, dir.path(), ApplyOptions::new()).unwrap();
let content_after_patch = fs::read_to_string(&file_path).unwrap();
assert_eq!(content_after_patch, new_text);
apply_patch_to_file(&inverted_patch, dir.path(), ApplyOptions::new()).unwrap();
let content_after_inversion = fs::read_to_string(&file_path).unwrap();
assert_eq!(content_after_inversion, old_text);
}
#[test]
fn test_apply_patches_to_dir() {
let dir = tempdir().unwrap();
let file1_path = dir.path().join("file1.txt");
let file2_path = dir.path().join("file2.txt");
fs::write(&file1_path, "foo\n").unwrap();
fs::write(&file2_path, "baz\n").unwrap();
let diff = indoc! {r#"
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-baz
+qux
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 2);
let batch_result = apply_patches_to_dir(&patches, dir.path(), ApplyOptions::new());
assert!(batch_result.all_succeeded());
assert!(batch_result.hard_failures().is_empty());
assert_eq!(batch_result.results.len(), 2);
let content1 = fs::read_to_string(file1_path).unwrap();
let content2 = fs::read_to_string(file2_path).unwrap();
assert_eq!(content1, "bar\n");
assert_eq!(content2, "qux\n");
}
mod ensure_path_is_safe_tests {
use mpatch::{ensure_path_is_safe, PatchError};
use std::fs;
use tempfile::tempdir;
#[test]
fn test_safe_path_succeeds() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let safe_path = "src/main.rs";
fs::create_dir_all(base_dir.join("src")).unwrap();
fs::write(base_dir.join(safe_path), "content").unwrap();
let result = ensure_path_is_safe(base_dir, safe_path.as_ref());
assert!(result.is_ok());
let resolved_path = result.unwrap();
assert!(resolved_path.ends_with(safe_path));
assert!(resolved_path.is_absolute());
}
#[test]
fn test_safe_path_to_nonexistent_file_succeeds() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let safe_path = "new/file.txt";
let result = ensure_path_is_safe(base_dir, safe_path.as_ref());
assert!(result.is_ok());
let resolved_path = result.unwrap();
assert!(resolved_path.ends_with(safe_path));
assert!(resolved_path.is_absolute());
assert!(base_dir.join("new").is_dir());
}
#[test]
fn test_traversal_path_fails() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let unsafe_path = "../evil.txt";
let result = ensure_path_is_safe(base_dir, unsafe_path.as_ref());
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
}
#[test]
fn test_traversal_path_to_nonexistent_file_fails() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let unsafe_path = "src/../../evil.txt";
let result = ensure_path_is_safe(base_dir, unsafe_path.as_ref());
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
}
#[test]
#[cfg(unix)]
fn test_absolute_path_fails() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
let unsafe_path = "/etc/passwd";
let result = ensure_path_is_safe(base_dir, unsafe_path.as_ref());
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
}
#[test]
fn test_path_normalization_within_project_succeeds() {
let dir = tempdir().unwrap();
let base_dir = dir.path();
fs::create_dir(base_dir.join("src")).unwrap();
let normalized_path = "src/../main.rs";
fs::write(base_dir.join("main.rs"), "content").unwrap();
let result = ensure_path_is_safe(base_dir, normalized_path.as_ref());
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.ends_with("main.rs"));
}
#[test]
fn test_path_traversal_does_not_create_directories() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let base_dir = dir.path().join("base");
fs::create_dir(&base_dir).unwrap();
let diff = indoc::indoc! {"
```diff
--- a/../evil_dir/evil.txt
+++ b/../evil_dir/evil.txt
@@ -0,0 +1 @@
+hacked
```
"};
let patch = &mpatch::parse_diffs(diff).unwrap()[0];
let options = mpatch::ApplyOptions::exact();
let result = mpatch::apply_patch_to_file(patch, &base_dir, options);
assert!(matches!(result, Err(PatchError::PathTraversal(_))));
let evil_dir = dir.path().join("evil_dir");
assert!(
!evil_dir.exists(),
"VULNERABILITY: Directory was created outside base_dir!"
);
}
}
mod hunk_finder_tests {
use super::*; use mpatch::Hunk;
fn setup_hunk(diff_content: &str) -> Hunk {
parse_diffs(diff_content).unwrap().remove(0).hunks.remove(0)
}
#[test]
fn test_default_finder_exact_match() {
let options = ApplyOptions::exact();
let finder = DefaultHunkFinder::new(&options);
let hunk = setup_hunk(indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line two
+line 2
line 3
```
"#});
let target_lines = vec!["line 1", "line two", "line 3"];
let (location, match_type) = finder.find_location(&hunk, &target_lines).unwrap();
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 3
}
);
assert!(matches!(match_type, MatchType::Exact));
}
#[test]
fn test_default_finder_fuzzy_match() {
let options = ApplyOptions::new();
let finder = DefaultHunkFinder::new(&options);
let hunk = setup_hunk(indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
context A
-line to change
+line was changed
context C
```
"#});
let target_lines = vec!["context A", "inserted line", "line to change", "context C"];
let (location, match_type) = finder.find_location(&hunk, &target_lines).unwrap();
assert_eq!(
location,
HunkLocation {
start_index: 0,
length: 4
}
);
assert!(matches!(match_type, MatchType::Fuzzy { .. }));
}
#[test]
fn test_default_finder_not_found() {
let options = ApplyOptions {
fuzz_factor: 0.9,
..Default::default()
};
let finder = DefaultHunkFinder::new(&options);
let hunk = setup_hunk(indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-foo
+bar
```
"#});
let target_lines = vec!["completely", "different", "content"];
let result = finder.find_location(&hunk, &target_lines);
assert!(matches!(
result,
Err(HunkApplyError::FuzzyMatchBelowThreshold { .. })
));
}
#[test]
fn test_default_finder_ambiguous_match() {
let options = ApplyOptions::exact();
let finder = DefaultHunkFinder::new(&options);
let hunk = setup_hunk(indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -2,1 +2,1 @@
-duplicate
+changed
```
"#});
let target_lines = vec!["duplicate", "", "duplicate"];
let result = finder.find_location(&hunk, &target_lines);
assert!(matches!(
result,
Err(HunkApplyError::AmbiguousExactMatch(_))
));
}
}
#[cfg(test)]
mod fuzzy_finder_diagnostics {
use mpatch::{ApplyOptions, DefaultHunkFinder, Hunk, HunkFinder, HunkLocation, MatchType};
#[test]
fn test_apply_options_convenience_constructors() {
let new_options = ApplyOptions::new();
assert!(!new_options.dry_run);
assert_eq!(new_options.fuzz_factor, 0.7);
let dry_run_options = ApplyOptions::dry_run();
assert!(dry_run_options.dry_run);
assert_eq!(dry_run_options.fuzz_factor, 0.7);
let exact_options = ApplyOptions::exact();
assert!(!exact_options.dry_run);
assert_eq!(exact_options.fuzz_factor, 0.0);
}
fn assert_fuzzy_location(
hunk_match_block: &[&str],
target_lines: &[&str],
expected_location: HunkLocation,
fuzz_factor: f32,
) {
let options = ApplyOptions {
fuzz_factor,
..Default::default()
};
let finder = DefaultHunkFinder::new(&options);
let hunk = Hunk {
lines: hunk_match_block.iter().map(|s| format!(" {}", s)).collect(), old_start_line: Some(1),
new_start_line: Some(1),
};
let result = finder.find_location(&hunk, &target_lines.iter().collect::<Vec<_>>());
match result {
Ok((location, match_type)) => {
assert!(
matches!(
match_type,
MatchType::Fuzzy { .. } | MatchType::ExactIgnoringWhitespace
),
"Match was not fuzzy or whitespace-insensitive as expected. Was: {:?}",
match_type
);
assert_eq!(
location, expected_location,
"Fuzzy location did not match expectation"
);
}
Err(e) => {
panic!("Finder failed when a fuzzy match was expected: {:?}", e);
}
}
}
#[test]
fn finder_single_insertion_middle() {
assert_fuzzy_location(
&["line A", "line C"],
&["line A", "line B", "line C"],
HunkLocation {
start_index: 0,
length: 3,
},
0.7,
);
}
#[test]
fn finder_single_insertion_start() {
let options = ApplyOptions::new();
let finder = DefaultHunkFinder::new(&options);
let hunk = Hunk {
lines: vec![" line A".to_string(), " line B".to_string()],
old_start_line: Some(1),
new_start_line: Some(1),
};
let target_lines = vec!["extra line", "line A", "line B"];
let (location, match_type) = finder.find_location(&hunk, &target_lines).unwrap();
assert!(
matches!(match_type, MatchType::Exact),
"Should have found an exact match, not {:?}",
match_type
);
assert_eq!(
location,
HunkLocation {
start_index: 1,
length: 2,
},
"Exact match location is incorrect"
);
}
#[test]
fn finder_single_deletion_middle() {
assert_fuzzy_location(
&["line A", "line B", "line C"],
&["line A", "line C"],
HunkLocation {
start_index: 0,
length: 2,
},
0.7,
);
}
#[test]
fn finder_multiple_insertions() {
assert_fuzzy_location(
&["context A", "line to change", "context C"],
&[
"context A",
"extra line 1",
"line to change",
"extra line 2",
"context C",
],
HunkLocation {
start_index: 0,
length: 5,
},
0.7,
);
}
#[test]
fn finder_mixed_change_modification() {
assert_fuzzy_location(
&["A", "B", "C"],
&["A", "X", "C"],
HunkLocation {
start_index: 0,
length: 3,
},
0.7,
);
}
}
#[cfg(test)]
mod parse_single_patch_tests {
use indoc::indoc;
use mpatch::{parse_single_patch, SingleParseError};
const SUCCESS_DIFF: &str = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
```
"#};
#[test]
fn test_success_case() {
let patch = parse_single_patch(SUCCESS_DIFF).unwrap();
assert_eq!(patch.file_path.to_str(), Some("file.txt"));
assert_eq!(patch.hunks.len(), 1);
}
#[test]
fn test_err_no_patches_found() {
let diff = "Just some text, no diff block.";
let result = parse_single_patch(diff);
assert!(matches!(result, Err(SingleParseError::NoPatchesFound)));
}
#[test]
fn test_err_multiple_patches_in_one_block() {
let diff = indoc! {r#"
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-a
+b
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-c
+d
```
"#};
let result = parse_single_patch(diff);
assert!(matches!(
result,
Err(SingleParseError::MultiplePatchesFound(2))
));
}
#[test]
fn test_err_multiple_patches_in_separate_blocks() {
let diff = indoc! {r#"
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-a
+b
```
```diff
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-c
+d
```
"#};
let result = parse_single_patch(diff);
assert!(matches!(
result,
Err(SingleParseError::MultiplePatchesFound(2))
));
}
#[test]
fn test_err_parse_error_propagates() {
let diff = indoc! {r#"
```diff
@@ -1 +1 @@
-a
+b
```
"#}; let result = parse_single_patch(diff);
assert!(matches!(result, Err(SingleParseError::NoPatchesFound)));
}
}
#[test]
fn test_strict_apply_variants() {
let original_content = "line 1\nline 2\nline 3\n\nline 5\nline 6\nline 7\n";
let successful_diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
```
"#};
let partial_fail_diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
@@ -5,3 +5,3 @@
line 5
-line WRONG
+line six
line 7
```
"#};
let successful_patch = &parse_diffs(successful_diff).unwrap()[0];
let failing_patch = &parse_diffs(partial_fail_diff).unwrap()[0];
let success_options = ApplyOptions::new();
let success_result_content =
try_apply_patch_to_content(successful_patch, Some(original_content), &success_options)
.unwrap();
assert!(success_result_content.report.all_applied_cleanly());
assert_eq!(
success_result_content.new_content,
"line 1\nline two\nline 3\n\nline 5\nline 6\nline 7\n"
);
let failing_options = ApplyOptions::exact();
let failure_result_content =
try_apply_patch_to_content(failing_patch, Some(original_content), &failing_options);
assert!(failure_result_content.is_err());
if let Err(StrictApplyError::PartialApply { report }) = failure_result_content {
assert!(!report.all_applied_cleanly());
assert_eq!(report.failures().len(), 1);
assert_eq!(report.failures()[0].hunk_index, 2);
} else {
panic!(
"Expected PartialApply error, got {:?}",
failure_result_content
);
}
let original_lines: Vec<_> = original_content.lines().collect();
let failure_result_lines =
try_apply_patch_to_lines(failing_patch, Some(&original_lines), &failing_options);
assert!(matches!(
failure_result_lines,
Err(StrictApplyError::PartialApply { .. })
));
let dir = tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs::write(&file_path, original_content).unwrap();
let failure_result_file = try_apply_patch_to_file(failing_patch, dir.path(), failing_options);
assert!(matches!(
failure_result_file,
Err(StrictApplyError::PartialApply { .. })
));
}
#[cfg(test)]
mod patch_content_str_tests {
use super::*;
use indoc::indoc;
use mpatch::{patch_content_str, OneShotError, StrictApplyError};
const ORIGINAL: &str = "line 1\nline 2\nline 3\n";
const SUCCESS_DIFF: &str = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line two
line 3
```
"#};
const EXPECTED: &str = "line 1\nline two\nline 3\n";
#[test]
fn test_success_case() {
let options = ApplyOptions::new();
let new_content = patch_content_str(SUCCESS_DIFF, Some(ORIGINAL), &options).unwrap();
assert_eq!(new_content, EXPECTED);
}
#[test]
fn test_file_creation_success() {
let creation_diff = indoc! {r#"
```diff
--- a/new.txt
+++ b/new.txt
@@ -0,0 +1,2 @@
+Hello
+World
```
"#};
let options = ApplyOptions::new();
let new_content = patch_content_str(creation_diff, None, &options).unwrap();
assert_eq!(new_content, "Hello\nWorld\n");
}
#[test]
fn test_err_no_patches_found() {
let diff = "Just some text, no diff block.";
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(ORIGINAL), &options);
assert!(matches!(result, Err(OneShotError::NoPatchesFound)));
}
#[test]
fn test_err_multiple_patches_found() {
let diff = indoc! {r#"
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-a
+b
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-c
+d
```
"#};
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(ORIGINAL), &options);
assert!(matches!(result, Err(OneShotError::MultiplePatchesFound(2))));
}
#[test]
fn test_err_parse_error() {
let diff = indoc! {r#"
```diff
@@ -1 +1 @@
-a
+b
```
"#}; let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(ORIGINAL), &options);
assert!(matches!(result, Err(OneShotError::NoPatchesFound)));
}
#[test]
fn test_err_apply_error() {
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-WRONG CONTEXT
+line two
line 3
```
"#};
let options = ApplyOptions::exact();
let result = patch_content_str(diff, Some(ORIGINAL), &options);
assert!(matches!(result, Err(OneShotError::Apply(_))));
if let Err(OneShotError::Apply(StrictApplyError::PartialApply { report })) = result {
assert!(!report.all_applied_cleanly());
} else {
panic!("Expected a PartialApply error");
}
}
}
#[test]
fn test_patch_and_hunk_display_format() {
let patch = Patch {
file_path: "src/main.rs".into(),
hunks: vec![
Hunk {
lines: vec![
" fn main() {".to_string(),
"- println!(\"old\");".to_string(),
"+ println!(\"new\");".to_string(),
" }".to_string(),
],
old_start_line: Some(1),
new_start_line: Some(1),
},
Hunk {
lines: vec![
" // some comment".to_string(),
"-// old comment".to_string(),
"+// new comment".to_string(),
],
old_start_line: Some(10),
new_start_line: Some(10),
},
],
ends_with_newline: true,
};
let expected_output = concat!(
"--- a/src/main.rs\n",
"+++ b/src/main.rs\n",
"@@ -1,3 +1,3 @@\n",
" fn main() {\n",
"- println!(\"old\");\n",
"+ println!(\"new\");\n",
" }\n",
"@@ -10,2 +10,2 @@\n",
" // some comment\n",
"-// old comment\n",
"+// new comment\n",
);
assert_eq!(
patch.to_string(),
expected_output,
"Test for standard patch failed"
);
let mut patch_no_newline = patch.clone();
patch_no_newline.ends_with_newline = false;
let expected_output_no_newline =
format!("{}{}", expected_output, "\\ No newline at end of file");
assert_eq!(
patch_no_newline.to_string(),
expected_output_no_newline,
"Test for patch with no newline failed"
);
let empty_patch = Patch {
file_path: "empty.txt".into(),
hunks: vec![],
ends_with_newline: true,
};
let expected_empty = "--- a/empty.txt\n+++ b/empty.txt\n";
assert_eq!(
empty_patch.to_string(),
expected_empty,
"Test for empty patch failed"
);
let empty_patch_no_newline = Patch {
file_path: "empty.txt".into(),
hunks: vec![],
ends_with_newline: false,
};
assert_eq!(
empty_patch_no_newline.to_string(),
expected_empty,
"Test for empty patch with no newline failed"
);
let creation_patch = Patch {
file_path: "new_file.txt".into(),
hunks: vec![Hunk {
lines: vec!["+line 1".to_string(), "+line 2".to_string()],
old_start_line: Some(0),
new_start_line: Some(1),
}],
ends_with_newline: true,
};
let expected_creation = concat!(
"--- a/new_file.txt\n",
"+++ b/new_file.txt\n",
"@@ -0,0 +1,2 @@\n",
"+line 1\n",
"+line 2\n",
);
assert_eq!(
creation_patch.to_string(),
expected_creation,
"Test for creation patch failed"
);
let single_hunk = Hunk {
lines: vec![
" context".to_string(),
"-deleted".to_string(),
"+added".to_string(),
],
old_start_line: Some(5),
new_start_line: Some(5),
};
let expected_hunk_str = "@@ -5,2 +5,2 @@\n context\n-deleted\n+added\n";
assert_eq!(
single_hunk.to_string(),
expected_hunk_str,
"Test for direct hunk display failed"
);
}
#[test]
fn test_apply_result_helpers() {
use mpatch::{ApplyResult, HunkApplyError, HunkApplyStatus, HunkLocation, MatchType};
let all_success = ApplyResult {
hunk_results: vec![
HunkApplyStatus::Applied {
location: HunkLocation {
start_index: 0,
length: 1,
},
match_type: MatchType::Exact,
replaced_lines: vec![],
},
HunkApplyStatus::SkippedNoChanges,
],
};
assert!(all_success.all_applied_cleanly());
assert!(!all_success.has_failures());
assert_eq!(all_success.success_count(), 2);
assert_eq!(all_success.failure_count(), 0);
let mixed_result = ApplyResult {
hunk_results: vec![
HunkApplyStatus::Applied {
location: HunkLocation {
start_index: 0,
length: 1,
},
match_type: MatchType::Exact,
replaced_lines: vec![],
},
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
HunkApplyStatus::SkippedNoChanges,
HunkApplyStatus::Failed(HunkApplyError::AmbiguousExactMatch(vec![])),
],
};
assert!(!mixed_result.all_applied_cleanly());
assert!(mixed_result.has_failures());
assert_eq!(mixed_result.success_count(), 2);
assert_eq!(mixed_result.failure_count(), 2);
let all_failures = ApplyResult {
hunk_results: vec![
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
],
};
assert!(!all_failures.all_applied_cleanly());
assert!(all_failures.has_failures());
assert_eq!(all_failures.success_count(), 0);
assert_eq!(all_failures.failure_count(), 2);
let empty_result = ApplyResult {
hunk_results: vec![],
};
assert!(empty_result.all_applied_cleanly());
assert!(!empty_result.has_failures());
assert_eq!(empty_result.success_count(), 0);
assert_eq!(empty_result.failure_count(), 0);
}
#[test]
fn test_parse_conflict_markers() {
let diff = indoc! {r#"
```diff
fn main() {
<<<<
println!("Old");
====
println!("New");
>>>>
}
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let patch = &patches[0];
assert_eq!(patch.file_path.to_str().unwrap(), "patch_target");
let hunk = &patch.hunks[0];
assert_eq!(hunk.context_lines(), vec!["fn main() {", "}"]);
assert_eq!(hunk.removed_lines(), vec![" println!(\"Old\");"]);
assert_eq!(hunk.added_lines(), vec![" println!(\"New\");"]);
}
#[test]
fn test_conflict_markers_git_style_labels() {
let diff = indoc! {r#"
```diff
<<<<<<< HEAD
Current Code
=======
Incoming Code
>>>>>>> feature/new-stuff
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["Current Code"]);
assert_eq!(hunk.added_lines(), vec!["Incoming Code"]);
}
#[test]
fn test_conflict_markers_multiple_blocks_in_one_file() {
let diff = indoc! {r#"
```diff
Context Start
<<<<
Old 1
====
New 1
>>>>
Middle Context
<<<<
Old 2
====
New 2
>>>>
Context End
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let hunk = &patches[0].hunks[0];
let lines = &hunk.lines;
assert!(lines.contains(&" Context Start".to_string()));
assert!(lines.contains(&"-Old 1".to_string()));
assert!(lines.contains(&"+New 1".to_string()));
assert!(lines.contains(&" Middle Context".to_string()));
assert!(lines.contains(&"-Old 2".to_string()));
assert!(lines.contains(&"+New 2".to_string()));
assert!(lines.contains(&" Context End".to_string()));
}
#[test]
fn test_conflict_markers_pure_addition() {
let diff = indoc! {r#"
```diff
<<<<
====
New Line
>>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert!(hunk.removed_lines().is_empty());
assert_eq!(hunk.added_lines(), vec!["New Line"]);
}
#[test]
fn test_conflict_markers_pure_deletion() {
let diff = indoc! {r#"
```diff
<<<<
Old Line
====
>>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["Old Line"]);
assert!(hunk.added_lines().is_empty());
}
#[test]
fn test_conflict_markers_apply_end_to_end() {
let original = indoc! {r#"
fn main() {
let x = 1;
println!("Old logic: {}", x);
return;
}
"#};
let diff = indoc! {r#"
```diff
fn main() {
let x = 1;
<<<<
println!("Old logic: {}", x);
====
println!("New logic: {}", x + 1);
>>>>
return;
}
```
"#};
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
let expected = indoc! {r#"
fn main() {
let x = 1;
println!("New logic: {}", x + 1);
return;
}
"#};
assert_eq!(result, expected);
}
#[test]
fn test_conflict_markers_ignore_normal_text() {
let diff = indoc! {r#"
```diff
Just some random text
that is not a diff
and has no markers.
```
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_conflict_markers_indented() {
let diff = indoc! {r#"
```diff
fn main() {
<<<<
old_code();
====
new_code();
>>>>
}
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert!(hunk.removed_lines()[0].contains("old_code"));
assert!(hunk.added_lines()[0].contains("new_code"));
}
#[test]
fn test_conflict_markers_missing_separator() {
let diff = indoc! {r#"
```diff
<<<<
delete me
>>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["delete me"]);
assert!(hunk.added_lines().is_empty());
}
#[test]
fn test_conflict_markers_missing_start() {
let diff = indoc! {r#"
```diff
====
add me
>>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_conflict_markers_unclosed() {
let diff = indoc! {r#"
```diff
<<<<
delete me
====
add me
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["delete me"]);
assert_eq!(hunk.added_lines(), vec!["add me"]);
}
#[test]
fn test_conflict_markers_false_positive_check() {
let diff = indoc! {r#"
```diff
fn main() {
let x = 1 << 2;
}
```
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_conflict_markers_with_context() {
let diff = indoc! {r#"
```diff
context before
<<<<
old
====
new
>>>>
context after
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.lines[0], " context before");
assert!(hunk.lines.contains(&"-old".to_string()));
assert!(hunk.lines.contains(&"+new".to_string()));
assert_eq!(hunk.lines.last().unwrap(), " context after");
}
#[test]
fn test_conflict_markers_malformed_sequence() {
let diff = indoc! {r#"
```diff
====
middle
<<<<
start
>>>>
end
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.added_lines(), vec!["middle"]);
assert_eq!(hunk.removed_lines(), vec!["start"]);
}
#[test]
fn test_conflict_markers_in_comments_ignored() {
let diff = indoc! {r#"
```diff
// <<<< this is a comment
old code
// ====
new code
// >>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_conflict_markers_with_trailing_text() {
let diff = indoc! {r#"
```diff
<<<< start of conflict
old
==== middle of conflict
new
>>>> end of conflict
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["old"]);
assert_eq!(hunk.added_lines(), vec!["new"]);
}
#[test]
fn test_conflict_markers_empty_block() {
let diff = indoc! {r#"
```diff
<<<<
====
>>>>
```
"#};
let patches = parse_diffs(diff).unwrap();
let hunk = &patches[0].hunks[0];
assert!(!hunk.has_changes());
}
#[test]
fn test_malformed_diff_returns_error_not_ignored() {
let diff = indoc! {r#"
```diff
@@ -1 +1 @@
-foo
+bar
```
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty());
}
#[test]
fn test_detect_markdown_standard() {
let content = indoc! {r#"
Here is a change:
```diff
--- a/file.rs
+++ b/file.rs
@@ -1 +1 @@
-old
+new
```
"#};
assert_eq!(detect_patch(content), PatchFormat::Markdown);
}
#[test]
fn test_parse_closing_fence_longer_than_opening() {
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-a
+b
````
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
}
#[test]
fn test_parse_shorter_closing_fence_ignored() {
let diff = indoc! {r#"
````diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-a
+b
```
Still inside block
````
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].hunks[0].added_lines(), vec!["b"]);
}
#[test]
fn test_parse_multiple_blocks_mixed_fences() {
let diff = indoc! {r#"
```diff
--- a/file1
+++ b/file1
@@ -1 +1 @@
-a
+b
```
````diff
--- a/file2
+++ b/file2
@@ -1 +1 @@
-c
+d
````
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1");
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2");
}
#[test]
fn test_parse_conflict_markers_variable_fence() {
let diff = indoc! {r#"
````
<<<<
old
====
new
>>>>
````
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_parse_fence_trailing_whitespace() {
let diff = "```diff \n--- a/f\n+++ b/f\n@@ -1 +1 @@\n-a\n+b\n``` ";
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
}
#[test]
fn test_nested_diff_block_is_ignored() {
let diff = indoc! {r#"
````
Here is an example of a patch:
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
```
````
"#};
let patches = parse_diffs(diff).unwrap();
assert!(patches.is_empty(), "Nested diff block should be ignored");
}
#[test]
fn test_parse_diff_with_nested_indented_code_block() {
let diff = indoc! {r#"
```diff
--- README.md
+++ README.md
@@ -1,3 +1,3 @@
1. Step one
```bash
- old_command
+ new_command
```
2. Step two
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].hunks[0].added_lines(), vec![" new_command"]);
}
#[test]
fn test_parse_diff_with_fence_like_context_line() {
let diff = indoc! {r#"
```diff
--- README.md
+++ README.md
@@ -1,3 +1,3 @@
text
```
more text
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let hunk = &patches[0].hunks[0];
assert_eq!(hunk.lines.len(), 3);
assert_eq!(hunk.lines[1], " ```");
assert_eq!(hunk.lines[2], " more text");
}
#[test]
fn test_detect_markdown_patch_keyword() {
let content = indoc! {r#"
```patch
--- a/file
+++ b/file
```
"#};
assert_eq!(detect_patch(content), PatchFormat::Markdown);
}
#[test]
fn test_detect_markdown_with_language_hint() {
let content = indoc! {r#"
```rust, diff
--- a/file
+++ b/file
```
"#};
assert_eq!(detect_patch(content), PatchFormat::Markdown);
}
#[test]
fn test_detect_unified_git_header() {
let content = indoc! {r#"
diff --git a/src/main.rs b/src/main.rs
index 88d9554..e0c99b6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
"#};
assert_eq!(detect_patch(content), PatchFormat::Unified);
}
#[test]
fn test_detect_unified_standard_headers() {
let content = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-foo
+bar
"#};
assert_eq!(detect_patch(content), PatchFormat::Unified);
}
#[test]
fn test_detect_unified_hunk_only() {
let content = indoc! {r#"
@@ -10,4 +10,4 @@
ctx
-old
+new
ctx
"#};
assert_eq!(detect_patch(content), PatchFormat::Unified);
}
#[test]
fn test_detect_conflict_markers_standard() {
let content = indoc! {r#"
<<<<
old code
====
new code
>>>>
"#};
assert_eq!(detect_patch(content), PatchFormat::Conflict);
}
#[test]
fn test_detect_conflict_markers_git_style() {
let content = indoc! {r#"
<<<<<<< HEAD
current change
=======
incoming change
>>>>>>> feature-branch
"#};
assert_eq!(detect_patch(content), PatchFormat::Conflict);
}
#[test]
fn test_detect_conflict_markers_missing_middle() {
let content = indoc! {r#"
<<<<
delete me
>>>>
"#};
assert_eq!(detect_patch(content), PatchFormat::Conflict);
}
#[test]
fn test_detect_conflict_markers_missing_end() {
let content = indoc! {r#"
<<<<
old
====
new
"#};
assert_eq!(detect_patch(content), PatchFormat::Conflict);
}
#[test]
fn test_detect_false_positive_bitwise_shift() {
let content = "let x = 1 << 2;";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_detect_false_positive_comparison() {
let content = "if x <= y && a >= b {}";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_detect_false_positive_list_item() {
let content = "--- this is just a list item";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_detect_false_positive_hr() {
let content = "---\n\n# Title";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_detect_false_positive_plus_list() {
let content = "+++ Just a list item";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_detect_unified_requires_plus_after_minus() {
let content = "--- a/file\nnot a plus line";
assert_eq!(detect_patch(content), PatchFormat::Unknown);
}
#[test]
fn test_parse_auto_markdown() {
let content = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
```
"#};
let patches = parse_auto(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file.txt");
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_parse_auto_raw_diff() {
let content = indoc! {r#"
--- a/raw.txt
+++ b/raw.txt
@@ -1 +1 @@
-old
+new
"#};
let patches = parse_auto(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "raw.txt");
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_parse_auto_conflict_markers() {
let content = indoc! {r#"
<<<<
old
====
new
>>>>
"#};
let patches = parse_auto(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "patch_target");
assert_eq!(patches[0].hunks[0].removed_lines(), vec!["old"]);
assert_eq!(patches[0].hunks[0].added_lines(), vec!["new"]);
}
#[test]
fn test_parse_auto_fallback_to_raw() {
let content = "--- a/file\n+++ b/file";
assert_eq!(detect_patch(content), PatchFormat::Unified);
let content = "Just random text";
let result = parse_auto(content).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_patch_content_str_accepts_raw_diff() {
let diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
"#};
let original = "old\n";
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
assert_eq!(result, "new\n");
}
#[test]
fn test_patch_content_str_accepts_markdown() {
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
```
"#};
let original = "old\n";
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
assert_eq!(result, "new\n");
}
#[test]
fn test_parse_auto_multiple_raw_patches() {
let content = indoc! {r#"
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-a
+b
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-c
+d
"#};
let patches = parse_auto(content).unwrap();
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2.txt");
}
#[test]
fn test_cli_simulation_raw_diff_input() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("raw.txt");
fs::write(&file_path, "old content\n").unwrap();
let patch_content = indoc! {r#"
--- a/raw.txt
+++ b/raw.txt
@@ -1 +1 @@
-old content
+new content
"#};
let patches = parse_auto(patch_content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "raw.txt");
let options = ApplyOptions::new();
let result = apply_patches_to_dir(&patches, dir.path(), options);
assert!(result.all_succeeded());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "new content\n");
}
#[test]
fn test_cli_simulation_conflict_marker_input() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("patch_target");
fs::write(&file_path, "line 1\nold\nline 3\n").unwrap();
let patch_content = indoc! {r#"
<<<<
old
====
new
>>>>
"#};
let patches = parse_auto(patch_content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "patch_target");
let options = ApplyOptions::new();
let result = apply_patches_to_dir(&patches, dir.path(), options);
assert!(result.all_succeeded());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "line 1\nnew\nline 3\n");
}
#[test]
fn test_cli_simulation_markdown_input() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("md.txt");
fs::write(&file_path, "old\n").unwrap();
let patch_content = indoc! {r#"
Here is a fix:
```diff
--- a/md.txt
+++ b/md.txt
@@ -1 +1 @@
-old
+new
```
"#};
let patches = parse_auto(patch_content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "md.txt");
let options = ApplyOptions::new();
let result = apply_patches_to_dir(&patches, dir.path(), options);
assert!(result.all_succeeded());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "new\n");
}
#[test]
fn test_patch_from_texts_uses_raw_parser() {
let old_text = "line 1\nline 2\n";
let new_text = "line 1\nline modified\n";
let patch = Patch::from_texts("test.txt", old_text, new_text, 3).unwrap();
assert_eq!(patch.file_path.to_str(), Some("test.txt"));
assert_eq!(patch.hunks.len(), 1);
assert_eq!(patch.hunks[0].removed_lines(), vec!["line 2"]);
assert_eq!(patch.hunks[0].added_lines(), vec!["line modified"]);
}
#[test]
fn test_apply_patch_to_hard_link() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let target_path = dir.path().join("target.txt");
let link_path = dir.path().join("link.txt");
fs::write(&target_path, "line 1\nline 2\n").unwrap();
fs::hard_link(&target_path, &link_path).unwrap();
let diff = indoc! {"
```diff
--- a/link.txt
+++ b/link.txt
@@ -1,2 +1,2 @@
line 1
-line 2
+line two
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let link_content = fs::read_to_string(&link_path).unwrap();
assert_eq!(link_content, "line 1\nline two\n");
let target_content = fs::read_to_string(&target_path).unwrap();
assert_eq!(target_content, "line 1\nline two\n");
}
#[test]
#[cfg(unix)]
fn test_apply_patch_to_symlink() {
use std::os::unix::fs::symlink;
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let target_path = dir.path().join("target.txt");
let link_path = dir.path().join("link.txt");
fs::write(&target_path, "line 1\nline 2\n").unwrap();
symlink(&target_path, &link_path).unwrap();
let diff = indoc! {"
```diff
--- a/link.txt
+++ b/link.txt
@@ -1,2 +1,2 @@
line 1
-line 2
+line two
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(result.diff.is_none());
let target_content = fs::read_to_string(&target_path).unwrap();
assert_eq!(target_content, "line 1\nline two\n");
let metadata = fs::symlink_metadata(&link_path).unwrap();
assert!(metadata.file_type().is_symlink());
}
#[test]
#[cfg(unix)]
fn test_apply_patch_to_symlink_preserves_link() {
use std::os::unix::fs::symlink;
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let target_file = dir.path().join("target.txt");
fs::write(&target_file, "original content\n").unwrap();
let symlink_path = dir.path().join("link.txt");
symlink("target.txt", &symlink_path).unwrap();
let diff = indoc! {"
```diff
--- a/link.txt
+++ b/link.txt
@@ -1 +1 @@
-original content
+patched content
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let metadata = fs::symlink_metadata(&symlink_path).unwrap();
assert!(metadata.file_type().is_symlink());
let content = fs::read_to_string(&target_file).unwrap();
assert_eq!(content, "patched content\n");
}
#[test]
fn test_apply_patch_to_hardlink_preserves_link() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let target_file = dir.path().join("target.txt");
fs::write(&target_file, "original content\n").unwrap();
let hardlink_path = dir.path().join("link.txt");
fs::hard_link(&target_file, &hardlink_path).unwrap();
let diff = indoc! {"
```diff
--- a/link.txt
+++ b/link.txt
@@ -1 +1 @@
-original content
+patched content
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&target_file).unwrap();
assert_eq!(content, "patched content\n");
let link_content = fs::read_to_string(&hardlink_path).unwrap();
assert_eq!(link_content, "patched content\n");
fs::write(&target_file, "modified again\n").unwrap();
let link_content_after = fs::read_to_string(&hardlink_path).unwrap();
assert_eq!(link_content_after, "modified again\n");
}
#[test]
#[cfg(unix)]
fn test_apply_patch_to_symlink_deletion() {
use std::os::unix::fs::symlink;
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let target_file = dir.path().join("target.txt");
fs::write(&target_file, "original content\n").unwrap();
let symlink_path = dir.path().join("link.txt");
symlink("target.txt", &symlink_path).unwrap();
let diff = indoc! {"
```diff
--- a/link.txt
+++ b/link.txt
@@ -1 +0,0 @@
-original content
```
"};
let patch = &parse_diffs(diff).unwrap()[0];
let options = ApplyOptions::exact();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
assert!(!symlink_path.exists());
assert!(fs::symlink_metadata(&symlink_path).is_err());
assert!(target_file.exists());
let content = fs::read_to_string(&target_file).unwrap();
assert_eq!(content, "original content\n");
}
mod fuzzy_logic_edge_cases {
use indoc::indoc;
use mpatch::{apply_patch_to_file, parse_auto, parse_diffs, ApplyOptions};
use std::fs;
use tempfile::tempdir;
#[test]
fn test_fuzzy_insertion_clobbers_context() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("main.rs");
let original_content = indoc! {r#"
fn main() {
// comment (modified)
println!("Hello");
}
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/main.rs
+++ b/main.rs
@@ -1,3 +1,4 @@
fn main() {
// comment (original)
+ let x = 1;
println!("Hello");
}
```
"#};
let patches = parse_diffs(diff).unwrap();
let patch = &patches[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = indoc! {r#"
fn main() {
// comment (modified)
let x = 1;
println!("Hello");
}
"#};
assert_eq!(
content, expected_content,
"Fuzzy insertion clobbered the local file context!"
);
}
#[test]
fn test_fuzzy_interleaved_local_edits() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("config.toml");
let original_content = indoc! {r#"
[server]
host = "localhost"
# Local comment
port = 8080
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/config.toml
+++ b/config.toml
@@ -1,3 +1,3 @@
[server]
host = "localhost"
-port = 8080
+port = 9090
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = indoc! {r#"
[server]
host = "localhost"
# Local comment
port = 9090
"#};
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_indentation_context_preserved() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("style.css");
let original_content = "body {\n\tcolor: red;\n\tbackground: white;\n}\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/style.css
+++ b/style.css
@@ -1,4 +1,4 @@
body {
color: red;
- background: white;
+ background: black;
}
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "body {\n\tcolor: red;\n\tbackground: black;\n}\n";
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_extra_newlines_in_target() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("list.txt");
let original_content = "item 1\n\nitem 2\n\nitem 3\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/list.txt
+++ b/list.txt
@@ -1,3 +1,3 @@
item 1
-item 2
+item two
item 3
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "item 1\n\nitem two\n\nitem 3\n";
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_restore_truncated_context_at_eof() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("truncated.rs");
let original_content = "fn main() {\n println!(\"hi\");\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/truncated.rs
+++ b/truncated.rs
@@ -1,3 +1,4 @@
fn main() {
println!("hi");
}
+// end
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "fn main() {\n println!(\"hi\");\n}\n// end\n";
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_skip_stale_context_middle() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("stale.txt");
let original_content = "line A\nline C\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/stale.txt
+++ b/stale.txt
@@ -1,4 +1,4 @@
line A
line B
-line C
+line changed
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "line A\nline changed\n";
assert_eq!(content, expected);
}
#[test]
fn test_conflict_markers_adjacent() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("patch_target");
let original_content = "block1\nblock2\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
<<<<
block1
====
new1
>>>>
<<<<
block2
====
new2
>>>>
"#};
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "new1\nnew2\n";
assert_eq!(content, expected);
}
#[test]
fn test_hunks_out_of_order() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("order.txt");
let original_content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/order.txt
+++ b/order.txt
@@ -5,1 +5,1 @@
-line 5
+line five
@@ -1,1 +1,1 @@
-line 1
+line one
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = "line one\nline 2\nline 3\nline 4\nline five\n";
assert_eq!(content, expected);
}
#[test]
fn test_large_offset_application() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("offset.txt");
let mut content = String::new();
for _ in 0..100 {
content.push_str("prefix\n");
}
content.push_str("target\n");
fs::write(&file_path, &content).unwrap();
let diff = indoc! {r#"
```diff
--- a/offset.txt
+++ b/offset.txt
@@ -1,1 +1,1 @@
-target
+hit
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::exact(); let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let file_content = fs::read_to_string(&file_path).unwrap();
assert!(file_content.ends_with("hit\n"));
}
#[test]
fn test_fuzzy_anchor_indentation_drift_with_coincidental_match() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("anchor_drift.rs");
let mut original_content = String::new();
for i in 0..50 {
original_content.push_str(&format!("// noise A {}\n", i));
}
original_content.push_str(" println!(\"My unique anchor line\");\n");
for i in 0..50 {
original_content.push_str(&format!("// noise B {}\n", i));
}
original_content.push_str(" let x = 1;\n");
original_content.push_str(" println!(\"My unique anchor line\");\n");
original_content.push_str(" let y = 2;\n");
for i in 0..50 {
original_content.push_str(&format!("// noise C {}\n", i));
}
fs::write(&file_path, &original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/anchor_drift.rs
+++ b/anchor_drift.rs
@@ -100,3 +100,3 @@
let x = 1;
println!("My unique anchor line");
- let y = 2;
+ let y = 3;
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(
result.report.all_applied_cleanly(),
"Patch should apply cleanly by finding the anchor despite indentation drift"
);
let content = fs::read_to_string(&file_path).unwrap();
assert!(content.contains(" let y = 3;\n"));
}
#[test]
fn test_fuzzy_reconstruction_misalignment_bug() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("model.py");
let original_content = [
" yins[t] = 1",
" ",
" age_series = 1",
" yin_series = 1",
" ",
" age_norm = 1",
" yin_norm = 1",
" ",
" self.dynamic = 1",
]
.join("\n")
+ "\n";
fs::write(&file_path, original_content).unwrap();
let diff = [
"```diff",
"--- a/model.py",
"+++ b/model.py",
"@@ -1,9 +1,9 @@",
" yins[t] = 1",
" ",
"- age_series = 1",
"- yin_series = 1",
"+ age_series = 2",
"+ yin_series = 2",
" ",
"- age_norm = 1",
"- yin_norm = 1",
"+ age_norm = 2",
"+ yin_norm = 2",
" ",
" self.dynamic = 1",
"```",
]
.join("\n")
+ "\n";
let patches = parse_diffs(&diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = [
" yins[t] = 1",
" ",
" age_series = 2",
" yin_series = 2",
" ",
" age_norm = 2",
" yin_norm = 2",
" ",
" self.dynamic = 1",
]
.join("\n")
+ "\n";
assert_eq!(content, expected);
}
}
mod extended_stress_tests {
use indoc::indoc;
use mpatch::{apply_patch_to_file, parse_auto, ApplyOptions};
use std::fs;
use tempfile::tempdir;
#[test]
fn test_fuzzy_crlf_mismatch() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("crlf.txt");
fs::write(&file_path, "line1\r\nline2\r\nline3\r\n").unwrap();
let diff =
"--- a/crlf.txt\n+++ b/crlf.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+line two\n line3\n";
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "line1\nline two\nline3\n");
}
#[test]
fn test_fuzzy_unicode_context() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("unicode.txt");
fs::write(&file_path, "Start\nHello 🌍\nEnd\n").unwrap();
let diff = indoc! {r#"
--- a/unicode.txt
+++ b/unicode.txt
@@ -1,3 +1,3 @@
Start
-Hello 🌎
+Hello 🌏
End
"#};
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::new(); let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "Start\nHello 🌏\nEnd\n");
}
#[test]
fn test_fuzzy_repeated_lines_ambiguity_resolution() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("repeat.txt");
let content = "A\nB\nC\n\nA\nB\nC\n\nA\nB\nC\n";
fs::write(&file_path, content).unwrap();
let diff = indoc! {r#"
--- a/repeat.txt
+++ b/repeat.txt
@@ -5,3 +5,3 @@
A
-B modified
+B changed
C
"#};
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let new_content = fs::read_to_string(&file_path).unwrap();
let expected = "A\nB\nC\n\nA\nB changed\nC\n\nA\nB\nC\n";
assert_eq!(new_content, expected);
}
#[test]
fn test_apply_patch_with_huge_offset() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("offset.txt");
let mut content = String::new();
for _ in 0..500 {
content.push_str("noise\n");
}
content.push_str("target\n");
for _ in 0..500 {
content.push_str("noise\n");
}
fs::write(&file_path, &content).unwrap();
let diff = indoc! {r#"
--- a/offset.txt
+++ b/offset.txt
@@ -1,1 +1,1 @@
-target
+hit
"#};
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let new_content = fs::read_to_string(&file_path).unwrap();
assert!(new_content.contains("hit\n"));
assert!(!new_content.contains("target\n"));
}
#[test]
fn test_parse_auto_mixed_formats() {
let content = indoc! {r#"
Some text
```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-a
+b
```
<<<<
old
====
new
>>>>
"#};
let patches = parse_auto(content).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
}
}
#[test]
fn test_smart_indentation_adjustment() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("indent.rs");
let original_content = indoc! {r#"
fn main() {
println!("Hello");
}
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/indent.rs
+++ b/indent.rs
@@ -1,3 +1,4 @@
fn main() {
println!("Hello");
+ println!("World");
}
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = indoc! {r#"
fn main() {
println!("Hello");
println!("World");
}
"#};
assert_eq!(content, expected);
}
#[test]
fn test_smart_indentation_ignores_empty_lines_with_trailing_whitespace() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty_whitespace.py");
let original_content =
"def main():\n print(\"hello\")\n \n # Validation Loop\n";
fs::write(&file_path, original_content).unwrap();
let diff = "```diff\n--- a/empty_whitespace.py\n+++ b/empty_whitespace.py\n@@ -1,4 +1,6 @@\n def main():\n print(\"hello\")\n \n+ print(\"inserted\")\n+\n # Validation Loop\n```";
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n print(\"hello\")\n \n print(\"inserted\")\n\n # Validation Loop\n";
assert_eq!(content, expected);
}
#[test]
fn test_fuzzy_match_ignores_indentation() {
let original = "line1\nline2\nline3\n";
let diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line1
- line2
+ line two
line3
"#};
let patch = parse_patches(diff).unwrap().remove(0);
let options = ApplyOptions::new();
let result = try_apply_patch_to_content(&patch, Some(original), &options).unwrap();
assert_eq!(result.new_content, "line1\nline two\nline3\n");
}
#[test]
fn test_fuzzy_insertion_clobbers_context() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("main.rs");
let original_content = indoc! {r#"
fn main() {
// comment (modified)
println!("Hello");
}
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/main.rs
+++ b/main.rs
@@ -1,3 +1,4 @@
fn main() {
// comment (original)
+ let x = 1;
println!("Hello");
}
```
"#};
let patches = parse_diffs(diff).unwrap();
let patch = &patches[0];
let options = ApplyOptions::new();
let result = apply_patch_to_file(patch, dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected_content = indoc! {r#"
fn main() {
// comment (modified)
let x = 1;
println!("Hello");
}
"#};
assert_eq!(
content, expected_content,
"Fuzzy insertion clobbered the local file context!"
);
}
#[test]
fn test_git_diff_header_is_not_absorbed_into_previous_hunk() {
let diff = indoc! {r#"
--- a/file1.txt
+++ b/file1.txt
@@ -1,1 +1,1 @@
-foo
+bar
diff --git a/file2.txt b/file2.txt
index 1234567..89abcdef 100644
--- a/file2.txt
+++ b/file2.txt
@@ -1,1 +1,1 @@
-baz
+qux
"#};
let patches = parse_patches(diff).unwrap();
assert_eq!(patches.len(), 2);
let hunk1 = &patches[0].hunks[0];
assert_eq!(
hunk1.lines.len(),
2,
"Hunk 1 absorbed git headers as context lines! Lines: {:?}",
hunk1.lines
);
assert!(!hunk1.lines.iter().any(|l| l.contains("diff --git")));
assert!(!hunk1.lines.iter().any(|l| l.contains("index")));
}
#[test]
fn test_new_file_mode_header_is_not_context() {
let diff = indoc! {r#"
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,1 +5,1 @@
fn existing() {}
diff --git a/tests/new_test.rs b/tests/new_test.rs
new file mode 100644
index 0000000..1234567
--- /dev/null
+++ b/tests/new_test.rs
@@ -0,0 +1 @@
+#[test] fn t() {}
"#};
let patches = parse_patches(diff).unwrap();
assert_eq!(patches.len(), 2);
let hunk1 = &patches[0].hunks[0];
assert_eq!(
hunk1.lines.len(),
1,
"Hunk 1 absorbed new file headers! Lines: {:?}",
hunk1.lines
);
assert_eq!(hunk1.lines[0], " fn existing() {}");
}
#[test]
fn test_deleted_file_mode_header_is_not_context() {
let diff = indoc! {r#"
--- a/keep.txt
+++ b/keep.txt
@@ -1 +1 @@
keep
diff --git a/delete.txt b/delete.txt
deleted file mode 100644
index 1234567..0000000
--- a/delete.txt
+++ /dev/null
@@ -1 +0,0 @@
-content
"#};
let patches = parse_patches(diff).unwrap();
assert_eq!(patches.len(), 2);
let hunk1 = &patches[0].hunks[0];
assert_eq!(
hunk1.lines.len(),
1,
"Hunk 1 absorbed deleted file headers! Lines: {:?}",
hunk1.lines
);
assert_eq!(hunk1.lines[0], " keep");
}
#[test]
fn test_markdown_block_with_git_headers() {
let content = indoc! {r#"
```diff
--- a/f1
+++ b/f1
@@ -1 +1 @@
-a
+b
diff --git a/f2 b/f2
index 111..222
--- a/f2
+++ b/f2
@@ -1 +1 @@
-c
+d
```
"#};
let patches = parse_diffs(content).unwrap();
assert_eq!(patches.len(), 2);
let hunk1 = &patches[0].hunks[0];
assert_eq!(
hunk1.lines.len(),
2,
"Markdown parser absorbed git headers into hunk"
);
}
#[test]
fn test_extended_git_headers() {
let diff = indoc! {r#"
--- a/f1
+++ b/f1
@@ -1 +1 @@
context
diff --git a/old b/new
similarity index 100%
rename from old
rename to new
--- a/old
+++ b/new
@@ -1 +1 @@
context
"#};
let patches = parse_patches(diff).unwrap();
assert_eq!(patches.len(), 2);
let hunk1 = &patches[0].hunks[0];
assert_eq!(
hunk1.lines.len(),
1,
"Hunk absorbed rename/similarity headers"
);
assert_eq!(hunk1.lines[0], " context");
}
#[test]
fn test_fuzzy_indentation_drift() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("drift.md");
let original_content = indoc! {r#"
# Header
* Item 1
* Item 2
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/drift.md
+++ b/drift.md
@@ -1,3 +1,4 @@
# Header
* Item 1
- * Item 2
+ * Item Two
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
let expected = indoc! {r#"
# Header
* Item 1
* Item Two
"#};
assert_eq!(
content, expected,
"Indentation should be dynamically adjusted based on local context"
);
}
#[test]
fn test_invert_simple_modification() {
let diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-old
+new
"#};
let patches = parse_auto(diff).unwrap();
let inverted = invert_patches(&patches);
assert_eq!(inverted.len(), 1);
let hunk = &inverted[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["new"]);
assert_eq!(hunk.added_lines(), vec!["old"]);
}
#[test]
fn test_invert_creation_becomes_deletion() {
let diff = indoc! {r#"
--- /dev/null
+++ b/new.txt
@@ -0,0 +1 @@
+content
"#};
let patches = parse_auto(diff).unwrap();
assert!(patches[0].is_creation());
let inverted = invert_patches(&patches);
assert!(inverted[0].is_deletion());
let hunk = &inverted[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["content"]);
assert!(hunk.added_lines().is_empty());
}
#[test]
fn test_invert_deletion_becomes_creation() {
let diff = indoc! {r#"
--- a/old.txt
+++ /dev/null
@@ -1 +0,0 @@
-content
"#};
let patches = parse_auto(diff).unwrap();
assert!(patches[0].is_deletion());
let inverted = invert_patches(&patches);
assert!(inverted[0].is_creation());
let hunk = &inverted[0].hunks[0];
assert!(hunk.removed_lines().is_empty());
assert_eq!(hunk.added_lines(), vec!["content"]);
}
#[test]
fn test_double_inversion_is_identity() {
let diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
ctx
-old
+new
"#};
let original = parse_auto(diff).unwrap();
let once = invert_patches(&original);
let twice = invert_patches(&once);
assert_eq!(original[0].hunks[0].lines, twice[0].hunks[0].lines);
}
#[test]
fn test_apply_inverted_patch_undoes_changes() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs::write(&file_path, "context\nnew value\n").unwrap();
let diff = indoc! {r#"
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
context
-old value
+new value
"#};
let patches = parse_auto(diff).unwrap();
let reversed_patches = invert_patches(&patches);
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&reversed_patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly());
let content = fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "context\nold value\n");
}
#[test]
fn test_invert_multiple_files() {
let diff = indoc! {r#"
--- a/f1
+++ b/f1
@@ -1 +1 @@
-a
+b
--- a/f2
+++ b/f2
@@ -1 +1 @@
-c
+d
"#};
let patches = parse_auto(diff).unwrap();
let inverted = invert_patches(&patches);
assert_eq!(inverted.len(), 2);
assert_eq!(inverted[0].hunks[0].removed_lines(), vec!["b"]);
assert_eq!(inverted[0].hunks[0].added_lines(), vec!["a"]);
assert_eq!(inverted[1].hunks[0].removed_lines(), vec!["d"]);
assert_eq!(inverted[1].hunks[0].added_lines(), vec!["c"]);
}
#[test]
fn test_invert_conflict_markers() {
let diff = indoc! {r#"
<<<<
old
====
new
>>>>
"#};
let patches = parse_auto(diff).unwrap();
let inverted = invert_patches(&patches);
let hunk = &inverted[0].hunks[0];
assert_eq!(hunk.removed_lines(), vec!["new"]);
assert_eq!(hunk.added_lines(), vec!["old"]);
}
#[test]
fn test_invert_mixed_hunk() {
let diff = indoc! {r#"
--- a/file
+++ b/file
@@ -1,4 +1,4 @@
ctx1
-del1
+add1
ctx2
-del2
+add2
"#};
let patches = parse_auto(diff).unwrap();
let inverted = invert_patches(&patches);
let hunk = &inverted[0].hunks[0];
let lines = &hunk.lines;
assert_eq!(lines[0], " ctx1");
assert_eq!(lines[1], "+del1");
assert_eq!(lines[2], "-add1");
assert_eq!(lines[3], " ctx2");
assert_eq!(lines[4], "+del2");
assert_eq!(lines[5], "-add2");
}
#[test]
fn test_invert_empty_patch_list() {
let patches: Vec<Patch> = vec![];
let inverted = invert_patches(&patches);
assert!(inverted.is_empty());
}
#[test]
fn test_complex_apply_and_reverse_cycle() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("complex_cycle.txt");
let original_content = indoc! {r#"
fn main() {
// Part 1: Modification
let x = 10;
println!("Value: {}", x);
// Part 2: Deletion
let unused = "delete me";
let unused_2 = "delete me too";
// Part 3: Addition
return;
}
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
--- a/complex_cycle.txt
+++ b/complex_cycle.txt
@@ -3,3 +3,3 @@
// Part 1: Modification
- let x = 10;
+ let x = 20;
println!("Value: {}", x);
@@ -6,4 +6,1 @@
// Part 2: Deletion
- let unused = "delete me";
- let unused_2 = "delete me too";
-
// Part 3: Addition
@@ -11,2 +8,3 @@
// Part 3: Addition
+ println!("Done");
return;
"#};
let patches = parse_auto(diff).unwrap();
let options = ApplyOptions::exact();
let result = apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
assert!(result.report.all_applied_cleanly(), "Forward patch failed");
let reversed_patches = invert_patches(&patches);
let result_rev = apply_patch_to_file(&reversed_patches[0], dir.path(), options).unwrap();
assert!(
result_rev.report.all_applied_cleanly(),
"Reverse patch failed"
);
let restored_content = fs::read_to_string(&file_path).unwrap();
assert_eq!(
restored_content, original_content,
"Reverse patch did not restore original content exactly"
);
}
#[test]
fn test_newline_only_file_preservation() {
let original = "content\n";
let diff = indoc! {r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-content
+
```
"#};
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
assert_eq!(result, "\n");
assert_eq!(result.len(), 1);
}
#[test]
fn test_conflict_marker_detection_false_positive_markdown_header() {
let content = indoc! {r#"
My Document Title
=================
This is just a normal markdown file.
"#};
assert_eq!(detect_patch(content), PatchFormat::Unknown);
let patches = parse_auto(content).unwrap();
assert!(
patches.is_empty(),
"Should not have found patches in a standard MD header"
);
}
#[test]
fn test_conflict_marker_detection_requires_start_and_end() {
let only_start = "<<<< Just some text";
assert_eq!(detect_patch(only_start), PatchFormat::Unknown);
let valid_conflict = "<<<<\nold\n>>>>";
assert_eq!(detect_patch(valid_conflict), PatchFormat::Conflict);
}
#[test]
fn test_smart_indentation_tabs_to_tabs() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("tabs.py");
let original_content = "def main():\n\tprint(\"hello\")\n";
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/tabs.py
+++ b/tabs.py
@@ -1,2 +1,3 @@
def main():
print("hello")
+ print("world")
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = "def main():\n\tprint(\"hello\")\n\tprint(\"world\")\n";
assert_eq!(
content, expected,
"Indentation should have converted spaces to tabs"
);
}
#[test]
fn test_parse_empty_hunk_header_allowed() {
let diff = indoc! {r#"
--- a/empty_hunk.txt
+++ b/empty_hunk.txt
@@ -0,0 +0,0 @@
"#};
let patches = parse_auto(diff).unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].hunks.len(), 1);
assert!(patches[0].hunks[0].lines.is_empty());
}
#[test]
fn test_markdown_fence_indentation_strictness() {
let diff = indoc! {r#"
```diff
--- a/code.md
+++ b/code.md
@@ -1,3 +1,3 @@
Some text
```
More text
```
"#};
let patches = parse_diffs(diff).unwrap();
assert_eq!(patches.len(), 1);
let hunk = &patches[0].hunks[0];
assert!(hunk.lines.iter().any(|l| l == " ```"));
assert!(hunk.lines.iter().any(|l| l == " More text"));
}
#[test]
fn test_conflict_marker_pure_deletion_no_separator() {
let original = "line 1\nline 2\nline 3\n";
let diff = indoc! {r#"
line 1
<<<<
line 2
>>>>
line 3
"#};
let options = ApplyOptions::new();
let result = patch_content_str(diff, Some(original), &options).unwrap();
assert_eq!(result, "line 1\nline 3\n");
}
#[test]
fn test_apply_patch_to_empty_file_resulting_in_newline() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("newline.txt");
let diff = indoc! {r#"
--- /dev/null
+++ b/newline.txt
@@ -0,0 +1 @@
+
"#};
let patches = parse_auto(diff).unwrap();
apply_patch_to_file(&patches[0], dir.path(), ApplyOptions::new()).unwrap();
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "\n");
}
#[test]
fn test_multiple_file_creations_with_empty_lines_between() {
let diff = indoc! {r#"
--- /dev/null
+++ b/file1.txt
@@ -0,0 +1,2 @@
+content 1
+content 2
--- /dev/null
+++ b/file2.txt
@@ -0,0 +1,2 @@
+content 3
+content 4
"#};
let patches = parse_patches(diff).unwrap();
assert_eq!(patches.len(), 2);
assert!(patches[0].is_creation());
assert_eq!(patches[0].file_path.to_str().unwrap(), "file1.txt");
assert_eq!(
patches[0].hunks[0].added_lines(),
vec!["content 1", "content 2"]
);
assert!(patches[0].hunks[0].context_lines().is_empty());
assert!(patches[1].is_creation());
assert_eq!(patches[1].file_path.to_str().unwrap(), "file2.txt");
assert_eq!(
patches[1].hunks[0].added_lines(),
vec!["content 3", "content 4"]
);
}
#[test]
fn test_smart_indentation_outdented_added_line_fallback() {
let _ = env_logger::builder().is_test(true).try_init();
let dir = tempdir().unwrap();
let file_path = dir.path().join("outdent.rs");
let original_content = indoc! {r#"
println!("Hello");
"#};
fs::write(&file_path, original_content).unwrap();
let diff = indoc! {r#"
```diff
--- a/outdent.rs
+++ b/outdent.rs
@@ -1,1 +1,3 @@
println!("Hello");
+ }
+}
```
"#};
let patches = parse_diffs(diff).unwrap();
let options = ApplyOptions::new();
apply_patch_to_file(&patches[0], dir.path(), options).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let expected = indoc! {r#"
println!("Hello");
}
}
"#};
assert_eq!(content, expected);
}