use edamame::diff::engine::{compute, HunkIdAllocator};
use edamame::diff::hunk::{HunkKind, InlineSide};
use edamame::diff::{Decision, DiffState};
fn run(old: &str, new: &str) -> Vec<edamame::diff::Hunk> {
let mut ids = HunkIdAllocator::new();
compute(old, new, &mut ids).hunks
}
fn any_hunk_covers(hunks: &[edamame::diff::Hunk], old_line: usize, new_line: usize) -> bool {
hunks
.iter()
.any(|h| h.old_lines.contains(&old_line) || h.new_lines.contains(&new_line))
}
#[test]
fn identical_text_yields_no_hunks() {
assert!(run("alpha\nbeta\ngamma\n", "alpha\nbeta\ngamma\n").is_empty());
}
#[test]
fn pure_insert_at_end_of_file() {
let hunks = run("a\nb\n", "a\nb\nc\n");
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].kind, HunkKind::Insert);
assert_eq!(hunks[0].old_lines, 2..2);
assert_eq!(hunks[0].new_lines, 2..3);
}
#[test]
fn pure_delete_in_middle_of_file() {
let hunks = run("a\nb\nc\n", "a\nc\n");
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].kind, HunkKind::Delete);
assert_eq!(hunks[0].old_lines, 1..2);
assert_eq!(hunks[0].new_lines.start, 1);
assert_eq!(hunks[0].new_lines.end, 1);
}
#[test]
fn replace_emits_inline_word_spans_on_paired_lines() {
let hunks = run("alpha bravo charlie\n", "alpha bravo DELTA\n");
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].kind, HunkKind::Replace);
let inline = &hunks[0].inline;
assert!(inline.iter().any(|s| s.side == InlineSide::Old));
assert!(inline.iter().any(|s| s.side == InlineSide::New));
}
#[test]
fn multi_hunk_keeps_order_and_unique_ids() {
let old = "a\nb\nc\nd\ne\n";
let new = "A\nb\nC\nd\nE\n";
let hunks = run(old, new);
assert!(hunks.len() >= 3, "expected several hunks, got {hunks:?}");
let mut ids: Vec<_> = hunks.iter().map(|h| h.id).collect();
ids.sort_by_key(|id| id.0);
ids.dedup();
assert_eq!(ids.len(), hunks.len(), "ids must be unique");
}
#[test]
fn empty_old_is_one_big_insert() {
let hunks = run("", "first\nsecond\n");
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].kind, HunkKind::Insert);
assert_eq!(hunks[0].new_lines.start, 0);
assert_eq!(hunks[0].new_lines.end, 2);
}
#[test]
fn empty_new_is_one_big_delete() {
let hunks = run("first\nsecond\n", "");
assert_eq!(hunks.len(), 1);
assert_eq!(hunks[0].kind, HunkKind::Delete);
}
#[test]
fn ropey_line_range_invariants() {
use ropey::Rope;
let with_nl = Rope::from_str("a\nb\n");
assert_eq!(with_nl.len_lines(), 3);
assert_eq!(with_nl.byte_to_line(with_nl.len_bytes()), 2);
let without_nl = Rope::from_str("a\nb");
assert_eq!(without_nl.len_lines(), 2);
assert_eq!(without_nl.byte_to_line(without_nl.len_bytes()), 1);
}
const TABLE_OLD: &str = "\
intro paragraph\n\
\n\
| Name | Score |\n\
|-------|-------|\n\
| alpha | 1 |\n\
| bravo | 2 |\n\
| gamma | 3 |\n";
#[test]
fn changed_table_row_splits_into_per_row_hunk() {
let new = TABLE_OLD.replace("| bravo | 2 |", "| bravo | 9 |");
let hunks = run(TABLE_OLD, &new);
assert_eq!(hunks.len(), 1, "one changed row → one hunk: {hunks:?}");
assert_eq!(hunks[0].kind, HunkKind::Replace);
assert_eq!(hunks[0].old_lines, 5..6);
assert_eq!(hunks[0].new_lines, 5..6);
}
#[test]
fn two_changed_table_rows_yield_two_hunks() {
let new = TABLE_OLD
.replace("| alpha | 1 |", "| alpha | 7 |")
.replace("| gamma | 3 |", "| gamma | 8 |");
let hunks = run(TABLE_OLD, &new);
assert_eq!(
hunks.len(),
2,
"two non-adjacent rows → two hunks: {hunks:?}"
);
assert!(hunks.iter().all(|h| h.kind == HunkKind::Replace));
assert_eq!(hunks[0].old_lines, 4..5); assert_eq!(hunks[1].old_lines, 6..7); }
#[test]
fn non_uniform_table_falls_back_to_monolithic_hunk() {
let old = "| A | B |\n|---|---|\n| 1 | 2 |\n";
let new = "| A | B | C |\n|---|---|\n| 1 | 2 |\n";
let hunks = run(old, new);
assert_eq!(
hunks.len(),
1,
"non-uniform table must not row-split: {hunks:?}"
);
assert_eq!(hunks[0].kind, HunkKind::Replace);
assert!(any_hunk_covers(&hunks, 0, 0));
}
#[test]
fn hunk_straddling_table_boundary_keeps_non_table_line() {
let old = "intro text\n| A | B |\n|---|---|\n| 1 | 2 |\n";
let new = "intro CHANGED\n| A | C |\n|---|---|\n| 1 | 2 |\n";
let hunks = run(old, new);
assert!(
any_hunk_covers(&hunks, 0, 0),
"the intro-paragraph change must remain reviewable, got {hunks:?}",
);
}
#[test]
fn straddling_boundary_reject_all_round_trips_to_old() {
let old = "intro text\n| A | B |\n|---|---|\n| 1 | 2 |\n";
let new = "intro CHANGED\n| A | C |\n|---|---|\n| 1 | 2 |\n";
let mut state = DiffState::new(old, new).expect("differing inputs");
state.bulk_decide(Decision::Rejected);
assert_eq!(
state.resolved_rope().expect("all resolved").to_string(),
old,
"reject-all must reproduce the original text",
);
let mut state = DiffState::new(old, new).expect("differing inputs");
state.bulk_decide(Decision::Accepted);
assert_eq!(
state.resolved_rope().expect("all resolved").to_string(),
new,
"accept-all must reproduce the new text",
);
}
#[test]
fn table_split_into_two_keeps_both_changes_reviewable() {
let old = "\
| h1 | h2 |\n\
|----|----|\n\
| a | 1 |\n\
| b | 2 |\n\
| c | 3 |\n\
| d | 4 |\n\
| e | 5 |\n\
| f | 6 |\n";
let new = "\
| h1 | h2 |\n\
|----|----|\n\
| a | 9 |\n\
| b | 2 |\n\
| c | 3 |\n\
\n\
| h1 | h2 |\n\
|----|----|\n\
| d | 4 |\n\
| e | 9 |\n\
| f | 6 |\n";
let hunks = run(old, new);
assert!(
any_hunk_covers(&hunks, 2, 2),
"the `a` row change must stay reviewable: {hunks:?}",
);
assert!(
any_hunk_covers(&hunks, 6, 9),
"the `e` row change must stay reviewable: {hunks:?}",
);
let mut state = DiffState::new(old, new).expect("differing inputs");
state.bulk_decide(Decision::Accepted);
assert_eq!(
state.resolved_rope().expect("all resolved").to_string(),
new,
"accept-all must reproduce the new text with no dropped or duplicated rows",
);
}
#[test]
fn two_changed_rows_accept_all_round_trips_without_duplication() {
let old = TABLE_OLD;
let new = TABLE_OLD
.replace("| alpha | 1 |", "| alpha | 7 |")
.replace("| gamma | 3 |", "| gamma | 8 |");
let mut state = DiffState::new(old, &new).expect("differing inputs");
state.bulk_decide(Decision::Accepted);
assert_eq!(
state.resolved_rope().expect("all resolved").to_string(),
new,
"accept-all must reproduce the new text with no duplicated rows",
);
}
#[test]
fn uneven_table_sets_advisory_flag() {
let old = "| A | B |\n|---|---|\n| 1 | 2 |\n";
let uneven_new = "| A | B |\n|---|---|\n| 1 | 2 | 3 |\n";
let mut ids = HunkIdAllocator::new();
let result = compute(old, uneven_new, &mut ids);
assert!(
result.uneven_table_fallback,
"uneven table must raise the fallback flag: {:?}",
result.hunks,
);
let uniform_new = "| A | B |\n|---|---|\n| 9 | 2 |\n";
let mut ids = HunkIdAllocator::new();
let result = compute(old, uniform_new, &mut ids);
assert!(
!result.uneven_table_fallback,
"uniform table must not raise the fallback flag",
);
}