diff --git a/crates/gitbutler-changeset/Cargo.toml b/crates/gitbutler-changeset/Cargo.toml index 5db318d2c..a1353443f 100644 --- a/crates/gitbutler-changeset/Cargo.toml +++ b/crates/gitbutler-changeset/Cargo.toml @@ -2,6 +2,10 @@ name = "gitbutler-changeset" version = "0.0.0" edition = "2021" +publish = false + +[lib] +doctest = false [dependencies] thiserror.workspace = true diff --git a/crates/gitbutler-changeset/src/diff/hunk.rs b/crates/gitbutler-changeset/src/diff/hunk.rs index 62b0ab28f..20ec56785 100644 --- a/crates/gitbutler-changeset/src/diff/hunk.rs +++ b/crates/gitbutler-changeset/src/diff/hunk.rs @@ -101,137 +101,3 @@ pub trait FormatHunk: RawHunk { } impl FormatHunk for T where T: RawHunk {} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Debug, Clone, PartialEq, Eq)] - struct TestHunk { - removal_start: usize, - addition_start: usize, - changes: Vec, - } - - impl super::RawHunk for TestHunk { - type ChangeIterator = std::vec::IntoIter; - - fn get_removal_start(&self) -> usize { - self.removal_start - } - - fn get_addition_start(&self) -> usize { - self.addition_start - } - - fn changes(&self) -> Self::ChangeIterator { - self.changes.clone().into_iter() - } - } - - impl fmt::Display for TestHunk { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.fmt_unified(f) - } - } - - #[test] - fn empty_hunk() { - let hunk = TestHunk { - removal_start: 1, - addition_start: 1, - changes: vec![], - }; - - assert_eq!(format!("{hunk}"), ""); - } - - #[test] - fn single_removal() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 38, - changes: vec![super::Change::Removal("Hello, world!".to_string())], - }; - - assert_eq!( - format!("{hunk}"), - "@@ -30 +38,0 @@\n-Hello, world!\n\\ No newline at end of file\n" - ); - } - - #[test] - fn single_removal_trailing_nl() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 38, - changes: vec![super::Change::Removal("Hello, world!\n".to_string())], - }; - - assert_eq!(format!("{hunk}"), "@@ -30 +38,0 @@\n-Hello, world!\n"); - } - - #[test] - fn single_addition() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 38, - changes: vec![super::Change::Addition("Hello, world!".to_string())], - }; - - assert_eq!( - format!("{hunk}"), - "@@ -30,0 +38 @@\n+Hello, world!\n\\ No newline at end of file\n" - ); - } - - #[test] - fn single_addition_trailing_nl() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 38, - changes: vec![super::Change::Addition("Hello, world!\n".to_string())], - }; - - assert_eq!(format!("{hunk}"), "@@ -30,0 +38 @@\n+Hello, world!\n"); - } - - #[test] - fn single_modified_line() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 38, - changes: vec![ - super::Change::Removal("Hello, world!".to_string()), - super::Change::Addition("Hello, GitButler!\n".to_string()), - ], - }; - - assert_eq!( - format!("{hunk}"), - "@@ -30 +38 @@\n-Hello, world!\n\\ No newline at end of file\n+Hello, GitButler!\n" - ); - } - - #[test] - fn preserve_change_order() { - let hunk = TestHunk { - removal_start: 30, - addition_start: 20, - changes: vec![ - super::Change::Addition("Hello, GitButler!\n".to_string()), - super::Change::Removal("Hello, world!\n".to_string()), - super::Change::Removal("Hello, world 2!\n".to_string()), - super::Change::Addition("Hello, GitButler 2!\n".to_string()), - super::Change::Removal("Hello, world 3!".to_string()), - super::Change::Addition("Hello, GitButler 3!\n".to_string()), - super::Change::Addition("Hello, GitButler 4!".to_string()), - ], - }; - - assert_eq!( - format!("{hunk}"), - "@@ -30,3 +20,4 @@\n+Hello, GitButler!\n-Hello, world!\n-Hello, world 2!\n+Hello, GitButler 2!\n-Hello, world 3!\n\\ No newline at end of file\n+Hello, GitButler 3!\n+Hello, GitButler 4!\n\\ No newline at end of file\n" - ); - } -} diff --git a/crates/gitbutler-changeset/src/signature.rs b/crates/gitbutler-changeset/src/signature.rs index c636d4723..77744d92b 100644 --- a/crates/gitbutler-changeset/src/signature.rs +++ b/crates/gitbutler-changeset/src/signature.rs @@ -175,131 +175,3 @@ impl> From for Signature { fn bigrams(s: &[u8]) -> impl Iterator + '_ { s.iter().copied().zip(s.iter().skip(1).copied()) } - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! assert_score { - ($sig:ident, $s:expr, $e:expr) => { - let score = $sig.score_str($s); - if (score - $e).abs() >= 0.1 { - panic!( - "expected score of {} for string {:?}, got {}", - $e, $s, score - ); - } - }; - } - - #[test] - fn score_signature() { - let sig = Signature::from("hello world"); - - // NOTE: The scores here are not exact, but are close enough - // to be useful for testing purposes, hence why some have the same - // "score" but different strings. - assert_score!(sig, "hello world", 1.0); - assert_score!(sig, "hello world!", 0.95); - assert_score!(sig, "hello world!!", 0.9); - assert_score!(sig, "hello world!!!", 0.85); - assert_score!(sig, "hello world!!!!", 0.8); - assert_score!(sig, "hello world!!!!!", 0.75); - assert_score!(sig, "hello world!!!!!!", 0.7); - assert_score!(sig, "hello world!!!!!!!", 0.65); - assert_score!(sig, "hello world!!!!!!!!", 0.62); - assert_score!(sig, "hello world!!!!!!!!!", 0.6); - assert_score!(sig, "hello world!!!!!!!!!!", 0.55); - } - - #[test] - fn score_ignores_whitespace() { - let sig = Signature::from("hello world"); - - assert_score!(sig, "hello world", 1.0); - assert_score!(sig, "hello world ", 1.0); - assert_score!(sig, "hello\nworld ", 1.0); - assert_score!(sig, "hello\n\tworld ", 1.0); - assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0); - } - - const TEXT1: &str = include_str!("../fixture/text1.txt"); - const TEXT2: &str = include_str!("../fixture/text2.txt"); - const TEXT3: &str = include_str!("../fixture/text3.txt"); - const CODE1: &str = include_str!("../fixture/code1.txt"); - const CODE2: &str = include_str!("../fixture/code2.txt"); - const CODE3: &str = include_str!("../fixture/code3.txt"); - const CODE4: &str = include_str!("../fixture/code4.txt"); - const LARGE1: &str = include_str!("../fixture/large1.txt"); - const LARGE2: &str = include_str!("../fixture/large2.txt"); - - macro_rules! real_test { - ($a: ident, $b: ident, are_similar) => { - paste::paste! { - #[test] - #[allow(non_snake_case)] - fn []() { - let a = Signature::from($a); - let b = Signature::from($b); - assert!(a.score_str($b) >= 0.95); - assert!(b.score_str($a) >= 0.95); - } - } - }; - ($a: ident, $b: ident, are_not_similar) => { - paste::paste! { - #[test] - #[allow(non_snake_case)] - fn []() { - let a = Signature::from($a); - let b = Signature::from($b); - assert!(a.score_str($b) < 0.95); - assert!(b.score_str($a) < 0.95); - } - } - }; - } - - // Only similar pairs: - // - TEXT1, TEXT2 - // - CODE1, CODE2 - // - LARGE1, LARGE2 - real_test!(TEXT1, TEXT2, are_similar); - real_test!(CODE1, CODE2, are_similar); - real_test!(LARGE1, LARGE2, are_similar); - - // Check all other combos - real_test!(TEXT1, TEXT3, are_not_similar); - real_test!(TEXT1, CODE1, are_not_similar); - real_test!(TEXT1, CODE2, are_not_similar); - real_test!(TEXT1, CODE3, are_not_similar); - real_test!(TEXT1, CODE4, are_not_similar); - real_test!(TEXT1, LARGE1, are_not_similar); - real_test!(TEXT1, LARGE2, are_not_similar); - real_test!(TEXT2, TEXT3, are_not_similar); - real_test!(TEXT2, CODE1, are_not_similar); - real_test!(TEXT2, CODE2, are_not_similar); - real_test!(TEXT2, CODE3, are_not_similar); - real_test!(TEXT2, CODE4, are_not_similar); - real_test!(TEXT2, LARGE1, are_not_similar); - real_test!(TEXT2, LARGE2, are_not_similar); - real_test!(TEXT3, CODE1, are_not_similar); - real_test!(TEXT3, CODE2, are_not_similar); - real_test!(TEXT3, CODE3, are_not_similar); - real_test!(TEXT3, CODE4, are_not_similar); - real_test!(TEXT3, LARGE1, are_not_similar); - real_test!(TEXT3, LARGE2, are_not_similar); - real_test!(CODE1, CODE3, are_not_similar); - real_test!(CODE1, CODE4, are_not_similar); - real_test!(CODE1, LARGE1, are_not_similar); - real_test!(CODE1, LARGE2, are_not_similar); - real_test!(CODE2, CODE3, are_not_similar); - real_test!(CODE2, CODE4, are_not_similar); - real_test!(CODE2, LARGE1, are_not_similar); - real_test!(CODE2, LARGE2, are_not_similar); - real_test!(CODE3, CODE4, are_not_similar); - real_test!(CODE3, LARGE1, are_not_similar); - real_test!(CODE3, LARGE2, are_not_similar); - real_test!(CODE4, LARGE1, are_not_similar); - real_test!(CODE4, LARGE2, are_not_similar); -} diff --git a/crates/gitbutler-changeset/src/span.rs b/crates/gitbutler-changeset/src/span.rs index 813ce5659..b519cb1c9 100644 --- a/crates/gitbutler-changeset/src/span.rs +++ b/crates/gitbutler-changeset/src/span.rs @@ -105,101 +105,3 @@ impl LineSpan { }) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn span_new() { - for s in 0..20 { - for e in s + 1..=20 { - let span = LineSpan::new(s, e); - assert_eq!(span.start(), s); - assert_eq!(span.end(), e); - } - } - } - - #[test] - fn span_extract() { - let lines = [ - "Hello, world!", - "This is a test.", - "This is another test.\r", - "This is a third test.\r", - "This is a fourth test.", - "This is a fifth test.\r", - "This is a sixth test.", - "This is a seventh test.\r", - "This is an eighth test.", - "This is a ninth test.\r", - "This is a tenth test.", // note no newline at end - ]; - - let full_text = lines.join("\n"); - - // calculate the known character offsets of each line - let mut offsets = vec![]; - let mut start = 0; - for (i, line) in lines.iter().enumerate() { - // If it's not the last line, add 1 for the newline character. - let end = start + line.len() + (i != (lines.len() - 1)) as usize; - offsets.push((start, end)); - start = end; - } - - // Test single-line extraction - for i in 0..lines.len() - 1 { - let span = LineSpan::new(i, i + 1); - let expected = &full_text[offsets[i].0..offsets[i].1]; - let (extracted, start_offset, end_offset) = span.extract(&full_text).unwrap(); - - assert_eq!(extracted, expected); - assert_eq!((start_offset, end_offset), (offsets[i].0, offsets[i].1)); - } - - // Test multi-line extraction - for i in 0..lines.len() { - for j in i..=lines.len() { - let span = LineSpan::new(i, j); - - assert!(span.line_count() == (j - i)); - - if i == j { - assert!(span.is_empty()); - continue; - } - - let expected_start = offsets[i].0; - let expected_end = offsets[j - 1].1; - let expected_text = &full_text[expected_start..expected_end]; - - let (extracted, start_offset, end_offset) = span.extract(&full_text).unwrap(); - assert_eq!(extracted, expected_text); - assert_eq!((start_offset, end_offset), (expected_start, expected_end)); - } - } - } - - #[test] - fn span_intersects() { - let span = LineSpan::new(5, 11); // Exclusive end - - assert!(span.intersects(&LineSpan::new(10, 11))); // Intersect at start - assert!(span.intersects(&LineSpan::new(0, 11))); // Fully contained - assert!(span.intersects(&LineSpan::new(10, 15))); // Partial overlap - assert!(span.intersects(&LineSpan::new(4, 6))); // Intersect at end - assert!(span.intersects(&LineSpan::new(5, 6))); // Exact match start - assert!(span.intersects(&LineSpan::new(0, 6))); // Overlap at end - assert!(span.intersects(&LineSpan::new(0, 8))); // Overlap middle - assert!(span.intersects(&LineSpan::new(0, 10))); // Overlap up to end - assert!(span.intersects(&LineSpan::new(9, 10))); // Overlap at single point - assert!(span.intersects(&LineSpan::new(7, 9))); // Overlap inside - - // Test cases where there should be no intersection due to exclusive end - assert!(!span.intersects(&LineSpan::new(0, 5))); // Before start - assert!(!span.intersects(&LineSpan::new(11, 20))); // After end - assert!(!span.intersects(&LineSpan::new(11, 12))); // Just after end - } -} diff --git a/crates/gitbutler-changeset/tests/changeset.rs b/crates/gitbutler-changeset/tests/changeset.rs new file mode 100644 index 000000000..6f9ef3222 --- /dev/null +++ b/crates/gitbutler-changeset/tests/changeset.rs @@ -0,0 +1,3 @@ +mod diff; +mod signature; +mod span; diff --git a/crates/gitbutler-changeset/tests/diff/mod.rs b/crates/gitbutler-changeset/tests/diff/mod.rs new file mode 100644 index 000000000..0f873e290 --- /dev/null +++ b/crates/gitbutler-changeset/tests/diff/mod.rs @@ -0,0 +1,133 @@ +mod hunk { + use gitbutler_changeset::{Change, FormatHunk, RawHunk}; + use std::fmt; + + #[derive(Debug, Clone, PartialEq, Eq)] + struct TestHunk { + removal_start: usize, + addition_start: usize, + changes: Vec, + } + + impl RawHunk for TestHunk { + type ChangeIterator = std::vec::IntoIter; + + fn get_removal_start(&self) -> usize { + self.removal_start + } + + fn get_addition_start(&self) -> usize { + self.addition_start + } + + fn changes(&self) -> Self::ChangeIterator { + self.changes.clone().into_iter() + } + } + + impl fmt::Display for TestHunk { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_unified(f) + } + } + + #[test] + fn empty_hunk() { + let hunk = TestHunk { + removal_start: 1, + addition_start: 1, + changes: vec![], + }; + + assert_eq!(format!("{hunk}"), ""); + } + + #[test] + fn single_removal() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 38, + changes: vec![Change::Removal("Hello, world!".to_string())], + }; + + assert_eq!( + format!("{hunk}"), + "@@ -30 +38,0 @@\n-Hello, world!\n\\ No newline at end of file\n" + ); + } + + #[test] + fn single_removal_trailing_nl() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 38, + changes: vec![Change::Removal("Hello, world!\n".to_string())], + }; + + assert_eq!(format!("{hunk}"), "@@ -30 +38,0 @@\n-Hello, world!\n"); + } + + #[test] + fn single_addition() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 38, + changes: vec![Change::Addition("Hello, world!".to_string())], + }; + + assert_eq!( + format!("{hunk}"), + "@@ -30,0 +38 @@\n+Hello, world!\n\\ No newline at end of file\n" + ); + } + + #[test] + fn single_addition_trailing_nl() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 38, + changes: vec![Change::Addition("Hello, world!\n".to_string())], + }; + + assert_eq!(format!("{hunk}"), "@@ -30,0 +38 @@\n+Hello, world!\n"); + } + + #[test] + fn single_modified_line() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 38, + changes: vec![ + Change::Removal("Hello, world!".to_string()), + Change::Addition("Hello, GitButler!\n".to_string()), + ], + }; + + assert_eq!( + format!("{hunk}"), + "@@ -30 +38 @@\n-Hello, world!\n\\ No newline at end of file\n+Hello, GitButler!\n" + ); + } + + #[test] + fn preserve_change_order() { + let hunk = TestHunk { + removal_start: 30, + addition_start: 20, + changes: vec![ + Change::Addition("Hello, GitButler!\n".to_string()), + Change::Removal("Hello, world!\n".to_string()), + Change::Removal("Hello, world 2!\n".to_string()), + Change::Addition("Hello, GitButler 2!\n".to_string()), + Change::Removal("Hello, world 3!".to_string()), + Change::Addition("Hello, GitButler 3!\n".to_string()), + Change::Addition("Hello, GitButler 4!".to_string()), + ], + }; + + assert_eq!( + format!("{hunk}"), + "@@ -30,3 +20,4 @@\n+Hello, GitButler!\n-Hello, world!\n-Hello, world 2!\n+Hello, GitButler 2!\n-Hello, world 3!\n\\ No newline at end of file\n+Hello, GitButler 3!\n+Hello, GitButler 4!\n\\ No newline at end of file\n" + ); + } +} diff --git a/crates/gitbutler-changeset/fixture/code1.txt b/crates/gitbutler-changeset/tests/fixtures/code1.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/code1.txt rename to crates/gitbutler-changeset/tests/fixtures/code1.txt diff --git a/crates/gitbutler-changeset/fixture/code2.txt b/crates/gitbutler-changeset/tests/fixtures/code2.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/code2.txt rename to crates/gitbutler-changeset/tests/fixtures/code2.txt diff --git a/crates/gitbutler-changeset/fixture/code3.txt b/crates/gitbutler-changeset/tests/fixtures/code3.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/code3.txt rename to crates/gitbutler-changeset/tests/fixtures/code3.txt diff --git a/crates/gitbutler-changeset/fixture/code4.txt b/crates/gitbutler-changeset/tests/fixtures/code4.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/code4.txt rename to crates/gitbutler-changeset/tests/fixtures/code4.txt diff --git a/crates/gitbutler-changeset/fixture/large1.txt b/crates/gitbutler-changeset/tests/fixtures/large1.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/large1.txt rename to crates/gitbutler-changeset/tests/fixtures/large1.txt diff --git a/crates/gitbutler-changeset/fixture/large2.txt b/crates/gitbutler-changeset/tests/fixtures/large2.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/large2.txt rename to crates/gitbutler-changeset/tests/fixtures/large2.txt diff --git a/crates/gitbutler-changeset/fixture/text1.txt b/crates/gitbutler-changeset/tests/fixtures/text1.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/text1.txt rename to crates/gitbutler-changeset/tests/fixtures/text1.txt diff --git a/crates/gitbutler-changeset/fixture/text2.txt b/crates/gitbutler-changeset/tests/fixtures/text2.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/text2.txt rename to crates/gitbutler-changeset/tests/fixtures/text2.txt diff --git a/crates/gitbutler-changeset/fixture/text3.txt b/crates/gitbutler-changeset/tests/fixtures/text3.txt similarity index 100% rename from crates/gitbutler-changeset/fixture/text3.txt rename to crates/gitbutler-changeset/tests/fixtures/text3.txt diff --git a/crates/gitbutler-changeset/tests/signature/mod.rs b/crates/gitbutler-changeset/tests/signature/mod.rs new file mode 100644 index 000000000..fb1b989b4 --- /dev/null +++ b/crates/gitbutler-changeset/tests/signature/mod.rs @@ -0,0 +1,124 @@ +use gitbutler_changeset::Signature; + +macro_rules! assert_score { + ($sig:ident, $s:expr, $e:expr) => { + let score = $sig.score_str($s); + if (score - $e).abs() >= 0.1 { + panic!( + "expected score of {} for string {:?}, got {}", + $e, $s, score + ); + } + }; +} + +#[test] +fn score_signature() { + let sig = Signature::from("hello world"); + + // NOTE: The scores here are not exact, but are close enough + // to be useful for testing purposes, hence why some have the same + // "score" but different strings. + assert_score!(sig, "hello world", 1.0); + assert_score!(sig, "hello world!", 0.95); + assert_score!(sig, "hello world!!", 0.9); + assert_score!(sig, "hello world!!!", 0.85); + assert_score!(sig, "hello world!!!!", 0.8); + assert_score!(sig, "hello world!!!!!", 0.75); + assert_score!(sig, "hello world!!!!!!", 0.7); + assert_score!(sig, "hello world!!!!!!!", 0.65); + assert_score!(sig, "hello world!!!!!!!!", 0.62); + assert_score!(sig, "hello world!!!!!!!!!", 0.6); + assert_score!(sig, "hello world!!!!!!!!!!", 0.55); +} + +#[test] +fn score_ignores_whitespace() { + let sig = Signature::from("hello world"); + + assert_score!(sig, "hello world", 1.0); + assert_score!(sig, "hello world ", 1.0); + assert_score!(sig, "hello\nworld ", 1.0); + assert_score!(sig, "hello\n\tworld ", 1.0); + assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0); +} + +const TEXT1: &str = include_str!("../fixtures/text1.txt"); +const TEXT2: &str = include_str!("../fixtures/text2.txt"); +const TEXT3: &str = include_str!("../fixtures/text3.txt"); +const CODE1: &str = include_str!("../fixtures/code1.txt"); +const CODE2: &str = include_str!("../fixtures/code2.txt"); +const CODE3: &str = include_str!("../fixtures/code3.txt"); +const CODE4: &str = include_str!("../fixtures/code4.txt"); +const LARGE1: &str = include_str!("../fixtures/large1.txt"); +const LARGE2: &str = include_str!("../fixtures/large2.txt"); + +macro_rules! real_test { + ($a: ident, $b: ident, are_similar) => { + paste::paste! { + #[test] + #[allow(non_snake_case)] + fn [<$a _ $b _are_similar>]() { + let a = Signature::from($a); + let b = Signature::from($b); + assert!(a.score_str($b) >= 0.95); + assert!(b.score_str($a) >= 0.95); + } + } + }; + ($a: ident, $b: ident, are_not_similar) => { + paste::paste! { + #[test] + #[allow(non_snake_case)] + fn [<$a _ $b _are_not_similar>]() { + let a = Signature::from($a); + let b = Signature::from($b); + assert!(a.score_str($b) < 0.95); + assert!(b.score_str($a) < 0.95); + } + } + }; +} + +// Only similar pairs: +// - TEXT1, TEXT2 +// - CODE1, CODE2 +// - LARGE1, LARGE2 +real_test!(TEXT1, TEXT2, are_similar); +real_test!(CODE1, CODE2, are_similar); +real_test!(LARGE1, LARGE2, are_similar); + +// Check all other combos +real_test!(TEXT1, TEXT3, are_not_similar); +real_test!(TEXT1, CODE1, are_not_similar); +real_test!(TEXT1, CODE2, are_not_similar); +real_test!(TEXT1, CODE3, are_not_similar); +real_test!(TEXT1, CODE4, are_not_similar); +real_test!(TEXT1, LARGE1, are_not_similar); +real_test!(TEXT1, LARGE2, are_not_similar); +real_test!(TEXT2, TEXT3, are_not_similar); +real_test!(TEXT2, CODE1, are_not_similar); +real_test!(TEXT2, CODE2, are_not_similar); +real_test!(TEXT2, CODE3, are_not_similar); +real_test!(TEXT2, CODE4, are_not_similar); +real_test!(TEXT2, LARGE1, are_not_similar); +real_test!(TEXT2, LARGE2, are_not_similar); +real_test!(TEXT3, CODE1, are_not_similar); +real_test!(TEXT3, CODE2, are_not_similar); +real_test!(TEXT3, CODE3, are_not_similar); +real_test!(TEXT3, CODE4, are_not_similar); +real_test!(TEXT3, LARGE1, are_not_similar); +real_test!(TEXT3, LARGE2, are_not_similar); +real_test!(CODE1, CODE3, are_not_similar); +real_test!(CODE1, CODE4, are_not_similar); +real_test!(CODE1, LARGE1, are_not_similar); +real_test!(CODE1, LARGE2, are_not_similar); +real_test!(CODE2, CODE3, are_not_similar); +real_test!(CODE2, CODE4, are_not_similar); +real_test!(CODE2, LARGE1, are_not_similar); +real_test!(CODE2, LARGE2, are_not_similar); +real_test!(CODE3, CODE4, are_not_similar); +real_test!(CODE3, LARGE1, are_not_similar); +real_test!(CODE3, LARGE2, are_not_similar); +real_test!(CODE4, LARGE1, are_not_similar); +real_test!(CODE4, LARGE2, are_not_similar); diff --git a/crates/gitbutler-changeset/tests/span/mod.rs b/crates/gitbutler-changeset/tests/span/mod.rs new file mode 100644 index 000000000..e03a55c2b --- /dev/null +++ b/crates/gitbutler-changeset/tests/span/mod.rs @@ -0,0 +1,94 @@ +use gitbutler_changeset::LineSpan; + +#[test] +fn new() { + for s in 0..20 { + for e in s + 1..=20 { + let span = LineSpan::new(s, e); + assert_eq!(span.start(), s); + assert_eq!(span.end(), e); + } + } +} + +#[test] +fn extract() { + let lines = [ + "Hello, world!", + "This is a test.", + "This is another test.\r", + "This is a third test.\r", + "This is a fourth test.", + "This is a fifth test.\r", + "This is a sixth test.", + "This is a seventh test.\r", + "This is an eighth test.", + "This is a ninth test.\r", + "This is a tenth test.", // note no newline at end + ]; + + let full_text = lines.join("\n"); + + // calculate the known character offsets of each line + let mut offsets = vec![]; + let mut start = 0; + for (i, line) in lines.iter().enumerate() { + // If it's not the last line, add 1 for the newline character. + let end = start + line.len() + (i != (lines.len() - 1)) as usize; + offsets.push((start, end)); + start = end; + } + + // Test single-line extraction + for i in 0..lines.len() - 1 { + let span = LineSpan::new(i, i + 1); + let expected = &full_text[offsets[i].0..offsets[i].1]; + let (extracted, start_offset, end_offset) = span.extract(&full_text).unwrap(); + + assert_eq!(extracted, expected); + assert_eq!((start_offset, end_offset), (offsets[i].0, offsets[i].1)); + } + + // Test multi-line extraction + for i in 0..lines.len() { + for j in i..=lines.len() { + let span = LineSpan::new(i, j); + + assert!(span.line_count() == (j - i)); + + if i == j { + assert!(span.is_empty()); + continue; + } + + let expected_start = offsets[i].0; + let expected_end = offsets[j - 1].1; + let expected_text = &full_text[expected_start..expected_end]; + + let (extracted, start_offset, end_offset) = span.extract(&full_text).unwrap(); + assert_eq!(extracted, expected_text); + assert_eq!((start_offset, end_offset), (expected_start, expected_end)); + } + } +} + +#[test] +fn intersects() { + let span = LineSpan::new(5, 11); // Exclusive end + + assert!(span.intersects(&LineSpan::new(10, 11))); // Intersect at start + assert!(span.intersects(&LineSpan::new(0, 11))); // Fully contained + assert!(span.intersects(&LineSpan::new(10, 15))); // Partial overlap + assert!(span.intersects(&LineSpan::new(4, 6))); // Intersect at end + assert!(span.intersects(&LineSpan::new(5, 6))); // Exact match start + assert!(span.intersects(&LineSpan::new(0, 6))); // Overlap at end + assert!(span.intersects(&LineSpan::new(0, 8))); // Overlap middle + assert!(span.intersects(&LineSpan::new(0, 10))); // Overlap up to end + assert!(span.intersects(&LineSpan::new(9, 10))); // Overlap at single point + assert!(span.intersects(&LineSpan::new(7, 9))); // Overlap inside + + // Test cases where there should be no intersection due to exclusive end + assert!(!span.intersects(&LineSpan::new(0, 5))); // Before start + assert!(!span.intersects(&LineSpan::new(11, 20))); // After end + assert!(!span.intersects(&LineSpan::new(11, 12))); // Just after end +}