use std::mem::swap; use expect_test::expect; // use git::bstr::BStr; // use git_repository as git; use gix_imara_diff::BasicLineDiffPrinter; use gix_imara_diff::InternedInput; use gix_imara_diff::sources::words; use gix_imara_diff::{Algorithm, Diff, UnifiedDiffConfig}; const ALL_ALGORITHMS: [Algorithm; 2] = [Algorithm::Histogram, Algorithm::Myers]; mod fuzzed { use std::time::{Duration, Instant}; use arbitrary::Arbitrary; use gix_imara_diff::{Algorithm, Diff, InternedInput}; #[derive(Debug, Arbitrary)] struct ComprehensiveDiffInput<'a> { before: &'a [u8], before_str: &'a str, after: &'a [u8], after_str: &'a str, } fn run_comprehensive_diff_fuzz_case( ComprehensiveDiffInput { before, before_str, after, after_str, }: ComprehensiveDiffInput<'_>, ) { let input = InternedInput::new(before, after); for algorithm in [Algorithm::Histogram, Algorithm::Myers, Algorithm::MyersMinimal] { let mut diff = Diff::compute(algorithm, &input); let _ = diff.count_additions(); let _ = diff.count_removals(); for hunk in diff.hunks() { let _ = hunk.is_pure_insertion(); let _ = hunk.is_pure_removal(); let _ = hunk.invert(); } diff.postprocess_no_heuristic(&input); diff.postprocess_lines(&input); } let input = InternedInput::new(before_str, after_str); let mut word_input = InternedInput::default(); let mut word_diff = Diff::default(); let diff = Diff::compute(Algorithm::Myers, &input); for hunk in diff.hunks() { hunk.latin_word_diff(&input, &mut word_input, &mut word_diff); } } #[test] fn timeout_regression() { let input = ComprehensiveDiffInput::arbitrary(&mut arbitrary::Unstructured::new(include_bytes!( "../fixtures/clusterfuzz-testcase-minimized-gix-imara-diff-comprehensive_diff-6497314075377664" ))) .expect("testcase matches the historical fuzz target input layout"); let start = Instant::now(); run_comprehensive_diff_fuzz_case(input); let elapsed = start.elapsed(); let expected = Duration::from_secs(3); assert!( elapsed < expected, "clusterfuzz regression took {:?}, expected less than {:?}", elapsed, expected ); } } #[test] fn words_tokenizer() { let text = "Hello, imara!\n (foo-bar_baz)"; let tokens = words(text).collect::>(); assert_eq!( tokens, vec![ "Hello", ",", " ", "imara", "!", "\n", " ", "(", "foo", "-", "bar_baz", ")" ] ); } #[test] fn replace() { let before = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; println!("hello world") } "#; let after = r#"const TEST: i32 = 0; fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; println!("hello world"); println!("hello foo {TEST}"); } "#; let input = InternedInput::new(before, after); for algorithm in ALL_ALGORITHMS { let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); expect![[r#" @@ -1,5 +1,8 @@ +const TEST: i32 = 0; fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; - println!("hello world") + println!("hello world"); + println!("hello foo {TEST}"); } + "#]] .assert_eq( &diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), ); } } #[test] fn identical_files() { let file = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; }"#; for algorithm in ALL_ALGORITHMS { let input = InternedInput::new(file, file); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); assert_eq!( diff.unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), "" ); } } #[test] fn simple_insert() { let before = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; }"#; let after = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; println("hello world") }"#; let mut input = InternedInput::new(before, after); for algorithm in ALL_ALGORITHMS { let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); expect![[r#" @@ -1,4 +1,5 @@ fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; + println("hello world") } "#]] .assert_eq( &diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), ); swap(&mut input.before, &mut input.after); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); expect![[r#" @@ -1,5 +1,4 @@ fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; - println("hello world") } "#]] .assert_eq( &diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), ); swap(&mut input.before, &mut input.after); } } #[test] fn unified_diff_context_lines_near_input_start_and_end() { let before = r#"a b c d e f g h i "#; let after = r#"a b c d edit f g h i "#; let input = InternedInput::new(before, after); for algorithm in ALL_ALGORITHMS { let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); expect![[r#" @@ -2,7 +2,7 @@ b c d -e +edit f g h "#]] .assert_eq( &diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), ); } } mod latin_word_diff { use crate::ALL_ALGORITHMS; use gix_imara_diff::sources::words; use gix_imara_diff::{Diff, InternedInput, Token}; use std::mem::swap; use std::ops::Range; #[test] fn pure_insertion_or_removal() { let before = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; }"#; let after = r#"fn foo() -> Bar{ let mut foo = 2.0; foo *= 100 / 2; println("hello world") }"#; let mut input = InternedInput::new(before, after); for algorithm in ALL_ALGORITHMS { let mut diff_input = InternedInput::default(); let mut out = Diff::default(); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); let mut hunks = diff.hunks(); let hunk = hunks.next().expect("missing first hunk"); hunk.latin_word_diff(&input, &mut diff_input, &mut out); hunks = out.hunks(); let first = hunks.next().expect("missing first inner hunk"); assert!(first.is_pure_insertion()); assert_eq!(first.before, 0..0); assert_eq!(first.after, 0..words(" println(\"hello world\")\n").count() as u32); assert_eq!(hunks.next(), None); assert_eq!(hunks.next(), None); assert_eq!( visualise(&diff_input, &first.after, &diff_input.after), " |println|(|\"|hello| |world|\"|)|\n" ); swap(&mut input.before, &mut input.after); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); hunks = diff.hunks(); let hunk = hunks.next().expect("missing first hunk"); hunk.latin_word_diff(&input, &mut diff_input, &mut out); hunks = out.hunks(); let first = hunks.next().expect("missing first inner hunk"); assert!(first.is_pure_removal()); assert_eq!(first.before, 0..words(" println(\"hello world\")\n").count() as u32); assert_eq!(first.after, 0..0); assert_eq!(hunks.next(), None); assert_eq!(hunks.next(), None); assert_eq!( visualise(&diff_input, &first.before, &diff_input.before), " |println|(|\"|hello| |world|\"|)|\n" ); swap(&mut input.before, &mut input.after); } } #[test] fn modification() { let before = r#"fn foo() -> Bar { let mut foo = 2.0; foo *= 100 / 2; }"#; let after = r#"fn foo() -> Bar { let mut foo = 3.0 * 2.0; foo += 100 / 2; }"#; let mut input = InternedInput::new(before, after); for algorithm in ALL_ALGORITHMS { let mut diff_input = InternedInput::default(); let mut out = Diff::default(); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); let mut hunks = diff.hunks(); let hunk = hunks.next().expect("missing first hunk"); hunk.latin_word_diff(&input, &mut diff_input, &mut out); hunks = out.hunks(); let first = hunks.next().expect("missing first inner hunk"); assert!(first.is_pure_insertion()); let off = words(" let mut foo = ").count() as u32; assert_eq!(first.before, off..off); let ins = words("3.0 * ").count() as u32; assert_eq!(first.after, off..ins + off); assert_eq!(visualise(&diff_input, &first.before, &diff_input.before), ""); assert_eq!(visualise(&diff_input, &first.after, &diff_input.after), "3|.|0| |*| "); let second = hunks.next().expect("missing second inner hunk"); let off = words( r#" let mut foo = 2.0; foo "#, ) .count() as u32; assert_eq!(second.before, off..1 + off); assert_eq!(second.after, ins + off..1 + ins + off); assert_eq!(visualise(&diff_input, &second.before, &diff_input.before), "*"); assert_eq!(visualise(&diff_input, &second.after, &diff_input.after), "+"); assert_eq!(hunks.next(), None); assert_eq!(hunks.next(), None); swap(&mut input.before, &mut input.after); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); hunks = diff.hunks(); let hunk = hunks.next().expect("missing first hunk"); hunk.latin_word_diff(&input, &mut diff_input, &mut out); hunks = out.hunks(); let first = hunks.next().expect("missing first inner hunk"); assert!(first.is_pure_removal()); let off = words(" let mut foo = ").count() as u32; let rem = words("3.0 * ").count() as u32; assert_eq!(first.before, off..rem + off); assert_eq!(first.after, off..off); let second = hunks.next().expect("missing second inner hunk"); let off = words( r#" let mut foo = 2.0; foo "#, ) .count() as u32; assert_eq!(second.before, rem + off..1 + rem + off); assert_eq!(second.after, off..1 + off); assert_eq!(hunks.next(), None); assert_eq!(hunks.next(), None); swap(&mut input.before, &mut input.after); } } fn visualise(diff_input: &InternedInput<&str>, token_ids: &Range, tokens: &[Token]) -> String { token_ids .clone() .map(|id| { let id = id as usize; diff_input.interner[tokens[id]] }) .collect::>() .join("|") } } #[test] #[cfg(not(miri))] fn hand_checked_unidiffs() { let before = r#"use crate::{ alpha::Alpha, beta::Beta, gamma::Gamma, }; use std::{ collections::{HashMap, HashSet}, path::Path, }; pub struct Engine { cache: HashMap, steps: Vec<&'static str>, } impl Engine { pub fn new() -> Self { Self { cache: HashMap::new(), steps: vec!["parse", "render"], } } pub fn update(&mut self, path: &Path) { let _ = path; self.steps.push("scan"); } } fn unchanged_one() { println!("one"); } fn unchanged_two() { println!("two"); } pub enum Error { InvalidPath, Unknown, } pub struct Layer { pub depth: usize, } impl Layer { pub fn parse(&self) -> Result<(), Error> { Ok(()) } } "#; let after = r#"use crate::{ alpha::Alpha, beta::Beta, gamma::Gamma, }; use std::{ collections::HashMap, mem::replace, path::Path, }; pub struct Engine { cache: HashMap, steps: Vec<&'static str>, dirty: bool, } impl Engine { pub fn new() -> Self { Self { cache: HashMap::new(), steps: vec!["parse", "render"], dirty: false, } } pub fn update(&mut self, path: &Path) { let _previous = replace(&mut self.dirty, true); let _ = path; self.steps.push("scan"); } } fn unchanged_one() { println!("one"); } fn unchanged_two() { println!("two"); } pub enum Error { InvalidPath, InvalidState, Unknown, } pub struct Layer { pub depth: u32, } impl Layer { pub fn parse(&self) -> Result<(), Error> { Ok(()) } } "#; for algorithm in ALL_ALGORITHMS { println!("{algorithm:?}"); let input = InternedInput::new(before, after); let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); expect![[r#" @@ -5,13 +5,15 @@ }; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, + mem::replace, path::Path, }; pub struct Engine { cache: HashMap, steps: Vec<&'static str>, + dirty: bool, } impl Engine { @@ -19,10 +21,12 @@ Self { cache: HashMap::new(), steps: vec!["parse", "render"], + dirty: false, } } pub fn update(&mut self, path: &Path) { + let _previous = replace(&mut self.dirty, true); let _ = path; self.steps.push("scan"); } @@ -38,11 +42,12 @@ pub enum Error { InvalidPath, + InvalidState, Unknown, } pub struct Layer { - pub depth: usize, + pub depth: u32, } impl Layer { "#]] .assert_eq( &diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(), ); } } #[test] fn postprocess() { let before = r#" /* * Stay on the safe side. if read_directory() has run once on * "dir", some sticky flag may have been left. Clear them all. */ clear_sticky(dir); /* * exclude patterns are treated like positive ones in * create_simplify. Usually exclude patterns should be a * subset of positive ones, which has no impacts on * foo * bar * test */ foo "#; let after = r#" /* * exclude patterns are treated like positive ones in * create_simplify. Usually exclude patterns should be a * subset of positive ones, which has no impacts on * foo * bar * test */ foo "#; let input = InternedInput::new(before, after); for algorithm in [Algorithm::Histogram, Algorithm::Myers] { let mut diff = Diff::compute(algorithm, &input); diff.postprocess_lines(&input); let diff = diff .unified_diff( &BasicLineDiffPrinter(&input.interner), UnifiedDiffConfig::default(), &input, ) .to_string(); expect![[r#" @@ -1,10 +1,4 @@ - /* - * Stay on the safe side. if read_directory() has run once on - * "dir", some sticky flag may have been left. Clear them all. - */ - clear_sticky(dir); - /* * exclude patterns are treated like positive ones in * create_simplify. Usually exclude patterns should be a "#]] .assert_eq(&diff); } }