#[cfg(test)]
mod tests {
use crate::edit;
use crate::edit::lossless::DiffFormat;
use rowan::ast::AstNode;
#[test]
fn test_context_diff_no_changes() {
let input = r#"*** a/file.txt 2024-01-01
--- b/file.txt 2024-01-01
***************
*** 1,3 ****
line 1
line 2
line 3
--- 1,3 ----
line 1
line 2
line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert_eq!(patch.detect_format(), Some(DiffFormat::Context));
let file = patch.context_diff_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let old_section = hunk.old_section().unwrap();
let old_lines: Vec<_> = old_section
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::CONTEXT_LINE)
.collect();
assert_eq!(old_lines.len(), 3);
}
#[test]
fn test_context_diff_only_additions() {
let input = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,2 ****
line 1
line 2
--- 1,4 ----
line 1
+ inserted line
+ another insert
line 2
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let new_section = hunk.new_section().unwrap();
let add_lines: Vec<_> = new_section
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::ADD_LINE)
.collect();
assert_eq!(add_lines.len(), 2);
}
#[test]
fn test_context_diff_only_deletions() {
let input = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,4 ****
line 1
- delete me
- also delete
line 2
--- 1,2 ----
line 1
line 2
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let old_section = hunk.old_section().unwrap();
let delete_lines: Vec<_> = old_section
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::DELETE_LINE)
.collect();
assert_eq!(delete_lines.len(), 2);
}
#[test]
fn test_context_diff_empty_file() {
let input = r#"*** /dev/null
--- b/newfile.txt
***************
*** 0 ****
--- 1,3 ----
+ line 1
+ line 2
+ line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
assert_eq!(file.old_file().unwrap().path().unwrap().text(), "/dev/null");
}
#[test]
fn test_context_diff_delete_entire_file() {
let input = r#"*** a/oldfile.txt
--- /dev/null
***************
*** 1,3 ****
- line 1
- line 2
- line 3
--- 0 ****
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
assert_eq!(file.new_file().unwrap().path().unwrap().text(), "/dev/null");
}
#[test]
fn test_context_diff_single_line_file() {
let input = r#"*** a/single.txt
--- b/single.txt
***************
*** 1 ****
! old content
--- 1 ----
! new content
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let old_section = hunk.old_section().unwrap();
let old_changes: Vec<_> = old_section
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::CONTEXT_CHANGE_LINE)
.collect();
assert_eq!(old_changes.len(), 1);
}
#[test]
fn test_context_diff_multiple_hunks() {
let input = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,3 ****
line 1
! old line 2
line 3
--- 1,3 ----
line 1
! new line 2
line 3
***************
*** 10,12 ****
line 10
! old line 11
line 12
--- 10,12 ----
line 10
! new line 11
line 12
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
let hunks: Vec<_> = file.hunks().collect();
assert_eq!(hunks.len(), 2);
}
#[test]
fn test_ed_diff_empty_content() {
let input = r#"5a
.
3d
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert_eq!(patch.detect_format(), Some(DiffFormat::Ed));
let commands: Vec<_> = patch.ed_commands().collect();
assert_eq!(commands.len(), 2);
let add_cmd = commands[0].as_add().unwrap();
let content: Vec<_> = add_cmd.content_lines().collect();
assert_eq!(content.len(), 0);
}
#[test]
fn test_ed_diff_multi_line_change() {
let input = r#"2,5c
new line 2
new line 3
new line 4
new line 5
.
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let cmd = patch.ed_commands().next().unwrap();
let change = cmd.as_change().unwrap();
let (start, end) = change.line_numbers();
assert_eq!(start, Some(2));
assert_eq!(end, Some(5));
let content: Vec<_> = change.content_lines().collect();
assert_eq!(content.len(), 4);
}
#[test]
fn test_ed_diff_single_char_content() {
let input = r#"1a
x
y
z
.
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let cmd = patch.ed_commands().next().unwrap();
let add = cmd.as_add().unwrap();
let content: Vec<_> = add.content_lines().collect();
assert_eq!(content.len(), 3); assert_eq!(content[0].text().unwrap(), "x");
assert_eq!(content[1].text().unwrap(), "y");
assert_eq!(content[2].text().unwrap(), "z");
}
#[test]
fn test_ed_diff_delete_range() {
let input = r#"10,20d
5d
1,3d
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let commands: Vec<_> = patch.ed_commands().collect();
assert_eq!(commands.len(), 3);
let del1 = commands[0].as_delete().unwrap();
let (start, end) = del1.line_numbers();
assert_eq!(start, Some(10));
assert_eq!(end, Some(20));
let del2 = commands[1].as_delete().unwrap();
let (start, end) = del2.line_numbers();
assert_eq!(start, Some(5));
assert_eq!(end, None);
}
#[test]
fn test_ed_diff_append_at_end() {
let input = r#"999a
This is appended at the end
.
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let cmd = patch.ed_commands().next().unwrap();
let add = cmd.as_add().unwrap();
let (line, _) = add.line_numbers();
assert_eq!(line, Some(999));
}
#[test]
fn test_normal_diff_no_changes() {
let input = r#"1,3c1,3
< line 1
< line 2
< line 3
---
> line 1
> line 2
> line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert_eq!(patch.detect_format(), Some(DiffFormat::Normal));
let hunk = patch.normal_hunks().next().unwrap();
assert!(hunk.old_lines().is_some());
assert!(hunk.new_lines().is_some());
}
#[test]
fn test_normal_diff_only_additions() {
let input = r#"0a1,3
> new line 1
> new line 2
> new line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let hunk = patch.normal_hunks().next().unwrap();
assert!(hunk.old_lines().is_none());
assert!(hunk.new_lines().is_some());
let new_lines = hunk.new_lines().unwrap();
let add_lines: Vec<_> = new_lines
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::ADD_LINE)
.collect();
assert_eq!(add_lines.len(), 3);
}
#[test]
fn test_normal_diff_only_deletions() {
let input = r#"1,3d0
< deleted line 1
< deleted line 2
< deleted line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let hunk = patch.normal_hunks().next().unwrap();
assert!(hunk.old_lines().is_some());
assert!(hunk.new_lines().is_none());
let old_lines = hunk.old_lines().unwrap();
let del_lines: Vec<_> = old_lines
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::DELETE_LINE)
.collect();
assert_eq!(del_lines.len(), 3);
}
#[test]
fn test_normal_diff_single_line_change() {
let input = r#"5c5
< old line 5
---
> new line 5
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let hunk = patch.normal_hunks().next().unwrap();
let cmd = hunk.command().unwrap();
let cmd_text: String = cmd
.syntax()
.children_with_tokens()
.filter_map(|it| it.into_token())
.map(|t| t.text().to_string())
.collect();
assert!(cmd_text.contains("5c5"));
}
#[test]
fn test_normal_diff_complex_ranges() {
let input = r#"1,10c1,5
< line 1
< line 2
< line 3
< line 4
< line 5
< line 6
< line 7
< line 8
< line 9
< line 10
---
> new line 1
> new line 2
> new line 3
> new line 4
> new line 5
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let hunk = patch.normal_hunks().next().unwrap();
let old_lines = hunk.old_lines().unwrap();
let old_count: usize = old_lines
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::DELETE_LINE)
.count();
assert_eq!(old_count, 10);
let new_lines = hunk.new_lines().unwrap();
let new_count: usize = new_lines
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::ADD_LINE)
.count();
assert_eq!(new_count, 5);
}
#[test]
fn test_ambiguous_separators() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,5 +1,5 @@
--- This line starts with --- but is content
+++ This line starts with +++ but is content
-*** This line starts with *** but is being removed
+*** This line starts with *** but is being added
@@ This line contains @@ but is content
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert_eq!(patch.detect_format(), Some(DiffFormat::Unified));
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
assert_eq!(lines.len(), 5);
}
#[test]
fn test_format_switching_mid_file() {
let input = r#"--- a/file1.txt
+++ b/file1.txt
@@ -1,2 +1,2 @@
line 1
-old line 2
+new line 2
*** a/file2.txt
--- b/file2.txt
***************
*** 1,2 ****
line 1
! old line 2
--- 1,2 ----
line 1
! new line 2
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
assert!(patch.context_diff_files().count() > 0);
}
#[test]
fn test_empty_hunks() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,0 +1,1 @@
+added line
*** a/file2.txt
--- b/file2.txt
***************
*** 1,2 ****
context
- deleted
--- 1,1 ----
context
3d2
< deleted line
0a1
> added line
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
assert!(patch.context_diff_files().count() > 0);
assert!(parsed.ok());
}
#[test]
fn test_windows_paths() {
let input = r#"--- C:\Users\test\file.txt
+++ C:\Users\test\file.txt
@@ -1,1 +1,1 @@
-old
+new
*** C:\Program Files\app\config.txt
--- C:\Program Files\app\config.txt
***************
*** 1 ****
! old config
--- 1 ----
! new config
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let unified_file = patch.patch_files().next().unwrap();
let old_path = unified_file.old_file().unwrap().path().unwrap();
assert!(old_path.text().contains("C:\\Users\\test\\file.txt"));
let context_files: Vec<_> = patch.context_diff_files().collect();
if !context_files.is_empty() {
let context_file = &context_files[0];
if let Some(old_file) = context_file.old_file() {
if let Some(path) = old_file.path() {
let path_text = path.text();
println!("Parsed context path: {:?}", path_text);
}
}
}
assert!(parsed.ok());
}
#[test]
fn test_paths_with_spaces_and_special_chars() {
let input = r#"--- "a/my file with spaces.txt"
+++ "b/my file with spaces.txt"
@@ -1,1 +1,1 @@
-old
+new
*** a/file(with)[special]#chars.txt
--- b/file(with)[special]#chars.txt
***************
*** 1 ****
! content
--- 1 ----
! new content
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
assert!(patch.context_diff_files().count() > 0);
}
#[test]
fn test_very_long_lines() {
let long_line = "x".repeat(1000);
let input = format!(
r#"--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-old
+{}
"#,
long_line
);
let parsed = edit::parse(&input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
let add_line = lines.iter().find(|l| l.as_add().is_some()).unwrap();
assert_eq!(add_line.text().unwrap().len(), 1000);
}
#[test]
fn test_unicode_in_diff_markers() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
normal line
->>> This looks like a conflict marker but isn't
+<<< This also looks like a conflict marker
@@@ This looks like a hunk header @@@
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
assert!(lines.len() >= 3);
assert!(lines.iter().any(|l| l.as_delete().is_some()));
assert!(lines.iter().any(|l| l.as_add().is_some()));
}
#[test]
fn test_binary_file_notation() {
let input = r#"--- a/binary.bin
+++ b/binary.bin
Binary files differ
*** a/image.png
--- b/image.png
Binary files differ
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
}
#[test]
fn test_malformed_ranges() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
line 1
+line 2
5,3c7,6
< this range is backwards
---
> but we try to parse it
***************
*** 5,3 ****
! backwards range
--- 7,6 ----
! in context diff too
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
}
#[test]
fn test_incomplete_ed_commands() {
let input = r#"5a
new line
another line"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.ed_commands().count() > 0);
}
#[test]
fn test_nested_diff_content() {
let input = r#"--- a/test.sh
+++ b/test.sh
@@ -1,5 +1,5 @@
#!/bin/bash
# This script generates diffs
-diff -u file1 file2 > output.patch
+diff -c file1 file2 > output.patch
echo "--- DONE ---"
echo "+++ COMPLETE +++"
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
assert!(lines.len() >= 5);
assert!(lines
.iter()
.any(|l| l.as_delete().is_some() && l.text().unwrap().contains("diff -u")));
assert!(lines
.iter()
.any(|l| l.as_add().is_some() && l.text().unwrap().contains("diff -c")));
}
#[test]
fn test_quoted_paths() {
let input = r#"--- "a/my file.txt" 2024-01-01
+++ "b/my file.txt" 2024-01-01
@@ -1,1 +1,1 @@
-old
+new
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let old_path = file.old_file().unwrap().path().unwrap();
assert!(old_path.text().contains("my") || old_path.text().contains("file.txt"));
}
#[test]
fn test_git_binary_patch() {
let input = r#"--- a/image.png
+++ b/image.png
GIT binary patch
delta 123
xc$@#Y&W0p@Fc
delta 456
zc%@#Y&W0p@Fc"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
}
#[test]
fn test_svn_property_changes() {
let input = r#"--- a/file.txt
+++ b/file.txt
Property changes on: file.txt
___________________________________________________________________
Added: svn:keywords
+ Id Rev Date
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
}
#[test]
fn test_perforce_style_markers() {
let input = r#"--- a/file.txt#1
+++ b/file.txt#2
@@ -1,1 +1,1 @@
-old
+new
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
assert!(file.old_file().is_some());
}
#[test]
fn test_timestamp_formats() {
let input = r#"--- a/file.txt Thu Jan 1 00:00:00 1970
+++ b/file.txt 2024-01-01 12:34:56.789012 +0000
@@ -1,1 +1,1 @@
-old
+new
*** a/file2.txt Thu, 01 Jan 1970 00:00:00 +0000
--- b/file2.txt Mon Jan 01 12:34:56 PST 2024
***************
*** 1 ****
! old
--- 1 ----
! new
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.patch_files().count() > 0);
assert!(patch.context_diff_files().count() > 0);
}
#[test]
fn test_no_newline_variations() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
line 1
-line 2
\ No newline at end of file
+line 2 modified
\ No newline at end of file
--- a/file2.txt
+++ b/file2.txt
@@ -1,1 +1,1 @@
-old
\ No newline at end of file
+new
\No newline at end of file
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let files: Vec<_> = patch.patch_files().collect();
assert_eq!(files.len(), 2);
}
#[test]
fn test_mixed_line_types_in_hunk() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,10 +1,10 @@
context 1
-delete 1
+add 1
context 2
-delete 2
-delete 3
+add 2
+add 3
context 3
+add 4
context 4
-delete 4
context 5
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
assert!(lines.iter().any(|l| l.as_context().is_some()));
assert!(lines.iter().any(|l| l.as_add().is_some()));
assert!(lines.iter().any(|l| l.as_delete().is_some()));
}
#[test]
fn test_context_diff_with_backslash() {
let input = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,3 ****
line 1
! old \n escaped
line 3
--- 1,3 ----
line 1
! new \t escaped
line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.context_diff_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let old_section = hunk.old_section().unwrap();
let change_lines: Vec<_> = old_section
.syntax()
.children()
.filter(|n| n.kind() == crate::edit::lex::SyntaxKind::CONTEXT_CHANGE_LINE)
.collect();
assert_eq!(change_lines.len(), 1);
}
#[test]
fn test_ed_diff_with_special_chars() {
let input = r#"5c
*** special ***
### chars ###
@@@ here @@@
.
10a
+++ more +++
--- special ---
.
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let commands: Vec<_> = patch.ed_commands().collect();
assert_eq!(commands.len(), 2);
let change = commands[0].as_change().unwrap();
let content: Vec<_> = change.content_lines().collect();
assert_eq!(content.len(), 3);
}
#[test]
fn test_normal_diff_edge_cases() {
let input = r#"0a1
> added at beginning
99d98
< deleted at end
1,1c1,1
< exactly the same
---
> exactly the same
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let hunks: Vec<_> = patch.normal_hunks().collect();
assert_eq!(hunks.len(), 3);
}
#[test]
fn test_incomplete_patches() {
let incomplete_unified = r#"--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3"#;
let parsed = edit::parse(incomplete_unified);
assert!(parsed.ok());
let incomplete_context = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,3 ****"#;
let parsed = edit::parse(incomplete_context);
assert!(parsed.ok());
let incomplete_normal = r#"5c5
< old line"#;
let parsed = edit::parse(incomplete_normal);
assert!(parsed.ok());
}
#[test]
fn test_multiple_files_different_formats() {
let input = r#"--- a/file1.txt
+++ b/file1.txt
@@ -1,1 +1,1 @@
-unified old
+unified new
*** a/file2.txt
--- b/file2.txt
***************
*** 1 ****
! context old
--- 1 ----
! context new
5c
ed style change
.
10c10
< normal old
---
> normal new
--- a/file3.txt
+++ b/file3.txt
@@ -1,1 +1,1 @@
-another unified
+another unified new
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let total_items = patch.patch_files().count()
+ patch.context_diff_files().count()
+ patch.ed_commands().count()
+ patch.normal_hunks().count();
assert!(total_items >= 4);
}
#[test]
fn test_whitespace_only_changes() {
let input = r#"--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+line 2
line 3
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
let file = patch.patch_files().next().unwrap();
let hunk = file.hunks().next().unwrap();
let lines: Vec<_> = hunk.lines().collect();
assert!(lines.iter().any(|l| l.as_delete().is_some()));
assert!(lines.iter().any(|l| l.as_add().is_some()));
}
#[test]
fn test_consecutive_diff_formats() {
let input = r#"3c3
< old
---
> new
5a6
> added
--- a/file.txt
+++ b/file.txt
@@ -1,1 +1,1 @@
-unified old
+unified new
*** a/file.txt
--- b/file.txt
***************
*** 1 ****
! context old
--- 1 ----
! context new
7d6
< deleted
"#;
let parsed = edit::parse(input);
assert!(parsed.ok());
let patch = parsed.tree();
assert!(patch.normal_hunks().count() >= 2);
assert!(patch.patch_files().count() >= 1);
assert!(patch.context_diff_files().count() >= 1);
}
}