#[allow(clippy::wildcard_imports)]
use super::*;
#[test]
fn filter_no_options() {
let (text, map) = filter_for_diff("hello\nworld\n", false, false);
assert_eq!(text, "hello\nworld");
assert_eq!(map, vec![0, 1]);
}
#[test]
fn filter_ignore_blanks() {
let (text, map) = filter_for_diff("a\n\nb\n \nc\n", false, true);
assert_eq!(text, "a\nb\nc");
assert_eq!(map, vec![0, 2, 4]);
}
#[test]
fn filter_ignore_whitespace() {
let (text, map) = filter_for_diff(" hello world \nfoo\n", true, false);
assert_eq!(text, "hello world\nfoo");
assert_eq!(map, vec![0, 1]);
}
#[test]
fn filter_both_options() {
let (text, map) = filter_for_diff(" a b \n\n c \n", true, true);
assert_eq!(text, "a b\nc");
assert_eq!(map, vec![0, 2]);
}
#[test]
fn filter_empty_input() {
let (text, map) = filter_for_diff("", false, false);
assert_eq!(text, "");
assert!(map.is_empty());
}
#[test]
fn filter_all_blank_lines() {
let (text, map) = filter_for_diff("\n\n\n", false, true);
assert_eq!(text, "");
assert!(map.is_empty());
}
#[test]
fn remap_basic() {
let chunks = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 1,
start_b: 0,
end_b: 1,
}];
let left_map = vec![2, 5];
let right_map = vec![1, 3];
let remapped = remap_chunks(chunks, &left_map, 10, &right_map, 8);
assert_eq!(remapped[0].start_a, 2);
assert_eq!(remapped[0].end_a, 5);
assert_eq!(remapped[0].start_b, 1);
assert_eq!(remapped[0].end_b, 3);
}
#[test]
fn remap_out_of_bounds_uses_total() {
let chunks = vec![DiffChunk {
tag: DiffTag::Delete,
start_a: 0,
end_a: 3, start_b: 0,
end_b: 2, }];
let left_map = vec![0, 1]; let right_map = vec![0]; let remapped = remap_chunks(chunks, &left_map, 10, &right_map, 5);
assert_eq!(remapped[0].end_a, 10); assert_eq!(remapped[0].end_b, 5); }
#[test]
fn remap_empty_chunks() {
let remapped = remap_chunks(vec![], &[0, 1], 2, &[0, 1], 2);
assert!(remapped.is_empty());
}
#[test]
fn format_size_bytes() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(999), "999 B");
}
#[test]
fn format_size_kilobytes() {
assert_eq!(format_size(1000), "1.0 kB");
assert_eq!(format_size(1500), "1.5 kB");
assert_eq!(format_size(999_999), "1000.0 kB");
}
#[test]
fn format_size_megabytes() {
assert_eq!(format_size(1_000_000), "1.0 MB");
assert_eq!(format_size(5_500_000), "5.5 MB");
}
#[test]
fn format_size_gigabytes() {
assert_eq!(format_size(1_000_000_000), "1.0 GB");
assert_eq!(format_size(2_500_000_000), "2.5 GB");
}
#[test]
fn count_changes_empty() {
assert_eq!(count_changes(&[]), 0);
}
#[test]
fn count_changes_all_equal() {
let chunks = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
}];
assert_eq!(count_changes(&chunks), 0);
}
#[test]
fn count_changes_mixed() {
let chunks = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 2,
start_b: 0,
end_b: 2,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 4,
start_b: 2,
end_b: 4,
},
DiffChunk {
tag: DiffTag::Equal,
start_a: 4,
end_a: 6,
start_b: 4,
end_b: 6,
},
DiffChunk {
tag: DiffTag::Delete,
start_a: 6,
end_a: 8,
start_b: 6,
end_b: 6,
},
DiffChunk {
tag: DiffTag::Insert,
start_a: 8,
end_a: 8,
start_b: 6,
end_b: 8,
},
];
assert_eq!(count_changes(&chunks), 3);
}
#[test]
fn unified_diff_basic() {
let left = "line1\nline2\nline3\n";
let right = "line1\nchanged\nline3\n";
let chunks = crate::myers::diff_lines(left, right);
let patch = generate_unified_diff("a.txt", "b.txt", left, right, &chunks);
assert!(patch.starts_with("--- a.txt\n"));
assert!(patch.contains("+++ b.txt\n"));
assert!(patch.contains("@@ "));
assert!(patch.contains("-line2"));
assert!(patch.contains("+changed"));
}
#[test]
fn unified_diff_identical() {
let text = "same\n";
let chunks = crate::myers::diff_lines(text, text);
let patch = generate_unified_diff("a", "b", text, text, &chunks);
assert!(patch.starts_with("--- a\n+++ b\n"));
assert!(
!patch.contains("@@ "),
"identical files should have no hunks"
);
}
#[test]
fn unified_diff_empty_to_content() {
let chunks = crate::myers::diff_lines("", "new\n");
let patch = generate_unified_diff("a", "b", "", "new\n", &chunks);
assert!(patch.contains("+new"));
}
#[test]
fn unified_diff_content_to_empty() {
let chunks = crate::myers::diff_lines("old\n", "");
let patch = generate_unified_diff("a", "b", "old\n", "", &chunks);
assert!(patch.contains("-old"));
}
#[test]
fn unified_diff_multi_hunk() {
let mut left_lines: Vec<&str> = Vec::new();
let mut right_lines: Vec<&str> = Vec::new();
for i in 0..20 {
if i == 2 {
left_lines.push("old_a");
right_lines.push("new_a");
} else if i == 18 {
left_lines.push("old_b");
right_lines.push("new_b");
} else {
left_lines.push("same");
right_lines.push("same");
}
}
let left = left_lines.join("\n") + "\n";
let right = right_lines.join("\n") + "\n";
let chunks = crate::myers::diff_lines(&left, &right);
let patch = generate_unified_diff("a", "b", &left, &right, &chunks);
let hunk_count = patch.lines().filter(|l| l.starts_with("@@ ")).count();
assert!(hunk_count >= 2, "expected 2+ hunks, got {hunk_count}");
}
#[test]
fn read_file_content_text() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("text.txt");
std::fs::write(&p, "hello\nworld\n").unwrap();
let (content, is_binary) = read_file_content(&p);
assert_eq!(content, "hello\nworld\n");
assert!(!is_binary);
}
#[test]
fn read_file_content_binary() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("bin");
std::fs::write(&p, b"hello\x00world").unwrap();
let (content, is_binary) = read_file_content(&p);
assert!(is_binary);
assert!(content.is_empty());
}
#[test]
fn read_file_content_nonexistent() {
let (content, is_binary) = read_file_content(std::path::Path::new("/nonexistent/path"));
assert!(content.is_empty());
assert!(!is_binary);
}
#[test]
fn read_file_content_empty_file() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("empty");
std::fs::write(&p, "").unwrap();
let (content, is_binary) = read_file_content(&p);
assert!(content.is_empty());
assert!(!is_binary);
}
mod unified_diff_proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn unified_diff_always_has_headers(
left in "([a-z ]{0,20}\n){0,10}",
right in "([a-z ]{0,20}\n){0,10}",
) {
let chunks = myers::diff_lines(&left, &right);
let patch = generate_unified_diff("a", "b", &left, &right, &chunks);
prop_assert!(patch.starts_with("--- a\n"), "missing --- header");
prop_assert!(patch.contains("+++ b\n"), "missing +++ header");
}
#[test]
fn unified_diff_hunks_are_well_formed(
left in "([a-z ]{0,20}\n){0,10}",
right in "([a-z ]{0,20}\n){0,10}",
) {
let chunks = myers::diff_lines(&left, &right);
let patch = generate_unified_diff("a", "b", &left, &right, &chunks);
for line in patch.lines() {
if line.starts_with("@@ ") {
prop_assert!(line.contains(" @@"), "malformed hunk header: {line}");
}
}
}
}
}
#[test]
fn luminance_white() {
assert!((hex_luminance("#ffffff") - 1.0).abs() < 0.001);
}
#[test]
fn luminance_black() {
assert!(hex_luminance("#000000").abs() < 0.001);
}
#[test]
fn luminance_without_hash() {
assert!((hex_luminance("ffffff") - 1.0).abs() < 0.001);
}
#[test]
fn luminance_dark_background() {
assert!(hex_luminance("#2e3436") < 0.5);
}
#[test]
fn luminance_light_background() {
assert!(hex_luminance("#fafafa") > 0.5);
}
#[test]
fn luminance_short_string_defaults_light() {
assert!((hex_luminance("#fff") - 1.0).abs() < 0.001);
}
#[test]
fn luminance_empty_defaults_light() {
assert!((hex_luminance("") - 1.0).abs() < 0.001);
}
#[test]
fn colour_functions_respect_dark_flag() {
IS_DARK_SCHEME.with(|c| c.set(false));
let light_bg = chunk_bg_insert();
let light_inline = inline_changed();
let light_search = search_match_bg();
IS_DARK_SCHEME.with(|c| c.set(true));
let dark_bg = chunk_bg_insert();
let dark_inline = inline_changed();
let dark_search = search_match_bg();
assert_ne!(light_bg, dark_bg);
assert_ne!(light_inline, dark_inline);
assert_ne!(light_search, dark_search);
IS_DARK_SCHEME.with(|c| c.set(false));
}
#[test]
fn all_colour_pairs_differ() {
IS_DARK_SCHEME.with(|c| c.set(false));
let light = (
chunk_bg_insert(),
chunk_bg_replace(),
chunk_bg_conflict(),
stroke_insert(),
stroke_replace(),
stroke_conflict(),
band_insert(),
band_replace(),
);
IS_DARK_SCHEME.with(|c| c.set(true));
let dark = (
chunk_bg_insert(),
chunk_bg_replace(),
chunk_bg_conflict(),
stroke_insert(),
stroke_replace(),
stroke_conflict(),
band_insert(),
band_replace(),
);
assert_ne!(light, dark);
IS_DARK_SCHEME.with(|c| c.set(false));
}
#[test]
fn conflict_flags_empty_chunks() {
let flags = conflict_flags(&[], Side::B, &[], Side::A);
assert!(flags.is_empty());
}
#[test]
fn conflict_flags_no_overlap() {
let my = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 3,
start_b: 0,
end_b: 3,
}];
let other = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 10,
end_a: 15,
start_b: 10,
end_b: 15,
}];
assert_eq!(conflict_flags(&my, Side::B, &other, Side::A), vec![false]);
}
#[test]
fn conflict_flags_with_overlap() {
let my = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 5,
end_a: 8,
start_b: 5,
end_b: 10,
},
];
let other = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 7,
end_a: 12,
start_b: 7,
end_b: 12,
}];
assert_eq!(
conflict_flags(&my, Side::B, &other, Side::A),
vec![false, true]
);
}
#[test]
fn conflict_flags_zero_width_insert() {
let my = vec![DiffChunk {
tag: DiffTag::Insert,
start_a: 5,
end_a: 5,
start_b: 5,
end_b: 8,
}];
let other = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 5,
end_a: 7,
start_b: 5,
end_b: 7,
}];
assert_eq!(conflict_flags(&my, Side::B, &other, Side::A), vec![true]);
}
#[test]
fn conflict_flags_all_equal() {
let my = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 10,
start_b: 0,
end_b: 10,
}];
let other = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 3,
end_a: 7,
start_b: 3,
end_b: 7,
}];
assert_eq!(conflict_flags(&my, Side::B, &other, Side::A), vec![false]);
}
mod conflict_flags_proptests {
use super::*;
use proptest::prelude::*;
fn arb_tag() -> impl Strategy<Value = DiffTag> {
prop_oneof![
Just(DiffTag::Equal),
Just(DiffTag::Replace),
Just(DiffTag::Insert),
Just(DiffTag::Delete),
]
}
fn arb_sorted_chunks() -> impl Strategy<Value = Vec<DiffChunk>> {
prop::collection::vec(
(
arb_tag(),
1..50_usize,
1..50_usize,
1..50_usize,
1..50_usize,
),
0..15,
)
.prop_map(|raw| {
let mut pos_a = 0_usize;
let mut pos_b = 0_usize;
raw.into_iter()
.map(|(tag, da, db, la, lb)| {
let start_a = pos_a + da;
let end_a = if tag == DiffTag::Delete || tag == DiffTag::Replace {
start_a + la
} else {
start_a
};
let start_b = pos_b + db;
let end_b = if tag == DiffTag::Insert || tag == DiffTag::Replace {
start_b + lb
} else {
start_b
};
pos_a = end_a;
pos_b = end_b;
DiffChunk {
tag,
start_a,
end_a,
start_b,
end_b,
}
})
.collect()
})
}
fn conflict_flags_naive(
my_chunks: &[DiffChunk],
my_mid: Side,
other_chunks: &[DiffChunk],
other_mid: Side,
) -> Vec<bool> {
let others: Vec<(usize, usize)> = other_chunks
.iter()
.filter(|oc| oc.tag != DiffTag::Equal)
.map(|oc| match other_mid {
Side::A => (oc.start_a, oc.end_a),
Side::B => (oc.start_b, oc.end_b),
})
.collect();
my_chunks
.iter()
.map(|mc| {
if mc.tag == DiffTag::Equal {
return false;
}
let (ms, me) = match my_mid {
Side::A => (mc.start_a, mc.end_a),
Side::B => (mc.start_b, mc.end_b),
};
others
.iter()
.any(|&(os, oe)| chunks_overlap(ms, me, os, oe))
})
.collect()
}
proptest! {
#[test]
fn conflict_flags_matches_naive(
my_chunks in arb_sorted_chunks(),
other_chunks in arb_sorted_chunks(),
) {
let result = conflict_flags(&my_chunks, Side::B, &other_chunks, Side::A);
let expected = conflict_flags_naive(&my_chunks, Side::B, &other_chunks, Side::A);
prop_assert_eq!(result, expected);
}
#[test]
fn conflict_flags_side_a_matches_naive(
my_chunks in arb_sorted_chunks(),
other_chunks in arb_sorted_chunks(),
) {
let result = conflict_flags(&my_chunks, Side::A, &other_chunks, Side::B);
let expected = conflict_flags_naive(&my_chunks, Side::A, &other_chunks, Side::B);
prop_assert_eq!(result, expected);
}
}
}
const TEST_BINDINGS: KeyBindings = KeyBindings {
alt_left: "copy-right-to-left",
alt_right: "copy-left-to-right",
alt_shift_left: "pull-chunk-from-left",
alt_shift_right: "pull-chunk-from-right",
extra_ctrl_shift: &[],
extra_ctrl: &[],
};
#[test]
fn key_alt_shift_left_maps_to_pull() {
use gtk4::gdk::{Key, ModifierType};
let mods = ModifierType::ALT_MASK | ModifierType::SHIFT_MASK;
assert_eq!(
map_key_to_action(Key::Left, mods, &TEST_BINDINGS),
Some("pull-chunk-from-left"),
);
}
#[test]
fn key_alt_shift_right_maps_to_pull() {
use gtk4::gdk::{Key, ModifierType};
let mods = ModifierType::ALT_MASK | ModifierType::SHIFT_MASK;
assert_eq!(
map_key_to_action(Key::Right, mods, &TEST_BINDINGS),
Some("pull-chunk-from-right"),
);
}
#[test]
fn key_alt_shift_down_falls_through_to_next_chunk() {
use gtk4::gdk::{Key, ModifierType};
let mods = ModifierType::ALT_MASK | ModifierType::SHIFT_MASK;
assert_eq!(
map_key_to_action(Key::Down, mods, &TEST_BINDINGS),
Some("next-chunk"),
);
}
#[test]
fn key_alt_shift_up_falls_through_to_prev_chunk() {
use gtk4::gdk::{Key, ModifierType};
let mods = ModifierType::ALT_MASK | ModifierType::SHIFT_MASK;
assert_eq!(
map_key_to_action(Key::Up, mods, &TEST_BINDINGS),
Some("prev-chunk"),
);
}
#[test]
fn key_alt_down_maps_to_next_chunk() {
use gtk4::gdk::{Key, ModifierType};
assert_eq!(
map_key_to_action(Key::Down, ModifierType::ALT_MASK, &TEST_BINDINGS),
Some("next-chunk"),
);
}
#[test]
fn key_alt_page_down_maps_to_next_pane() {
use gtk4::gdk::{Key, ModifierType};
assert_eq!(
map_key_to_action(Key::Page_Down, ModifierType::ALT_MASK, &TEST_BINDINGS),
Some("next-pane"),
);
}
#[test]
fn key_alt_delete_maps_to_delete_chunk() {
use gtk4::gdk::{Key, ModifierType};
assert_eq!(
map_key_to_action(Key::Delete, ModifierType::ALT_MASK, &TEST_BINDINGS),
Some("delete-chunk"),
);
}
#[test]
fn key_no_modifiers_returns_none() {
use gtk4::gdk::{Key, ModifierType};
assert_eq!(
map_key_to_action(Key::Down, ModifierType::empty(), &TEST_BINDINGS),
None,
);
}
#[test]
fn conflict_regions_no_overlap() {
let left = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 2,
start_b: 0,
end_b: 2,
}];
let right = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 5,
end_a: 7,
start_b: 5,
end_b: 7,
}];
assert!(gutter::middle_conflict_regions(&left, &right).is_empty());
}
#[test]
fn conflict_regions_basic_overlap() {
let left = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 2,
start_b: 2,
end_b: 4,
}];
let right = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 4,
start_b: 0,
end_b: 2,
}];
let regions = gutter::middle_conflict_regions(&left, &right);
assert_eq!(regions, vec![(2, 4)]);
}
#[test]
fn conflict_regions_filters_zero_width() {
let left = vec![DiffChunk {
tag: DiffTag::Delete,
start_a: 0,
end_a: 1,
start_b: 3,
end_b: 3,
}];
let right = vec![DiffChunk {
tag: DiffTag::Insert,
start_a: 3,
end_a: 3,
start_b: 0,
end_b: 1,
}];
let regions = gutter::middle_conflict_regions(&left, &right);
assert!(regions.is_empty());
}
#[test]
fn conflict_regions_merges_adjacent() {
let left = vec![
DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 1,
start_b: 0,
end_b: 2,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 3,
start_b: 3,
end_b: 5,
},
];
let right = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 1,
end_a: 4,
start_b: 0,
end_b: 3,
}];
let regions = gutter::middle_conflict_regions(&left, &right);
assert_eq!(regions.len(), 1);
assert!(regions[0].0 <= 1);
assert!(regions[0].1 >= 4);
}
#[test]
fn conflict_regions_equal_chunks_ignored() {
let left = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
}];
let right = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
}];
assert!(gutter::middle_conflict_regions(&left, &right).is_empty());
}
#[test]
fn merged_gutter_no_conflicts() {
let my = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 3,
start_b: 0,
end_b: 3,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 3,
end_a: 5,
start_b: 3,
end_b: 5,
},
];
let other = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
}];
let merged = gutter::merged_gutter_chunks(&my, &other, diff_state::Side::A);
assert!(merged.iter().all(|(_, is_conflict)| !is_conflict));
}
#[test]
fn merged_gutter_marks_conflict() {
let left = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 2,
start_b: 0,
end_b: 2,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 4,
start_b: 2,
end_b: 4,
},
];
let right = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 2,
start_b: 0,
end_b: 2,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 4,
start_b: 2,
end_b: 4,
},
];
let merged = gutter::merged_gutter_chunks(&left, &right, diff_state::Side::A);
assert!(merged.iter().any(|(_, is_conflict)| *is_conflict));
}