1mod line_diff;
4mod blob_merge;
5mod name_status;
6mod patch;
7mod merge_trees;
8mod name;
9pub mod format;
10pub mod range;
11pub mod render;
12pub mod ws;
13
14pub use sley_core::BString;
15
16pub use line_diff::*;
17pub use blob_merge::*;
18pub use name_status::*;
19pub use patch::*;
20pub use merge_trees::*;
21pub use format::*;
22
23#[cfg(test)]
24mod tests {
25 use super::*;
26 use crate::name_status::{
27 changed_tree_entries, collect_full_tree_pair, diff_name_status_maps,
28 diff_name_status_maps_with_renames, read_blob_bytes, TREE_ENTRY_MODE,
29 };
30 use crate::patch::{parse_leading_usize, split_blob_lines};
31 use sley_core::{ObjectFormat, ObjectId};
32 use sley_formats::RepositoryLayout;
33 use sley_index::Index;
34 use sley_object::{EncodedObject, ObjectType, Tree, TreeEntry};
35 use sley_odb::{FileObjectDatabase, ObjectWriter};
36 use std::fs;
37 use std::path::{Path, PathBuf};
38 use std::sync::atomic::{AtomicU64, Ordering};
39
40 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
41
42 #[test]
43 fn name_status_reports_added_from_index() {
44 let root = temp_root();
45 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
46 .expect("test operation should succeed");
47 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
48 let oid = db
49 .write_object(EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec()))
50 .expect("test operation should succeed");
51 let index = Index {
52 version: 2,
53 entries: vec![sley_index::IndexEntry {
54 ctime_seconds: 0,
55 ctime_nanoseconds: 0,
56 mtime_seconds: 0,
57 mtime_nanoseconds: 0,
58 dev: 0,
59 ino: 0,
60 mode: 0o100644,
61 uid: 0,
62 gid: 0,
63 size: 6,
64 oid,
65 flags: "hello.txt".len() as u16,
66 flags_extended: 0,
67 path: BString::from(b"hello.txt"),
68 }],
69 extensions: Vec::new(),
70 checksum: None,
71 };
72 fs::write(
73 layout.git_dir.join("index"),
74 index
75 .write_v2_sha1()
76 .expect("test operation should succeed"),
77 )
78 .expect("test operation should succeed");
79 fs::write(root.join("hello.txt"), b"hello\n").expect("test operation should succeed");
80 let changes = diff_name_status_head_worktree(&root, &layout.git_dir, ObjectFormat::Sha1)
81 .expect("test operation should succeed");
82 assert_eq!(changes[0].line(), "A\thello.txt");
83 fs::remove_dir_all(root).expect("test operation should succeed");
84 }
85
86 #[test]
87 fn tree_worktree_diff_treats_absent_skip_worktree_entries_as_clean() {
88 let root = temp_root();
89 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
90 .expect("test operation should succeed");
91 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
92 let oid = write_blob(&mut db, b"clean\n");
93 let tree = write_tree(
94 &mut db,
95 &[(b"removeme", 0o100644, oid), (b"untouched", 0o100644, oid)],
96 );
97 write_index(
98 &layout.git_dir,
99 vec![
100 skip_worktree_entry(b"removeme", oid),
101 skip_worktree_entry(b"untouched", oid),
102 ],
103 );
104
105 let changes = diff_name_status_tree_worktree_with_options(
106 &root,
107 &layout.git_dir,
108 ObjectFormat::Sha1,
109 &tree,
110 DiffNameStatusOptions::default(),
111 )
112 .expect("test operation should succeed");
113
114 assert!(
115 changes.is_empty(),
116 "absent sparse entries should not appear as deletes: {changes:?}"
117 );
118 fs::remove_dir_all(root).expect("test operation should succeed");
119 }
120
121 #[test]
122 fn tree_worktree_diff_trusts_present_skip_worktree_entry_when_sparse_disabled() {
123 let root = temp_root();
124 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
125 .expect("test operation should succeed");
126 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
127 let oid = write_blob(&mut db, b"clean\n");
128 let tree = write_tree(&mut db, &[(b"modified", 0o100644, oid)]);
129 write_index(&layout.git_dir, vec![skip_worktree_entry(b"modified", oid)]);
130 fs::write(root.join("modified"), b"dirty\n").expect("test operation should succeed");
131
132 let changes = diff_name_status_tree_worktree_with_options(
133 &root,
134 &layout.git_dir,
135 ObjectFormat::Sha1,
136 &tree,
137 DiffNameStatusOptions::default(),
138 )
139 .expect("test operation should succeed");
140
141 assert!(
142 changes.is_empty(),
143 "present skip-worktree dirt should be ignored when sparse checkout is disabled: {changes:?}"
144 );
145 fs::remove_dir_all(root).expect("test operation should succeed");
146 }
147
148 #[test]
149 fn tree_worktree_diff_adds_index_blob_for_present_skip_worktree_entry() {
150 let root = temp_root();
151 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
152 .expect("test operation should succeed");
153 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
154 let oid = write_blob(&mut db, b"");
155 let tree = write_tree(&mut db, &[]);
156 write_index(&layout.git_dir, vec![skip_worktree_entry(b"added", oid)]);
157 fs::write(root.join("added"), b"dirty\n").expect("test operation should succeed");
158
159 let changes = diff_name_status_tree_worktree_with_options(
160 &root,
161 &layout.git_dir,
162 ObjectFormat::Sha1,
163 &tree,
164 DiffNameStatusOptions::default(),
165 )
166 .expect("test operation should succeed");
167
168 assert_eq!(changes.len(), 1);
169 assert_eq!(changes[0].line(), "A\tadded");
170 assert_eq!(changes[0].new_oid, Some(oid));
171 fs::remove_dir_all(root).expect("test operation should succeed");
172 }
173
174 #[test]
175 fn index_worktree_diff_returns_staged_gitlinks() {
176 let root = temp_root();
177 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
178 .expect("test operation should succeed");
179 let oid = ObjectId::from_hex(
180 ObjectFormat::Sha1,
181 "1111111111111111111111111111111111111111",
182 )
183 .expect("test operation should succeed");
184 let index = Index {
185 version: 2,
186 entries: vec![sley_index::IndexEntry {
187 ctime_seconds: 0,
188 ctime_nanoseconds: 0,
189 mtime_seconds: 0,
190 mtime_nanoseconds: 0,
191 dev: 0,
192 ino: 0,
193 mode: sley_index::GITLINK_MODE,
194 uid: 0,
195 gid: 0,
196 size: 0,
197 oid,
198 flags: "deps/sub".len() as u16,
199 flags_extended: 0,
200 path: BString::from(b"deps/sub"),
201 }],
202 extensions: Vec::new(),
203 checksum: None,
204 };
205 fs::write(
206 layout.git_dir.join("index"),
207 index
208 .write_v2_sha1()
209 .expect("test operation should succeed"),
210 )
211 .expect("test operation should succeed");
212
213 let diff = diff_name_status_index_worktree_with_options_and_gitlinks(
214 &root,
215 &layout.git_dir,
216 ObjectFormat::Sha1,
217 DiffNameStatusOptions::default(),
218 )
219 .expect("test operation should succeed");
220
221 assert_eq!(diff.entries.len(), 1);
222 let gitlinks = diff.staged_gitlinks;
223 assert_eq!(gitlinks.len(), 1);
224 assert_eq!(gitlinks[0].path.as_bytes(), b"deps/sub");
225 assert_eq!(gitlinks[0].oid, oid);
226 fs::remove_dir_all(root).expect("test operation should succeed");
227 }
228
229 #[cfg(unix)]
230 #[test]
231 fn index_worktree_diff_ignores_untracked_dangling_symlink() {
232 use std::os::unix::fs::symlink;
233
234 let root = temp_root();
235 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
236 .expect("test operation should succeed");
237 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
238 let oid = db
239 .write_object(EncodedObject::new(ObjectType::Blob, b"clean\n".to_vec()))
240 .expect("test operation should succeed");
241 let index = Index {
242 version: 2,
243 entries: vec![sley_index::IndexEntry {
244 ctime_seconds: 0,
245 ctime_nanoseconds: 0,
246 mtime_seconds: 0,
247 mtime_nanoseconds: 0,
248 dev: 0,
249 ino: 0,
250 mode: 0o100644,
251 uid: 0,
252 gid: 0,
253 size: 6,
254 oid,
255 flags: "tracked.txt".len() as u16,
256 flags_extended: 0,
257 path: BString::from(b"tracked.txt"),
258 }],
259 extensions: Vec::new(),
260 checksum: None,
261 };
262 fs::write(
263 layout.git_dir.join("index"),
264 index
265 .write_v2_sha1()
266 .expect("test operation should succeed"),
267 )
268 .expect("test operation should succeed");
269 fs::write(root.join("tracked.txt"), b"clean\n").expect("test operation should succeed");
270 symlink("missing-target", root.join("untracked-link"))
271 .expect("test operation should succeed");
272
273 let changes = diff_name_status_index_worktree_with_options(
274 &root,
275 &layout.git_dir,
276 ObjectFormat::Sha1,
277 DiffNameStatusOptions {
278 detect_renames: false,
279 detect_copies: false,
280 find_copies_harder: false,
281 rename_empty: true,
282
283 ..Default::default()
284 },
285 )
286 .expect("untracked dangling symlink should be ignored");
287 assert!(changes.is_empty());
288 fs::remove_dir_all(root).expect("test operation should succeed");
289 }
290
291 #[test]
292 fn index_worktree_diff_trusts_non_racy_stat_cache() {
293 let root = temp_root();
294 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
295 .expect("test operation should succeed");
296 let worktree_path = root.join("tracked.txt");
297 fs::write(&worktree_path, b"clean\n").expect("test operation should succeed");
298 let metadata = fs::symlink_metadata(&worktree_path).expect("test operation should succeed");
299 let (mtime_seconds, mtime_nanoseconds) =
300 sley_index::file_mtime_parts(&metadata).expect("test operation should succeed");
301 let bogus_oid = ObjectId::from_hex(
302 ObjectFormat::Sha1,
303 "1111111111111111111111111111111111111111",
304 )
305 .expect("test operation should succeed");
306 let index = Index {
307 version: 2,
308 entries: vec![sley_index::IndexEntry {
309 ctime_seconds: 0,
310 ctime_nanoseconds: 0,
311 mtime_seconds: mtime_seconds as u32,
312 mtime_nanoseconds: mtime_nanoseconds as u32,
313 dev: 0,
314 ino: 0,
315 mode: sley_index::worktree_metadata_mode(&metadata),
316 uid: 0,
317 gid: 0,
318 size: metadata.len() as u32,
319 oid: bogus_oid,
320 flags: "tracked.txt".len() as u16,
321 flags_extended: 0,
322 path: BString::from(b"tracked.txt"),
323 }],
324 extensions: Vec::new(),
325 checksum: None,
326 };
327 std::thread::sleep(std::time::Duration::from_millis(1100));
328 fs::write(
329 layout.git_dir.join("index"),
330 index
331 .write_v2_sha1()
332 .expect("test operation should succeed"),
333 )
334 .expect("test operation should succeed");
335
336 let changes = diff_name_status_index_worktree(&root, &layout.git_dir, ObjectFormat::Sha1)
337 .expect("test operation should succeed");
338 assert!(
339 changes.is_empty(),
340 "a clean non-racy stat match must reuse the cached index oid"
341 );
342 fs::remove_dir_all(root).expect("test operation should succeed");
343 }
344
345 fn temp_root() -> PathBuf {
346 let path = std::env::temp_dir().join(format!(
347 "sley-diff-{}-{}",
348 std::process::id(),
349 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
350 ));
351 fs::create_dir_all(&path).expect("test operation should succeed");
352 path
353 }
354
355 fn merge_opts() -> MergeBlobOptions<'static> {
358 MergeBlobOptions {
359 ours_label: "ours",
360 theirs_label: "theirs",
361 base_label: "base",
362 style: ConflictStyle::Merge,
363 favor: MergeFavor::None,
364 ws_ignore: WsIgnore::EMPTY,
365 marker_size: 7,
366 }
367 }
368
369 #[test]
370 fn split_lines_preserves_content_and_newlines() {
371 let lines = split_lines(b"a\nb\nc\n");
372 assert_eq!(lines.len(), 3);
373 assert_eq!(lines[0].content, b"a\n");
374 assert!(lines[0].has_newline);
375 assert_eq!(lines[2].content, b"c\n");
376 assert!(lines[2].has_newline);
377 assert!(split_lines(b"").is_empty());
378 }
379
380 #[test]
381 fn split_lines_tracks_missing_final_newline() {
382 let lines = split_lines(b"a\nb");
383 assert_eq!(lines.len(), 2);
384 assert!(lines[0].has_newline);
385 assert!(!lines[1].has_newline);
386 assert_eq!(lines[1].content, b"b");
387 assert_eq!(lines[1].bytes_without_newline(), b"b");
388 let with_nl = split_lines(b"b\n");
390 assert_ne!(lines[1], with_nl[0]);
391 }
392
393 #[test]
394 fn myers_replace_single_line() {
395 let old = split_lines(b"a\nb\nc\n");
396 let new = split_lines(b"a\nx\nc\n");
397 assert_eq!(
398 myers_diff_lines(&old, &new),
399 vec![
400 DiffOp::Equal(1),
401 DiffOp::Delete(1),
402 DiffOp::Insert(1),
403 DiffOp::Equal(1),
404 ]
405 );
406 }
407
408 #[test]
409 fn myers_identical_is_single_equal() {
410 let old = split_lines(b"a\nb\nc\n");
411 let new = split_lines(b"a\nb\nc\n");
412 assert_eq!(myers_diff_lines(&old, &new), vec![DiffOp::Equal(3)]);
413 }
414
415 #[test]
416 fn myers_pure_insert_and_delete() {
417 let empty = split_lines(b"");
418 let two = split_lines(b"a\nb\n");
419 assert_eq!(myers_diff_lines(&empty, &two), vec![DiffOp::Insert(2)]);
420 assert_eq!(myers_diff_lines(&two, &empty), vec![DiffOp::Delete(2)]);
421
422 let old = split_lines(b"a\nb\nc\nd\n");
423 let new = split_lines(b"a\nc\nd\n");
424 assert_eq!(
425 myers_diff_lines(&old, &new),
426 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Equal(2)]
427 );
428 }
429
430 #[test]
431 fn myers_reconstructs_new_and_is_minimal() {
432 let old = split_lines(b"the\nquick\nbrown\nfox\n");
434 let new = split_lines(b"the\nlazy\nbrown\ncat\n");
435 let ops = myers_diff_lines(&old, &new);
436 let mut oi = 0usize;
437 let mut ni = 0usize;
438 let mut edits = 0usize;
439 let mut rebuilt: Vec<u8> = Vec::new();
440 for op in &ops {
441 match *op {
442 DiffOp::Equal(n) => {
443 for _ in 0..n {
444 assert_eq!(old[oi], new[ni]);
445 rebuilt.extend_from_slice(old[oi].content);
446 oi += 1;
447 ni += 1;
448 }
449 }
450 DiffOp::Delete(n) => {
451 oi += n;
452 edits += n;
453 }
454 DiffOp::Insert(n) => {
455 for _ in 0..n {
456 rebuilt.extend_from_slice(new[ni].content);
457 ni += 1;
458 }
459 edits += n;
460 }
461 }
462 }
463 assert_eq!(rebuilt, b"the\nlazy\nbrown\ncat\n");
464 assert_eq!(edits, 4);
466 }
467
468 #[test]
469 fn merge_non_overlapping_changes_is_clean() {
470 let base = b"a\nb\nc\nd\ne\n";
471 let ours = b"A\nb\nc\nd\ne\n";
472 let theirs = b"a\nb\nc\nd\nE\n";
473 let result = merge_blobs(base, ours, theirs, &merge_opts());
474 assert!(!result.conflicted);
475 assert_eq!(result.content, b"A\nb\nc\nd\nE\n");
476 }
477
478 #[test]
479 fn merge_identical_changes_no_conflict() {
480 let base = b"a\nb\nc\n";
481 let ours = b"a\nX\nc\n";
482 let theirs = b"a\nX\nc\n";
483 let result = merge_blobs(base, ours, theirs, &merge_opts());
484 assert!(!result.conflicted);
485 assert_eq!(result.content, b"a\nX\nc\n");
486 }
487
488 #[test]
489 fn merge_overlapping_change_emits_exact_markers() {
490 let base = b"a\nb\nc\n";
491 let ours = b"a\nOURS\nc\n";
492 let theirs = b"a\nTHEIRS\nc\n";
493 let result = merge_blobs(base, ours, theirs, &merge_opts());
494 assert!(result.conflicted);
495 assert_eq!(
496 result.content,
497 b"a\n<<<<<<< ours\nOURS\n=======\nTHEIRS\n>>>>>>> theirs\nc\n".to_vec(),
498 );
499 }
500
501 #[test]
502 fn merge_diff3_style_includes_base_section() {
503 let base = b"a\nb\nc\n";
504 let ours = b"a\nOURS\nc\n";
505 let theirs = b"a\nTHEIRS\nc\n";
506 let options = MergeBlobOptions {
507 style: ConflictStyle::Diff3,
508 ..merge_opts()
509 };
510 let result = merge_blobs(base, ours, theirs, &options);
511 assert!(result.conflicted);
512 assert_eq!(
513 result.content,
514 b"a\n<<<<<<< ours\nOURS\n||||||| base\nb\n=======\nTHEIRS\n>>>>>>> theirs\nc\n"
515 .to_vec(),
516 );
517 }
518
519 #[test]
520 fn merge_empty_label_omits_trailing_space() {
521 let base = b"a\nb\nc\n";
522 let ours = b"a\nOURS\nc\n";
523 let theirs = b"a\nTHEIRS\nc\n";
524 let options = MergeBlobOptions {
525 ours_label: "",
526 theirs_label: "",
527 base_label: "",
528 style: ConflictStyle::Merge,
529 favor: MergeFavor::None,
530 ws_ignore: WsIgnore::EMPTY,
531 marker_size: 7,
532 };
533 let result = merge_blobs(base, ours, theirs, &options);
534 assert!(result.conflicted);
535 assert_eq!(
537 result.content,
538 b"a\n<<<<<<<\nOURS\n=======\nTHEIRS\n>>>>>>>\nc\n".to_vec(),
539 );
540 }
541
542 #[test]
543 fn merge_add_add_empty_base_conflicts() {
544 let result = merge_blobs(b"", b"x\ny\n", b"p\nq\n", &merge_opts());
545 assert!(result.conflicted);
546 assert_eq!(
547 result.content,
548 b"<<<<<<< ours\nx\ny\n=======\np\nq\n>>>>>>> theirs\n".to_vec(),
549 );
550 }
551
552 #[test]
553 fn merge_ignore_space_change_resolves_clean_keeping_ours() {
554 let base = b"alpha beta\nsecond line\n";
559 let ours = b"alpha beta\nsecond line\n"; let theirs = b"alpha beta\nsecond CHANGED\n"; let options = MergeBlobOptions {
562 ws_ignore: WsIgnore {
563 space_change: true,
564 ..WsIgnore::EMPTY
565 },
566 ..merge_opts()
567 };
568 let result = merge_blobs(base, ours, theirs, &options);
569 assert!(
570 !result.conflicted,
571 "whitespace-only divergence is not a conflict"
572 );
573 assert_eq!(result.content, b"alpha beta\nsecond CHANGED\n".to_vec());
574 }
575
576 #[test]
577 fn merge_ignore_space_change_still_conflicts_on_real_divergence() {
578 let base = b"one\n";
581 let ours = b"OURS\n";
582 let theirs = b"THEIRS\n";
583 let options = MergeBlobOptions {
584 ws_ignore: WsIgnore {
585 space_change: true,
586 ..WsIgnore::EMPTY
587 },
588 ..merge_opts()
589 };
590 let result = merge_blobs(base, ours, theirs, &options);
591 assert!(result.conflicted);
592 }
593
594 #[test]
595 fn merge_add_add_empty_base_identical_is_clean() {
596 let result = merge_blobs(b"", b"x\ny\n", b"x\ny\n", &merge_opts());
597 assert!(!result.conflicted);
598 assert_eq!(result.content, b"x\ny\n");
599 }
600
601 #[test]
602 fn merge_deletion_one_side_takes_deletion() {
603 let result = merge_blobs(b"a\nb\nc\n", b"a\nc\n", b"a\nb\nc\n", &merge_opts());
605 assert!(!result.conflicted);
606 assert_eq!(result.content, b"a\nc\n");
607 }
608
609 #[test]
610 fn merge_deletion_vs_modification_conflicts() {
611 let result = merge_blobs(b"a\nb\nc\n", b"a\nc\n", b"a\nB!\nc\n", &merge_opts());
613 assert!(result.conflicted);
614 assert_eq!(
616 result.content,
617 b"a\n<<<<<<< ours\n=======\nB!\n>>>>>>> theirs\nc\n".to_vec(),
618 );
619 }
620
621 #[test]
622 fn merge_missing_final_newline_marker_starts_on_own_line() {
623 let base = b"a\nb";
626 let ours = b"a\nOURS";
627 let theirs = b"a\nTHEIRS";
628 let result = merge_blobs(base, ours, theirs, &merge_opts());
629 assert!(result.conflicted);
630 assert_eq!(
631 result.content,
632 b"a\n<<<<<<< ours\nOURS\n=======\nTHEIRS\n>>>>>>> theirs\n".to_vec(),
633 );
634 }
635
636 #[test]
637 fn merge_clean_preserves_missing_final_newline() {
638 let result = merge_blobs(b"a\nb\n", b"a\nb", b"a\nb\n", &merge_opts());
641 assert!(!result.conflicted);
642 assert_eq!(result.content, b"a\nb");
643 }
644
645 #[test]
646 fn merge_both_append_identical_tail_is_clean() {
647 let result = merge_blobs(b"a\n", b"a\nz\n", b"a\nz\n", &merge_opts());
648 assert!(!result.conflicted);
649 assert_eq!(result.content, b"a\nz\n");
650 }
651
652 #[test]
653 fn merge_when_ours_equals_base_yields_theirs() {
654 let base = b"b\na\n";
657 let theirs = b"b\nb\nc\na\nc\n";
658 let result = merge_blobs(base, base, theirs, &merge_opts());
659 assert!(!result.conflicted);
660 assert_eq!(result.content, theirs.to_vec());
661 }
662 fn applied(outcome: ApplyOutcome) -> Vec<u8> {
663 match outcome {
664 ApplyOutcome::Applied(bytes) => bytes,
665 ApplyOutcome::Rejected => panic!("expected Applied, got Rejected"),
666 }
667 }
668
669 #[test]
670 fn parse_multi_file_patch() {
671 let patch = b"\
672diff --git a/one.txt b/one.txt
673index aaaaaaa..bbbbbbb 100644
674--- a/one.txt
675+++ b/one.txt
676@@ -1,3 +1,3 @@
677 alpha
678-beta
679+BETA
680 gamma
681diff --git a/two.txt b/two.txt
682index ccccccc..ddddddd 100644
683--- a/two.txt
684+++ b/two.txt
685@@ -1,2 +1,3 @@
686 first
687+inserted
688 second
689";
690 let patches = parse_unified_patch(patch).expect("test operation should succeed");
691 assert_eq!(patches.len(), 2);
692
693 assert_eq!(patches[0].old_path.as_deref(), Some(b"one.txt".as_slice()));
694 assert_eq!(patches[0].new_path.as_deref(), Some(b"one.txt".as_slice()));
695 assert_eq!(patches[0].old_mode, Some(0o100644));
698 assert_eq!(
699 patches[0].old_oid_hex.as_deref(),
700 Some(b"aaaaaaa".as_slice())
701 );
702 assert_eq!(
703 patches[0].new_oid_hex.as_deref(),
704 Some(b"bbbbbbb".as_slice())
705 );
706 assert_eq!(patches[0].hunks.len(), 1);
707 let h = &patches[0].hunks[0];
708 assert_eq!(
709 (h.old_start, h.old_len, h.new_start, h.new_len),
710 (1, 3, 1, 3)
711 );
712 assert_eq!(
713 h.lines,
714 vec![
715 HunkLine::Context(b"alpha".to_vec()),
716 HunkLine::Delete(b"beta".to_vec()),
717 HunkLine::Insert(b"BETA".to_vec()),
718 HunkLine::Context(b"gamma".to_vec()),
719 ]
720 );
721
722 assert_eq!(patches[1].new_path.as_deref(), Some(b"two.txt".as_slice()));
723 assert_eq!(patches[1].hunks[0].new_len, 3);
724 }
725
726 #[test]
727 fn parse_default_hunk_range_length() {
728 let patch = b"\
730--- a/x
731+++ b/x
732@@ -1 +1,2 @@
733 line
734+added
735";
736 let patches = parse_unified_patch(patch).expect("test operation should succeed");
737 let h = &patches[0].hunks[0];
738 assert_eq!(
739 (h.old_start, h.old_len, h.new_start, h.new_len),
740 (1, 1, 1, 2)
741 );
742 }
743
744 #[test]
745 fn parse_hunk_header_before_file_errors() {
746 let patch = b"@@ -1,1 +1,1 @@\n context\n";
747 assert!(parse_unified_patch(patch).is_err());
748 }
749
750 #[test]
751 fn parse_mismatched_counts_errors() {
752 let patch = b"--- a/x\n+++ b/x\n@@ -1,2 +1,2 @@\n only\n+new\n";
754 assert!(parse_unified_patch(patch).is_err());
755 }
756
757 #[test]
758 fn apply_clean_hunk() {
759 let base = b"alpha\nbeta\ngamma\n";
760 let patch = parse_unified_patch(
761 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
762 )
763 .expect("test operation should succeed");
764 let out = applied(apply_file_patch(base, &patch[0]));
765 assert_eq!(out, b"alpha\nBETA\ngamma\n");
766 }
767
768 #[test]
769 fn apply_with_line_offset() {
770 let base = b"pre1\npre2\npre3\nalpha\nbeta\ngamma\ntail\n";
777 let patch = parse_unified_patch(
778 b"--- a/x\n+++ b/x\n@@ -2,4 +2,4 @@\n alpha\n-beta\n+BETA\n gamma\n tail\n",
779 )
780 .expect("test operation should succeed");
781 let out = applied(apply_file_patch(base, &patch[0]));
782 assert_eq!(out, b"pre1\npre2\npre3\nalpha\nBETA\ngamma\ntail\n");
783 }
784
785 #[test]
786 fn apply_with_negative_line_offset() {
787 let base = b"alpha\nbeta\ngamma\n";
789 let patch = parse_unified_patch(
790 b"--- a/x\n+++ b/x\n@@ -50,3 +50,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
791 )
792 .expect("test operation should succeed");
793 let out = applied(apply_file_patch(base, &patch[0]));
794 assert_eq!(out, b"alpha\nBETA\ngamma\n");
795 }
796
797 #[test]
798 fn apply_multiple_hunks() {
799 let base = b"a\nb\nc\nd\ne\nf\ng\nh\n";
800 let patch = parse_unified_patch(
801 b"--- a/x\n+++ b/x\n\
802@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n\
803@@ -6,3 +6,3 @@\n f\n-g\n+G\n h\n",
804 )
805 .expect("test operation should succeed");
806 let out = applied(apply_file_patch(base, &patch[0]));
807 assert_eq!(out, b"a\nB\nc\nd\ne\nf\nG\nh\n");
808 }
809
810 #[test]
811 fn reject_on_context_mismatch() {
812 let base = b"alpha\nDIFFERENT\ngamma\n";
813 let patch = parse_unified_patch(
814 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
815 )
816 .expect("test operation should succeed");
817 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
818 }
819
820 #[test]
821 fn reject_when_match_end_required_but_not_at_eof() {
822 let base = b"one\ntwo\nanchor\nalready\nappended\n";
829 let patch =
833 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -3,1 +3,3 @@\n anchor\n+added1\n+added2\n")
834 .expect("test operation should succeed");
835 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
836 }
837
838 #[test]
839 fn append_at_eof_matches_when_context_reaches_end() {
840 let base = b"one\ntwo\nanchor\n";
844 let patch =
845 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -3,1 +3,3 @@\n anchor\n+added1\n+added2\n")
846 .expect("test operation should succeed");
847 let out = applied(apply_file_patch(base, &patch[0]));
848 assert_eq!(out, b"one\ntwo\nanchor\nadded1\nadded2\n");
849 }
850
851 #[test]
852 fn reject_when_match_beginning_required_but_not_at_start() {
853 let base = b"junk\nalpha\nbeta\ngamma\n";
857 let patch =
858 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,2 +1,3 @@\n alpha\n+INSERT\n beta\n")
859 .expect("test operation should succeed");
860 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
861 }
862
863 #[test]
864 fn no_default_fuzz_rejects_on_trailing_context_mismatch() {
865 let base = b"alpha\nbeta\nDIVERGED\n";
871 let patch = parse_unified_patch(
872 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
873 )
874 .expect("test operation should succeed");
875 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
876 }
877
878 #[test]
879 fn parse_and_apply_new_file() {
880 let patch = parse_unified_patch(
881 b"\
882diff --git a/new.txt b/new.txt
883new file mode 100644
884index 0000000..1111111
885--- /dev/null
886+++ b/new.txt
887@@ -0,0 +1,2 @@
888+hello
889+world
890",
891 )
892 .expect("test operation should succeed");
893 assert!(patches_first_is_new(&patch));
894 assert_eq!(patch[0].old_path, None);
895 assert_eq!(patch[0].new_path.as_deref(), Some(b"new.txt".as_slice()));
896 assert_eq!(patch[0].new_mode, Some(0o100644));
897 let out = applied(apply_file_patch(b"garbage that is ignored", &patch[0]));
899 assert_eq!(out, b"hello\nworld\n");
900 }
901
902 fn patches_first_is_new(patches: &[FilePatch]) -> bool {
903 patches.first().map(|p| p.is_new).unwrap_or(false)
904 }
905
906 #[test]
907 fn parse_and_apply_delete_file() {
908 let patch = parse_unified_patch(
909 b"\
910diff --git a/gone.txt b/gone.txt
911deleted file mode 100644
912index 1111111..0000000
913--- a/gone.txt
914+++ /dev/null
915@@ -1,2 +0,0 @@
916-hello
917-world
918",
919 )
920 .expect("test operation should succeed");
921 assert!(patch[0].is_delete);
922 assert_eq!(patch[0].old_path.as_deref(), Some(b"gone.txt".as_slice()));
923 assert_eq!(patch[0].new_path, None);
924 assert_eq!(patch[0].old_mode, Some(0o100644));
925 let out = applied(apply_file_patch(b"hello\nworld\n", &patch[0]));
926 assert_eq!(out, b"");
927 }
928
929 #[test]
930 fn parse_rename_headers() {
931 let patch = parse_unified_patch(
932 b"\
933diff --git a/old/name.txt b/new/name.txt
934similarity index 100%
935rename from old/name.txt
936rename to new/name.txt
937",
938 )
939 .expect("test operation should succeed");
940 assert!(patch[0].is_rename);
941 assert_eq!(
942 patch[0].old_path.as_deref(),
943 Some(b"old/name.txt".as_slice())
944 );
945 assert_eq!(
946 patch[0].new_path.as_deref(),
947 Some(b"new/name.txt".as_slice())
948 );
949 assert!(patch[0].hunks.is_empty());
950 }
951
952 #[test]
953 fn parse_mode_change_headers() {
954 let patch = parse_unified_patch(
955 b"\
956diff --git a/script.sh b/script.sh
957old mode 100644
958new mode 100755
959",
960 )
961 .expect("test operation should succeed");
962 assert_eq!(patch[0].old_mode, Some(0o100644));
963 assert_eq!(patch[0].new_mode, Some(0o100755));
964 assert!(!patch[0].is_new);
965 assert!(!patch[0].is_delete);
966 }
967
968 #[test]
969 fn no_final_newline_base_preserved_when_untouched() {
970 let base = b"alpha\nbeta\nnotail"; let patch = parse_unified_patch(
980 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n-alpha\n+ALPHA\n beta\n notail\n\\ No newline at end of file\n",
981 )
982 .expect("test operation should succeed");
983 let out = applied(apply_file_patch(base, &patch[0]));
984 assert_eq!(out, b"ALPHA\nbeta\nnotail");
985 }
986
987 #[test]
988 fn no_final_newline_added_by_patch() {
989 let base = b"alpha\nbeta\n";
992 let patch = parse_unified_patch(
993 b"--- a/x\n+++ b/x\n@@ -2,1 +2,1 @@\n-beta\n+beta-notail\n\\ No newline at end of file\n",
994 )
995 .expect("test operation should succeed");
996 assert!(patch[0].hunks[0].new_no_newline);
997 assert!(!patch[0].hunks[0].old_no_newline);
998 let out = applied(apply_file_patch(base, &patch[0]));
999 assert_eq!(out, b"alpha\nbeta-notail");
1000 }
1001
1002 #[test]
1003 fn no_final_newline_in_base_matched_and_kept() {
1004 let base = b"alpha\nbeta"; let patch = parse_unified_patch(
1008 b"--- a/x\n+++ b/x\n@@ -1,2 +1,2 @@\n-alpha\n+ALPHA\n beta\n\\ No newline at end of file\n",
1009 )
1010 .expect("test operation should succeed");
1011 assert!(patch[0].hunks[0].old_no_newline);
1012 assert!(patch[0].hunks[0].new_no_newline);
1013 let out = applied(apply_file_patch(base, &patch[0]));
1014 assert_eq!(out, b"ALPHA\nbeta");
1015 }
1016
1017 #[test]
1018 fn no_final_newline_mismatch_rejected() {
1019 let base = b"alpha\nbeta\n"; let patch = parse_unified_patch(
1023 b"--- a/x\n+++ b/x\n@@ -2,1 +2,1 @@\n-beta\n\\ No newline at end of file\n+beta2\n",
1024 )
1025 .expect("test operation should succeed");
1026 assert!(patch[0].hunks[0].old_no_newline);
1027 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
1028 }
1029
1030 #[test]
1031 fn delete_with_no_final_newline() {
1032 let base = b"only line no newline";
1034 let patch = parse_unified_patch(
1035 b"--- a/x\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-only line no newline\n\\ No newline at end of file\n",
1036 )
1037 .expect("test operation should succeed");
1038 assert!(patch[0].is_delete);
1039 let out = applied(apply_file_patch(base, &patch[0]));
1040 assert_eq!(out, b"");
1041 }
1042
1043 #[test]
1044 fn apply_pure_insertion_hunk() {
1045 let base = b"first\nsecond\n";
1046 let patch =
1047 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,2 +1,3 @@\n first\n+middle\n second\n")
1048 .expect("test operation should succeed");
1049 let out = applied(apply_file_patch(base, &patch[0]));
1050 assert_eq!(out, b"first\nmiddle\nsecond\n");
1051 }
1052
1053 #[test]
1054 fn apply_pure_deletion_hunk() {
1055 let base = b"first\nmiddle\nsecond\n";
1056 let patch =
1057 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,3 +1,2 @@\n first\n-middle\n second\n")
1058 .expect("test operation should succeed");
1059 let out = applied(apply_file_patch(base, &patch[0]));
1060 assert_eq!(out, b"first\nsecond\n");
1061 }
1062
1063 #[test]
1064 fn apply_then_reparse_round_trip() {
1065 let base = b"l1\nl2\nl3\nl4\nl5\n";
1069 let text = b"--- a/f\n+++ b/f\n@@ -2,3 +2,4 @@\n l2\n-l3\n+L3\n+L3b\n l4\n";
1070 let p1 = parse_unified_patch(text).expect("test operation should succeed");
1071 let p2 = parse_unified_patch(text).expect("test operation should succeed");
1072 assert_eq!(p1, p2);
1073 let out = applied(apply_file_patch(base, &p1[0]));
1074 assert_eq!(out, b"l1\nl2\nL3\nL3b\nl4\nl5\n");
1075 }
1076
1077 #[test]
1078 fn empty_context_line_without_trailing_space() {
1079 let base = b"a\n\nb\n";
1082 let patch = parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n a\n\n-b\n+B\n")
1083 .expect("test operation should succeed");
1084 assert_eq!(patch[0].hunks[0].lines[1], HunkLine::Context(Vec::new()));
1085 let out = applied(apply_file_patch(base, &patch[0]));
1086 assert_eq!(out, b"a\n\nB\n");
1087 }
1088
1089 #[test]
1090 fn split_blob_lines_handles_edge_cases() {
1091 assert!(split_blob_lines(b"").is_empty());
1092 let single = split_blob_lines(b"abc");
1093 assert_eq!(single.len(), 1);
1094 assert!(single[0].no_newline);
1095 let terminated = split_blob_lines(b"abc\n");
1096 assert_eq!(terminated.len(), 1);
1097 assert!(!terminated[0].no_newline);
1098 let blank_then_eof = split_blob_lines(b"x\n");
1099 assert_eq!(blank_then_eof.len(), 1);
1100 }
1101
1102 #[test]
1105 fn similarity_identical_and_empty_conventions() {
1106 assert_eq!(blob_similarity(b"hello\nworld\n", b"hello\nworld\n"), 100);
1108 assert_eq!(blob_similarity(b"", b""), 100);
1110 assert_eq!(blob_similarity(b"", b"hello\n"), 0);
1112 assert_eq!(blob_similarity(b"hello\n", b""), 0);
1113 }
1114
1115 #[test]
1116 fn similarity_one_changed_line_is_75_and_symmetric() {
1117 let a = b"one\ntwo\nthree\nfour\nfive\n";
1123 let b = b"one\ntwo\nTHREE\nfour\nfive\n";
1124 assert_eq!(blob_similarity(a, b), 75);
1125 assert_eq!(blob_similarity(b, a), 75);
1127 }
1128
1129 #[test]
1130 fn similarity_one_edited_line_of_three_is_66_not_67() {
1131 assert_eq!(blob_similarity(b"a\nb\nc\n", b"a\nB\nc\n"), 66);
1137 assert_eq!(blob_similarity(b"a\nB\nc\n", b"a\nb\nc\n"), 66);
1138 }
1139
1140 #[test]
1141 fn similarity_small_append_is_88() {
1142 let a = b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\n";
1146 let b = b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\nADDED\n";
1147 assert_eq!(blob_similarity(a, b), 88);
1148 }
1149
1150 #[test]
1151 fn similarity_half_rewrite_is_50() {
1152 let a = b"l1\nl2\nl3\nl4\nl5\nl6\n";
1155 let b = b"l1\nl2\nl3\nX4\nX5\nX6\n";
1156 assert_eq!(blob_similarity(a, b), 50);
1157 }
1158
1159 fn write_blob(db: &mut FileObjectDatabase, bytes: &[u8]) -> ObjectId {
1163 db.write_object(EncodedObject::new(ObjectType::Blob, bytes.to_vec()))
1164 .expect("test operation should succeed")
1165 }
1166
1167 fn write_tree(db: &mut FileObjectDatabase, entries: &[(&[u8], u32, ObjectId)]) -> ObjectId {
1170 let mut tree_entries: Vec<TreeEntry> = entries
1171 .iter()
1172 .map(|(name, mode, oid)| TreeEntry {
1173 mode: *mode,
1174 name: BString::from(*name),
1175 oid: *oid,
1176 })
1177 .collect();
1178 tree_entries.sort_by(|a, b| a.name.cmp(&b.name));
1179 let tree = Tree {
1180 entries: tree_entries,
1181 };
1182 db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
1183 .expect("test operation should succeed")
1184 }
1185
1186 fn write_tree_in_order(
1187 db: &mut FileObjectDatabase,
1188 entries: &[(&[u8], u32, ObjectId)],
1189 ) -> ObjectId {
1190 let tree_entries: Vec<TreeEntry> = entries
1191 .iter()
1192 .map(|(name, mode, oid)| TreeEntry {
1193 mode: *mode,
1194 name: BString::from(*name),
1195 oid: *oid,
1196 })
1197 .collect();
1198 let tree = Tree {
1199 entries: tree_entries,
1200 };
1201 db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
1202 .expect("test operation should succeed")
1203 }
1204
1205 fn skip_worktree_entry(path: &[u8], oid: ObjectId) -> sley_index::IndexEntry {
1206 let mut entry = sley_index::IndexEntry {
1207 ctime_seconds: 0,
1208 ctime_nanoseconds: 0,
1209 mtime_seconds: 0,
1210 mtime_nanoseconds: 0,
1211 dev: 0,
1212 ino: 0,
1213 mode: 0o100644,
1214 uid: 0,
1215 gid: 0,
1216 size: 0,
1217 oid,
1218 flags: path.len() as u16,
1219 flags_extended: 0,
1220 path: BString::from(path),
1221 };
1222 entry.set_skip_worktree(true);
1223 entry
1224 }
1225
1226 fn write_index(git_dir: &Path, mut entries: Vec<sley_index::IndexEntry>) {
1227 entries.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
1228 let index = Index {
1229 version: 3,
1230 entries,
1231 extensions: Vec::new(),
1232 checksum: None,
1233 };
1234 fs::write(
1235 git_dir.join("index"),
1236 index.write_sha1().expect("test operation should succeed"),
1237 )
1238 .expect("test operation should succeed");
1239 }
1240
1241 #[test]
1242 fn inexact_rename_detected_with_plausible_score() {
1243 let root = temp_root();
1246 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1247 .expect("test operation should succeed");
1248 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1249
1250 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
1251 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
1252 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
1253 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
1254
1255 let opts = DiffNameStatusOptions {
1256 detect_renames: true,
1257 detect_copies: false,
1258 find_copies_harder: false,
1259 rename_empty: true,
1260 detect_inexact: true,
1261 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1262 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1263 rename_limit: 0,
1264
1265 ..Default::default()
1266 };
1267 let diff = diff_name_status_trees_with_options_and_diagnostics(
1268 &db,
1269 ObjectFormat::Sha1,
1270 &left,
1271 &right,
1272 opts,
1273 )
1274 .expect("test operation should succeed");
1275 let entries = diff.entries;
1276
1277 assert_eq!(
1278 entries.len(),
1279 1,
1280 "expected a single rename entry: {entries:?}"
1281 );
1282 assert_eq!(entries[0].status, NameStatus::Renamed(75));
1283 assert_eq!(
1284 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
1285 Some(b"a.txt".as_slice())
1286 );
1287 assert_eq!(entries[0].path, b"b.txt");
1288 assert_eq!(entries[0].line(), "R075\ta.txt\tb.txt");
1289 fs::remove_dir_all(root).expect("test operation should succeed");
1290 }
1291
1292 #[test]
1293 fn tree_diff_preserves_duplicate_entry_multiplicity() {
1294 let root = temp_root();
1295 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1296 .expect("test operation should succeed");
1297 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1298
1299 let blob_one = write_blob(&mut db, b"one\n");
1300 let blob_two = write_blob(&mut db, b"two\n");
1301 let inner_one_a = write_tree_in_order(&mut db, &[(b"inner", 0o100644, blob_one)]);
1302 let inner_one_b = write_tree_in_order(
1303 &mut db,
1304 &[
1305 (b"inner", 0o100644, blob_two),
1306 (b"inner", 0o100644, blob_two),
1307 (b"inner", 0o100644, blob_two),
1308 ],
1309 );
1310 let outer_one = write_tree_in_order(
1311 &mut db,
1312 &[
1313 (b"outer", TREE_ENTRY_MODE, inner_one_a),
1314 (b"outer", TREE_ENTRY_MODE, inner_one_b),
1315 ],
1316 );
1317 let inner_two = write_tree_in_order(
1318 &mut db,
1319 &[
1320 (b"inner", 0o100644, blob_one),
1321 (b"inner", 0o100644, blob_two),
1322 (b"inner", 0o100644, blob_two),
1323 (b"inner", 0o100644, blob_two),
1324 ],
1325 );
1326 let outer_two = write_tree_in_order(&mut db, &[(b"outer", TREE_ENTRY_MODE, inner_two)]);
1327 let outer_three = write_tree_in_order(&mut db, &[(b"renamed", 0o100644, blob_one)]);
1328
1329 let raw_options = DiffNameStatusOptions {
1330 detect_renames: false,
1331 detect_copies: false,
1332 find_copies_harder: false,
1333 rename_empty: true,
1334 ..Default::default()
1335 };
1336 let raw = diff_name_status_trees_with_options(
1337 &db,
1338 ObjectFormat::Sha1,
1339 &outer_one,
1340 &outer_two,
1341 raw_options,
1342 )
1343 .expect("test operation should succeed");
1344 assert_eq!(
1345 status_lines(&raw),
1346 vec![
1347 "A\touter/inner",
1348 "A\touter/inner",
1349 "A\touter/inner",
1350 "D\touter/inner",
1351 "D\touter/inner",
1352 "D\touter/inner",
1353 ]
1354 );
1355
1356 let rename_options = DiffNameStatusOptions {
1357 detect_renames: true,
1358 detect_copies: false,
1359 find_copies_harder: false,
1360 rename_empty: true,
1361 detect_inexact: true,
1362 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1363 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1364 rename_limit: 0,
1365
1366 ..Default::default()
1367 };
1368 let no_op_renames = diff_name_status_trees_with_options_and_diagnostics(
1369 &db,
1370 ObjectFormat::Sha1,
1371 &outer_one,
1372 &outer_two,
1373 rename_options,
1374 )
1375 .expect("test operation should succeed");
1376 assert!(no_op_renames.entries.is_empty());
1377
1378 let renamed = diff_name_status_trees_with_options_and_diagnostics(
1379 &db,
1380 ObjectFormat::Sha1,
1381 &outer_one,
1382 &outer_three,
1383 rename_options,
1384 )
1385 .expect("test operation should succeed");
1386 assert_eq!(
1387 status_lines(&renamed.entries),
1388 vec![
1389 "D\touter/inner",
1390 "D\touter/inner",
1391 "D\touter/inner",
1392 "R100\touter/inner\trenamed",
1393 ]
1394 );
1395
1396 fs::remove_dir_all(root).expect("test operation should succeed");
1397 }
1398
1399 #[test]
1400 fn inexact_rename_below_threshold_not_detected() {
1401 let root = temp_root();
1404 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1405 .expect("test operation should succeed");
1406 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1407
1408 let old = write_blob(&mut db, b"l1\nl2\nl3\nl4\nl5\nl6\n");
1409 let new = write_blob(&mut db, b"l1\nl2\nl3\nX4\nX5\nX6\n");
1410 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
1411 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
1412
1413 let opts = DiffNameStatusOptions {
1414 detect_renames: true,
1415 detect_copies: false,
1416 find_copies_harder: false,
1417 rename_empty: true,
1418 detect_inexact: true,
1419 rename_threshold: 60,
1420 copy_threshold: 60,
1421 rename_limit: 0,
1422
1423 ..Default::default()
1424 };
1425 let diff = diff_name_status_trees_with_options_and_diagnostics(
1426 &db,
1427 ObjectFormat::Sha1,
1428 &left,
1429 &right,
1430 opts,
1431 )
1432 .expect("test operation should succeed");
1433 let entries = diff.entries;
1434
1435 let statuses: Vec<_> = entries.iter().map(|e| e.status).collect();
1436 assert!(
1437 statuses.contains(&NameStatus::Added) && statuses.contains(&NameStatus::Deleted),
1438 "expected separate add/delete below threshold, got {entries:?}"
1439 );
1440 assert!(
1441 !statuses.iter().any(|s| matches!(s, NameStatus::Renamed(_))),
1442 "no rename should be reported below threshold: {entries:?}"
1443 );
1444
1445 let opts_low = DiffNameStatusOptions {
1448 rename_threshold: 50,
1449 ..opts
1450 };
1451 let entries_low = diff_name_status_trees_with_options_and_diagnostics(
1452 &db,
1453 ObjectFormat::Sha1,
1454 &left,
1455 &right,
1456 opts_low,
1457 )
1458 .expect("test operation should succeed");
1459 assert_eq!(entries_low.entries.len(), 1);
1460 assert_eq!(entries_low.entries[0].status, NameStatus::Renamed(50));
1461 fs::remove_dir_all(root).expect("test operation should succeed");
1462 }
1463
1464 #[test]
1465 fn exact_rename_scores_100_and_takes_priority() {
1466 let root = temp_root();
1469 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1470 .expect("test operation should succeed");
1471 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1472
1473 let oid = write_blob(&mut db, b"identical\ncontent\nhere\n");
1474 let left = write_tree(&mut db, &[(b"old.txt", 0o100644, oid)]);
1475 let right = write_tree(&mut db, &[(b"new.txt", 0o100644, oid)]);
1476
1477 for inexact in [false, true] {
1478 let opts = DiffNameStatusOptions {
1479 detect_renames: true,
1480 detect_copies: false,
1481 find_copies_harder: false,
1482 rename_empty: true,
1483 detect_inexact: inexact,
1484 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1485 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1486 rename_limit: 0,
1487
1488 ..Default::default()
1489 };
1490 let diff = diff_name_status_trees_with_options_and_diagnostics(
1491 &db,
1492 ObjectFormat::Sha1,
1493 &left,
1494 &right,
1495 opts,
1496 )
1497 .expect("test operation should succeed");
1498 let entries = diff.entries;
1499 assert_eq!(entries.len(), 1, "inexact={inexact}: {entries:?}");
1500 assert_eq!(entries[0].status, NameStatus::Renamed(100));
1501 assert_eq!(
1502 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
1503 Some(b"old.txt".as_slice())
1504 );
1505 assert_eq!(entries[0].path, b"new.txt");
1506 }
1507 fs::remove_dir_all(root).expect("test operation should succeed");
1508 }
1509
1510 #[test]
1511 fn inexact_copy_detected_with_score() {
1512 let root = temp_root();
1517 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1518 .expect("test operation should succeed");
1519 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1520
1521 let orig = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\neee\n");
1522 let copy = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\nEEE\n");
1523 let left = write_tree(&mut db, &[(b"orig.txt", 0o100644, orig.clone())]);
1524 let right = write_tree(
1525 &mut db,
1526 &[(b"orig.txt", 0o100644, orig), (b"copy.txt", 0o100644, copy)],
1527 );
1528
1529 let opts = DiffNameStatusOptions {
1530 detect_renames: true,
1531 detect_copies: true,
1532 find_copies_harder: true,
1533 rename_empty: true,
1534 detect_inexact: true,
1535 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1536 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1537 rename_limit: 0,
1538
1539 ..Default::default()
1540 };
1541 let diff = diff_name_status_trees_with_options_and_diagnostics(
1542 &db,
1543 ObjectFormat::Sha1,
1544 &left,
1545 &right,
1546 opts,
1547 )
1548 .expect("test operation should succeed");
1549 let entries = diff.entries;
1550
1551 let copy_entry = entries
1552 .iter()
1553 .find(|e| e.path == b"copy.txt")
1554 .unwrap_or_else(|| panic!("no copy.txt entry: {entries:?}"));
1555 assert_eq!(copy_entry.status, NameStatus::Copied(80));
1556 assert_eq!(
1557 copy_entry.old_path.as_ref().map(|p| p.as_bytes()),
1558 Some(b"orig.txt".as_slice())
1559 );
1560 assert!(
1562 entries.iter().all(|e| e.status != NameStatus::Deleted),
1563 "copy must not delete the source: {entries:?}"
1564 );
1565 fs::remove_dir_all(root).expect("test operation should succeed");
1566 }
1567
1568 #[test]
1569 fn inexact_copy_skipped_over_rename_limit() {
1570 let root = temp_root();
1578 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1579 .expect("test operation should succeed");
1580 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1581
1582 let orig = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\neee\n");
1583 let extra = write_blob(&mut db, b"111\n222\n333\n444\n555\n");
1584 let copy = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\nEEE\n");
1585 let left = write_tree(
1588 &mut db,
1589 &[
1590 (b"orig.txt", 0o100644, orig.clone()),
1591 (b"extra.txt", 0o100644, extra.clone()),
1592 ],
1593 );
1594 let right = write_tree(
1595 &mut db,
1596 &[
1597 (b"orig.txt", 0o100644, orig),
1598 (b"extra.txt", 0o100644, extra),
1599 (b"copy.txt", 0o100644, copy),
1600 ],
1601 );
1602
1603 let opts_for = |rename_limit| DiffNameStatusOptions {
1604 detect_renames: true,
1605 detect_copies: true,
1606 find_copies_harder: true,
1607 rename_empty: true,
1608 detect_inexact: true,
1609 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1610 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1611 rename_limit,
1612
1613 ..Default::default()
1614 };
1615
1616 let over = diff_name_status_trees_with_options_and_diagnostics(
1618 &db,
1619 ObjectFormat::Sha1,
1620 &left,
1621 &right,
1622 opts_for(1),
1623 )
1624 .expect("test operation should succeed");
1625 let copy_over = over
1626 .entries
1627 .iter()
1628 .find(|e| e.path == b"copy.txt")
1629 .unwrap_or_else(|| panic!("no copy.txt entry: {over:?}"));
1630 assert_eq!(
1631 copy_over.status,
1632 NameStatus::Added,
1633 "over rename_limit, copy must degrade to a plain Add: {over:?}"
1634 );
1635
1636 let under = diff_name_status_trees_with_options_and_diagnostics(
1638 &db,
1639 ObjectFormat::Sha1,
1640 &left,
1641 &right,
1642 opts_for(4),
1643 )
1644 .expect("test operation should succeed");
1645 let copy_under = under
1646 .entries
1647 .iter()
1648 .find(|e| e.path == b"copy.txt")
1649 .unwrap_or_else(|| panic!("no copy.txt entry: {under:?}"));
1650 assert_eq!(
1651 copy_under.status,
1652 NameStatus::Copied(80),
1653 "below rename_limit, copy detection is unaffected: {under:?}"
1654 );
1655
1656 fs::remove_dir_all(root).expect("test operation should succeed");
1657 }
1658
1659 #[test]
1660 fn inexact_rename_with_small_edit_scores_88() {
1661 let root = temp_root();
1664 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1665 .expect("test operation should succeed");
1666 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1667
1668 let old = write_blob(
1669 &mut db,
1670 b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\n",
1671 );
1672 let new = write_blob(
1673 &mut db,
1674 b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\nADDED\n",
1675 );
1676 let left = write_tree(&mut db, &[(b"src.txt", 0o100644, old)]);
1677 let right = write_tree(&mut db, &[(b"dst.txt", 0o100644, new)]);
1678
1679 let opts = DiffNameStatusOptions {
1680 detect_renames: true,
1681 detect_copies: false,
1682 find_copies_harder: false,
1683 rename_empty: true,
1684 ..Default::default()
1685 }
1686 .inexact();
1687 let diff = diff_name_status_trees_with_options_and_diagnostics(
1688 &db,
1689 ObjectFormat::Sha1,
1690 &left,
1691 &right,
1692 opts,
1693 )
1694 .expect("test operation should succeed");
1695 let entries = diff.entries;
1696
1697 assert_eq!(entries.len(), 1, "{entries:?}");
1698 assert_eq!(entries[0].status, NameStatus::Renamed(88));
1699 assert_eq!(
1700 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
1701 Some(b"src.txt".as_slice())
1702 );
1703 assert_eq!(entries[0].path, b"dst.txt");
1704 fs::remove_dir_all(root).expect("test operation should succeed");
1705 }
1706
1707 #[test]
1708 fn inexact_disabled_default_preserves_exact_only_behavior() {
1709 assert!(!DiffNameStatusOptions::default().detect_inexact);
1713 assert_eq!(
1714 DiffNameStatusOptions::default().rename_threshold,
1715 DEFAULT_RENAME_THRESHOLD
1716 );
1717
1718 let root = temp_root();
1719 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
1720 .expect("test operation should succeed");
1721 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
1722
1723 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
1724 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
1725 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
1726 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
1727
1728 let diff = diff_name_status_trees_with_options_and_diagnostics(
1729 &db,
1730 ObjectFormat::Sha1,
1731 &left,
1732 &right,
1733 DiffNameStatusOptions::default(),
1734 )
1735 .expect("test operation should succeed");
1736 let entries = diff.entries;
1737 let statuses: Vec<_> = entries.iter().map(|e| e.status).collect();
1738 assert!(statuses.contains(&NameStatus::Added));
1739 assert!(statuses.contains(&NameStatus::Deleted));
1740 assert!(!statuses.iter().any(|s| matches!(s, NameStatus::Renamed(_))));
1741 fs::remove_dir_all(root).expect("test operation should succeed");
1742 }
1743
1744 fn apply_ops(old: &[DiffLine<'_>], new: &[DiffLine<'_>], ops: &[DiffOp]) -> Vec<u8> {
1752 let mut oi = 0usize;
1753 let mut ni = 0usize;
1754 let mut rebuilt: Vec<u8> = Vec::new();
1755 for op in ops {
1756 match *op {
1757 DiffOp::Equal(n) => {
1758 for _ in 0..n {
1759 assert_eq!(old[oi], new[ni], "Equal op covered unequal lines");
1761 rebuilt.extend_from_slice(old[oi].content);
1762 oi += 1;
1763 ni += 1;
1764 }
1765 }
1766 DiffOp::Delete(n) => oi += n,
1767 DiffOp::Insert(n) => {
1768 for _ in 0..n {
1769 rebuilt.extend_from_slice(new[ni].content);
1770 ni += 1;
1771 }
1772 }
1773 }
1774 }
1775 assert_eq!(oi, old.len(), "script did not consume all of old");
1777 assert_eq!(ni, new.len(), "script did not consume all of new");
1778 rebuilt
1779 }
1780
1781 fn assert_valid_script(old_bytes: &[u8], new_bytes: &[u8], ops: &[DiffOp]) {
1784 let old = split_lines(old_bytes);
1785 let new = split_lines(new_bytes);
1786 let rebuilt = apply_ops(&old, &new, ops);
1787 assert_eq!(rebuilt, new_bytes, "script did not rebuild new");
1788 for pair in ops.windows(2) {
1789 let same_kind = matches!(
1790 (pair[0], pair[1]),
1791 (DiffOp::Equal(_), DiffOp::Equal(_))
1792 | (DiffOp::Delete(_), DiffOp::Delete(_))
1793 | (DiffOp::Insert(_), DiffOp::Insert(_))
1794 );
1795 assert!(!same_kind, "ops not coalesced: {:?}", ops);
1796 }
1797 }
1798
1799 fn check_all_algorithms(old_bytes: &[u8], new_bytes: &[u8]) {
1802 let old = split_lines(old_bytes);
1803 let new = split_lines(new_bytes);
1804 for algo in [
1805 DiffAlgorithm::Myers,
1806 DiffAlgorithm::Minimal,
1807 DiffAlgorithm::Patience,
1808 DiffAlgorithm::Histogram,
1809 ] {
1810 let ops = diff_lines_with_algorithm(&old, &new, algo);
1811 assert_valid_script(old_bytes, new_bytes, &ops);
1812 }
1813 }
1814
1815 #[test]
1816 fn patience_and_histogram_match_myers_on_simple_cases() {
1817 let cases: &[(&[u8], &[u8], Vec<DiffOp>)] = &[
1820 (
1821 b"a\nb\nc\n",
1822 b"a\nx\nc\n",
1823 vec![
1824 DiffOp::Equal(1),
1825 DiffOp::Delete(1),
1826 DiffOp::Insert(1),
1827 DiffOp::Equal(1),
1828 ],
1829 ),
1830 (b"a\nb\nc\n", b"a\nb\nc\n", vec![DiffOp::Equal(3)]),
1831 (b"", b"a\nb\n", vec![DiffOp::Insert(2)]),
1832 (b"a\nb\n", b"", vec![DiffOp::Delete(2)]),
1833 (
1834 b"a\nb\nc\nd\n",
1835 b"a\nc\nd\n",
1836 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Equal(2)],
1837 ),
1838 ];
1839 for (old_bytes, new_bytes, expected) in cases {
1840 let old = split_lines(old_bytes);
1841 let new = split_lines(new_bytes);
1842 assert_eq!(&patience_diff_lines(&old, &new), expected);
1843 assert_eq!(&histogram_diff_lines(&old, &new), expected);
1844 assert_eq!(&myers_diff_lines(&old, &new), expected);
1845 }
1846 }
1847
1848 #[test]
1849 fn patience_handles_both_empty() {
1850 let empty = split_lines(b"");
1851 assert!(patience_diff_lines(&empty, &empty).is_empty());
1852 assert!(histogram_diff_lines(&empty, &empty).is_empty());
1853 }
1854
1855 #[test]
1856 fn patience_aligns_unique_anchors_across_moved_block() {
1857 check_all_algorithms(
1862 b"alpha\nbeta\ngamma\ndelta\n",
1863 b"gamma\ndelta\nalpha\nbeta\n",
1864 );
1865 }
1866
1867 #[test]
1868 fn histogram_differs_from_myers_keeping_block_contiguous() {
1869 let old = b"b\na\n";
1875 let new = b"a\nb\nb\na\nb\n";
1876 let old_l = split_lines(old);
1877 let new_l = split_lines(new);
1878
1879 let myers = myers_diff_lines(&old_l, &new_l);
1880 let histogram = histogram_diff_lines(&old_l, &new_l);
1881
1882 assert_valid_script(old, new, &myers);
1884 assert_valid_script(old, new, &histogram);
1885
1886 assert_eq!(
1889 myers,
1890 vec![
1891 DiffOp::Insert(1),
1892 DiffOp::Equal(1),
1893 DiffOp::Insert(1),
1894 DiffOp::Equal(1),
1895 DiffOp::Insert(1),
1896 ]
1897 );
1898 assert_eq!(
1899 histogram,
1900 vec![DiffOp::Insert(2), DiffOp::Equal(2), DiffOp::Insert(1)]
1901 );
1902 assert_ne!(myers, histogram);
1904 }
1905
1906 #[test]
1907 fn patience_differs_from_myers_on_repeated_lines() {
1908 let old = b"b\na\n";
1914 let new = b"a\na\nb\n";
1915 let old_l = split_lines(old);
1916 let new_l = split_lines(new);
1917
1918 let myers = myers_diff_lines(&old_l, &new_l);
1919 let patience = patience_diff_lines(&old_l, &new_l);
1920
1921 assert_valid_script(old, new, &myers);
1922 assert_valid_script(old, new, &patience);
1923
1924 assert_eq!(
1925 myers,
1926 vec![DiffOp::Delete(1), DiffOp::Equal(1), DiffOp::Insert(2)]
1927 );
1928 assert_eq!(
1929 patience,
1930 vec![DiffOp::Insert(2), DiffOp::Equal(1), DiffOp::Delete(1)]
1931 );
1932 assert_ne!(myers, patience);
1933 }
1934
1935 #[test]
1936 fn realistic_function_insertion_all_valid() {
1937 let old = b"int f() {\n return 1;\n}\n";
1942 let new = b"int g() {\n return 2;\n}\n\nint f() {\n return 1;\n}\n";
1943 check_all_algorithms(old, new);
1944 }
1945
1946 #[test]
1947 fn histogram_anchors_on_rare_line_when_no_unique_line_exists() {
1948 check_all_algorithms(b"x\nx\nmid\nx\nx\n", b"x\nmid\nx\nx\nx\n");
1953 check_all_algorithms(
1954 b"dup\ndup\nrare\ndup\ndup\n",
1955 b"dup\nrare\ndup\ndup\ndup\ndup\n",
1956 );
1957 }
1958
1959 #[test]
1960 fn all_algorithms_treat_missing_final_newline_as_change() {
1961 let old = split_lines(b"a\nb");
1963 let new = split_lines(b"a\nb\n");
1964 for algo in [
1965 DiffAlgorithm::Myers,
1966 DiffAlgorithm::Minimal,
1967 DiffAlgorithm::Patience,
1968 DiffAlgorithm::Histogram,
1969 ] {
1970 let ops = diff_lines_with_algorithm(&old, &new, algo);
1971 assert_eq!(
1972 ops,
1973 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Insert(1)],
1974 "algorithm {:?} mishandled missing final newline",
1975 algo
1976 );
1977 }
1978 }
1979
1980 #[test]
1981 fn dispatcher_routes_each_variant() {
1982 let old = split_lines(b"a\nb\nc\n");
1983 let new = split_lines(b"a\nx\nc\n");
1984 assert_eq!(
1985 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Myers),
1986 myers_diff_lines(&old, &new)
1987 );
1988 assert_eq!(
1990 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Minimal),
1991 myers_diff_lines(&old, &new)
1992 );
1993 assert_eq!(
1994 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Patience),
1995 patience_diff_lines(&old, &new)
1996 );
1997 assert_eq!(
1998 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Histogram),
1999 histogram_diff_lines(&old, &new)
2000 );
2001 }
2002
2003 #[test]
2004 fn patience_recurses_into_gaps_between_anchors() {
2005 let old = b"head\nmid1\nmid2\ntail\n";
2008 let new = b"head\nMID\nmid2\ntail\n";
2009 let old_l = split_lines(old);
2010 let new_l = split_lines(new);
2011 let ops = patience_diff_lines(&old_l, &new_l);
2012 assert_eq!(
2013 ops,
2014 vec![
2015 DiffOp::Equal(1),
2016 DiffOp::Delete(1),
2017 DiffOp::Insert(1),
2018 DiffOp::Equal(2),
2019 ]
2020 );
2021 assert_valid_script(old, new, &ops);
2022 }
2023
2024 #[test]
2025 fn patience_falls_back_to_myers_with_no_unique_lines() {
2026 let old = b"a\na\nb\nb\n";
2030 let new = b"a\na\na\nb\n";
2031 let old_l = split_lines(old);
2032 let new_l = split_lines(new);
2033 let ops = patience_diff_lines(&old_l, &new_l);
2034 assert_valid_script(old, new, &ops);
2039 }
2040
2041 #[test]
2042 fn algorithms_agree_with_myers_when_all_lines_distinct() {
2043 let cases: &[(&[u8], &[u8])] = &[
2046 (b"a\nb\nc\nd\ne\n", b"a\nc\nd\nf\ne\n"),
2047 (b"1\n2\n3\n4\n5\n6\n", b"1\n3\n2\n4\n6\n5\n"),
2048 (b"q\nw\ne\nr\nt\ny\n", b"q\nw\nx\nr\nt\nz\n"),
2049 ];
2050 for (old_bytes, new_bytes) in cases {
2051 let old = split_lines(old_bytes);
2052 let new = split_lines(new_bytes);
2053 let myers = myers_diff_lines(&old, &new);
2054 assert_eq!(
2055 patience_diff_lines(&old, &new),
2056 myers,
2057 "patience must equal Myers when all lines are distinct: {:?}",
2058 old_bytes
2059 );
2060 assert_eq!(
2061 histogram_diff_lines(&old, &new),
2062 myers,
2063 "histogram must equal Myers when all lines are distinct: {:?}",
2064 old_bytes
2065 );
2066 }
2067 }
2068
2069 #[test]
2070 fn fuzz_all_algorithms_reconstruct_new() {
2071 let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
2075 let mut next = || {
2076 state = state
2077 .wrapping_mul(6364136223846793005)
2078 .wrapping_add(1442695040888963407);
2079 (state >> 33) as u32
2080 };
2081 let alphabet = [b"a\n", b"b\n", b"c\n", b"d\n"];
2082 let build = |rng: &mut dyn FnMut() -> u32| -> Vec<u8> {
2083 let len = (rng() % 9) as usize; let mut buf = Vec::new();
2085 for _ in 0..len {
2086 let pick = (rng() % alphabet.len() as u32) as usize;
2087 buf.extend_from_slice(alphabet[pick]);
2088 }
2089 if !buf.is_empty() && rng().is_multiple_of(4) {
2091 buf.pop();
2092 }
2093 buf
2094 };
2095 for _ in 0..400 {
2096 let old_bytes = build(&mut next);
2097 let new_bytes = build(&mut next);
2098 check_all_algorithms(&old_bytes, &new_bytes);
2099 }
2100 }
2101
2102 #[test]
2103 fn exhaustive_small_inputs_all_algorithms_reconstruct() {
2104 let syms = [b"a\n".to_vec(), b"b\n".to_vec(), b"c\n".to_vec()];
2109 let make = |n: usize, mut code: usize| -> Vec<u8> {
2110 let mut v = Vec::new();
2111 for _ in 0..n {
2112 v.extend_from_slice(&syms[code % 3]);
2113 code /= 3;
2114 }
2115 v
2116 };
2117 for la in 0..=5usize {
2118 for lb in 0..=5usize {
2119 for ca in 0..3usize.pow(la as u32) {
2120 for cb in 0..3usize.pow(lb as u32) {
2121 let ob = make(la, ca);
2122 let nb = make(lb, cb);
2123 let ol = split_lines(&ob);
2124 let nl = split_lines(&nb);
2125 assert_eq!(apply_ops(&ol, &nl, &myers_diff_lines(&ol, &nl)), nb);
2126 assert_eq!(apply_ops(&ol, &nl, &patience_diff_lines(&ol, &nl)), nb);
2127 assert_eq!(apply_ops(&ol, &nl, &histogram_diff_lines(&ol, &nl)), nb);
2128 }
2129 }
2130 }
2131 }
2132 }
2133
2134 #[test]
2135 fn fuzz_distinct_lines_patience_histogram_equal_myers() {
2136 let mut state: u64 = 0x1234_5678_9ABC_DEF0;
2140 let mut next = || {
2141 state = state
2142 .wrapping_mul(6364136223846793005)
2143 .wrapping_add(1442695040888963407);
2144 (state >> 33) as u32
2145 };
2146 for _ in 0..200 {
2147 let pick_subseq = |rng: &mut dyn FnMut() -> u32| -> Vec<u8> {
2150 let mut buf = Vec::new();
2151 for t in 0..10u32 {
2152 if rng().is_multiple_of(2) {
2153 buf.extend_from_slice(format!("{t}\n").as_bytes());
2154 }
2155 }
2156 buf
2157 };
2158 let old_bytes = pick_subseq(&mut next);
2159 let new_bytes = pick_subseq(&mut next);
2160 let old = split_lines(&old_bytes);
2161 let new = split_lines(&new_bytes);
2162 let myers = myers_diff_lines(&old, &new);
2163 assert_eq!(patience_diff_lines(&old, &new), myers);
2164 assert_eq!(histogram_diff_lines(&old, &new), myers);
2165 }
2166 }
2167
2168 fn status_lines(entries: &[NameStatusEntry]) -> Vec<String> {
2177 entries.iter().map(|entry| entry.line()).collect()
2178 }
2179
2180 fn assert_tree_diff_matches_full(
2183 db: &FileObjectDatabase,
2184 left: &ObjectId,
2185 right: &ObjectId,
2186 options: DiffNameStatusOptions,
2187 ) {
2188 let (full_left, full_right) = collect_full_tree_pair(db, ObjectFormat::Sha1, left, right)
2190 .expect("test operation should succeed");
2191 let reference = diff_name_status_maps(
2192 &full_left,
2193 &full_right,
2194 full_left.keys().chain(full_right.keys()),
2195 options,
2196 )
2197 .expect("test operation should succeed");
2198
2199 let (pruned_left, pruned_right) = changed_tree_entries(db, ObjectFormat::Sha1, left, right)
2201 .expect("test operation should succeed");
2202 let pruned = diff_name_status_maps(
2203 &pruned_left,
2204 &pruned_right,
2205 pruned_left.keys().chain(pruned_right.keys()),
2206 options,
2207 )
2208 .expect("test operation should succeed");
2209
2210 assert_eq!(
2211 status_lines(&reference),
2212 status_lines(&pruned),
2213 "pruned map diff diverged from full map diff for {options:?}"
2214 );
2215
2216 let public =
2219 diff_name_status_trees_with_options(db, ObjectFormat::Sha1, left, right, options)
2220 .expect("test operation should succeed");
2221 assert_eq!(
2222 status_lines(&reference),
2223 status_lines(&public),
2224 "public tree diff diverged from full map diff for {options:?}"
2225 );
2226
2227 for (path, tracked) in &pruned_left {
2231 assert_eq!(
2232 full_left.get(path),
2233 Some(tracked),
2234 "pruned left entry not present (or differs) in full left map: {:?}",
2235 String::from_utf8_lossy(path)
2236 );
2237 }
2238 for (path, tracked) in &pruned_right {
2239 assert_eq!(
2240 full_right.get(path),
2241 Some(tracked),
2242 "pruned right entry not present (or differs) in full right map: {:?}",
2243 String::from_utf8_lossy(path)
2244 );
2245 }
2246 for entry in &reference {
2249 let path = entry.path.as_bytes();
2250 match entry.status {
2251 NameStatus::Added => assert!(
2252 pruned_right.contains_key(path),
2253 "added path dropped by pruning: {:?}",
2254 String::from_utf8_lossy(path)
2255 ),
2256 NameStatus::Deleted => assert!(
2257 pruned_left.contains_key(path),
2258 "deleted path dropped by pruning: {:?}",
2259 String::from_utf8_lossy(path)
2260 ),
2261 NameStatus::Modified => {
2262 assert!(
2263 pruned_left.contains_key(path) && pruned_right.contains_key(path),
2264 "modified path dropped by pruning: {:?}",
2265 String::from_utf8_lossy(path)
2266 );
2267 }
2268 _ => {}
2269 }
2270 }
2271 }
2272
2273 fn assert_tree_diff_matches_full_all_modes(
2277 db: &FileObjectDatabase,
2278 left: &ObjectId,
2279 right: &ObjectId,
2280 ) {
2281 for detect_renames in [false, true] {
2282 for detect_copies in [false, true] {
2283 let options = DiffNameStatusOptions {
2284 detect_renames,
2285 detect_copies,
2286 find_copies_harder: false,
2287 rename_empty: true,
2288 ..Default::default()
2289 };
2290 assert_tree_diff_matches_full(db, left, right, options);
2291 }
2292 }
2293 }
2294
2295 fn structural_db() -> (PathBuf, FileObjectDatabase) {
2297 let root = temp_root();
2298 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
2299 .expect("test operation should succeed");
2300 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
2301 (root, db)
2302 }
2303
2304 #[test]
2305 fn pruned_walk_skips_identical_subtree_and_matches_full() {
2306 let (root, mut db) = structural_db();
2310
2311 let s1 = write_blob(&mut db, b"shared one\n");
2313 let s2 = write_blob(&mut db, b"shared two\n");
2314 let s3 = write_blob(&mut db, b"deep nested\n");
2315 let shared_inner = write_tree(&mut db, &[(b"c.txt", 0o100644, s3.clone())]);
2316 let shared = write_tree(
2317 &mut db,
2318 &[
2319 (b"a.txt", 0o100644, s1.clone()),
2320 (b"b.txt", 0o100644, s2.clone()),
2321 (b"inner", 0o040000, shared_inner.clone()),
2322 ],
2323 );
2324
2325 let app_old = write_blob(&mut db, b"version 1\n");
2327 let app_new = write_blob(&mut db, b"version 2\n");
2328 let app_left = write_tree(&mut db, &[(b"main.rs", 0o100644, app_old)]);
2329 let app_right = write_tree(&mut db, &[(b"main.rs", 0o100644, app_new)]);
2330
2331 let left = write_tree(
2332 &mut db,
2333 &[
2334 (b"app", 0o040000, app_left),
2335 (b"shared", 0o040000, shared.clone()),
2336 ],
2337 );
2338 let right = write_tree(
2339 &mut db,
2340 &[(b"app", 0o040000, app_right), (b"shared", 0o040000, shared)],
2341 );
2342
2343 let (pruned_left, pruned_right) =
2345 changed_tree_entries(&db, ObjectFormat::Sha1, &left, &right)
2346 .expect("test operation should succeed");
2347 assert_eq!(
2348 pruned_left.keys().collect::<Vec<_>>(),
2349 vec![&b"app/main.rs".to_vec()],
2350 "pruning should leave only the changed path on the left"
2351 );
2352 assert_eq!(
2353 pruned_right.keys().collect::<Vec<_>>(),
2354 vec![&b"app/main.rs".to_vec()],
2355 "pruning should leave only the changed path on the right"
2356 );
2357 assert!(
2358 !pruned_left.contains_key(b"shared/a.txt".as_slice()),
2359 "identical shared subtree must not appear in pruned maps"
2360 );
2361
2362 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
2363 fs::remove_dir_all(root).expect("test operation should succeed");
2364 }
2365
2366 #[test]
2367 fn pruned_walk_matches_full_for_add_delete_modify_nested() {
2368 let (root, mut db) = structural_db();
2371
2372 let keep = write_blob(&mut db, b"unchanged\n");
2373 let untouched_dir = write_tree(&mut db, &[(b"keep.txt", 0o100644, keep.clone())]);
2374
2375 let nested_old = write_blob(&mut db, b"nested old\n");
2376 let nested_new = write_blob(&mut db, b"nested new\n");
2377 let dir_left = write_tree(
2378 &mut db,
2379 &[
2380 (b"changed.txt", 0o100644, nested_old),
2381 (b"stable.txt", 0o100644, keep.clone()),
2382 ],
2383 );
2384 let dir_right = write_tree(
2385 &mut db,
2386 &[
2387 (b"changed.txt", 0o100644, nested_new),
2388 (b"stable.txt", 0o100644, keep.clone()),
2389 ],
2390 );
2391
2392 let only_left = write_blob(&mut db, b"will be deleted\n");
2393 let only_right = write_blob(&mut db, b"freshly added\n");
2394
2395 let left = write_tree(
2396 &mut db,
2397 &[
2398 (b"dir", 0o040000, dir_left),
2399 (b"gone.txt", 0o100644, only_left),
2400 (b"untouched", 0o040000, untouched_dir.clone()),
2401 ],
2402 );
2403 let right = write_tree(
2404 &mut db,
2405 &[
2406 (b"dir", 0o040000, dir_right),
2407 (b"new.txt", 0o100644, only_right),
2408 (b"untouched", 0o040000, untouched_dir),
2409 ],
2410 );
2411
2412 let entries = diff_name_status_trees_with_options(
2413 &db,
2414 ObjectFormat::Sha1,
2415 &left,
2416 &right,
2417 DiffNameStatusOptions {
2418 detect_renames: false,
2419 detect_copies: false,
2420 find_copies_harder: false,
2421 rename_empty: true,
2422
2423 ..Default::default()
2424 },
2425 )
2426 .expect("test operation should succeed");
2427 assert_eq!(
2428 status_lines(&entries),
2429 vec![
2430 "M\tdir/changed.txt".to_string(),
2431 "D\tgone.txt".to_string(),
2432 "A\tnew.txt".to_string(),
2433 ],
2434 "unexpected raw status for mixed nested diff"
2435 );
2436
2437 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
2438 fs::remove_dir_all(root).expect("test operation should succeed");
2439 }
2440
2441 #[test]
2442 fn pruned_walk_matches_full_for_rename_across_dirs() {
2443 let (root, mut db) = structural_db();
2447
2448 let moved = write_blob(&mut db, b"i get moved across directories\n");
2449 let companion = write_blob(&mut db, b"i stay put\n");
2450 let stable_dir = write_tree(&mut db, &[(b"keep.txt", 0o100644, companion.clone())]);
2451
2452 let src_dir = write_tree(&mut db, &[(b"file.txt", 0o100644, moved.clone())]);
2453 let dst_dir = write_tree(&mut db, &[(b"renamed.txt", 0o100644, moved.clone())]);
2454
2455 let left = write_tree(
2456 &mut db,
2457 &[
2458 (b"src", 0o040000, src_dir),
2459 (b"stable", 0o040000, stable_dir.clone()),
2460 ],
2461 );
2462 let right = write_tree(
2463 &mut db,
2464 &[
2465 (b"dst", 0o040000, dst_dir),
2466 (b"stable", 0o040000, stable_dir),
2467 ],
2468 );
2469
2470 let entries = diff_name_status_trees_with_options(
2471 &db,
2472 ObjectFormat::Sha1,
2473 &left,
2474 &right,
2475 DiffNameStatusOptions {
2476 detect_renames: true,
2477 detect_copies: false,
2478 find_copies_harder: false,
2479 rename_empty: true,
2480
2481 ..Default::default()
2482 },
2483 )
2484 .expect("test operation should succeed");
2485 assert_eq!(
2486 status_lines(&entries),
2487 vec!["R100\tsrc/file.txt\tdst/renamed.txt".to_string()],
2488 "rename across dirs should be detected on pruned set"
2489 );
2490
2491 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
2492 fs::remove_dir_all(root).expect("test operation should succeed");
2493 }
2494
2495 #[test]
2496 fn pruned_walk_matches_full_for_binary_and_mode_change() {
2497 let (root, mut db) = structural_db();
2501
2502 let bin_old = write_blob(&mut db, &[0u8, 159, 146, 150, 0, 255, 1, 2, 3]);
2503 let bin_new = write_blob(&mut db, &[0u8, 159, 146, 150, 0, 254, 9, 8, 7]);
2504 let script = write_blob(&mut db, b"#!/bin/sh\necho hi\n");
2505
2506 let left = write_tree(
2507 &mut db,
2508 &[
2509 (b"image.bin", 0o100644, bin_old),
2510 (b"run.sh", 0o100644, script.clone()),
2511 ],
2512 );
2513 let right = write_tree(
2514 &mut db,
2515 &[
2516 (b"image.bin", 0o100644, bin_new),
2517 (b"run.sh", 0o100755, script),
2519 ],
2520 );
2521
2522 let entries = diff_name_status_trees_with_options(
2523 &db,
2524 ObjectFormat::Sha1,
2525 &left,
2526 &right,
2527 DiffNameStatusOptions {
2528 detect_renames: false,
2529 detect_copies: false,
2530 find_copies_harder: false,
2531 rename_empty: true,
2532
2533 ..Default::default()
2534 },
2535 )
2536 .expect("test operation should succeed");
2537 assert_eq!(
2538 status_lines(&entries),
2539 vec!["M\timage.bin".to_string(), "M\trun.sh".to_string()],
2540 "binary edit and mode-only change should both be Modify"
2541 );
2542
2543 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
2544 fs::remove_dir_all(root).expect("test operation should succeed");
2545 }
2546
2547 #[test]
2548 fn pruned_walk_matches_full_for_dir_replaced_by_file() {
2549 let (root, mut db) = structural_db();
2554
2555 let inner_a = write_blob(&mut db, b"inner a\n");
2556 let inner_b = write_blob(&mut db, b"inner b\n");
2557 let thing_dir = write_tree(
2558 &mut db,
2559 &[(b"a.txt", 0o100644, inner_a), (b"b.txt", 0o100644, inner_b)],
2560 );
2561 let thing_file = write_blob(&mut db, b"now i am a file\n");
2562
2563 let other_file = write_blob(&mut db, b"i was a file\n");
2565 let other_inner = write_blob(&mut db, b"now nested\n");
2566 let other_dir = write_tree(&mut db, &[(b"x.txt", 0o100644, other_inner)]);
2567
2568 let left = write_tree(
2569 &mut db,
2570 &[
2571 (b"other", 0o100644, other_file),
2572 (b"thing", 0o040000, thing_dir),
2573 ],
2574 );
2575 let right = write_tree(
2576 &mut db,
2577 &[
2578 (b"other", 0o040000, other_dir),
2579 (b"thing", 0o100644, thing_file),
2580 ],
2581 );
2582
2583 let entries = diff_name_status_trees_with_options(
2584 &db,
2585 ObjectFormat::Sha1,
2586 &left,
2587 &right,
2588 DiffNameStatusOptions {
2589 detect_renames: false,
2590 detect_copies: false,
2591 find_copies_harder: false,
2592 rename_empty: true,
2593
2594 ..Default::default()
2595 },
2596 )
2597 .expect("test operation should succeed");
2598 assert_eq!(
2599 status_lines(&entries),
2600 vec![
2601 "D\tother".to_string(),
2602 "A\tother/x.txt".to_string(),
2603 "A\tthing".to_string(),
2604 "D\tthing/a.txt".to_string(),
2605 "D\tthing/b.txt".to_string(),
2606 ],
2607 "dir<->file swap should flatten to independent adds/deletes"
2608 );
2609
2610 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
2611 fs::remove_dir_all(root).expect("test operation should succeed");
2612 }
2613
2614 #[test]
2615 fn pruned_walk_matches_full_for_identical_trees() {
2616 let (root, mut db) = structural_db();
2619
2620 let blob = write_blob(&mut db, b"same\n");
2621 let sub = write_tree(&mut db, &[(b"f.txt", 0o100644, blob.clone())]);
2622 let tree = write_tree(
2623 &mut db,
2624 &[(b"sub", 0o040000, sub), (b"top.txt", 0o100644, blob)],
2625 );
2626
2627 let (pruned_left, pruned_right) =
2628 changed_tree_entries(&db, ObjectFormat::Sha1, &tree, &tree)
2629 .expect("test operation should succeed");
2630 assert!(
2631 pruned_left.is_empty() && pruned_right.is_empty(),
2632 "identical trees must produce no changed entries"
2633 );
2634
2635 let entries = diff_name_status_trees_with_options(
2636 &db,
2637 ObjectFormat::Sha1,
2638 &tree,
2639 &tree,
2640 DiffNameStatusOptions::default(),
2641 )
2642 .expect("test operation should succeed");
2643 assert!(entries.is_empty(), "identical trees must produce no diff");
2644
2645 assert_tree_diff_matches_full_all_modes(&db, &tree, &tree);
2646 fs::remove_dir_all(root).expect("test operation should succeed");
2647 }
2648
2649 #[test]
2650 fn find_copies_harder_uses_full_left_map_and_finds_unchanged_source() {
2651 let (root, mut db) = structural_db();
2656
2657 let template = write_blob(&mut db, b"reusable boilerplate content\n");
2660 let lib_dir = write_tree(&mut db, &[(b"template.txt", 0o100644, template.clone())]);
2661
2662 let trigger_old = write_blob(&mut db, b"trigger old\n");
2663 let trigger_new = write_blob(&mut db, b"trigger new\n");
2664
2665 let left = write_tree(
2666 &mut db,
2667 &[
2668 (b"lib", 0o040000, lib_dir.clone()),
2669 (b"trigger.txt", 0o100644, trigger_old),
2670 ],
2671 );
2672 let right = write_tree(
2673 &mut db,
2674 &[
2675 (b"copy.txt", 0o100644, template.clone()),
2676 (b"lib", 0o040000, lib_dir),
2677 (b"trigger.txt", 0o100644, trigger_new),
2678 ],
2679 );
2680
2681 let options = DiffNameStatusOptions {
2682 detect_renames: true,
2683 detect_copies: true,
2684 find_copies_harder: true,
2685 rename_empty: true,
2686 ..Default::default()
2687 };
2688
2689 let (full_left, full_right) =
2691 collect_full_tree_pair(&db, ObjectFormat::Sha1, &left, &right)
2692 .expect("test operation should succeed");
2693 let reference = diff_name_status_maps(
2694 &full_left,
2695 &full_right,
2696 full_left.keys().chain(full_right.keys()),
2697 options,
2698 )
2699 .expect("test operation should succeed");
2700
2701 let public =
2702 diff_name_status_trees_with_options(&db, ObjectFormat::Sha1, &left, &right, options)
2703 .expect("test operation should succeed");
2704 assert_eq!(
2705 status_lines(&reference),
2706 status_lines(&public),
2707 "find-copies-harder public diff must match full-map reference"
2708 );
2709 assert!(
2711 public
2712 .iter()
2713 .any(|entry| matches!(entry.status, NameStatus::Copied(_))
2714 && entry.old_path.as_ref().map(|p| p.as_bytes())
2715 == Some(b"lib/template.txt".as_slice())
2716 && entry.path == b"copy.txt"),
2717 "copy from unchanged source must be found with find_copies_harder: {public:?}"
2718 );
2719 fs::remove_dir_all(root).expect("test operation should succeed");
2720 }
2721
2722 #[test]
2723 fn pruned_walk_matches_full_with_inexact_rename_options() {
2724 let (root, mut db) = structural_db();
2727
2728 let untouched = write_blob(&mut db, b"untouched file\n");
2729 let untouched_dir = write_tree(&mut db, &[(b"u.txt", 0o100644, untouched.clone())]);
2730
2731 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
2733 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
2734
2735 let left = write_tree(
2736 &mut db,
2737 &[
2738 (b"a.txt", 0o100644, old),
2739 (b"keep", 0o040000, untouched_dir.clone()),
2740 ],
2741 );
2742 let right = write_tree(
2743 &mut db,
2744 &[
2745 (b"b.txt", 0o100644, new),
2746 (b"keep", 0o040000, untouched_dir),
2747 ],
2748 );
2749
2750 let options = DiffNameStatusOptions {
2751 detect_renames: true,
2752 detect_copies: false,
2753 find_copies_harder: false,
2754 rename_empty: true,
2755 detect_inexact: true,
2756 rename_threshold: DEFAULT_RENAME_THRESHOLD,
2757 copy_threshold: DEFAULT_RENAME_THRESHOLD,
2758 rename_limit: 0,
2759
2760 ..Default::default()
2761 };
2762
2763 let (full_left, full_right) =
2765 collect_full_tree_pair(&db, ObjectFormat::Sha1, &left, &right)
2766 .expect("test operation should succeed");
2767 let reference = diff_name_status_maps_with_renames(
2768 &full_left,
2769 &full_right,
2770 full_left.keys().chain(full_right.keys()),
2771 options,
2772 |oid| read_blob_bytes(&db, oid),
2773 )
2774 .expect("test operation should succeed");
2775
2776 let public = diff_name_status_trees_with_options_and_diagnostics(
2777 &db,
2778 ObjectFormat::Sha1,
2779 &left,
2780 &right,
2781 options,
2782 )
2783 .expect("test operation should succeed");
2784
2785 assert_eq!(
2786 status_lines(&reference),
2787 status_lines(&public.entries),
2788 "inexact rename via pruned walk must match full-map reference"
2789 );
2790 assert_eq!(
2791 status_lines(&public.entries),
2792 vec!["R075\ta.txt\tb.txt".to_string()],
2793 "expected a 75% inexact rename"
2794 );
2795 fs::remove_dir_all(root).expect("test operation should succeed");
2796 }
2797
2798 fn write_delta_varint(out: &mut Vec<u8>, mut value: u64) {
2799 loop {
2800 let mut byte = (value as u8) & 0x7f;
2801 value >>= 7;
2802 if value != 0 {
2803 byte |= 0x80;
2804 }
2805 out.push(byte);
2806 if value == 0 {
2807 break;
2808 }
2809 }
2810 }
2811
2812 fn lying_result_size_delta(declared_result_size: usize) -> (Vec<u8>, Vec<u8>) {
2815 let base = b"hello";
2816 let result = b"hello world";
2817 let mut delta = Vec::new();
2818 write_delta_varint(&mut delta, base.len() as u64);
2819 write_delta_varint(&mut delta, declared_result_size as u64);
2820 let suffix = &result[base.len()..];
2821 delta.push(0x90);
2822 delta.push(base.len() as u8);
2823 delta.push(suffix.len() as u8);
2824 delta.extend_from_slice(suffix);
2825 (base.to_vec(), delta)
2826 }
2827
2828 #[test]
2829 fn bounded_inflate_reserve_caps_attacker_declared_size() {
2830 use sley_pack::inflate::{bounded_inflate_reserve, MAX_INFLATE_RESERVE};
2831 assert_eq!(bounded_inflate_reserve(u64::MAX as usize, 10), 10 * 1032);
2832 assert_eq!(
2833 bounded_inflate_reserve(usize::MAX, usize::MAX),
2834 MAX_INFLATE_RESERVE
2835 );
2836 assert_eq!(bounded_inflate_reserve(1000, 500), 1000);
2837 assert_eq!(bounded_inflate_reserve(0, 0), 64);
2838 }
2839
2840 #[test]
2841 fn parse_leading_usize_errors_on_overflow() {
2842 assert_eq!(parse_leading_usize(b""), Ok(0));
2843 assert_eq!(parse_leading_usize(b"42 junk"), Ok(42));
2844 assert!(parse_leading_usize(b"999999999999999999999999999999999999999999").is_err());
2845 }
2846
2847 #[test]
2850 fn git_patch_delta_rejects_result_size_bomb_without_oom() {
2851 let bombs = [usize::MAX, 1024 * 1024 * 1024 * 1024];
2852 for declared in bombs {
2853 let (base, delta) = lying_result_size_delta(declared);
2854 let handle = std::thread::spawn(move || git_patch_delta(&base, &delta));
2855 let join_result = handle.join();
2856 assert!(
2857 join_result.is_ok(),
2858 "delta bomb (declared={declared}) panicked/aborted instead of erroring cleanly"
2859 );
2860 assert!(
2861 join_result.expect("parse thread should not panic on a delta bomb").is_none(),
2862 "delta bomb (declared={declared}) should be rejected as invalid"
2863 );
2864 }
2865 }
2866
2867 #[test]
2868 fn git_patch_delta_applies_legitimate_delta_after_result_size_bound() {
2869 let (base, delta) = lying_result_size_delta(b"hello world".len());
2870 let patched = git_patch_delta(&base, &delta).expect("legitimate delta should resolve");
2871 assert_eq!(patched, b"hello world");
2872 }
2873}