From 1c5d02bc0f4430e302ed837e389deae4d9006c8b Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Fri, 11 Sep 2026 20:43:58 +0530 Subject: [PATCH 01/11] feat!: default diff_timeout to 1.0s matching upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream JS defaults Diff_Timeout = 1.0; the crate defaulted to None, so callers silently got no deadline AND no half-match speedup. The golden corpus stays generated at Diff_Timeout = 0 for determinism, so golden.rs now builds every Dmp through oracle_dmp() (diff_timeout: None — the faithful mapping of the oracle config), and the header documents the split. Callers wanting the old behavior set diff_timeout: None explicitly. --- crates/dmp/src/types.rs | 2 +- crates/dmp/tests/golden.rs | 31 +++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/crates/dmp/src/types.rs b/crates/dmp/src/types.rs index be7474a..8142f9b 100644 --- a/crates/dmp/src/types.rs +++ b/crates/dmp/src/types.rs @@ -257,7 +257,7 @@ impl Dmp { pub fn new() -> Self { // it will give a new dmp object. Dmp { - diff_timeout: None, + diff_timeout: Some(1.0), patch_delete_threshold: 0.5, edit_cost: 0, match_distance: 1000, diff --git a/crates/dmp/tests/golden.rs b/crates/dmp/tests/golden.rs index 955ef0c..d3a143b 100644 --- a/crates/dmp/tests/golden.rs +++ b/crates/dmp/tests/golden.rs @@ -1,9 +1,12 @@ //! Golden-reference tests: validate the crate against vectors emitted by the vendored //! googlediff oracle (oracle/vendor/, regenerate with `node oracle/generate.mjs`). //! -//! Config parity: `Dmp::new()` defaults (diff_timeout None, edit_cost 0, margin 4, -//! maxbits 32, thresholds 0.5, distance 1000) equal the generator's oracle settings, -//! so plain `Dmp::new()` is the correct configuration for every test here. +//! Config parity: the oracle generates with `Diff_Timeout = 0` (no deadline, no +//! half-match) for determinism, while the crate DEFAULT is `Some(1.0)` to match +//! upstream's default. Every test here overrides to `diff_timeout: None` — the +//! faithful mapping of the oracle's `Diff_Timeout = 0` — via [`oracle_dmp`]. All +//! other defaults (edit_cost 0, margin 4, maxbits 32, thresholds 0.5, distance +//! 1000) equal the generator's settings. //! //! Known deviations from the oracle are pinned in tests/characterization.rs, not here. @@ -48,6 +51,14 @@ fn corpus() -> Vec { c.cases } +/// A `Dmp` configured like the oracle: no deadline, half-match disabled. +fn oracle_dmp() -> Dmp { + Dmp { + diff_timeout: None, + ..Dmp::new() + } +} + fn to_tuples(diffs: &[Diff]) -> Vec<(i32, String)> { diffs .iter() @@ -64,7 +75,7 @@ fn from_tuples(rows: &[(i32, String)]) -> Vec { #[test] fn golden_diff_main() { for c in corpus() { - let got = to_tuples(&Dmp::new().diff_main(&c.text1, &c.text2, true)); + let got = to_tuples(&oracle_dmp().diff_main(&c.text1, &c.text2, true)); assert_eq!(got, c.diff, "diff mismatch in case '{}'", c.name); } } @@ -73,7 +84,7 @@ fn golden_diff_main() { fn golden_diff_cleanup_semantic() { for c in corpus() { let mut diffs = from_tuples(&c.diff); - Dmp::new().diff_cleanup_semantic(&mut diffs); + oracle_dmp().diff_cleanup_semantic(&mut diffs); assert_eq!( to_tuples(&diffs), c.diff_semantic, @@ -88,7 +99,7 @@ fn golden_delta_encode() { for c in corpus() { let mut diffs = from_tuples(&c.diff); assert_eq!( - Dmp::new().diff_todelta(&mut diffs), + oracle_dmp().diff_todelta(&mut diffs), c.delta, "delta mismatch in case '{}'", c.name @@ -99,7 +110,7 @@ fn golden_delta_encode() { #[test] fn golden_delta_decode() { for c in corpus() { - let diffs = Dmp::new().diff_from_delta(&c.text1, &c.delta); + let diffs = oracle_dmp().diff_from_delta(&c.text1, &c.delta); assert_eq!( to_tuples(&diffs), c.diff, @@ -112,7 +123,7 @@ fn golden_delta_decode() { #[test] fn golden_patch_make() { for c in corpus() { - let mut d = Dmp::new(); + let mut d = oracle_dmp(); let mut patches = d.patch_make1(&c.text1, &c.text2); assert_eq!( d.patch_to_text(&mut patches), @@ -126,7 +137,7 @@ fn golden_patch_make() { #[test] fn golden_patch_text_roundtrip() { for c in corpus() { - let mut d = Dmp::new(); + let mut d = oracle_dmp(); let mut patches = d.patch_from_text(c.patch_text.clone()); assert_eq!( d.patch_to_text(&mut patches), @@ -140,7 +151,7 @@ fn golden_patch_text_roundtrip() { #[test] fn golden_patch_apply() { for c in corpus() { - let mut d = Dmp::new(); + let mut d = oracle_dmp(); let mut patches = d.patch_from_text(c.patch_text.clone()); let (applied, results) = d.patch_apply(&mut patches, &c.apply_to); let applied: String = applied.into_iter().collect(); From 5b6b5f580cf127968d146cd76274bb1d6c76d45c Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Fri, 11 Sep 2026 20:44:07 +0530 Subject: [PATCH 02/11] fix: line-id decoders reverse the surrogate-gap offset Line ids skip U+D800..U+DFFF on the encode side (slot 55296 becomes scalar 57344), but both decoders indexed their table by RAW id: past 55,295 unique lines diff_chars_tolines and the internal chars_tolines_arena either panicked out of bounds or silently rehydrated the line 2048 slots away. New id_to_slot() inverts the jump; ids below the gap are bit-identical to before. Fire test packs 57,500 unique lines through the arena and round-trips; the ported upstream 66,000-line charsToLines test covers the public path. --- crates/dmp/src/tokenize.rs | 39 +++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/dmp/src/tokenize.rs b/crates/dmp/src/tokenize.rs index b01043f..2516ba8 100644 --- a/crates/dmp/src/tokenize.rs +++ b/crates/dmp/src/tokenize.rs @@ -142,7 +142,7 @@ impl Dmp { for diff in diffs.iter_mut() { let mut text: String = "".to_string(); for ch in diff.text.chars() { - text += line_array[ch as usize].as_str(); + text += line_array[id_to_slot(ch as u32)].as_str(); } diff.text = text; } @@ -243,6 +243,18 @@ fn slot_to_id(slot: usize) -> u32 { } } +/// The inverse of [`slot_to_id`]: ids at or past the surrogate gap (U+E000) +/// map back to their slot by subtracting the gap width. Every decoder must go +/// through this — indexing by raw id reads 2048 slots past the real line once +/// a document exceeds 55,295 unique lines. +fn id_to_slot(id: u32) -> usize { + if id >= 57344 { + id as usize - 2048 + } else { + id as usize + } +} + /// True when `munge_arena` would pack `text` into a single line token: lines /// carry their trailing '\n', so that means the only '\n', if any, is the /// final char. Lets line mode skip the packing for one-line inputs, whose @@ -407,12 +419,12 @@ pub(crate) fn chars_tolines_arena(diffs: &mut [TDiff], store: &LineArena) { for diff in diffs.iter_mut() { let mut total = 0; for &ch in &diff.data { - let (s, e) = store.spans[ch as usize]; + let (s, e) = store.spans[id_to_slot(ch as u32)]; total += e - s; } let mut data: Vec = Vec::with_capacity(total); for &ch in &diff.data { - let (s, e) = store.spans[ch as usize]; + let (s, e) = store.spans[id_to_slot(ch as u32)]; data.extend(store.arena[s..e].chars()); } diff.data = data; @@ -561,6 +573,27 @@ impl GraphemePacker { mod tests { use super::*; + #[test] + fn arena_roundtrip_survives_the_surrogate_gap() { + // Line ids skip U+D800..U+DFFF on the encode side (slot 55296 becomes + // scalar 57344), so the decode must subtract the gap back. With 57,500 + // unique lines the high ids sit past the gap; a decoder indexing by raw + // id either panics or rehydrates the wrong line. + let mut text1 = String::new(); + for i in 0..57_500 { + text1.push_str(&format!("line {i}\n")); + } + let mut text2 = text1.clone(); + text2.push_str("one more line\n"); + let c1: Vec = text1.chars().collect(); + let c2: Vec = text2.chars().collect(); + let (p1, _p2, store) = lines_tochars_arena(&c1, &c2); + let mut diffs = vec![TDiff::new(0, p1.chars().collect::>())]; + chars_tolines_arena(&mut diffs, &store); + let rehydrated: String = diffs[0].data.iter().collect(); + assert_eq!(rehydrated, text1); + } + #[test] fn packs_to_one_line_agrees_with_the_arena() { // The single-line shortcut in line mode is only sound if this From 88b94ef06b390871263c59ed7f307706402b2159 Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Fri, 11 Sep 2026 20:44:18 +0530 Subject: [PATCH 03/11] test: port the upstream JS suite verbatim All 28 functions from google/diff-match-patch's javascript/tests/diff_match_patch_test.js (vendored beside the oracle for provenance), one #[test] per function in file order, throw cases split into #[should_panic] with the crate's messages. Inputs and expectations copied verbatim; the only marked exceptions are the untypable null-input throws, diff_prettyHtml (no API - commented-out port), the two wall-clock bounds, the Diff_Timeout=0 -> None mapping, and edit_cost=4 set explicitly where upstream relies on the JS default. Porting this suite is what exposed the surrogate-gap decoder bug fixed in the previous commit (the 66,000-line charsToLines round-trip); 31/31 green with it. --- .../oracle/vendor/diff_match_patch_test.js | 966 ++++++++++++ crates/dmp/tests/upstream_js_suite.rs | 1319 +++++++++++++++++ 2 files changed, 2285 insertions(+) create mode 100644 crates/dmp/oracle/vendor/diff_match_patch_test.js create mode 100644 crates/dmp/tests/upstream_js_suite.rs diff --git a/crates/dmp/oracle/vendor/diff_match_patch_test.js b/crates/dmp/oracle/vendor/diff_match_patch_test.js new file mode 100644 index 0000000..109e56a --- /dev/null +++ b/crates/dmp/oracle/vendor/diff_match_patch_test.js @@ -0,0 +1,966 @@ +/** + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// If expected and actual are the equivalent, pass the test. +function assertEquivalent(msg, expected, actual) { + if (typeof actual == 'undefined') { + // msg is optional. + actual = expected; + expected = msg; + msg = 'Expected: \'' + expected + '\' Actual: \'' + actual + '\''; + } + if (_equivalent(expected, actual)) { + return assertEquals(msg, String(expected), String(actual)); + } else { + return assertEquals(msg, expected, actual); + } +} + + +// Are a and b the equivalent? -- Recursive. +function _equivalent(a, b) { + if (a == b) { + return true; + } + if (typeof a == 'object' && typeof b == 'object' && a !== null && b !== null) { + if (a.toString() != b.toString()) { + return false; + } + for (var p in a) { + if (a.hasOwnProperty(p) && !_equivalent(a[p], b[p])) { + return false; + } + } + for (var p in b) { + if (a.hasOwnProperty(p) && !_equivalent(a[p], b[p])) { + return false; + } + } + return true; + } + return false; +} + + +function diff_rebuildtexts(diffs) { + // Construct the two texts which made up the diff originally. + var text1 = ''; + var text2 = ''; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] != DIFF_INSERT) { + text1 += diffs[x][1]; + } + if (diffs[x][0] != DIFF_DELETE) { + text2 += diffs[x][1]; + } + } + return [text1, text2]; +} + +var dmp = new diff_match_patch(); + + +// DIFF TEST FUNCTIONS + + +function testDiffCommonPrefix() { + // Detect any common prefix. + // Null case. + assertEquals(0, dmp.diff_commonPrefix('abc', 'xyz')); + + // Non-null case. + assertEquals(4, dmp.diff_commonPrefix('1234abcdef', '1234xyz')); + + // Whole case. + assertEquals(4, dmp.diff_commonPrefix('1234', '1234xyz')); +} + +function testDiffCommonSuffix() { + // Detect any common suffix. + // Null case. + assertEquals(0, dmp.diff_commonSuffix('abc', 'xyz')); + + // Non-null case. + assertEquals(4, dmp.diff_commonSuffix('abcdef1234', 'xyz1234')); + + // Whole case. + assertEquals(4, dmp.diff_commonSuffix('1234', 'xyz1234')); +} + +function testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + // Null case. + assertEquals(0, dmp.diff_commonOverlap_('', 'abcd')); + + // Whole case. + assertEquals(3, dmp.diff_commonOverlap_('abc', 'abcd')); + + // No overlap. + assertEquals(0, dmp.diff_commonOverlap_('123456', 'abcd')); + + // Overlap. + assertEquals(3, dmp.diff_commonOverlap_('123456xxx', 'xxxabcd')); + + // Unicode. + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assertEquals(0, dmp.diff_commonOverlap_('fi', '\ufb01i')); +} + +function testDiffHalfMatch() { + // Detect a halfmatch. + dmp.Diff_Timeout = 1; + // No match. + assertEquals(null, dmp.diff_halfMatch_('1234567890', 'abcdef')); + + assertEquals(null, dmp.diff_halfMatch_('12345', '23')); + + // Single Match. + assertEquivalent(['12', '90', 'a', 'z', '345678'], dmp.diff_halfMatch_('1234567890', 'a345678z')); + + assertEquivalent(['a', 'z', '12', '90', '345678'], dmp.diff_halfMatch_('a345678z', '1234567890')); + + assertEquivalent(['abc', 'z', '1234', '0', '56789'], dmp.diff_halfMatch_('abc56789z', '1234567890')); + + assertEquivalent(['a', 'xyz', '1', '7890', '23456'], dmp.diff_halfMatch_('a23456xyz', '1234567890')); + + // Multiple Matches. + assertEquivalent(['12123', '123121', 'a', 'z', '1234123451234'], dmp.diff_halfMatch_('121231234123451234123121', 'a1234123451234z')); + + assertEquivalent(['', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='], dmp.diff_halfMatch_('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-=')); + + assertEquivalent(['-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'], dmp.diff_halfMatch_('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy')); + + // Non-optimal halfmatch. + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertEquivalent(['qHillo', 'w', 'x', 'Hulloy', 'HelloHe'], dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); + + // Optimal no halfmatch. + dmp.Diff_Timeout = 0; + assertEquals(null, dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); +} + +function testDiffLinesToChars() { + function assertLinesToCharsResultEquals(a, b) { + assertEquals(a.chars1, b.chars1); + assertEquals(a.chars2, b.chars2); + assertEquivalent(a.lineArray, b.lineArray); + } + + // Convert lines down to characters. + assertLinesToCharsResultEquals({chars1: '\x01\x02\x01', chars2: '\x02\x01\x02', lineArray: ['', 'alpha\n', 'beta\n']}, dmp.diff_linesToChars_('alpha\nbeta\nalpha\n', 'beta\nalpha\nbeta\n')); + + assertLinesToCharsResultEquals({chars1: '', chars2: '\x01\x02\x03\x03', lineArray: ['', 'alpha\r\n', 'beta\r\n', '\r\n']}, dmp.diff_linesToChars_('', 'alpha\r\nbeta\r\n\r\n\r\n')); + + assertLinesToCharsResultEquals({chars1: '\x01', chars2: '\x02', lineArray: ['', 'a', 'b']}, dmp.diff_linesToChars_('a', 'b')); + + // More than 256 to reveal any 8-bit limitations. + var n = 300; + var lineList = []; + var charList = []; + for (var i = 1; i < n + 1; i++) { + lineList[i - 1] = i + '\n'; + charList[i - 1] = String.fromCharCode(i); + } + assertEquals(n, lineList.length); + var lines = lineList.join(''); + var chars = charList.join(''); + assertEquals(n, chars.length); + lineList.unshift(''); + assertLinesToCharsResultEquals({chars1: chars, chars2: '', lineArray: lineList}, dmp.diff_linesToChars_(lines, '')); +} + +function testDiffCharsToLines() { + // Convert chars up to lines. + var diffs = [[DIFF_EQUAL, '\x01\x02\x01'], [DIFF_INSERT, '\x02\x01\x02']]; + dmp.diff_charsToLines_(diffs, ['', 'alpha\n', 'beta\n']); + assertEquivalent([[DIFF_EQUAL, 'alpha\nbeta\nalpha\n'], [DIFF_INSERT, 'beta\nalpha\nbeta\n']], diffs); + + // More than 256 to reveal any 8-bit limitations. + var n = 300; + var lineList = []; + var charList = []; + for (var i = 1; i < n + 1; i++) { + lineList[i - 1] = i + '\n'; + charList[i - 1] = String.fromCharCode(i); + } + assertEquals(n, lineList.length); + var lines = lineList.join(''); + var chars = charList.join(''); + assertEquals(n, chars.length); + lineList.unshift(''); + var diffs = [[DIFF_DELETE, chars]]; + dmp.diff_charsToLines_(diffs, lineList); + assertEquivalent([[DIFF_DELETE, lines]], diffs); + + // More than 65536 to verify any 16-bit limitation. + lineList = []; + for (var i = 0; i < 66000; i++) { + lineList[i] = i + '\n'; + } + chars = lineList.join(''); + var results = dmp.diff_linesToChars_(chars, ''); + diffs = [[DIFF_INSERT, results.chars1]]; + dmp.diff_charsToLines_(diffs, results.lineArray); + assertEquals(chars, diffs[0][1]); +} + +function testDiffCleanupMerge() { + // Cleanup a messy diff. + // Null case. + var diffs = []; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([], diffs); + + // No change case. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']], diffs); + + // Merge equalities. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'b'], [DIFF_EQUAL, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'abc']], diffs); + + // Merge deletions. + diffs = [[DIFF_DELETE, 'a'], [DIFF_DELETE, 'b'], [DIFF_DELETE, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'abc']], diffs); + + // Merge insertions. + diffs = [[DIFF_INSERT, 'a'], [DIFF_INSERT, 'b'], [DIFF_INSERT, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'abc']], diffs); + + // Merge interweave. + diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b'], [DIFF_DELETE, 'c'], [DIFF_INSERT, 'd'], [DIFF_EQUAL, 'e'], [DIFF_EQUAL, 'f']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'ac'], [DIFF_INSERT, 'bd'], [DIFF_EQUAL, 'ef']], diffs); + + // Prefix and suffix detection. + diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'c']], diffs); + + // Prefix and suffix detection with equalities. + diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc'], [DIFF_EQUAL, 'y']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'cy']], diffs); + + // Slide edit left. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_INSERT, 'ba'], [DIFF_EQUAL, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'ac']], diffs); + + // Slide edit right. + diffs = [[DIFF_EQUAL, 'c'], [DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'ca'], [DIFF_INSERT, 'ba']], diffs); + + // Slide edit left recursive. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'ac'], [DIFF_EQUAL, 'x']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'acx']], diffs); + + // Slide edit right recursive. + diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'ca'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'xca'], [DIFF_DELETE, 'cba']], diffs); + + // Empty merge. + diffs = [[DIFF_DELETE, 'b'], [DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'a'], [DIFF_EQUAL, 'bc']], diffs); + + // Empty equality. + diffs = [[DIFF_EQUAL, ''], [DIFF_INSERT, 'a'], [DIFF_EQUAL, 'b']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'a'], [DIFF_EQUAL, 'b']], diffs); +} + +function testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + // Null case. + var diffs = []; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([], diffs); + + // Blank lines. + diffs = [[DIFF_EQUAL, 'AAA\r\n\r\nBBB'], [DIFF_INSERT, '\r\nDDD\r\n\r\nBBB'], [DIFF_EQUAL, '\r\nEEE']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'AAA\r\n\r\n'], [DIFF_INSERT, 'BBB\r\nDDD\r\n\r\n'], [DIFF_EQUAL, 'BBB\r\nEEE']], diffs); + + // Line boundaries. + diffs = [[DIFF_EQUAL, 'AAA\r\nBBB'], [DIFF_INSERT, ' DDD\r\nBBB'], [DIFF_EQUAL, ' EEE']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'AAA\r\n'], [DIFF_INSERT, 'BBB DDD\r\n'], [DIFF_EQUAL, 'BBB EEE']], diffs); + + // Word boundaries. + diffs = [[DIFF_EQUAL, 'The c'], [DIFF_INSERT, 'ow and the c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_INSERT, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); + + // Alphanumeric boundaries. + diffs = [[DIFF_EQUAL, 'The-c'], [DIFF_INSERT, 'ow-and-the-c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The-'], [DIFF_INSERT, 'cow-and-the-'], [DIFF_EQUAL, 'cat.']], diffs); + + // Hitting the start. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'ax']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'aax']], diffs); + + // Hitting the end. + diffs = [[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'xaa'], [DIFF_DELETE, 'a']], diffs); + + // Sentence boundaries. + diffs = [[DIFF_EQUAL, 'The xxx. The '], [DIFF_INSERT, 'zzz. The '], [DIFF_EQUAL, 'yyy.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The xxx.'], [DIFF_INSERT, ' The zzz.'], [DIFF_EQUAL, ' The yyy.']], diffs); +} + +function testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + // Null case. + var diffs = []; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([], diffs); + + // No elimination #1. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']], diffs); + + // No elimination #2. + diffs = [[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']], diffs); + + // Simple elimination. + diffs = [[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, 'c']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'b']], diffs); + + // Backpass elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_EQUAL, 'cd'], [DIFF_DELETE, 'e'], [DIFF_EQUAL, 'f'], [DIFF_INSERT, 'g']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcdef'], [DIFF_INSERT, 'cdfg']], diffs); + + // Multiple eliminations. + diffs = [[DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2'], [DIFF_EQUAL, '_'], [DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'AB_AB'], [DIFF_INSERT, '1A2_1A2']], diffs); + + // Word boundaries. + diffs = [[DIFF_EQUAL, 'The c'], [DIFF_DELETE, 'ow and the c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_DELETE, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); + + // No overlap elimination. + diffs = [[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']], diffs); + + // Overlap elimination. + diffs = [[DIFF_DELETE, 'abcxxx'], [DIFF_INSERT, 'xxxdef']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xxx'], [DIFF_INSERT, 'def']], diffs); + + // Reverse overlap elimination. + diffs = [[DIFF_DELETE, 'xxxabc'], [DIFF_INSERT, 'defxxx']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_INSERT, 'def'], [DIFF_EQUAL, 'xxx'], [DIFF_DELETE, 'abc']], diffs); + + // Two overlap eliminations. + diffs = [[DIFF_DELETE, 'abcd1212'], [DIFF_INSERT, '1212efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A3'], [DIFF_INSERT, '3BC']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcd'], [DIFF_EQUAL, '1212'], [DIFF_INSERT, 'efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A'], [DIFF_EQUAL, '3'], [DIFF_INSERT, 'BC']], diffs); +} + +function testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + // Null case. + var diffs = []; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([], diffs); + + // No elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']], diffs); + + // Four-edit elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xyz34']], diffs); + + // Three-edit elimination. + diffs = [[DIFF_INSERT, '12'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'xcd'], [DIFF_INSERT, '12x34']], diffs); + + // Backpass elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xy'], [DIFF_INSERT, '34'], [DIFF_EQUAL, 'z'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '56']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xy34z56']], diffs); + + // High cost elimination. + dmp.Diff_EditCost = 5; + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abwxyzcd'], [DIFF_INSERT, '12wxyz34']], diffs); + dmp.Diff_EditCost = 4; +} + +function testDiffPrettyHtml() { + // Pretty print. + var diffs = [[DIFF_EQUAL, 'a\n'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c&d']]; + assertEquals('a¶
<B>b</B>c&d', dmp.diff_prettyHtml(diffs)); +} + +function testDiffText() { + // Compute the source and destination texts. + var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy']]; + assertEquals('jumps over the lazy', dmp.diff_text1(diffs)); + + assertEquals('jumped over a lazy', dmp.diff_text2(diffs)); +} + +function testDiffDelta() { + // Convert a diff into delta string. + var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy'], [DIFF_INSERT, 'old dog']]; + var text1 = dmp.diff_text1(diffs); + assertEquals('jumps over the lazy', text1); + + var delta = dmp.diff_toDelta(diffs); + assertEquals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); + + // Generates error (19 != 20). + try { + dmp.diff_fromDelta(text1 + 'x', delta); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Generates error (19 != 18). + try { + dmp.diff_fromDelta(text1.substring(1), delta); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + try { + dmp.diff_fromDelta('', '+%c3%xy'); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Test deltas with special characters. + diffs = [[DIFF_EQUAL, '\u0680 \x00 \t %'], [DIFF_DELETE, '\u0681 \x01 \n ^'], [DIFF_INSERT, '\u0682 \x02 \\ |']]; + text1 = dmp.diff_text1(diffs); + assertEquals('\u0680 \x00 \t %\u0681 \x01 \n ^', text1); + + delta = dmp.diff_toDelta(diffs); + assertEquals('=7\t-7\t+%DA%82 %02 %5C %7C', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); + + // Verify pool of unchanged characters. + diffs = [[DIFF_INSERT, 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ']]; + var text2 = dmp.diff_text2(diffs); + assertEquals('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', text2); + + delta = dmp.diff_toDelta(diffs); + assertEquals('+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta('', delta)); + + // 160 kb string. + var a = 'abcdefghij'; + for (var i = 0; i < 14; i++) { + a += a; + } + diffs = [[DIFF_INSERT, a]]; + delta = dmp.diff_toDelta(diffs); + assertEquals('+' + a, delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta('', delta)); +} + +function testDiffXIndex() { + // Translate a location in text1 to text2. + // Translation on equality. + assertEquals(5, dmp.diff_xIndex([[DIFF_DELETE, 'a'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']], 2)); + + // Translation on deletion. + assertEquals(1, dmp.diff_xIndex([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '1234'], [DIFF_EQUAL, 'xyz']], 3)); +} + +function testDiffLevenshtein() { + // Levenshtein with trailing equality. + assertEquals(4, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']])); + // Levenshtein with leading equality. + assertEquals(4, dmp.diff_levenshtein([[DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234']])); + // Levenshtein with middle equality. + assertEquals(7, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xyz'], [DIFF_INSERT, '1234']])); +} + +function testDiffBisect() { + // Normal. + var a = 'cat'; + var b = 'map'; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + assertEquivalent([[DIFF_DELETE, 'c'], [DIFF_INSERT, 'm'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 't'], [DIFF_INSERT, 'p']], dmp.diff_bisect_(a, b, Number.MAX_VALUE)); + + // Timeout. + assertEquivalent([[DIFF_DELETE, 'cat'], [DIFF_INSERT, 'map']], dmp.diff_bisect_(a, b, 0)); +} + +function testDiffMain() { + // Perform a trivial diff. + // Null case. + assertEquivalent([], dmp.diff_main('', '', false)); + + // Equality. + assertEquivalent([[DIFF_EQUAL, 'abc']], dmp.diff_main('abc', 'abc', false)); + + // Simple insertion. + assertEquivalent([[DIFF_EQUAL, 'ab'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'ab123c', false)); + + // Simple deletion. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'bc']], dmp.diff_main('a123bc', 'abc', false)); + + // Two insertions. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'b'], [DIFF_INSERT, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'a123b456c', false)); + + // Two deletions. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('a123b456c', 'abc', false)); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + // Simple cases. + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b']], dmp.diff_main('a', 'b', false)); + + assertEquivalent([[DIFF_DELETE, 'Apple'], [DIFF_INSERT, 'Banana'], [DIFF_EQUAL, 's are a'], [DIFF_INSERT, 'lso'], [DIFF_EQUAL, ' fruit.']], dmp.diff_main('Apples are a fruit.', 'Bananas are also fruit.', false)); + + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, '\u0680'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, '\t'], [DIFF_INSERT, '\0']], dmp.diff_main('ax\t', '\u0680x\0', false)); + + // Overlaps. + assertEquivalent([[DIFF_DELETE, '1'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 'y'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '2'], [DIFF_INSERT, 'xab']], dmp.diff_main('1ayb2', 'abxab', false)); + + assertEquivalent([[DIFF_INSERT, 'xaxcx'], [DIFF_EQUAL, 'abc'], [DIFF_DELETE, 'y']], dmp.diff_main('abcy', 'xaxcxabc', false)); + + assertEquivalent([[DIFF_DELETE, 'ABCD'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'bcd'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'efghijklmnopqrs'], [DIFF_DELETE, 'EFGHIJKLMNOefg']], dmp.diff_main('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', 'a-bcd-efghijklmnopqrs', false)); + + // Large equality. + assertEquivalent([[DIFF_INSERT, ' '], [DIFF_EQUAL, 'a'], [DIFF_INSERT, 'nd'], [DIFF_EQUAL, ' [[Pennsylvania]]'], [DIFF_DELETE, ' and [[New']], dmp.diff_main('a [[Pennsylvania]] and [[New', ' and [[Pennsylvania]]', false)); + + // Timeout. + dmp.Diff_Timeout = 0.1; // 100ms + var a = '`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n'; + var b = 'I am the very model of a modern major general,\nI\'ve information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n'; + // Increase the text lengths by 1024 times to ensure a timeout. + for (var i = 0; i < 10; i++) { + a += a; + b += b; + } + var startTime = (new Date()).getTime(); + dmp.diff_main(a, b); + var endTime = (new Date()).getTime(); + // Test that we took at least the timeout period. + assertTrue(dmp.Diff_Timeout * 1000 <= endTime - startTime); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + assertTrue(dmp.Diff_Timeout * 1000 * 2 > endTime - startTime); + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + // Simple line-mode. + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n'; + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Single line-mode. + a = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; + b = 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'; + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Overlap line-mode. + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n'; + var texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); + var texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); + assertEquivalent(texts_textmode, texts_linemode); + + // Test null inputs. + try { + dmp.diff_main(null, null); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + + +// MATCH TEST FUNCTIONS + + +function testMatchAlphabet() { + // Initialise the bitmasks for Bitap. + // Unique. + assertEquivalent({'a':4, 'b':2, 'c':1}, dmp.match_alphabet_('abc')); + + // Duplicates. + assertEquivalent({'a':37, 'b':18, 'c':8}, dmp.match_alphabet_('abcaba')); +} + +function testMatchBitap() { + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5; + // Exact matches. + assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 5)); + + assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 0)); + + // Fuzzy matches. + assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxhi', 0)); + + assertEquals(2, dmp.match_bitap_('abcdefghijk', 'cdefxyhijk', 5)); + + assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'bxy', 1)); + + // Overflow. + assertEquals(2, dmp.match_bitap_('123456789xx0', '3456789x0', 2)); + + // Threshold test. + dmp.Match_Threshold = 0.4; + assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); + + dmp.Match_Threshold = 0.3; + assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); + + dmp.Match_Threshold = 0.0; + assertEquals(1, dmp.match_bitap_('abcdefghijk', 'bcdef', 1)); + dmp.Match_Threshold = 0.5; + + // Multiple select. + assertEquals(0, dmp.match_bitap_('abcdexyzabcde', 'abccde', 3)); + + assertEquals(8, dmp.match_bitap_('abcdexyzabcde', 'abccde', 5)); + + // Distance test. + dmp.Match_Distance = 10; // Strict location. + assertEquals(-1, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); + + assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 1)); + + dmp.Match_Distance = 1000; // Loose location. + assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); +} + +function testMatchMain() { + // Full match. + // Shortcut matches. + assertEquals(0, dmp.match_main('abcdef', 'abcdef', 1000)); + + assertEquals(-1, dmp.match_main('', 'abcdef', 1)); + + assertEquals(3, dmp.match_main('abcdef', '', 3)); + + assertEquals(3, dmp.match_main('abcdef', 'de', 3)); + + // Beyond end match. + assertEquals(3, dmp.match_main("abcdef", "defy", 4)); + + // Oversized pattern. + assertEquals(0, dmp.match_main("abcdef", "abcdefy", 0)); + + // Complex match. + assertEquals(4, dmp.match_main('I am the very model of a modern major general.', ' that berry ', 5)); + + // Test null inputs. + try { + dmp.match_main(null, null, 0); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + + +// PATCH TEST FUNCTIONS + + +function testPatchObj() { + // Patch Object. + var p = new diff_match_patch.patch_obj(); + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, '\nlaz']]; + var strp = p.toString(); + assertEquals('@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n', strp); +} + +function testPatchFromText() { + assertEquivalent([], dmp.patch_fromText(strp)); + + var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; + assertEquals(strp, dmp.patch_fromText(strp)[0].toString()); + + assertEquals('@@ -1 +1 @@\n-a\n+b\n', dmp.patch_fromText('@@ -1 +1 @@\n-a\n+b\n')[0].toString()); + + assertEquals('@@ -1,3 +0,0 @@\n-abc\n', dmp.patch_fromText('@@ -1,3 +0,0 @@\n-abc\n')[0].toString()); + + assertEquals('@@ -0,0 +1,3 @@\n+abc\n', dmp.patch_fromText('@@ -0,0 +1,3 @@\n+abc\n')[0].toString()); + + // Generates error. + try { + dmp.patch_fromText('Bad\nPatch\n'); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + +function testPatchToText() { + var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + var p = dmp.patch_fromText(strp); + assertEquals(strp, dmp.patch_toText(p)); + + strp = '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n'; + p = dmp.patch_fromText(strp); + assertEquals(strp, dmp.patch_toText(p)); +} + +function testPatchAddContext() { + dmp.Patch_Margin = 4; + var p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps over the lazy dog.'); + assertEquals('@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n', p.toString()); + + // Same, but not enough trailing context. + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps.'); + assertEquals('@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n', p.toString()); + + // Same, but not enough leading context. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps.'); + assertEquals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n', p.toString()); + + // Same, but with ambiguity. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps. The quick brown fox crashes.'); + assertEquals('@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n', p.toString()); +} + +function testPatchMake() { + // Null case. + var patches = dmp.patch_make('', ''); + assertEquals('', dmp.patch_toText(patches)); + + var text1 = 'The quick brown fox jumps over the lazy dog.'; + var text2 = 'That quick brown fox jumped over a lazy dog.'; + // Text2+Text1 inputs. + var expectedPatch = '@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n'; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Text2 inputs. + expectedPatch = '@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + patches = dmp.patch_make(text1, text2); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Diff input. + var diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Diff inputs. + patches = dmp.patch_make(text1, diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Text2+Diff inputs (deprecated). + patches = dmp.patch_make(text1, text2, diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Character encoding. + patches = dmp.patch_make('`1234567890-=[]\\;\',./', '~!@#$%^&*()_+{}|:"<>?'); + assertEquals('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n', dmp.patch_toText(patches)); + + // Character decoding. + diffs = [[DIFF_DELETE, '`1234567890-=[]\\;\',./'], [DIFF_INSERT, '~!@#$%^&*()_+{}|:"<>?']]; + assertEquivalent(diffs, dmp.patch_fromText('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')[0].diffs); + + // Long string with repeats. + text1 = ''; + for (var x = 0; x < 100; x++) { + text1 += 'abcdef'; + } + text2 = text1 + '123'; + expectedPatch = '@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n'; + patches = dmp.patch_make(text1, text2); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Test null inputs. + try { + dmp.patch_make(null); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + +function testPatchSplitMax() { + // Assumes that dmp.Match_MaxBits is 32. + var patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz01234567890', 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0'); + dmp.patch_splitMax(patches); + assertEquals('@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n', dmp.patch_toText(patches)); + + patches = dmp.patch_make('abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 'abcdefuvwxyz'); + var oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + assertEquals(oldToText, dmp.patch_toText(patches)); + + patches = dmp.patch_make('1234567890123456789012345678901234567890123456789012345678901234567890', 'abc'); + dmp.patch_splitMax(patches); + assertEquals('@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n', dmp.patch_toText(patches)); + + patches = dmp.patch_make('abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1', 'abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1'); + dmp.patch_splitMax(patches); + assertEquals('@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n', dmp.patch_toText(patches)); +} + +function testPatchAddPadding() { + // Both edges full. + var patches = dmp.patch_make('', 'test'); + assertEquals('@@ -0,0 +1,4 @@\n+test\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n', dmp.patch_toText(patches)); + + // Both edges partial. + patches = dmp.patch_make('XY', 'XtestY'); + assertEquals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n', dmp.patch_toText(patches)); + + // Both edges none. + patches = dmp.patch_make('XXXXYYYY', 'XXXXtestYYYY'); + assertEquals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); +} + +function testPatchApply() { + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5; + dmp.Patch_DeleteThreshold = 0.5; + // Null case. + var patches = dmp.patch_make('', ''); + var results = dmp.patch_apply(patches, 'Hello world.'); + assertEquivalent(['Hello world.', []], results); + + // Exact match. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'That quick brown fox jumped over a lazy dog.'); + results = dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + assertEquivalent(['That quick brown fox jumped over a lazy dog.', [true, true]], results); + + // Partial match. + results = dmp.patch_apply(patches, 'The quick red rabbit jumps over the tired tiger.'); + assertEquivalent(['That quick red rabbit jumped over a tired tiger.', [true, true]], results); + + // Failed match. + results = dmp.patch_apply(patches, 'I am the very model of a modern major general.'); + assertEquivalent(['I am the very model of a modern major general.', [false, false]], results); + + // Big delete, small change. + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y'); + assertEquivalent(['xabcy', [true, true]], results); + + // Big delete, big change 1. + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + assertEquivalent(['xabc12345678901234567890---------------++++++++++---------------12345678901234567890y', [false, true]], results); + + // Big delete, big change 2. + dmp.Patch_DeleteThreshold = 0.6; + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + assertEquivalent(['xabcy', [true, true]], results); + dmp.Patch_DeleteThreshold = 0.5; + + // Compensate for failed patch. + dmp.Match_Threshold = 0.0; + dmp.Match_Distance = 0; + patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz--------------------1234567890', 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890'); + results = dmp.patch_apply(patches, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890'); + assertEquivalent(['ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890', [false, true]], results); + dmp.Match_Threshold = 0.5; + dmp.Match_Distance = 1000; + + // No side effects. + patches = dmp.patch_make('', 'test'); + var patchstr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ''); + assertEquals(patchstr, dmp.patch_toText(patches)); + + // No side effects with major delete. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'Woof'); + patchstr = dmp.patch_toText(patches); + dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + assertEquals(patchstr, dmp.patch_toText(patches)); + + // Edge exact match. + patches = dmp.patch_make('', 'test'); + results = dmp.patch_apply(patches, ''); + assertEquivalent(['test', [true]], results); + + // Near edge exact match. + patches = dmp.patch_make('XY', 'XtestY'); + results = dmp.patch_apply(patches, 'XY'); + assertEquivalent(['XtestY', [true]], results); + + // Edge partial match. + patches = dmp.patch_make('y', 'y123'); + results = dmp.patch_apply(patches, 'x'); + assertEquivalent(['x123', [true]], results); +} diff --git a/crates/dmp/tests/upstream_js_suite.rs b/crates/dmp/tests/upstream_js_suite.rs new file mode 100644 index 0000000..6f34bf4 --- /dev/null +++ b/crates/dmp/tests/upstream_js_suite.rs @@ -0,0 +1,1319 @@ +// Faithful port of Google's upstream diff-match-patch JavaScript test suite: +// https://github.com/google/diff-match-patch/blob/master/javascript/tests/diff_match_patch_test.js +// (vendored at crates/dmp/oracle/vendor/diff_match_patch_test.js). +// +// One #[test] per upstream test function, in the same order as the JS file. +// Inputs and expected values are copied verbatim from the JS; failing tests +// are deliberate breakage discoveries, not porting bugs. +// +// Skipped as untypable in Rust (null-input throw tests): +// - testDiffMain: dmp.diff_main(null, null) +// - testMatchMain: dmp.match_main(null, null, 0) +// - testPatchMake: dmp.patch_make(null) + +use std::collections::HashMap; + +use diff_match_patch::{Diff, Dmp, Patch}; + +fn d(operation: i32, text: &str) -> Diff { + Diff::new(operation, text.to_string()) +} + +fn chars(text: &str) -> Vec { + text.chars().collect() +} + +fn svec(items: &[&str]) -> Vec { + items.iter().map(|item| item.to_string()).collect() +} + +// Port of the JS diff_rebuildtexts helper. +fn diff_rebuildtexts(diffs: &[Diff]) -> (String, String) { + let mut text1 = String::new(); + let mut text2 = String::new(); + for diff in diffs { + if diff.operation != 1 { + text1 += &diff.text; + } + if diff.operation != -1 { + text2 += &diff.text; + } + } + (text1, text2) +} + +// DIFF TEST FUNCTIONS + +#[test] +fn upstream_diff_common_prefix() { + let mut dmp = Dmp::new(); + // Null case. + assert_eq!(0, dmp.diff_common_prefix(&chars("abc"), &chars("xyz"))); + + // Non-null case. + assert_eq!(4, dmp.diff_common_prefix(&chars("1234abcdef"), &chars("1234xyz"))); + + // Whole case. + assert_eq!(4, dmp.diff_common_prefix(&chars("1234"), &chars("1234xyz"))); +} + +#[test] +fn upstream_diff_common_suffix() { + let mut dmp = Dmp::new(); + // Null case. + assert_eq!(0, dmp.diff_common_suffix(&chars("abc"), &chars("xyz"))); + + // Non-null case. + assert_eq!(4, dmp.diff_common_suffix(&chars("abcdef1234"), &chars("xyz1234"))); + + // Whole case. + assert_eq!(4, dmp.diff_common_suffix(&chars("1234"), &chars("xyz1234"))); +} + +#[test] +fn upstream_diff_common_overlap() { + let mut dmp = Dmp::new(); + // Null case. + assert_eq!(0, dmp.diff_common_overlap(&chars(""), &chars("abcd"))); + + // Whole case. + assert_eq!(3, dmp.diff_common_overlap(&chars("abc"), &chars("abcd"))); + + // No overlap. + assert_eq!(0, dmp.diff_common_overlap(&chars("123456"), &chars("abcd"))); + + // Overlap. + assert_eq!(3, dmp.diff_common_overlap(&chars("123456xxx"), &chars("xxxabcd"))); + + // Unicode. + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assert_eq!(0, dmp.diff_common_overlap(&chars("fi"), &chars("\u{fb01}i"))); +} + +#[test] +fn upstream_diff_half_match() { + let mut dmp = Dmp::new(); + dmp.diff_timeout = Some(1.0); + // JS returns null for "no half-match"; the crate returns an empty Vec. + let no_match: Vec = vec![]; + // No match. + assert_eq!(no_match, dmp.diff_half_match(&chars("1234567890"), &chars("abcdef"))); + + assert_eq!(no_match, dmp.diff_half_match(&chars("12345"), &chars("23"))); + + // Single Match. + assert_eq!( + svec(&["12", "90", "a", "z", "345678"]), + dmp.diff_half_match(&chars("1234567890"), &chars("a345678z")) + ); + + assert_eq!( + svec(&["a", "z", "12", "90", "345678"]), + dmp.diff_half_match(&chars("a345678z"), &chars("1234567890")) + ); + + assert_eq!( + svec(&["abc", "z", "1234", "0", "56789"]), + dmp.diff_half_match(&chars("abc56789z"), &chars("1234567890")) + ); + + assert_eq!( + svec(&["a", "xyz", "1", "7890", "23456"]), + dmp.diff_half_match(&chars("a23456xyz"), &chars("1234567890")) + ); + + // Multiple Matches. + assert_eq!( + svec(&["12123", "123121", "a", "z", "1234123451234"]), + dmp.diff_half_match(&chars("121231234123451234123121"), &chars("a1234123451234z")) + ); + + assert_eq!( + svec(&["", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="]), + dmp.diff_half_match(&chars("x-=-=-=-=-=-=-=-=-=-=-=-="), &chars("xx-=-=-=-=-=-=-=")) + ); + + assert_eq!( + svec(&["-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"]), + dmp.diff_half_match(&chars("-=-=-=-=-=-=-=-=-=-=-=-=y"), &chars("-=-=-=-=-=-=-=yy")) + ); + + // Non-optimal halfmatch. + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assert_eq!( + svec(&["qHillo", "w", "x", "Hulloy", "HelloHe"]), + dmp.diff_half_match(&chars("qHilloHelloHew"), &chars("xHelloHeHulloy")) + ); + + // Optimal no halfmatch. + dmp.diff_timeout = None; // JS Diff_Timeout = 0 maps to None (no deadline). + assert_eq!( + no_match, + dmp.diff_half_match(&chars("qHilloHelloHew"), &chars("xHelloHeHulloy")) + ); +} + +#[test] +fn upstream_diff_lines_to_chars() { + let mut dmp = Dmp::new(); + // Convert lines down to characters. + assert_eq!( + ( + "\x01\x02\x01".to_string(), + "\x02\x01\x02".to_string(), + svec(&["", "alpha\n", "beta\n"]) + ), + dmp.diff_lines_tochars(&chars("alpha\nbeta\nalpha\n"), &chars("beta\nalpha\nbeta\n")) + ); + + assert_eq!( + ( + "".to_string(), + "\x01\x02\x03\x03".to_string(), + svec(&["", "alpha\r\n", "beta\r\n", "\r\n"]) + ), + dmp.diff_lines_tochars(&chars(""), &chars("alpha\r\nbeta\r\n\r\n\r\n")) + ); + + assert_eq!( + ("\x01".to_string(), "\x02".to_string(), svec(&["", "a", "b"])), + dmp.diff_lines_tochars(&chars("a"), &chars("b")) + ); + + // More than 256 to reveal any 8-bit limitations. + let n = 300; + let mut line_list: Vec = vec![]; + let mut char_list: Vec = vec![]; + for i in 1..n + 1 { + line_list.push(i.to_string() + "\n"); + char_list.push(char::from_u32(i).expect("codepoints 1..=300 are valid scalars")); + } + assert_eq!(n as usize, line_list.len()); + let lines = line_list.join(""); + let joined_chars: String = char_list.into_iter().collect(); + assert_eq!(n as usize, joined_chars.chars().count()); + line_list.insert(0, "".to_string()); + assert_eq!( + (joined_chars, "".to_string(), line_list), + dmp.diff_lines_tochars(&chars(&lines), &vec![]) + ); +} + +#[test] +fn upstream_diff_chars_to_lines() { + let mut dmp = Dmp::new(); + // Convert chars up to lines. + let mut diffs = vec![d(0, "\x01\x02\x01"), d(1, "\x02\x01\x02")]; + dmp.diff_chars_tolines(&mut diffs, &svec(&["", "alpha\n", "beta\n"])); + assert_eq!(vec![d(0, "alpha\nbeta\nalpha\n"), d(1, "beta\nalpha\nbeta\n")], diffs); + + // More than 256 to reveal any 8-bit limitations. + let n = 300; + let mut line_list: Vec = vec![]; + let mut char_list: Vec = vec![]; + for i in 1..n + 1 { + line_list.push(i.to_string() + "\n"); + char_list.push(char::from_u32(i).expect("codepoints 1..=300 are valid scalars")); + } + assert_eq!(n as usize, line_list.len()); + let lines = line_list.join(""); + let joined_chars: String = char_list.into_iter().collect(); + assert_eq!(n as usize, joined_chars.chars().count()); + line_list.insert(0, "".to_string()); + let mut diffs = vec![Diff::new(-1, joined_chars)]; + dmp.diff_chars_tolines(&mut diffs, &line_list); + assert_eq!(vec![Diff::new(-1, lines)], diffs); + + // More than 65536 to verify any 16-bit limitation. + // ADAPTED (UTF-16 -> scalar): JS exercises surrogate-pair line hashes; in + // Rust the packer must instead skip the U+D800..U+DFFF scalar gap. The + // roundtrip assertion itself is verbatim. + let mut line_list: Vec = vec![]; + for i in 0..66000 { + line_list.push(i.to_string() + "\n"); + } + let text = line_list.join(""); + let results = dmp.diff_lines_tochars(&chars(&text), &vec![]); + let mut diffs = vec![Diff::new(1, results.0)]; + dmp.diff_chars_tolines(&mut diffs, &results.2); + assert_eq!(text, diffs[0].text); +} + +#[test] +fn upstream_diff_cleanup_merge() { + let mut dmp = Dmp::new(); + // Cleanup a messy diff. + // Null case. + let mut diffs: Vec = vec![]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(Vec::::new(), diffs); + + // No change case. + diffs = vec![d(0, "a"), d(-1, "b"), d(1, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "a"), d(-1, "b"), d(1, "c")], diffs); + + // Merge equalities. + diffs = vec![d(0, "a"), d(0, "b"), d(0, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "abc")], diffs); + + // Merge deletions. + diffs = vec![d(-1, "a"), d(-1, "b"), d(-1, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(-1, "abc")], diffs); + + // Merge insertions. + diffs = vec![d(1, "a"), d(1, "b"), d(1, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(1, "abc")], diffs); + + // Merge interweave. + diffs = vec![d(-1, "a"), d(1, "b"), d(-1, "c"), d(1, "d"), d(0, "e"), d(0, "f")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(-1, "ac"), d(1, "bd"), d(0, "ef")], diffs); + + // Prefix and suffix detection. + diffs = vec![d(-1, "a"), d(1, "abc"), d(-1, "dc")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "a"), d(-1, "d"), d(1, "b"), d(0, "c")], diffs); + + // Prefix and suffix detection with equalities. + diffs = vec![d(0, "x"), d(-1, "a"), d(1, "abc"), d(-1, "dc"), d(0, "y")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "xa"), d(-1, "d"), d(1, "b"), d(0, "cy")], diffs); + + // Slide edit left. + diffs = vec![d(0, "a"), d(1, "ba"), d(0, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(1, "ab"), d(0, "ac")], diffs); + + // Slide edit right. + diffs = vec![d(0, "c"), d(1, "ab"), d(0, "a")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "ca"), d(1, "ba")], diffs); + + // Slide edit left recursive. + diffs = vec![d(0, "a"), d(-1, "b"), d(0, "c"), d(-1, "ac"), d(0, "x")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(-1, "abc"), d(0, "acx")], diffs); + + // Slide edit right recursive. + diffs = vec![d(0, "x"), d(-1, "ca"), d(0, "c"), d(-1, "b"), d(0, "a")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(0, "xca"), d(-1, "cba")], diffs); + + // Empty merge. + diffs = vec![d(-1, "b"), d(1, "ab"), d(0, "c")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(1, "a"), d(0, "bc")], diffs); + + // Empty equality. + diffs = vec![d(0, ""), d(1, "a"), d(0, "b")]; + dmp.diff_cleanup_merge(&mut diffs); + assert_eq!(vec![d(1, "a"), d(0, "b")], diffs); +} + +#[test] +fn upstream_diff_cleanup_semantic_lossless() { + let mut dmp = Dmp::new(); + // Slide diffs to match logical boundaries. + // Null case. + let mut diffs: Vec = vec![]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(Vec::::new(), diffs); + + // Blank lines. + diffs = vec![ + d(0, "AAA\r\n\r\nBBB"), + d(1, "\r\nDDD\r\n\r\nBBB"), + d(0, "\r\nEEE"), + ]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!( + vec![d(0, "AAA\r\n\r\n"), d(1, "BBB\r\nDDD\r\n\r\n"), d(0, "BBB\r\nEEE")], + diffs + ); + + // Line boundaries. + diffs = vec![d(0, "AAA\r\nBBB"), d(1, " DDD\r\nBBB"), d(0, " EEE")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(0, "AAA\r\n"), d(1, "BBB DDD\r\n"), d(0, "BBB EEE")], diffs); + + // Word boundaries. + diffs = vec![d(0, "The c"), d(1, "ow and the c"), d(0, "at.")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(0, "The "), d(1, "cow and the "), d(0, "cat.")], diffs); + + // Alphanumeric boundaries. + diffs = vec![d(0, "The-c"), d(1, "ow-and-the-c"), d(0, "at.")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(0, "The-"), d(1, "cow-and-the-"), d(0, "cat.")], diffs); + + // Hitting the start. + diffs = vec![d(0, "a"), d(-1, "a"), d(0, "ax")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(-1, "a"), d(0, "aax")], diffs); + + // Hitting the end. + diffs = vec![d(0, "xa"), d(-1, "a"), d(0, "a")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(0, "xaa"), d(-1, "a")], diffs); + + // Sentence boundaries. + diffs = vec![d(0, "The xxx. The "), d(1, "zzz. The "), d(0, "yyy.")]; + dmp.diff_cleanup_semantic_lossless(&mut diffs); + assert_eq!(vec![d(0, "The xxx."), d(1, " The zzz."), d(0, " The yyy.")], diffs); +} + +#[test] +fn upstream_diff_cleanup_semantic() { + let mut dmp = Dmp::new(); + // Cleanup semantically trivial equalities. + // Null case. + let mut diffs: Vec = vec![]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(Vec::::new(), diffs); + + // No elimination #1. + diffs = vec![d(-1, "ab"), d(1, "cd"), d(0, "12"), d(-1, "e")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "ab"), d(1, "cd"), d(0, "12"), d(-1, "e")], diffs); + + // No elimination #2. + diffs = vec![d(-1, "abc"), d(1, "ABC"), d(0, "1234"), d(-1, "wxyz")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "abc"), d(1, "ABC"), d(0, "1234"), d(-1, "wxyz")], diffs); + + // Simple elimination. + diffs = vec![d(-1, "a"), d(0, "b"), d(-1, "c")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "abc"), d(1, "b")], diffs); + + // Backpass elimination. + diffs = vec![d(-1, "ab"), d(0, "cd"), d(-1, "e"), d(0, "f"), d(1, "g")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "abcdef"), d(1, "cdfg")], diffs); + + // Multiple eliminations. + diffs = vec![ + d(1, "1"), + d(0, "A"), + d(-1, "B"), + d(1, "2"), + d(0, "_"), + d(1, "1"), + d(0, "A"), + d(-1, "B"), + d(1, "2"), + ]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "AB_AB"), d(1, "1A2_1A2")], diffs); + + // Word boundaries. + diffs = vec![d(0, "The c"), d(-1, "ow and the c"), d(0, "at.")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(0, "The "), d(-1, "cow and the "), d(0, "cat.")], diffs); + + // No overlap elimination. + diffs = vec![d(-1, "abcxx"), d(1, "xxdef")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "abcxx"), d(1, "xxdef")], diffs); + + // Overlap elimination. + diffs = vec![d(-1, "abcxxx"), d(1, "xxxdef")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(-1, "abc"), d(0, "xxx"), d(1, "def")], diffs); + + // Reverse overlap elimination. + diffs = vec![d(-1, "xxxabc"), d(1, "defxxx")]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!(vec![d(1, "def"), d(0, "xxx"), d(-1, "abc")], diffs); + + // Two overlap eliminations. + diffs = vec![ + d(-1, "abcd1212"), + d(1, "1212efghi"), + d(0, "----"), + d(-1, "A3"), + d(1, "3BC"), + ]; + dmp.diff_cleanup_semantic(&mut diffs); + assert_eq!( + vec![ + d(-1, "abcd"), + d(0, "1212"), + d(1, "efghi"), + d(0, "----"), + d(-1, "A"), + d(0, "3"), + d(1, "BC") + ], + diffs + ); +} + +#[test] +fn upstream_diff_cleanup_efficiency() { + let mut dmp = Dmp::new(); + // Cleanup operationally trivial equalities. + dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) + // Null case. + let mut diffs: Vec = vec![]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!(Vec::::new(), diffs); + + // No elimination. + diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!( + vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")], + diffs + ); + + // Four-edit elimination. + diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "xyz"), d(-1, "cd"), d(1, "34")]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!(vec![d(-1, "abxyzcd"), d(1, "12xyz34")], diffs); + + // Three-edit elimination. + diffs = vec![d(1, "12"), d(0, "x"), d(-1, "cd"), d(1, "34")]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!(vec![d(-1, "xcd"), d(1, "12x34")], diffs); + + // Backpass elimination. + diffs = vec![ + d(-1, "ab"), + d(1, "12"), + d(0, "xy"), + d(1, "34"), + d(0, "z"), + d(-1, "cd"), + d(1, "56"), + ]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!(vec![d(-1, "abxyzcd"), d(1, "12xy34z56")], diffs); + + // High cost elimination. + dmp.edit_cost = 5; + diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")]; + dmp.diff_cleanup_efficiency(&mut diffs); + assert_eq!(vec![d(-1, "abwxyzcd"), d(1, "12wxyz34")], diffs); + // JS restores the shared global dmp (Diff_EditCost = 4); moot with a + // per-test Dmp, and porting it verbatim trips unused_assignments. +} + +// MISSING API: diff_pretty_html not implemented in this crate. +// Upstream testDiffPrettyHtml: +// +// #[test] +// fn upstream_diff_pretty_html() { +// let mut dmp = Dmp::new(); +// // Pretty print. +// let mut diffs = vec![d(0, "a\n"), d(-1, "b"), d(1, "c&d")]; +// assert_eq!( +// "a¶
<B>b</B>c&d", +// dmp.diff_pretty_html(&mut diffs) +// ); +// } + +#[test] +fn upstream_diff_text() { + let mut dmp = Dmp::new(); + // Compute the source and destination texts. + let mut diffs = vec![ + d(0, "jump"), + d(-1, "s"), + d(1, "ed"), + d(0, " over "), + d(-1, "the"), + d(1, "a"), + d(0, " lazy"), + ]; + assert_eq!("jumps over the lazy", dmp.diff_text1(&mut diffs)); + + assert_eq!("jumped over a lazy", dmp.diff_text2(&mut diffs)); +} + +#[test] +fn upstream_diff_delta() { + let mut dmp = Dmp::new(); + // Convert a diff into delta string. + let mut diffs = vec![ + d(0, "jump"), + d(-1, "s"), + d(1, "ed"), + d(0, " over "), + d(-1, "the"), + d(1, "a"), + d(0, " lazy"), + d(1, "old dog"), + ]; + let mut text1 = dmp.diff_text1(&mut diffs); + assert_eq!("jumps over the lazy", text1); + + let mut delta = dmp.diff_todelta(&mut diffs); + assert_eq!("=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); + + // Convert delta string into a diff. + assert_eq!(diffs, dmp.diff_from_delta(&text1, &delta)); + + // The three "Generates error" cases are ported as the #[should_panic] + // tests immediately below this function. + + // Test deltas with special characters. + diffs = vec![ + d(0, "\u{0680} \x00 \t %"), + d(-1, "\u{0681} \x01 \n ^"), + d(1, "\u{0682} \x02 \\ |"), + ]; + text1 = dmp.diff_text1(&mut diffs); + assert_eq!("\u{0680} \x00 \t %\u{0681} \x01 \n ^", text1); + + delta = dmp.diff_todelta(&mut diffs); + assert_eq!("=7\t-7\t+%DA%82 %02 %5C %7C", delta); + + // Convert delta string into a diff. + assert_eq!(diffs, dmp.diff_from_delta(&text1, &delta)); + + // Verify pool of unchanged characters. + diffs = vec![d(1, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")]; + let text2 = dmp.diff_text2(&mut diffs); + assert_eq!("A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", text2); + + delta = dmp.diff_todelta(&mut diffs); + assert_eq!("+A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", delta); + + // Convert delta string into a diff. + assert_eq!(diffs, dmp.diff_from_delta("", &delta)); + + // 160 kb string. + let mut a = "abcdefghij".to_string(); + for _i in 0..14 { + let doubled = a.clone(); + a += &doubled; + } + diffs = vec![Diff::new(1, a.clone())]; + delta = dmp.diff_todelta(&mut diffs); + assert_eq!("+".to_string() + &a, delta); + + // Convert delta string into a diff. + assert_eq!(diffs, dmp.diff_from_delta("", &delta)); +} + +// JS: "Generates error (19 != 20)." — dmp.diff_fromDelta(text1 + 'x', delta) +#[test] +#[should_panic(expected = "Delta length (19) does not equal source text length (20)")] +fn upstream_diff_delta_error_text_too_long() { + let mut dmp = Dmp::new(); + let mut diffs = vec![ + d(0, "jump"), + d(-1, "s"), + d(1, "ed"), + d(0, " over "), + d(-1, "the"), + d(1, "a"), + d(0, " lazy"), + d(1, "old dog"), + ]; + let text1 = dmp.diff_text1(&mut diffs); + let delta = dmp.diff_todelta(&mut diffs); + dmp.diff_from_delta(&(text1 + "x"), &delta); +} + +// JS: "Generates error (19 != 18)." — dmp.diff_fromDelta(text1.substring(1), delta) +#[test] +#[should_panic(expected = "Delta length (19) larger than source text length (18)")] +fn upstream_diff_delta_error_text_too_short() { + let mut dmp = Dmp::new(); + let mut diffs = vec![ + d(0, "jump"), + d(-1, "s"), + d(1, "ed"), + d(0, " over "), + d(-1, "the"), + d(1, "a"), + d(0, " lazy"), + d(1, "old dog"), + ]; + let text1 = dmp.diff_text1(&mut diffs); + let delta = dmp.diff_todelta(&mut diffs); + dmp.diff_from_delta(&text1[1..], &delta); +} + +// JS: "Generates error (%c3%xy invalid Unicode)." — dmp.diff_fromDelta('', '+%c3%xy') +#[test] +#[should_panic(expected = "Illegal escape in diff_from_delta: %c3%xy")] +fn upstream_diff_delta_error_invalid_unicode() { + let mut dmp = Dmp::new(); + dmp.diff_from_delta("", "+%c3%xy"); +} + +#[test] +fn upstream_diff_x_index() { + let mut dmp = Dmp::new(); + // Translate a location in text1 to text2. + // Translation on equality. + assert_eq!( + 5, + dmp.diff_xindex(&vec![d(-1, "a"), d(1, "1234"), d(0, "xyz")], 2) + ); + + // Translation on deletion. + assert_eq!( + 1, + dmp.diff_xindex(&vec![d(0, "a"), d(-1, "1234"), d(0, "xyz")], 3) + ); +} + +#[test] +fn upstream_diff_levenshtein() { + let mut dmp = Dmp::new(); + // Levenshtein with trailing equality. + assert_eq!( + 4, + dmp.diff_levenshtein(&vec![d(-1, "abc"), d(1, "1234"), d(0, "xyz")]) + ); + // Levenshtein with leading equality. + assert_eq!( + 4, + dmp.diff_levenshtein(&vec![d(0, "xyz"), d(-1, "abc"), d(1, "1234")]) + ); + // Levenshtein with middle equality. + assert_eq!( + 7, + dmp.diff_levenshtein(&vec![d(-1, "abc"), d(0, "xyz"), d(1, "1234")]) + ); +} + +#[test] +fn upstream_diff_bisect() { + let mut dmp = Dmp::new(); + // Normal. + let a = chars("cat"); + let b = chars("map"); + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + // JS passes deadline = Number.MAX_VALUE; the crate's diff_bisect derives + // its deadline from diff_timeout, so None is the no-deadline equivalent. + dmp.diff_timeout = None; + assert_eq!( + vec![d(-1, "c"), d(1, "m"), d(0, "a"), d(-1, "t"), d(1, "p")], + dmp.diff_bisect(&a, &b) + ); + + // Timeout. + // JS passes deadline = 0 (already expired); Some(0.0) expires immediately. + dmp.diff_timeout = Some(0.0); + assert_eq!(vec![d(-1, "cat"), d(1, "map")], dmp.diff_bisect(&a, &b)); +} + +#[test] +fn upstream_diff_main() { + let mut dmp = Dmp::new(); + // Perform a trivial diff. + // Null case. + assert_eq!(Vec::::new(), dmp.diff_main("", "", false)); + + // Equality. + assert_eq!(vec![d(0, "abc")], dmp.diff_main("abc", "abc", false)); + + // Simple insertion. + assert_eq!( + vec![d(0, "ab"), d(1, "123"), d(0, "c")], + dmp.diff_main("abc", "ab123c", false) + ); + + // Simple deletion. + assert_eq!( + vec![d(0, "a"), d(-1, "123"), d(0, "bc")], + dmp.diff_main("a123bc", "abc", false) + ); + + // Two insertions. + assert_eq!( + vec![d(0, "a"), d(1, "123"), d(0, "b"), d(1, "456"), d(0, "c")], + dmp.diff_main("abc", "a123b456c", false) + ); + + // Two deletions. + assert_eq!( + vec![d(0, "a"), d(-1, "123"), d(0, "b"), d(-1, "456"), d(0, "c")], + dmp.diff_main("a123b456c", "abc", false) + ); + + // Perform a real diff. + // Switch off the timeout. + dmp.diff_timeout = None; // JS Diff_Timeout = 0 maps to None (no deadline). + // Simple cases. + assert_eq!(vec![d(-1, "a"), d(1, "b")], dmp.diff_main("a", "b", false)); + + assert_eq!( + vec![ + d(-1, "Apple"), + d(1, "Banana"), + d(0, "s are a"), + d(1, "lso"), + d(0, " fruit.") + ], + dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", false) + ); + + assert_eq!( + vec![d(-1, "a"), d(1, "\u{0680}"), d(0, "x"), d(-1, "\t"), d(1, "\0")], + dmp.diff_main("ax\t", "\u{0680}x\0", false) + ); + + // Overlaps. + assert_eq!( + vec![d(-1, "1"), d(0, "a"), d(-1, "y"), d(0, "b"), d(-1, "2"), d(1, "xab")], + dmp.diff_main("1ayb2", "abxab", false) + ); + + assert_eq!( + vec![d(1, "xaxcx"), d(0, "abc"), d(-1, "y")], + dmp.diff_main("abcy", "xaxcxabc", false) + ); + + assert_eq!( + vec![ + d(-1, "ABCD"), + d(0, "a"), + d(-1, "="), + d(1, "-"), + d(0, "bcd"), + d(-1, "="), + d(1, "-"), + d(0, "efghijklmnopqrs"), + d(-1, "EFGHIJKLMNOefg") + ], + dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false) + ); + + // Large equality. + assert_eq!( + vec![ + d(1, " "), + d(0, "a"), + d(1, "nd"), + d(0, " [[Pennsylvania]]"), + d(-1, " and [[New") + ], + dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false) + ); + + // Timeout. + // The JS wall-clock assertions (elapsed >= Diff_Timeout and elapsed < + // 2 * Diff_Timeout) are non-deterministic and are not ported; the setup + // and the timed diff_main call are kept verbatim. + dmp.diff_timeout = Some(0.1); // 100ms + let mut a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n".to_string(); + let mut b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n".to_string(); + // Increase the text lengths by 1024 times to ensure a timeout. + for _i in 0..10 { + let doubled_a = a.clone(); + a += &doubled_a; + let doubled_b = b.clone(); + b += &doubled_b; + } + dmp.diff_main(&a, &b, true); + dmp.diff_timeout = None; // JS Diff_Timeout = 0 maps to None (no deadline). + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + // Simple line-mode. + let a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + let b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; + assert_eq!(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Single line-mode. + let a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + let b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; + assert_eq!(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Overlap line-mode. + let a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + let b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; + let texts_linemode = diff_rebuildtexts(&dmp.diff_main(a, b, true)); + let texts_textmode = diff_rebuildtexts(&dmp.diff_main(a, b, false)); + assert_eq!(texts_textmode, texts_linemode); + + // Test null inputs: skipped, untypable in Rust (see file header). +} + +// MATCH TEST FUNCTIONS + +#[test] +fn upstream_match_alphabet() { + let mut dmp = Dmp::new(); + // Initialise the bitmasks for Bitap. + // Unique. + let mut expected: HashMap = HashMap::new(); + expected.insert('a', 4); + expected.insert('b', 2); + expected.insert('c', 1); + assert_eq!(expected, dmp.match_alphabet(&chars("abc"))); + + // Duplicates. + let mut expected: HashMap = HashMap::new(); + expected.insert('a', 37); + expected.insert('b', 18); + expected.insert('c', 8); + assert_eq!(expected, dmp.match_alphabet(&chars("abcaba"))); +} + +#[test] +fn upstream_match_bitap() { + let mut dmp = Dmp::new(); + // Bitap algorithm. + dmp.match_distance = 100; + dmp.match_threshold = 0.5; + // Exact matches. + assert_eq!(5, dmp.match_bitap(&chars("abcdefghijk"), &chars("fgh"), 5)); + + assert_eq!(5, dmp.match_bitap(&chars("abcdefghijk"), &chars("fgh"), 0)); + + // Fuzzy matches. + assert_eq!(4, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxhi"), 0)); + + assert_eq!(2, dmp.match_bitap(&chars("abcdefghijk"), &chars("cdefxyhijk"), 5)); + + assert_eq!(-1, dmp.match_bitap(&chars("abcdefghijk"), &chars("bxy"), 1)); + + // Overflow. + assert_eq!(2, dmp.match_bitap(&chars("123456789xx0"), &chars("3456789x0"), 2)); + + // Threshold test. + dmp.match_threshold = 0.4; + assert_eq!(4, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1)); + + dmp.match_threshold = 0.3; + assert_eq!(-1, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1)); + + dmp.match_threshold = 0.0; + assert_eq!(1, dmp.match_bitap(&chars("abcdefghijk"), &chars("bcdef"), 1)); + dmp.match_threshold = 0.5; + + // Multiple select. + assert_eq!(0, dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 3)); + + assert_eq!(8, dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 5)); + + // Distance test. + dmp.match_distance = 10; // Strict location. + assert_eq!( + -1, + dmp.match_bitap(&chars("abcdefghijklmnopqrstuvwxyz"), &chars("abcdefg"), 24) + ); + + assert_eq!( + 0, + dmp.match_bitap(&chars("abcdefghijklmnopqrstuvwxyz"), &chars("abcdxxefg"), 1) + ); + + dmp.match_distance = 1000; // Loose location. + assert_eq!( + 0, + dmp.match_bitap(&chars("abcdefghijklmnopqrstuvwxyz"), &chars("abcdefg"), 24) + ); +} + +#[test] +fn upstream_match_main() { + let mut dmp = Dmp::new(); + // Full match. + // Shortcut matches. + assert_eq!(0, dmp.match_main("abcdef", "abcdef", 1000)); + + assert_eq!(-1, dmp.match_main("", "abcdef", 1)); + + assert_eq!(3, dmp.match_main("abcdef", "", 3)); + + assert_eq!(3, dmp.match_main("abcdef", "de", 3)); + + // Beyond end match. + assert_eq!(3, dmp.match_main("abcdef", "defy", 4)); + + // Oversized pattern. + assert_eq!(0, dmp.match_main("abcdef", "abcdefy", 0)); + + // Complex match. + assert_eq!( + 4, + dmp.match_main("I am the very model of a modern major general.", " that berry ", 5) + ); + + // Test null inputs: skipped, untypable in Rust (see file header). +} + +// PATCH TEST FUNCTIONS + +#[test] +fn upstream_patch_obj() { + // Patch Object. + let mut patch = Patch::new(vec![], 0, 0, 0, 0); + patch.start1 = 20; + patch.start2 = 21; + patch.length1 = 18; + patch.length2 = 17; + patch.diffs = vec![ + d(0, "jump"), + d(-1, "s"), + d(1, "ed"), + d(0, " over "), + d(-1, "the"), + d(1, "a"), + d(0, "\nlaz"), + ]; + assert_eq!( + "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n", + patch.to_string() + ); +} + +#[test] +fn upstream_patch_from_text() { + let mut dmp = Dmp::new(); + // JS passes the hoisted-but-unassigned `strp` (undefined, falsy), which + // patch_fromText treats as empty input; the Rust equivalent is "". + assert_eq!(Vec::::new(), dmp.patch_from_text("".to_string())); + + let strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + assert_eq!(strp, dmp.patch_from_text(strp.to_string())[0].to_string()); + + assert_eq!( + "@@ -1 +1 @@\n-a\n+b\n", + dmp.patch_from_text("@@ -1 +1 @@\n-a\n+b\n".to_string())[0].to_string() + ); + + assert_eq!( + "@@ -1,3 +0,0 @@\n-abc\n", + dmp.patch_from_text("@@ -1,3 +0,0 @@\n-abc\n".to_string())[0].to_string() + ); + + assert_eq!( + "@@ -0,0 +1,3 @@\n+abc\n", + dmp.patch_from_text("@@ -0,0 +1,3 @@\n+abc\n".to_string())[0].to_string() + ); + + // The "Generates error" case is ported as the #[should_panic] test below. +} + +// JS: "Generates error." — dmp.patch_fromText('Bad\nPatch\n') +#[test] +#[should_panic(expected = "Invalid patch string: Bad")] +fn upstream_patch_from_text_error_bad_patch() { + let mut dmp = Dmp::new(); + dmp.patch_from_text("Bad\nPatch\n".to_string()); +} + +#[test] +fn upstream_patch_to_text() { + let mut dmp = Dmp::new(); + let strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + let mut patches = dmp.patch_from_text(strp.to_string()); + assert_eq!(strp, dmp.patch_to_text(&mut patches)); + + let strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; + let mut patches = dmp.patch_from_text(strp.to_string()); + assert_eq!(strp, dmp.patch_to_text(&mut patches)); +} + +#[test] +fn upstream_patch_add_context() { + let mut dmp = Dmp::new(); + dmp.patch_margin = 4; + let mut p = dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); + dmp.patch_add_context(&mut p, &mut chars("The quick brown fox jumps over the lazy dog.")); + assert_eq!("@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", p.to_string()); + + // Same, but not enough trailing context. + let mut p = dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); + dmp.patch_add_context(&mut p, &mut chars("The quick brown fox jumps.")); + assert_eq!("@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.to_string()); + + // Same, but not enough leading context. + let mut p = dmp.patch_from_text("@@ -3 +3,2 @@\n-e\n+at\n".to_string())[0].clone(); + dmp.patch_add_context(&mut p, &mut chars("The quick brown fox jumps.")); + assert_eq!("@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", p.to_string()); + + // Same, but with ambiguity. + let mut p = dmp.patch_from_text("@@ -3 +3,2 @@\n-e\n+at\n".to_string())[0].clone(); + dmp.patch_add_context( + &mut p, + &mut chars("The quick brown fox jumps. The quick brown fox crashes."), + ); + assert_eq!( + "@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", + p.to_string() + ); +} + +#[test] +fn upstream_patch_make() { + let mut dmp = Dmp::new(); + // patch_make runs diff_cleanup_efficiency internally, so the JS + // constructor default matters here. + dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) + // Null case. + let mut patches = dmp.patch_make1("", ""); + assert_eq!("", dmp.patch_to_text(&mut patches)); + + let text1 = "The quick brown fox jumps over the lazy dog."; + let text2 = "That quick brown fox jumped over a lazy dog."; + // Text2+Text1 inputs. + let expected_patch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + let mut patches = dmp.patch_make1(text2, text1); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Text1+Text2 inputs. + let expected_patch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + let mut patches = dmp.patch_make1(text1, text2); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Diff input. + let mut diffs = dmp.diff_main(text1, text2, false); + let mut patches = dmp.patch_make2(&mut diffs); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Text1+Diff inputs. + let mut patches = dmp.patch_make4(text1, &mut diffs); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Text1+Text2+Diff inputs (deprecated). + let mut patches = dmp.patch_make3(text1, text2, &mut diffs); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Character encoding. + let mut patches = dmp.patch_make1("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?"); + assert_eq!( + "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", + dmp.patch_to_text(&mut patches) + ); + + // Character decoding. + let diffs = vec![d(-1, "`1234567890-=[]\\;',./"), d(1, "~!@#$%^&*()_+{}|:\"<>?")]; + assert_eq!( + diffs, + dmp.patch_from_text( + "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n" + .to_string() + )[0] + .diffs + ); + + // Long string with repeats. + let mut text1 = String::new(); + for _x in 0..100 { + text1 += "abcdef"; + } + let text2 = text1.clone() + "123"; + let expected_patch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; + let mut patches = dmp.patch_make1(&text1, &text2); + assert_eq!(expected_patch, dmp.patch_to_text(&mut patches)); + + // Test null inputs: skipped, untypable in Rust (see file header). +} + +#[test] +fn upstream_patch_split_max() { + // Assumes that dmp.Match_MaxBits is 32. (Crate default is 32.) + let mut dmp = Dmp::new(); + dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) + let mut patches = dmp.patch_make1( + "abcdefghijklmnopqrstuvwxyz01234567890", + "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0", + ); + dmp.patch_splitmax(&mut patches); + assert_eq!( + "@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", + dmp.patch_to_text(&mut patches) + ); + + let mut patches = dmp.patch_make1( + "abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", + "abcdefuvwxyz", + ); + let old_to_text = dmp.patch_to_text(&mut patches); + dmp.patch_splitmax(&mut patches); + assert_eq!(old_to_text, dmp.patch_to_text(&mut patches)); + + let mut patches = dmp.patch_make1( + "1234567890123456789012345678901234567890123456789012345678901234567890", + "abc", + ); + dmp.patch_splitmax(&mut patches); + assert_eq!( + "@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", + dmp.patch_to_text(&mut patches) + ); + + let mut patches = dmp.patch_make1( + "abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", + "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1", + ); + dmp.patch_splitmax(&mut patches); + assert_eq!( + "@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", + dmp.patch_to_text(&mut patches) + ); +} + +#[test] +fn upstream_patch_add_padding() { + let mut dmp = Dmp::new(); + dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) + // Both edges full. + let mut patches = dmp.patch_make1("", "test"); + assert_eq!("@@ -0,0 +1,4 @@\n+test\n", dmp.patch_to_text(&mut patches)); + dmp.patch_add_padding(&mut patches); + assert_eq!( + "@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", + dmp.patch_to_text(&mut patches) + ); + + // Both edges partial. + let mut patches = dmp.patch_make1("XY", "XtestY"); + assert_eq!("@@ -1,2 +1,6 @@\n X\n+test\n Y\n", dmp.patch_to_text(&mut patches)); + dmp.patch_add_padding(&mut patches); + assert_eq!( + "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", + dmp.patch_to_text(&mut patches) + ); + + // Both edges none. + let mut patches = dmp.patch_make1("XXXXYYYY", "XXXXtestYYYY"); + assert_eq!("@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_to_text(&mut patches)); + dmp.patch_add_padding(&mut patches); + assert_eq!("@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_to_text(&mut patches)); +} + +#[test] +fn upstream_patch_apply() { + let mut dmp = Dmp::new(); + dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) + dmp.match_distance = 1000; + dmp.match_threshold = 0.5; + dmp.patch_delete_threshold = 0.5; + // Null case. + let mut patches = dmp.patch_make1("", ""); + let results = dmp.patch_apply(&mut patches, "Hello world."); + assert_eq!((chars("Hello world."), vec![]), results); + + // Exact match. + let mut patches = dmp.patch_make1( + "The quick brown fox jumps over the lazy dog.", + "That quick brown fox jumped over a lazy dog.", + ); + let results = dmp.patch_apply(&mut patches, "The quick brown fox jumps over the lazy dog."); + assert_eq!( + (chars("That quick brown fox jumped over a lazy dog."), vec![true, true]), + results + ); + + // Partial match. + let results = dmp.patch_apply(&mut patches, "The quick red rabbit jumps over the tired tiger."); + assert_eq!( + (chars("That quick red rabbit jumped over a tired tiger."), vec![true, true]), + results + ); + + // Failed match. + let results = dmp.patch_apply(&mut patches, "I am the very model of a modern major general."); + assert_eq!( + (chars("I am the very model of a modern major general."), vec![false, false]), + results + ); + + // Big delete, small change. + let mut patches = dmp.patch_make1( + "x1234567890123456789012345678901234567890123456789012345678901234567890y", + "xabcy", + ); + let results = dmp.patch_apply( + &mut patches, + "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y", + ); + assert_eq!((chars("xabcy"), vec![true, true]), results); + + // Big delete, big change 1. + let mut patches = dmp.patch_make1( + "x1234567890123456789012345678901234567890123456789012345678901234567890y", + "xabcy", + ); + let results = dmp.patch_apply( + &mut patches, + "x12345678901234567890---------------++++++++++---------------12345678901234567890y", + ); + assert_eq!( + ( + chars("xabc12345678901234567890---------------++++++++++---------------12345678901234567890y"), + vec![false, true] + ), + results + ); + + // Big delete, big change 2. + dmp.patch_delete_threshold = 0.6; + let mut patches = dmp.patch_make1( + "x1234567890123456789012345678901234567890123456789012345678901234567890y", + "xabcy", + ); + let results = dmp.patch_apply( + &mut patches, + "x12345678901234567890---------------++++++++++---------------12345678901234567890y", + ); + assert_eq!((chars("xabcy"), vec![true, true]), results); + dmp.patch_delete_threshold = 0.5; + + // Compensate for failed patch. + dmp.match_threshold = 0.0; + dmp.match_distance = 0; + let mut patches = dmp.patch_make1( + "abcdefghijklmnopqrstuvwxyz--------------------1234567890", + "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890", + ); + let results = dmp.patch_apply( + &mut patches, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890", + ); + assert_eq!( + ( + chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890"), + vec![false, true] + ), + results + ); + dmp.match_threshold = 0.5; + dmp.match_distance = 1000; + + // No side effects. + let mut patches = dmp.patch_make1("", "test"); + let patchstr = dmp.patch_to_text(&mut patches); + dmp.patch_apply(&mut patches, ""); + assert_eq!(patchstr, dmp.patch_to_text(&mut patches)); + + // No side effects with major delete. + let mut patches = dmp.patch_make1("The quick brown fox jumps over the lazy dog.", "Woof"); + let patchstr = dmp.patch_to_text(&mut patches); + dmp.patch_apply(&mut patches, "The quick brown fox jumps over the lazy dog."); + assert_eq!(patchstr, dmp.patch_to_text(&mut patches)); + + // Edge exact match. + let mut patches = dmp.patch_make1("", "test"); + let results = dmp.patch_apply(&mut patches, ""); + assert_eq!((chars("test"), vec![true]), results); + + // Near edge exact match. + let mut patches = dmp.patch_make1("XY", "XtestY"); + let results = dmp.patch_apply(&mut patches, "XY"); + assert_eq!((chars("XtestY"), vec![true]), results); + + // Edge partial match. + let mut patches = dmp.patch_make1("y", "y123"); + let results = dmp.patch_apply(&mut patches, "x"); + assert_eq!((chars("x123"), vec![true]), results); +} From 996b117692ff7fbdbdf4f68c95483479b522767c Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 16:53:35 +0530 Subject: [PATCH 04/11] use engine::half_match only if deadline is set and length is greater than 1000 --- .gitignore | 1 + crates/dmp/src/diff.rs | 6 +- crates/dmp/src/types.rs | 5 ++ crates/dmp/tests/characterization.rs | 85 ++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index eb5a316..c507849 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +.idea diff --git a/crates/dmp/src/diff.rs b/crates/dmp/src/diff.rs index 70c7002..ec16858 100644 --- a/crates/dmp/src/diff.rs +++ b/crates/dmp/src/diff.rs @@ -441,9 +441,9 @@ fn compute( } } - // Check to see if the problem can be split in two (only when a deadline is - // set: half-match trades optimality for speed). - if deadline.is_some() { + if deadline.is_some() + && (dmp.half_match_min_len == 0 || old.len() + new.len() >= dmp.half_match_min_len) + { if let Some(hm) = engine::half_match(old, new) { // A half-match was found, send both pairs off for separate processing. let mid_common: Vec = T::to_tokens(&old[hm.old_a..hm.old_a + hm.common]); diff --git a/crates/dmp/src/types.rs b/crates/dmp/src/types.rs index 8142f9b..c77379d 100644 --- a/crates/dmp/src/types.rs +++ b/crates/dmp/src/types.rs @@ -26,6 +26,10 @@ pub enum Segmentation { pub struct Dmp { // Number of seconds to map a diff before giving up (None for infinity). pub diff_timeout: Option, + /// Minimum combined input length (in tokens of the active granularity) + /// before `diff_main` may take the half-match speedup when a deadline is + /// set. + pub half_match_min_len: usize, // Cost of an empty edit operation in terms of edit characters. pub edit_cost: i32, /*How far to search for a match (0 = exact location, 1000+ = broad match). @@ -258,6 +262,7 @@ impl Dmp { // it will give a new dmp object. Dmp { diff_timeout: Some(1.0), + half_match_min_len: 10_000, patch_delete_threshold: 0.5, edit_cost: 0, match_distance: 1000, diff --git a/crates/dmp/tests/characterization.rs b/crates/dmp/tests/characterization.rs index 208ec38..e8616f4 100644 --- a/crates/dmp/tests/characterization.rs +++ b/crates/dmp/tests/characterization.rs @@ -63,3 +63,88 @@ fn patch_make_absorbs_margin_sized_equality() { fn edit_cost_defaults_to_zero() { assert_eq!(Dmp::new().edit_cost, 0); } + +/// The half-match speedup is COST-aware, not just consent-aware — a deliberate +/// extension over upstream, where any non-zero `Diff_Timeout` enables half-match +/// even on inputs the optimal search solves in microseconds. Here half-match +/// additionally requires the combined input length to reach +/// `half_match_min_len` (tokens); below it, a deadline caller still gets the +/// OPTIMAL diff. `half_match_min_len = 0` restores upstream semantics. +#[test] +fn small_input_with_deadline_still_gets_the_optimal_diff() { + // The repeated-run shift, word-tokenized (each word packs into one char, so + // half-match splits on the repeated middle and produces substitution + // confetti on both flanks); the optimal diff is one deletion at the front + // and one insertion at the back. Char-level diff_main of the same strings + // happens to be optimal either way — the token path is the honest fire. + let word_diff = |dmp: &mut Dmp| -> Vec<(i32, String)> { + let (chars1, chars2, words) = + dmp.diff_words_tochars(&"word1 noop noop noop".to_string(), &"noop noop noop word1".to_string()); + let mut diffs = dmp.diff_main(&chars1, &chars2, false); + dmp.diff_chars_tolines(&mut diffs, &words); + diffs.into_iter().map(|d| (d.operation, d.text)).collect() + }; + let mut optimal = Dmp { + diff_timeout: None, + ..Dmp::new() + }; + let want = word_diff(&mut optimal); + assert_eq!( + want, + vec![ + (-1, "word1 ".to_string()), + (0, "noop noop noop".to_string()), + (1, " word1".to_string()), + ], + "sanity: the optimal shape this test defends" + ); + let mut timed = Dmp::new(); // diff_timeout Some(1.0), default half_match_min_len + assert_eq!(word_diff(&mut timed), want); +} + +/// Boundary pair for `half_match_min_len`, plus the `0` = upstream-semantics +/// escape hatch. The word-packed inputs are 7 + 7 = 14 tokens: at a threshold +/// of exactly 14 half-match is allowed (the upstream confetti split); at 15 the +/// optimal path runs; at 0 upstream behavior applies regardless of size. +#[test] +fn half_match_min_len_boundary_and_upstream_escape_hatch() { + let word_diff = |dmp: &mut Dmp| -> Vec<(i32, String)> { + let (chars1, chars2, words) = dmp.diff_words_tochars( + &"word1 noop noop noop".to_string(), + &"noop noop noop word1".to_string(), + ); + let mut diffs = dmp.diff_main(&chars1, &chars2, false); + dmp.diff_chars_tolines(&mut diffs, &words); + diffs.into_iter().map(|d| (d.operation, d.text)).collect() + }; + let optimal = vec![ + (-1, "word1 ".to_string()), + (0, "noop noop noop".to_string()), + (1, " word1".to_string()), + ]; + let half_match_split = vec![ + (-1, "word1".to_string()), + (1, "noop".to_string()), + (0, " noop noop ".to_string()), + (-1, "noop".to_string()), + (1, "word1".to_string()), + ]; + + let mut at = Dmp { + half_match_min_len: 14, + ..Dmp::new() + }; + assert_eq!(word_diff(&mut at), half_match_split, "at the threshold"); + + let mut below = Dmp { + half_match_min_len: 15, + ..Dmp::new() + }; + assert_eq!(word_diff(&mut below), optimal, "one below the threshold"); + + let mut upstream = Dmp { + half_match_min_len: 0, + ..Dmp::new() + }; + assert_eq!(word_diff(&mut upstream), half_match_split, "0 = upstream"); +} From 6d1e46319a04a99d2dbbd7a90a907dba7ac1d4fd Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 18:03:54 +0530 Subject: [PATCH 05/11] remove half_match_min_len --- crates/dmp/src/diff.rs | 4 +- crates/dmp/src/types.rs | 5 - crates/dmp/tests/characterization.rs | 84 -------- crates/dmp/tests/upstream_js_suite.rs | 292 +++++++++++++++++++++----- 4 files changed, 237 insertions(+), 148 deletions(-) diff --git a/crates/dmp/src/diff.rs b/crates/dmp/src/diff.rs index ec16858..7938ad6 100644 --- a/crates/dmp/src/diff.rs +++ b/crates/dmp/src/diff.rs @@ -441,9 +441,7 @@ fn compute( } } - if deadline.is_some() - && (dmp.half_match_min_len == 0 || old.len() + new.len() >= dmp.half_match_min_len) - { + if deadline.is_some() { if let Some(hm) = engine::half_match(old, new) { // A half-match was found, send both pairs off for separate processing. let mid_common: Vec = T::to_tokens(&old[hm.old_a..hm.old_a + hm.common]); diff --git a/crates/dmp/src/types.rs b/crates/dmp/src/types.rs index c77379d..8142f9b 100644 --- a/crates/dmp/src/types.rs +++ b/crates/dmp/src/types.rs @@ -26,10 +26,6 @@ pub enum Segmentation { pub struct Dmp { // Number of seconds to map a diff before giving up (None for infinity). pub diff_timeout: Option, - /// Minimum combined input length (in tokens of the active granularity) - /// before `diff_main` may take the half-match speedup when a deadline is - /// set. - pub half_match_min_len: usize, // Cost of an empty edit operation in terms of edit characters. pub edit_cost: i32, /*How far to search for a match (0 = exact location, 1000+ = broad match). @@ -262,7 +258,6 @@ impl Dmp { // it will give a new dmp object. Dmp { diff_timeout: Some(1.0), - half_match_min_len: 10_000, patch_delete_threshold: 0.5, edit_cost: 0, match_distance: 1000, diff --git a/crates/dmp/tests/characterization.rs b/crates/dmp/tests/characterization.rs index e8616f4..f851a95 100644 --- a/crates/dmp/tests/characterization.rs +++ b/crates/dmp/tests/characterization.rs @@ -64,87 +64,3 @@ fn edit_cost_defaults_to_zero() { assert_eq!(Dmp::new().edit_cost, 0); } -/// The half-match speedup is COST-aware, not just consent-aware — a deliberate -/// extension over upstream, where any non-zero `Diff_Timeout` enables half-match -/// even on inputs the optimal search solves in microseconds. Here half-match -/// additionally requires the combined input length to reach -/// `half_match_min_len` (tokens); below it, a deadline caller still gets the -/// OPTIMAL diff. `half_match_min_len = 0` restores upstream semantics. -#[test] -fn small_input_with_deadline_still_gets_the_optimal_diff() { - // The repeated-run shift, word-tokenized (each word packs into one char, so - // half-match splits on the repeated middle and produces substitution - // confetti on both flanks); the optimal diff is one deletion at the front - // and one insertion at the back. Char-level diff_main of the same strings - // happens to be optimal either way — the token path is the honest fire. - let word_diff = |dmp: &mut Dmp| -> Vec<(i32, String)> { - let (chars1, chars2, words) = - dmp.diff_words_tochars(&"word1 noop noop noop".to_string(), &"noop noop noop word1".to_string()); - let mut diffs = dmp.diff_main(&chars1, &chars2, false); - dmp.diff_chars_tolines(&mut diffs, &words); - diffs.into_iter().map(|d| (d.operation, d.text)).collect() - }; - let mut optimal = Dmp { - diff_timeout: None, - ..Dmp::new() - }; - let want = word_diff(&mut optimal); - assert_eq!( - want, - vec![ - (-1, "word1 ".to_string()), - (0, "noop noop noop".to_string()), - (1, " word1".to_string()), - ], - "sanity: the optimal shape this test defends" - ); - let mut timed = Dmp::new(); // diff_timeout Some(1.0), default half_match_min_len - assert_eq!(word_diff(&mut timed), want); -} - -/// Boundary pair for `half_match_min_len`, plus the `0` = upstream-semantics -/// escape hatch. The word-packed inputs are 7 + 7 = 14 tokens: at a threshold -/// of exactly 14 half-match is allowed (the upstream confetti split); at 15 the -/// optimal path runs; at 0 upstream behavior applies regardless of size. -#[test] -fn half_match_min_len_boundary_and_upstream_escape_hatch() { - let word_diff = |dmp: &mut Dmp| -> Vec<(i32, String)> { - let (chars1, chars2, words) = dmp.diff_words_tochars( - &"word1 noop noop noop".to_string(), - &"noop noop noop word1".to_string(), - ); - let mut diffs = dmp.diff_main(&chars1, &chars2, false); - dmp.diff_chars_tolines(&mut diffs, &words); - diffs.into_iter().map(|d| (d.operation, d.text)).collect() - }; - let optimal = vec![ - (-1, "word1 ".to_string()), - (0, "noop noop noop".to_string()), - (1, " word1".to_string()), - ]; - let half_match_split = vec![ - (-1, "word1".to_string()), - (1, "noop".to_string()), - (0, " noop noop ".to_string()), - (-1, "noop".to_string()), - (1, "word1".to_string()), - ]; - - let mut at = Dmp { - half_match_min_len: 14, - ..Dmp::new() - }; - assert_eq!(word_diff(&mut at), half_match_split, "at the threshold"); - - let mut below = Dmp { - half_match_min_len: 15, - ..Dmp::new() - }; - assert_eq!(word_diff(&mut below), optimal, "one below the threshold"); - - let mut upstream = Dmp { - half_match_min_len: 0, - ..Dmp::new() - }; - assert_eq!(word_diff(&mut upstream), half_match_split, "0 = upstream"); -} diff --git a/crates/dmp/tests/upstream_js_suite.rs b/crates/dmp/tests/upstream_js_suite.rs index 6f34bf4..e4d9e45 100644 --- a/crates/dmp/tests/upstream_js_suite.rs +++ b/crates/dmp/tests/upstream_js_suite.rs @@ -51,7 +51,10 @@ fn upstream_diff_common_prefix() { assert_eq!(0, dmp.diff_common_prefix(&chars("abc"), &chars("xyz"))); // Non-null case. - assert_eq!(4, dmp.diff_common_prefix(&chars("1234abcdef"), &chars("1234xyz"))); + assert_eq!( + 4, + dmp.diff_common_prefix(&chars("1234abcdef"), &chars("1234xyz")) + ); // Whole case. assert_eq!(4, dmp.diff_common_prefix(&chars("1234"), &chars("1234xyz"))); @@ -64,7 +67,10 @@ fn upstream_diff_common_suffix() { assert_eq!(0, dmp.diff_common_suffix(&chars("abc"), &chars("xyz"))); // Non-null case. - assert_eq!(4, dmp.diff_common_suffix(&chars("abcdef1234"), &chars("xyz1234"))); + assert_eq!( + 4, + dmp.diff_common_suffix(&chars("abcdef1234"), &chars("xyz1234")) + ); // Whole case. assert_eq!(4, dmp.diff_common_suffix(&chars("1234"), &chars("xyz1234"))); @@ -83,12 +89,18 @@ fn upstream_diff_common_overlap() { assert_eq!(0, dmp.diff_common_overlap(&chars("123456"), &chars("abcd"))); // Overlap. - assert_eq!(3, dmp.diff_common_overlap(&chars("123456xxx"), &chars("xxxabcd"))); + assert_eq!( + 3, + dmp.diff_common_overlap(&chars("123456xxx"), &chars("xxxabcd")) + ); // Unicode. // Some overly clever languages (C#) may treat ligatures as equal to their // component letters. E.g. U+FB01 == 'fi' - assert_eq!(0, dmp.diff_common_overlap(&chars("fi"), &chars("\u{fb01}i"))); + assert_eq!( + 0, + dmp.diff_common_overlap(&chars("fi"), &chars("\u{fb01}i")) + ); } #[test] @@ -98,7 +110,10 @@ fn upstream_diff_half_match() { // JS returns null for "no half-match"; the crate returns an empty Vec. let no_match: Vec = vec![]; // No match. - assert_eq!(no_match, dmp.diff_half_match(&chars("1234567890"), &chars("abcdef"))); + assert_eq!( + no_match, + dmp.diff_half_match(&chars("1234567890"), &chars("abcdef")) + ); assert_eq!(no_match, dmp.diff_half_match(&chars("12345"), &chars("23"))); @@ -126,17 +141,26 @@ fn upstream_diff_half_match() { // Multiple Matches. assert_eq!( svec(&["12123", "123121", "a", "z", "1234123451234"]), - dmp.diff_half_match(&chars("121231234123451234123121"), &chars("a1234123451234z")) + dmp.diff_half_match( + &chars("121231234123451234123121"), + &chars("a1234123451234z") + ) ); assert_eq!( svec(&["", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="]), - dmp.diff_half_match(&chars("x-=-=-=-=-=-=-=-=-=-=-=-="), &chars("xx-=-=-=-=-=-=-=")) + dmp.diff_half_match( + &chars("x-=-=-=-=-=-=-=-=-=-=-=-="), + &chars("xx-=-=-=-=-=-=-=") + ) ); assert_eq!( svec(&["-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"]), - dmp.diff_half_match(&chars("-=-=-=-=-=-=-=-=-=-=-=-=y"), &chars("-=-=-=-=-=-=-=yy")) + dmp.diff_half_match( + &chars("-=-=-=-=-=-=-=-=-=-=-=-=y"), + &chars("-=-=-=-=-=-=-=yy") + ) ); // Non-optimal halfmatch. @@ -164,7 +188,10 @@ fn upstream_diff_lines_to_chars() { "\x02\x01\x02".to_string(), svec(&["", "alpha\n", "beta\n"]) ), - dmp.diff_lines_tochars(&chars("alpha\nbeta\nalpha\n"), &chars("beta\nalpha\nbeta\n")) + dmp.diff_lines_tochars( + &chars("alpha\nbeta\nalpha\n"), + &chars("beta\nalpha\nbeta\n") + ) ); assert_eq!( @@ -177,7 +204,11 @@ fn upstream_diff_lines_to_chars() { ); assert_eq!( - ("\x01".to_string(), "\x02".to_string(), svec(&["", "a", "b"])), + ( + "\x01".to_string(), + "\x02".to_string(), + svec(&["", "a", "b"]) + ), dmp.diff_lines_tochars(&chars("a"), &chars("b")) ); @@ -206,7 +237,10 @@ fn upstream_diff_chars_to_lines() { // Convert chars up to lines. let mut diffs = vec![d(0, "\x01\x02\x01"), d(1, "\x02\x01\x02")]; dmp.diff_chars_tolines(&mut diffs, &svec(&["", "alpha\n", "beta\n"])); - assert_eq!(vec![d(0, "alpha\nbeta\nalpha\n"), d(1, "beta\nalpha\nbeta\n")], diffs); + assert_eq!( + vec![d(0, "alpha\nbeta\nalpha\n"), d(1, "beta\nalpha\nbeta\n")], + diffs + ); // More than 256 to reveal any 8-bit limitations. let n = 300; @@ -270,7 +304,14 @@ fn upstream_diff_cleanup_merge() { assert_eq!(vec![d(1, "abc")], diffs); // Merge interweave. - diffs = vec![d(-1, "a"), d(1, "b"), d(-1, "c"), d(1, "d"), d(0, "e"), d(0, "f")]; + diffs = vec![ + d(-1, "a"), + d(1, "b"), + d(-1, "c"), + d(1, "d"), + d(0, "e"), + d(0, "f"), + ]; dmp.diff_cleanup_merge(&mut diffs); assert_eq!(vec![d(-1, "ac"), d(1, "bd"), d(0, "ef")], diffs); @@ -332,24 +373,37 @@ fn upstream_diff_cleanup_semantic_lossless() { ]; dmp.diff_cleanup_semantic_lossless(&mut diffs); assert_eq!( - vec![d(0, "AAA\r\n\r\n"), d(1, "BBB\r\nDDD\r\n\r\n"), d(0, "BBB\r\nEEE")], + vec![ + d(0, "AAA\r\n\r\n"), + d(1, "BBB\r\nDDD\r\n\r\n"), + d(0, "BBB\r\nEEE") + ], diffs ); // Line boundaries. diffs = vec![d(0, "AAA\r\nBBB"), d(1, " DDD\r\nBBB"), d(0, " EEE")]; dmp.diff_cleanup_semantic_lossless(&mut diffs); - assert_eq!(vec![d(0, "AAA\r\n"), d(1, "BBB DDD\r\n"), d(0, "BBB EEE")], diffs); + assert_eq!( + vec![d(0, "AAA\r\n"), d(1, "BBB DDD\r\n"), d(0, "BBB EEE")], + diffs + ); // Word boundaries. diffs = vec![d(0, "The c"), d(1, "ow and the c"), d(0, "at.")]; dmp.diff_cleanup_semantic_lossless(&mut diffs); - assert_eq!(vec![d(0, "The "), d(1, "cow and the "), d(0, "cat.")], diffs); + assert_eq!( + vec![d(0, "The "), d(1, "cow and the "), d(0, "cat.")], + diffs + ); // Alphanumeric boundaries. diffs = vec![d(0, "The-c"), d(1, "ow-and-the-c"), d(0, "at.")]; dmp.diff_cleanup_semantic_lossless(&mut diffs); - assert_eq!(vec![d(0, "The-"), d(1, "cow-and-the-"), d(0, "cat.")], diffs); + assert_eq!( + vec![d(0, "The-"), d(1, "cow-and-the-"), d(0, "cat.")], + diffs + ); // Hitting the start. diffs = vec![d(0, "a"), d(-1, "a"), d(0, "ax")]; @@ -364,7 +418,10 @@ fn upstream_diff_cleanup_semantic_lossless() { // Sentence boundaries. diffs = vec![d(0, "The xxx. The "), d(1, "zzz. The "), d(0, "yyy.")]; dmp.diff_cleanup_semantic_lossless(&mut diffs); - assert_eq!(vec![d(0, "The xxx."), d(1, " The zzz."), d(0, " The yyy.")], diffs); + assert_eq!( + vec![d(0, "The xxx."), d(1, " The zzz."), d(0, " The yyy.")], + diffs + ); } #[test] @@ -384,7 +441,10 @@ fn upstream_diff_cleanup_semantic() { // No elimination #2. diffs = vec![d(-1, "abc"), d(1, "ABC"), d(0, "1234"), d(-1, "wxyz")]; dmp.diff_cleanup_semantic(&mut diffs); - assert_eq!(vec![d(-1, "abc"), d(1, "ABC"), d(0, "1234"), d(-1, "wxyz")], diffs); + assert_eq!( + vec![d(-1, "abc"), d(1, "ABC"), d(0, "1234"), d(-1, "wxyz")], + diffs + ); // Simple elimination. diffs = vec![d(-1, "a"), d(0, "b"), d(-1, "c")]; @@ -414,7 +474,10 @@ fn upstream_diff_cleanup_semantic() { // Word boundaries. diffs = vec![d(0, "The c"), d(-1, "ow and the c"), d(0, "at.")]; dmp.diff_cleanup_semantic(&mut diffs); - assert_eq!(vec![d(0, "The "), d(-1, "cow and the "), d(0, "cat.")], diffs); + assert_eq!( + vec![d(0, "The "), d(-1, "cow and the "), d(0, "cat.")], + diffs + ); // No overlap elimination. diffs = vec![d(-1, "abcxx"), d(1, "xxdef")]; @@ -459,21 +522,39 @@ fn upstream_diff_cleanup_efficiency() { let mut dmp = Dmp::new(); // Cleanup operationally trivial equalities. dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) - // Null case. + // Null case. let mut diffs: Vec = vec![]; dmp.diff_cleanup_efficiency(&mut diffs); assert_eq!(Vec::::new(), diffs); // No elimination. - diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")]; + diffs = vec![ + d(-1, "ab"), + d(1, "12"), + d(0, "wxyz"), + d(-1, "cd"), + d(1, "34"), + ]; dmp.diff_cleanup_efficiency(&mut diffs); assert_eq!( - vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")], + vec![ + d(-1, "ab"), + d(1, "12"), + d(0, "wxyz"), + d(-1, "cd"), + d(1, "34") + ], diffs ); // Four-edit elimination. - diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "xyz"), d(-1, "cd"), d(1, "34")]; + diffs = vec![ + d(-1, "ab"), + d(1, "12"), + d(0, "xyz"), + d(-1, "cd"), + d(1, "34"), + ]; dmp.diff_cleanup_efficiency(&mut diffs); assert_eq!(vec![d(-1, "abxyzcd"), d(1, "12xyz34")], diffs); @@ -497,7 +578,13 @@ fn upstream_diff_cleanup_efficiency() { // High cost elimination. dmp.edit_cost = 5; - diffs = vec![d(-1, "ab"), d(1, "12"), d(0, "wxyz"), d(-1, "cd"), d(1, "34")]; + diffs = vec![ + d(-1, "ab"), + d(1, "12"), + d(0, "wxyz"), + d(-1, "cd"), + d(1, "34"), + ]; dmp.diff_cleanup_efficiency(&mut diffs); assert_eq!(vec![d(-1, "abwxyzcd"), d(1, "12wxyz34")], diffs); // JS restores the shared global dmp (Diff_EditCost = 4); moot with a @@ -580,10 +667,16 @@ fn upstream_diff_delta() { // Verify pool of unchanged characters. diffs = vec![d(1, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")]; let text2 = dmp.diff_text2(&mut diffs); - assert_eq!("A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", text2); + assert_eq!( + "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", + text2 + ); delta = dmp.diff_todelta(&mut diffs); - assert_eq!("+A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", delta); + assert_eq!( + "+A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", + delta + ); // Convert delta string into a diff. assert_eq!(diffs, dmp.diff_from_delta("", &delta)); @@ -747,7 +840,7 @@ fn upstream_diff_main() { // Perform a real diff. // Switch off the timeout. dmp.diff_timeout = None; // JS Diff_Timeout = 0 maps to None (no deadline). - // Simple cases. + // Simple cases. assert_eq!(vec![d(-1, "a"), d(1, "b")], dmp.diff_main("a", "b", false)); assert_eq!( @@ -762,13 +855,26 @@ fn upstream_diff_main() { ); assert_eq!( - vec![d(-1, "a"), d(1, "\u{0680}"), d(0, "x"), d(-1, "\t"), d(1, "\0")], + vec![ + d(-1, "a"), + d(1, "\u{0680}"), + d(0, "x"), + d(-1, "\t"), + d(1, "\0") + ], dmp.diff_main("ax\t", "\u{0680}x\0", false) ); // Overlaps. assert_eq!( - vec![d(-1, "1"), d(0, "a"), d(-1, "y"), d(0, "b"), d(-1, "2"), d(1, "xab")], + vec![ + d(-1, "1"), + d(0, "a"), + d(-1, "y"), + d(0, "b"), + d(-1, "2"), + d(1, "xab") + ], dmp.diff_main("1ayb2", "abxab", false) ); @@ -789,7 +895,11 @@ fn upstream_diff_main() { d(0, "efghijklmnopqrs"), d(-1, "EFGHIJKLMNOefg") ], - dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false) + dmp.diff_main( + "ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", + "a-bcd-efghijklmnopqrs", + false + ) ); // Large equality. @@ -801,7 +911,11 @@ fn upstream_diff_main() { d(0, " [[Pennsylvania]]"), d(-1, " and [[New") ], - dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false) + dmp.diff_main( + "a [[Pennsylvania]] and [[New", + " and [[Pennsylvania]]", + false + ) ); // Timeout. @@ -876,30 +990,54 @@ fn upstream_match_bitap() { assert_eq!(5, dmp.match_bitap(&chars("abcdefghijk"), &chars("fgh"), 0)); // Fuzzy matches. - assert_eq!(4, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxhi"), 0)); + assert_eq!( + 4, + dmp.match_bitap(&chars("abcdefghijk"), &chars("efxhi"), 0) + ); - assert_eq!(2, dmp.match_bitap(&chars("abcdefghijk"), &chars("cdefxyhijk"), 5)); + assert_eq!( + 2, + dmp.match_bitap(&chars("abcdefghijk"), &chars("cdefxyhijk"), 5) + ); assert_eq!(-1, dmp.match_bitap(&chars("abcdefghijk"), &chars("bxy"), 1)); // Overflow. - assert_eq!(2, dmp.match_bitap(&chars("123456789xx0"), &chars("3456789x0"), 2)); + assert_eq!( + 2, + dmp.match_bitap(&chars("123456789xx0"), &chars("3456789x0"), 2) + ); // Threshold test. dmp.match_threshold = 0.4; - assert_eq!(4, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1)); + assert_eq!( + 4, + dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1) + ); dmp.match_threshold = 0.3; - assert_eq!(-1, dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1)); + assert_eq!( + -1, + dmp.match_bitap(&chars("abcdefghijk"), &chars("efxyhi"), 1) + ); dmp.match_threshold = 0.0; - assert_eq!(1, dmp.match_bitap(&chars("abcdefghijk"), &chars("bcdef"), 1)); + assert_eq!( + 1, + dmp.match_bitap(&chars("abcdefghijk"), &chars("bcdef"), 1) + ); dmp.match_threshold = 0.5; // Multiple select. - assert_eq!(0, dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 3)); + assert_eq!( + 0, + dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 3) + ); - assert_eq!(8, dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 5)); + assert_eq!( + 8, + dmp.match_bitap(&chars("abcdexyzabcde"), &chars("abccde"), 5) + ); // Distance test. dmp.match_distance = 10; // Strict location. @@ -942,7 +1080,11 @@ fn upstream_match_main() { // Complex match. assert_eq!( 4, - dmp.match_main("I am the very model of a modern major general.", " that berry ", 5) + dmp.match_main( + "I am the very model of a modern major general.", + " that berry ", + 5 + ) ); // Test null inputs: skipped, untypable in Rust (see file header). @@ -1025,14 +1167,25 @@ fn upstream_patch_to_text() { fn upstream_patch_add_context() { let mut dmp = Dmp::new(); dmp.patch_margin = 4; - let mut p = dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); - dmp.patch_add_context(&mut p, &mut chars("The quick brown fox jumps over the lazy dog.")); - assert_eq!("@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", p.to_string()); + let mut p = + dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); + dmp.patch_add_context( + &mut p, + &mut chars("The quick brown fox jumps over the lazy dog."), + ); + assert_eq!( + "@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", + p.to_string() + ); // Same, but not enough trailing context. - let mut p = dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); + let mut p = + dmp.patch_from_text("@@ -21,4 +21,10 @@\n-jump\n+somersault\n".to_string())[0].clone(); dmp.patch_add_context(&mut p, &mut chars("The quick brown fox jumps.")); - assert_eq!("@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.to_string()); + assert_eq!( + "@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", + p.to_string() + ); // Same, but not enough leading context. let mut p = dmp.patch_from_text("@@ -3 +3,2 @@\n-e\n+at\n".to_string())[0].clone(); @@ -1057,7 +1210,7 @@ fn upstream_patch_make() { // patch_make runs diff_cleanup_efficiency internally, so the JS // constructor default matters here. dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) - // Null case. + // Null case. let mut patches = dmp.patch_make1("", ""); assert_eq!("", dmp.patch_to_text(&mut patches)); @@ -1095,7 +1248,10 @@ fn upstream_patch_make() { ); // Character decoding. - let diffs = vec![d(-1, "`1234567890-=[]\\;',./"), d(1, "~!@#$%^&*()_+{}|:\"<>?")]; + let diffs = vec![ + d(-1, "`1234567890-=[]\\;',./"), + d(1, "~!@#$%^&*()_+{}|:\"<>?"), + ]; assert_eq!( diffs, dmp.patch_from_text( @@ -1166,7 +1322,7 @@ fn upstream_patch_split_max() { fn upstream_patch_add_padding() { let mut dmp = Dmp::new(); dmp.edit_cost = 4; // JS default Diff_EditCost = 4; crate default is 0 (documented deviation) - // Both edges full. + // Both edges full. let mut patches = dmp.patch_make1("", "test"); assert_eq!("@@ -0,0 +1,4 @@\n+test\n", dmp.patch_to_text(&mut patches)); dmp.patch_add_padding(&mut patches); @@ -1177,7 +1333,10 @@ fn upstream_patch_add_padding() { // Both edges partial. let mut patches = dmp.patch_make1("XY", "XtestY"); - assert_eq!("@@ -1,2 +1,6 @@\n X\n+test\n Y\n", dmp.patch_to_text(&mut patches)); + assert_eq!( + "@@ -1,2 +1,6 @@\n X\n+test\n Y\n", + dmp.patch_to_text(&mut patches) + ); dmp.patch_add_padding(&mut patches); assert_eq!( "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", @@ -1186,9 +1345,15 @@ fn upstream_patch_add_padding() { // Both edges none. let mut patches = dmp.patch_make1("XXXXYYYY", "XXXXtestYYYY"); - assert_eq!("@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_to_text(&mut patches)); + assert_eq!( + "@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", + dmp.patch_to_text(&mut patches) + ); dmp.patch_add_padding(&mut patches); - assert_eq!("@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_to_text(&mut patches)); + assert_eq!( + "@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", + dmp.patch_to_text(&mut patches) + ); } #[test] @@ -1210,21 +1375,36 @@ fn upstream_patch_apply() { ); let results = dmp.patch_apply(&mut patches, "The quick brown fox jumps over the lazy dog."); assert_eq!( - (chars("That quick brown fox jumped over a lazy dog."), vec![true, true]), + ( + chars("That quick brown fox jumped over a lazy dog."), + vec![true, true] + ), results ); // Partial match. - let results = dmp.patch_apply(&mut patches, "The quick red rabbit jumps over the tired tiger."); + let results = dmp.patch_apply( + &mut patches, + "The quick red rabbit jumps over the tired tiger.", + ); assert_eq!( - (chars("That quick red rabbit jumped over a tired tiger."), vec![true, true]), + ( + chars("That quick red rabbit jumped over a tired tiger."), + vec![true, true] + ), results ); // Failed match. - let results = dmp.patch_apply(&mut patches, "I am the very model of a modern major general."); + let results = dmp.patch_apply( + &mut patches, + "I am the very model of a modern major general.", + ); assert_eq!( - (chars("I am the very model of a modern major general."), vec![false, false]), + ( + chars("I am the very model of a modern major general."), + vec![false, false] + ), results ); From dc2e5c555beeaa767dddc164b34e3154e0832862 Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 18:08:38 +0530 Subject: [PATCH 06/11] cargo fmt characterization.rs --- crates/dmp/tests/characterization.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/dmp/tests/characterization.rs b/crates/dmp/tests/characterization.rs index f851a95..208ec38 100644 --- a/crates/dmp/tests/characterization.rs +++ b/crates/dmp/tests/characterization.rs @@ -63,4 +63,3 @@ fn patch_make_absorbs_margin_sized_equality() { fn edit_cost_defaults_to_zero() { assert_eq!(Dmp::new().edit_cost, 0); } - From 42de50540ff2bcd7f1e07808cbf6d572353a7b17 Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 18:21:29 +0530 Subject: [PATCH 07/11] clippy fixes --- crates/dmp/src/engine.rs | 5 +---- crates/dmp/tests/integration_test.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/dmp/src/engine.rs b/crates/dmp/src/engine.rs index 0334120..4cccdc9 100644 --- a/crates/dmp/src/engine.rs +++ b/crates/dmp/src/engine.rs @@ -158,10 +158,7 @@ pub(crate) fn find_sub(hay: &[T], needle: &[T], from: usize) -> Option i = j, - None => return None, - } + i = skip_to(hay, i + 1, &needle[0])?; } else { len = kmp.get_or_insert_with(|| Kmp::new(needle)).fail(len); } diff --git a/crates/dmp/tests/integration_test.rs b/crates/dmp/tests/integration_test.rs index 9115720..49dcc89 100644 --- a/crates/dmp/tests/integration_test.rs +++ b/crates/dmp/tests/integration_test.rs @@ -315,7 +315,7 @@ pub fn test_diff_words_tochars() { let (chars1, chars2, hash_arr) = dmp.diff_words_tochars(&old_string, &new_string); let mut res_diffs = dmp.diff_main(&chars1, &chars2, true); dmp.diff_chars_tolines(&mut res_diffs, &hash_arr); - println!("{:?}", &res_diffs); + println!("{:?}", res_diffs); assert_eq!(diff_arr, res_diffs); } From 31dcc16b55e961e547a2c5f776ba12956424913b Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 18:26:28 +0530 Subject: [PATCH 08/11] - update version to 0.3.1 from 0.3.0 --- Cargo.lock | 2 +- crates/dmp/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d6e32d..d4610d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "diff_match_patch" -version = "0.3.0" +version = "0.3.1" dependencies = [ "percent-encoding", "serde", diff --git a/crates/dmp/Cargo.toml b/crates/dmp/Cargo.toml index 732773f..3755c4c 100644 --- a/crates/dmp/Cargo.toml +++ b/crates/dmp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diff_match_patch" -version = "0.3.0" +version = "0.3.1" authors = [ "chandan ", "ajit ", From 8d954c0ef2123a8b4374222f6c77ca88cdfce878 Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 20:36:43 +0530 Subject: [PATCH 09/11] use `use web_time::Instant` for wasm --- Cargo.lock | 11 +++++++++++ crates/dmp/Cargo.toml | 6 ++++++ crates/dmp/src/diff.rs | 7 ++++++- crates/dmp/src/engine.rs | 3 +++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d4610d1..bfb6f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,7 @@ dependencies = [ "serde", "serde_json", "unicode-segmentation", + "web-time", ] [[package]] @@ -1082,6 +1083,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/dmp/Cargo.toml b/crates/dmp/Cargo.toml index 3755c4c..e172a1a 100644 --- a/crates/dmp/Cargo.toml +++ b/crates/dmp/Cargo.toml @@ -36,6 +36,12 @@ grapheme = ["dep:unicode-segmentation"] percent-encoding = "2" unicode-segmentation = { version = "1", optional = true } +# std::time::Instant::now() panics on wasm32-unknown-unknown ("time not implemented"); +# web-time backs it with performance.now() there and re-exports std::time verbatim on +# every other target, so the deadline machinery works identically everywhere. +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +web-time = "1" + [dev-dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/dmp/src/diff.rs b/crates/dmp/src/diff.rs index 7938ad6..2718f5e 100644 --- a/crates/dmp/src/diff.rs +++ b/crates/dmp/src/diff.rs @@ -4,7 +4,12 @@ use crate::engine; use crate::types::{Diff, DiffToken, Dmp, TDiff}; -use std::time::{Duration, Instant}; +use std::time::Duration; + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +use web_time::Instant; +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +use std::time::Instant; // The historic public API takes &Vec/&mut Vec/&String; frozen by the // drop-in compatibility contract. diff --git a/crates/dmp/src/engine.rs b/crates/dmp/src/engine.rs index 4cccdc9..f4169df 100644 --- a/crates/dmp/src/engine.rs +++ b/crates/dmp/src/engine.rs @@ -3,6 +3,9 @@ // functions over `&[T: Eq]` — no Dmp state, no text; orchestration and // text materialization live in diff.rs. +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +use web_time::Instant; +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use std::time::Instant; // Chunk width for the common-run scans. Runs are scanned token by token From a00fe196d1179e95b72f0a8e10f32609a641676e Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Tue, 15 Sep 2026 20:37:54 +0530 Subject: [PATCH 10/11] format --- crates/dmp/src/diff.rs | 4 ++-- crates/dmp/src/engine.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dmp/src/diff.rs b/crates/dmp/src/diff.rs index 2718f5e..2385579 100644 --- a/crates/dmp/src/diff.rs +++ b/crates/dmp/src/diff.rs @@ -6,10 +6,10 @@ use crate::engine; use crate::types::{Diff, DiffToken, Dmp, TDiff}; use std::time::Duration; -#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] -use web_time::Instant; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use std::time::Instant; +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +use web_time::Instant; // The historic public API takes &Vec/&mut Vec/&String; frozen by the // drop-in compatibility contract. diff --git a/crates/dmp/src/engine.rs b/crates/dmp/src/engine.rs index f4169df..15b0df6 100644 --- a/crates/dmp/src/engine.rs +++ b/crates/dmp/src/engine.rs @@ -3,10 +3,10 @@ // functions over `&[T: Eq]` — no Dmp state, no text; orchestration and // text materialization live in diff.rs. -#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] -use web_time::Instant; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use std::time::Instant; +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +use web_time::Instant; // Chunk width for the common-run scans. Runs are scanned token by token // first — most probes (e.g. the bisect snake walk) mismatch within a couple From 5b6b08d9184eccbe4d33bd67f39edc13d45d04a8 Mon Sep 17 00:00:00 2001 From: Pallav Jha Date: Wed, 16 Sep 2026 11:54:28 +0530 Subject: [PATCH 11/11] remoe diff_match_patch_test.js --- .../oracle/vendor/diff_match_patch_test.js | 966 ------------------ 1 file changed, 966 deletions(-) delete mode 100644 crates/dmp/oracle/vendor/diff_match_patch_test.js diff --git a/crates/dmp/oracle/vendor/diff_match_patch_test.js b/crates/dmp/oracle/vendor/diff_match_patch_test.js deleted file mode 100644 index 109e56a..0000000 --- a/crates/dmp/oracle/vendor/diff_match_patch_test.js +++ /dev/null @@ -1,966 +0,0 @@ -/** - * Diff Match and Patch -- Test Harness - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -// If expected and actual are the equivalent, pass the test. -function assertEquivalent(msg, expected, actual) { - if (typeof actual == 'undefined') { - // msg is optional. - actual = expected; - expected = msg; - msg = 'Expected: \'' + expected + '\' Actual: \'' + actual + '\''; - } - if (_equivalent(expected, actual)) { - return assertEquals(msg, String(expected), String(actual)); - } else { - return assertEquals(msg, expected, actual); - } -} - - -// Are a and b the equivalent? -- Recursive. -function _equivalent(a, b) { - if (a == b) { - return true; - } - if (typeof a == 'object' && typeof b == 'object' && a !== null && b !== null) { - if (a.toString() != b.toString()) { - return false; - } - for (var p in a) { - if (a.hasOwnProperty(p) && !_equivalent(a[p], b[p])) { - return false; - } - } - for (var p in b) { - if (a.hasOwnProperty(p) && !_equivalent(a[p], b[p])) { - return false; - } - } - return true; - } - return false; -} - - -function diff_rebuildtexts(diffs) { - // Construct the two texts which made up the diff originally. - var text1 = ''; - var text2 = ''; - for (var x = 0; x < diffs.length; x++) { - if (diffs[x][0] != DIFF_INSERT) { - text1 += diffs[x][1]; - } - if (diffs[x][0] != DIFF_DELETE) { - text2 += diffs[x][1]; - } - } - return [text1, text2]; -} - -var dmp = new diff_match_patch(); - - -// DIFF TEST FUNCTIONS - - -function testDiffCommonPrefix() { - // Detect any common prefix. - // Null case. - assertEquals(0, dmp.diff_commonPrefix('abc', 'xyz')); - - // Non-null case. - assertEquals(4, dmp.diff_commonPrefix('1234abcdef', '1234xyz')); - - // Whole case. - assertEquals(4, dmp.diff_commonPrefix('1234', '1234xyz')); -} - -function testDiffCommonSuffix() { - // Detect any common suffix. - // Null case. - assertEquals(0, dmp.diff_commonSuffix('abc', 'xyz')); - - // Non-null case. - assertEquals(4, dmp.diff_commonSuffix('abcdef1234', 'xyz1234')); - - // Whole case. - assertEquals(4, dmp.diff_commonSuffix('1234', 'xyz1234')); -} - -function testDiffCommonOverlap() { - // Detect any suffix/prefix overlap. - // Null case. - assertEquals(0, dmp.diff_commonOverlap_('', 'abcd')); - - // Whole case. - assertEquals(3, dmp.diff_commonOverlap_('abc', 'abcd')); - - // No overlap. - assertEquals(0, dmp.diff_commonOverlap_('123456', 'abcd')); - - // Overlap. - assertEquals(3, dmp.diff_commonOverlap_('123456xxx', 'xxxabcd')); - - // Unicode. - // Some overly clever languages (C#) may treat ligatures as equal to their - // component letters. E.g. U+FB01 == 'fi' - assertEquals(0, dmp.diff_commonOverlap_('fi', '\ufb01i')); -} - -function testDiffHalfMatch() { - // Detect a halfmatch. - dmp.Diff_Timeout = 1; - // No match. - assertEquals(null, dmp.diff_halfMatch_('1234567890', 'abcdef')); - - assertEquals(null, dmp.diff_halfMatch_('12345', '23')); - - // Single Match. - assertEquivalent(['12', '90', 'a', 'z', '345678'], dmp.diff_halfMatch_('1234567890', 'a345678z')); - - assertEquivalent(['a', 'z', '12', '90', '345678'], dmp.diff_halfMatch_('a345678z', '1234567890')); - - assertEquivalent(['abc', 'z', '1234', '0', '56789'], dmp.diff_halfMatch_('abc56789z', '1234567890')); - - assertEquivalent(['a', 'xyz', '1', '7890', '23456'], dmp.diff_halfMatch_('a23456xyz', '1234567890')); - - // Multiple Matches. - assertEquivalent(['12123', '123121', 'a', 'z', '1234123451234'], dmp.diff_halfMatch_('121231234123451234123121', 'a1234123451234z')); - - assertEquivalent(['', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='], dmp.diff_halfMatch_('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-=')); - - assertEquivalent(['-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'], dmp.diff_halfMatch_('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy')); - - // Non-optimal halfmatch. - // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy - assertEquivalent(['qHillo', 'w', 'x', 'Hulloy', 'HelloHe'], dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); - - // Optimal no halfmatch. - dmp.Diff_Timeout = 0; - assertEquals(null, dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); -} - -function testDiffLinesToChars() { - function assertLinesToCharsResultEquals(a, b) { - assertEquals(a.chars1, b.chars1); - assertEquals(a.chars2, b.chars2); - assertEquivalent(a.lineArray, b.lineArray); - } - - // Convert lines down to characters. - assertLinesToCharsResultEquals({chars1: '\x01\x02\x01', chars2: '\x02\x01\x02', lineArray: ['', 'alpha\n', 'beta\n']}, dmp.diff_linesToChars_('alpha\nbeta\nalpha\n', 'beta\nalpha\nbeta\n')); - - assertLinesToCharsResultEquals({chars1: '', chars2: '\x01\x02\x03\x03', lineArray: ['', 'alpha\r\n', 'beta\r\n', '\r\n']}, dmp.diff_linesToChars_('', 'alpha\r\nbeta\r\n\r\n\r\n')); - - assertLinesToCharsResultEquals({chars1: '\x01', chars2: '\x02', lineArray: ['', 'a', 'b']}, dmp.diff_linesToChars_('a', 'b')); - - // More than 256 to reveal any 8-bit limitations. - var n = 300; - var lineList = []; - var charList = []; - for (var i = 1; i < n + 1; i++) { - lineList[i - 1] = i + '\n'; - charList[i - 1] = String.fromCharCode(i); - } - assertEquals(n, lineList.length); - var lines = lineList.join(''); - var chars = charList.join(''); - assertEquals(n, chars.length); - lineList.unshift(''); - assertLinesToCharsResultEquals({chars1: chars, chars2: '', lineArray: lineList}, dmp.diff_linesToChars_(lines, '')); -} - -function testDiffCharsToLines() { - // Convert chars up to lines. - var diffs = [[DIFF_EQUAL, '\x01\x02\x01'], [DIFF_INSERT, '\x02\x01\x02']]; - dmp.diff_charsToLines_(diffs, ['', 'alpha\n', 'beta\n']); - assertEquivalent([[DIFF_EQUAL, 'alpha\nbeta\nalpha\n'], [DIFF_INSERT, 'beta\nalpha\nbeta\n']], diffs); - - // More than 256 to reveal any 8-bit limitations. - var n = 300; - var lineList = []; - var charList = []; - for (var i = 1; i < n + 1; i++) { - lineList[i - 1] = i + '\n'; - charList[i - 1] = String.fromCharCode(i); - } - assertEquals(n, lineList.length); - var lines = lineList.join(''); - var chars = charList.join(''); - assertEquals(n, chars.length); - lineList.unshift(''); - var diffs = [[DIFF_DELETE, chars]]; - dmp.diff_charsToLines_(diffs, lineList); - assertEquivalent([[DIFF_DELETE, lines]], diffs); - - // More than 65536 to verify any 16-bit limitation. - lineList = []; - for (var i = 0; i < 66000; i++) { - lineList[i] = i + '\n'; - } - chars = lineList.join(''); - var results = dmp.diff_linesToChars_(chars, ''); - diffs = [[DIFF_INSERT, results.chars1]]; - dmp.diff_charsToLines_(diffs, results.lineArray); - assertEquals(chars, diffs[0][1]); -} - -function testDiffCleanupMerge() { - // Cleanup a messy diff. - // Null case. - var diffs = []; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([], diffs); - - // No change case. - diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']], diffs); - - // Merge equalities. - diffs = [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'b'], [DIFF_EQUAL, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'abc']], diffs); - - // Merge deletions. - diffs = [[DIFF_DELETE, 'a'], [DIFF_DELETE, 'b'], [DIFF_DELETE, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_DELETE, 'abc']], diffs); - - // Merge insertions. - diffs = [[DIFF_INSERT, 'a'], [DIFF_INSERT, 'b'], [DIFF_INSERT, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_INSERT, 'abc']], diffs); - - // Merge interweave. - diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b'], [DIFF_DELETE, 'c'], [DIFF_INSERT, 'd'], [DIFF_EQUAL, 'e'], [DIFF_EQUAL, 'f']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_DELETE, 'ac'], [DIFF_INSERT, 'bd'], [DIFF_EQUAL, 'ef']], diffs); - - // Prefix and suffix detection. - diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'c']], diffs); - - // Prefix and suffix detection with equalities. - diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc'], [DIFF_EQUAL, 'y']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'cy']], diffs); - - // Slide edit left. - diffs = [[DIFF_EQUAL, 'a'], [DIFF_INSERT, 'ba'], [DIFF_EQUAL, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'ac']], diffs); - - // Slide edit right. - diffs = [[DIFF_EQUAL, 'c'], [DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'a']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'ca'], [DIFF_INSERT, 'ba']], diffs); - - // Slide edit left recursive. - diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'ac'], [DIFF_EQUAL, 'x']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'acx']], diffs); - - // Slide edit right recursive. - diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'ca'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'a']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_EQUAL, 'xca'], [DIFF_DELETE, 'cba']], diffs); - - // Empty merge. - diffs = [[DIFF_DELETE, 'b'], [DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'c']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_INSERT, 'a'], [DIFF_EQUAL, 'bc']], diffs); - - // Empty equality. - diffs = [[DIFF_EQUAL, ''], [DIFF_INSERT, 'a'], [DIFF_EQUAL, 'b']]; - dmp.diff_cleanupMerge(diffs); - assertEquivalent([[DIFF_INSERT, 'a'], [DIFF_EQUAL, 'b']], diffs); -} - -function testDiffCleanupSemanticLossless() { - // Slide diffs to match logical boundaries. - // Null case. - var diffs = []; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([], diffs); - - // Blank lines. - diffs = [[DIFF_EQUAL, 'AAA\r\n\r\nBBB'], [DIFF_INSERT, '\r\nDDD\r\n\r\nBBB'], [DIFF_EQUAL, '\r\nEEE']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'AAA\r\n\r\n'], [DIFF_INSERT, 'BBB\r\nDDD\r\n\r\n'], [DIFF_EQUAL, 'BBB\r\nEEE']], diffs); - - // Line boundaries. - diffs = [[DIFF_EQUAL, 'AAA\r\nBBB'], [DIFF_INSERT, ' DDD\r\nBBB'], [DIFF_EQUAL, ' EEE']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'AAA\r\n'], [DIFF_INSERT, 'BBB DDD\r\n'], [DIFF_EQUAL, 'BBB EEE']], diffs); - - // Word boundaries. - diffs = [[DIFF_EQUAL, 'The c'], [DIFF_INSERT, 'ow and the c'], [DIFF_EQUAL, 'at.']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_INSERT, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); - - // Alphanumeric boundaries. - diffs = [[DIFF_EQUAL, 'The-c'], [DIFF_INSERT, 'ow-and-the-c'], [DIFF_EQUAL, 'at.']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'The-'], [DIFF_INSERT, 'cow-and-the-'], [DIFF_EQUAL, 'cat.']], diffs); - - // Hitting the start. - diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'ax']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'aax']], diffs); - - // Hitting the end. - diffs = [[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'a']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'xaa'], [DIFF_DELETE, 'a']], diffs); - - // Sentence boundaries. - diffs = [[DIFF_EQUAL, 'The xxx. The '], [DIFF_INSERT, 'zzz. The '], [DIFF_EQUAL, 'yyy.']]; - dmp.diff_cleanupSemanticLossless(diffs); - assertEquivalent([[DIFF_EQUAL, 'The xxx.'], [DIFF_INSERT, ' The zzz.'], [DIFF_EQUAL, ' The yyy.']], diffs); -} - -function testDiffCleanupSemantic() { - // Cleanup semantically trivial equalities. - // Null case. - var diffs = []; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([], diffs); - - // No elimination #1. - diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']], diffs); - - // No elimination #2. - diffs = [[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']], diffs); - - // Simple elimination. - diffs = [[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, 'c']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'b']], diffs); - - // Backpass elimination. - diffs = [[DIFF_DELETE, 'ab'], [DIFF_EQUAL, 'cd'], [DIFF_DELETE, 'e'], [DIFF_EQUAL, 'f'], [DIFF_INSERT, 'g']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abcdef'], [DIFF_INSERT, 'cdfg']], diffs); - - // Multiple eliminations. - diffs = [[DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2'], [DIFF_EQUAL, '_'], [DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'AB_AB'], [DIFF_INSERT, '1A2_1A2']], diffs); - - // Word boundaries. - diffs = [[DIFF_EQUAL, 'The c'], [DIFF_DELETE, 'ow and the c'], [DIFF_EQUAL, 'at.']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_DELETE, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); - - // No overlap elimination. - diffs = [[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']], diffs); - - // Overlap elimination. - diffs = [[DIFF_DELETE, 'abcxxx'], [DIFF_INSERT, 'xxxdef']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xxx'], [DIFF_INSERT, 'def']], diffs); - - // Reverse overlap elimination. - diffs = [[DIFF_DELETE, 'xxxabc'], [DIFF_INSERT, 'defxxx']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_INSERT, 'def'], [DIFF_EQUAL, 'xxx'], [DIFF_DELETE, 'abc']], diffs); - - // Two overlap eliminations. - diffs = [[DIFF_DELETE, 'abcd1212'], [DIFF_INSERT, '1212efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A3'], [DIFF_INSERT, '3BC']]; - dmp.diff_cleanupSemantic(diffs); - assertEquivalent([[DIFF_DELETE, 'abcd'], [DIFF_EQUAL, '1212'], [DIFF_INSERT, 'efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A'], [DIFF_EQUAL, '3'], [DIFF_INSERT, 'BC']], diffs); -} - -function testDiffCleanupEfficiency() { - // Cleanup operationally trivial equalities. - dmp.Diff_EditCost = 4; - // Null case. - var diffs = []; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([], diffs); - - // No elimination. - diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']], diffs); - - // Four-edit elimination. - diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xyz34']], diffs); - - // Three-edit elimination. - diffs = [[DIFF_INSERT, '12'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([[DIFF_DELETE, 'xcd'], [DIFF_INSERT, '12x34']], diffs); - - // Backpass elimination. - diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xy'], [DIFF_INSERT, '34'], [DIFF_EQUAL, 'z'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '56']]; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xy34z56']], diffs); - - // High cost elimination. - dmp.Diff_EditCost = 5; - diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; - dmp.diff_cleanupEfficiency(diffs); - assertEquivalent([[DIFF_DELETE, 'abwxyzcd'], [DIFF_INSERT, '12wxyz34']], diffs); - dmp.Diff_EditCost = 4; -} - -function testDiffPrettyHtml() { - // Pretty print. - var diffs = [[DIFF_EQUAL, 'a\n'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c&d']]; - assertEquals('a¶
<B>b</B>c&d', dmp.diff_prettyHtml(diffs)); -} - -function testDiffText() { - // Compute the source and destination texts. - var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy']]; - assertEquals('jumps over the lazy', dmp.diff_text1(diffs)); - - assertEquals('jumped over a lazy', dmp.diff_text2(diffs)); -} - -function testDiffDelta() { - // Convert a diff into delta string. - var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy'], [DIFF_INSERT, 'old dog']]; - var text1 = dmp.diff_text1(diffs); - assertEquals('jumps over the lazy', text1); - - var delta = dmp.diff_toDelta(diffs); - assertEquals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog', delta); - - // Convert delta string into a diff. - assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); - - // Generates error (19 != 20). - try { - dmp.diff_fromDelta(text1 + 'x', delta); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } - - // Generates error (19 != 18). - try { - dmp.diff_fromDelta(text1.substring(1), delta); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } - - // Generates error (%c3%xy invalid Unicode). - try { - dmp.diff_fromDelta('', '+%c3%xy'); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } - - // Test deltas with special characters. - diffs = [[DIFF_EQUAL, '\u0680 \x00 \t %'], [DIFF_DELETE, '\u0681 \x01 \n ^'], [DIFF_INSERT, '\u0682 \x02 \\ |']]; - text1 = dmp.diff_text1(diffs); - assertEquals('\u0680 \x00 \t %\u0681 \x01 \n ^', text1); - - delta = dmp.diff_toDelta(diffs); - assertEquals('=7\t-7\t+%DA%82 %02 %5C %7C', delta); - - // Convert delta string into a diff. - assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); - - // Verify pool of unchanged characters. - diffs = [[DIFF_INSERT, 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ']]; - var text2 = dmp.diff_text2(diffs); - assertEquals('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', text2); - - delta = dmp.diff_toDelta(diffs); - assertEquals('+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', delta); - - // Convert delta string into a diff. - assertEquivalent(diffs, dmp.diff_fromDelta('', delta)); - - // 160 kb string. - var a = 'abcdefghij'; - for (var i = 0; i < 14; i++) { - a += a; - } - diffs = [[DIFF_INSERT, a]]; - delta = dmp.diff_toDelta(diffs); - assertEquals('+' + a, delta); - - // Convert delta string into a diff. - assertEquivalent(diffs, dmp.diff_fromDelta('', delta)); -} - -function testDiffXIndex() { - // Translate a location in text1 to text2. - // Translation on equality. - assertEquals(5, dmp.diff_xIndex([[DIFF_DELETE, 'a'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']], 2)); - - // Translation on deletion. - assertEquals(1, dmp.diff_xIndex([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '1234'], [DIFF_EQUAL, 'xyz']], 3)); -} - -function testDiffLevenshtein() { - // Levenshtein with trailing equality. - assertEquals(4, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']])); - // Levenshtein with leading equality. - assertEquals(4, dmp.diff_levenshtein([[DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234']])); - // Levenshtein with middle equality. - assertEquals(7, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xyz'], [DIFF_INSERT, '1234']])); -} - -function testDiffBisect() { - // Normal. - var a = 'cat'; - var b = 'map'; - // Since the resulting diff hasn't been normalized, it would be ok if - // the insertion and deletion pairs are swapped. - // If the order changes, tweak this test as required. - assertEquivalent([[DIFF_DELETE, 'c'], [DIFF_INSERT, 'm'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 't'], [DIFF_INSERT, 'p']], dmp.diff_bisect_(a, b, Number.MAX_VALUE)); - - // Timeout. - assertEquivalent([[DIFF_DELETE, 'cat'], [DIFF_INSERT, 'map']], dmp.diff_bisect_(a, b, 0)); -} - -function testDiffMain() { - // Perform a trivial diff. - // Null case. - assertEquivalent([], dmp.diff_main('', '', false)); - - // Equality. - assertEquivalent([[DIFF_EQUAL, 'abc']], dmp.diff_main('abc', 'abc', false)); - - // Simple insertion. - assertEquivalent([[DIFF_EQUAL, 'ab'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'ab123c', false)); - - // Simple deletion. - assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'bc']], dmp.diff_main('a123bc', 'abc', false)); - - // Two insertions. - assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'b'], [DIFF_INSERT, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'a123b456c', false)); - - // Two deletions. - assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('a123b456c', 'abc', false)); - - // Perform a real diff. - // Switch off the timeout. - dmp.Diff_Timeout = 0; - // Simple cases. - assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b']], dmp.diff_main('a', 'b', false)); - - assertEquivalent([[DIFF_DELETE, 'Apple'], [DIFF_INSERT, 'Banana'], [DIFF_EQUAL, 's are a'], [DIFF_INSERT, 'lso'], [DIFF_EQUAL, ' fruit.']], dmp.diff_main('Apples are a fruit.', 'Bananas are also fruit.', false)); - - assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, '\u0680'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, '\t'], [DIFF_INSERT, '\0']], dmp.diff_main('ax\t', '\u0680x\0', false)); - - // Overlaps. - assertEquivalent([[DIFF_DELETE, '1'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 'y'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '2'], [DIFF_INSERT, 'xab']], dmp.diff_main('1ayb2', 'abxab', false)); - - assertEquivalent([[DIFF_INSERT, 'xaxcx'], [DIFF_EQUAL, 'abc'], [DIFF_DELETE, 'y']], dmp.diff_main('abcy', 'xaxcxabc', false)); - - assertEquivalent([[DIFF_DELETE, 'ABCD'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'bcd'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'efghijklmnopqrs'], [DIFF_DELETE, 'EFGHIJKLMNOefg']], dmp.diff_main('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', 'a-bcd-efghijklmnopqrs', false)); - - // Large equality. - assertEquivalent([[DIFF_INSERT, ' '], [DIFF_EQUAL, 'a'], [DIFF_INSERT, 'nd'], [DIFF_EQUAL, ' [[Pennsylvania]]'], [DIFF_DELETE, ' and [[New']], dmp.diff_main('a [[Pennsylvania]] and [[New', ' and [[Pennsylvania]]', false)); - - // Timeout. - dmp.Diff_Timeout = 0.1; // 100ms - var a = '`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n'; - var b = 'I am the very model of a modern major general,\nI\'ve information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n'; - // Increase the text lengths by 1024 times to ensure a timeout. - for (var i = 0; i < 10; i++) { - a += a; - b += b; - } - var startTime = (new Date()).getTime(); - dmp.diff_main(a, b); - var endTime = (new Date()).getTime(); - // Test that we took at least the timeout period. - assertTrue(dmp.Diff_Timeout * 1000 <= endTime - startTime); - // Test that we didn't take forever (be forgiving). - // Theoretically this test could fail very occasionally if the - // OS task swaps or locks up for a second at the wrong moment. - assertTrue(dmp.Diff_Timeout * 1000 * 2 > endTime - startTime); - dmp.Diff_Timeout = 0; - - // Test the linemode speedup. - // Must be long to pass the 100 char cutoff. - // Simple line-mode. - a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; - b = 'abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n'; - assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); - - // Single line-mode. - a = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; - b = 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'; - assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); - - // Overlap line-mode. - a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; - b = 'abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n'; - var texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); - var texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); - assertEquivalent(texts_textmode, texts_linemode); - - // Test null inputs. - try { - dmp.diff_main(null, null); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } -} - - -// MATCH TEST FUNCTIONS - - -function testMatchAlphabet() { - // Initialise the bitmasks for Bitap. - // Unique. - assertEquivalent({'a':4, 'b':2, 'c':1}, dmp.match_alphabet_('abc')); - - // Duplicates. - assertEquivalent({'a':37, 'b':18, 'c':8}, dmp.match_alphabet_('abcaba')); -} - -function testMatchBitap() { - // Bitap algorithm. - dmp.Match_Distance = 100; - dmp.Match_Threshold = 0.5; - // Exact matches. - assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 5)); - - assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 0)); - - // Fuzzy matches. - assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxhi', 0)); - - assertEquals(2, dmp.match_bitap_('abcdefghijk', 'cdefxyhijk', 5)); - - assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'bxy', 1)); - - // Overflow. - assertEquals(2, dmp.match_bitap_('123456789xx0', '3456789x0', 2)); - - // Threshold test. - dmp.Match_Threshold = 0.4; - assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); - - dmp.Match_Threshold = 0.3; - assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); - - dmp.Match_Threshold = 0.0; - assertEquals(1, dmp.match_bitap_('abcdefghijk', 'bcdef', 1)); - dmp.Match_Threshold = 0.5; - - // Multiple select. - assertEquals(0, dmp.match_bitap_('abcdexyzabcde', 'abccde', 3)); - - assertEquals(8, dmp.match_bitap_('abcdexyzabcde', 'abccde', 5)); - - // Distance test. - dmp.Match_Distance = 10; // Strict location. - assertEquals(-1, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); - - assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 1)); - - dmp.Match_Distance = 1000; // Loose location. - assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); -} - -function testMatchMain() { - // Full match. - // Shortcut matches. - assertEquals(0, dmp.match_main('abcdef', 'abcdef', 1000)); - - assertEquals(-1, dmp.match_main('', 'abcdef', 1)); - - assertEquals(3, dmp.match_main('abcdef', '', 3)); - - assertEquals(3, dmp.match_main('abcdef', 'de', 3)); - - // Beyond end match. - assertEquals(3, dmp.match_main("abcdef", "defy", 4)); - - // Oversized pattern. - assertEquals(0, dmp.match_main("abcdef", "abcdefy", 0)); - - // Complex match. - assertEquals(4, dmp.match_main('I am the very model of a modern major general.', ' that berry ', 5)); - - // Test null inputs. - try { - dmp.match_main(null, null, 0); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } -} - - -// PATCH TEST FUNCTIONS - - -function testPatchObj() { - // Patch Object. - var p = new diff_match_patch.patch_obj(); - p.start1 = 20; - p.start2 = 21; - p.length1 = 18; - p.length2 = 17; - p.diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, '\nlaz']]; - var strp = p.toString(); - assertEquals('@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n', strp); -} - -function testPatchFromText() { - assertEquivalent([], dmp.patch_fromText(strp)); - - var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; - assertEquals(strp, dmp.patch_fromText(strp)[0].toString()); - - assertEquals('@@ -1 +1 @@\n-a\n+b\n', dmp.patch_fromText('@@ -1 +1 @@\n-a\n+b\n')[0].toString()); - - assertEquals('@@ -1,3 +0,0 @@\n-abc\n', dmp.patch_fromText('@@ -1,3 +0,0 @@\n-abc\n')[0].toString()); - - assertEquals('@@ -0,0 +1,3 @@\n+abc\n', dmp.patch_fromText('@@ -0,0 +1,3 @@\n+abc\n')[0].toString()); - - // Generates error. - try { - dmp.patch_fromText('Bad\nPatch\n'); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } -} - -function testPatchToText() { - var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; - var p = dmp.patch_fromText(strp); - assertEquals(strp, dmp.patch_toText(p)); - - strp = '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n'; - p = dmp.patch_fromText(strp); - assertEquals(strp, dmp.patch_toText(p)); -} - -function testPatchAddContext() { - dmp.Patch_Margin = 4; - var p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; - dmp.patch_addContext_(p, 'The quick brown fox jumps over the lazy dog.'); - assertEquals('@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n', p.toString()); - - // Same, but not enough trailing context. - p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; - dmp.patch_addContext_(p, 'The quick brown fox jumps.'); - assertEquals('@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n', p.toString()); - - // Same, but not enough leading context. - p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; - dmp.patch_addContext_(p, 'The quick brown fox jumps.'); - assertEquals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n', p.toString()); - - // Same, but with ambiguity. - p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; - dmp.patch_addContext_(p, 'The quick brown fox jumps. The quick brown fox crashes.'); - assertEquals('@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n', p.toString()); -} - -function testPatchMake() { - // Null case. - var patches = dmp.patch_make('', ''); - assertEquals('', dmp.patch_toText(patches)); - - var text1 = 'The quick brown fox jumps over the lazy dog.'; - var text2 = 'That quick brown fox jumped over a lazy dog.'; - // Text2+Text1 inputs. - var expectedPatch = '@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n'; - // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. - patches = dmp.patch_make(text2, text1); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Text1+Text2 inputs. - expectedPatch = '@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; - patches = dmp.patch_make(text1, text2); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Diff input. - var diffs = dmp.diff_main(text1, text2, false); - patches = dmp.patch_make(diffs); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Text1+Diff inputs. - patches = dmp.patch_make(text1, diffs); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Text1+Text2+Diff inputs (deprecated). - patches = dmp.patch_make(text1, text2, diffs); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Character encoding. - patches = dmp.patch_make('`1234567890-=[]\\;\',./', '~!@#$%^&*()_+{}|:"<>?'); - assertEquals('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n', dmp.patch_toText(patches)); - - // Character decoding. - diffs = [[DIFF_DELETE, '`1234567890-=[]\\;\',./'], [DIFF_INSERT, '~!@#$%^&*()_+{}|:"<>?']]; - assertEquivalent(diffs, dmp.patch_fromText('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')[0].diffs); - - // Long string with repeats. - text1 = ''; - for (var x = 0; x < 100; x++) { - text1 += 'abcdef'; - } - text2 = text1 + '123'; - expectedPatch = '@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n'; - patches = dmp.patch_make(text1, text2); - assertEquals(expectedPatch, dmp.patch_toText(patches)); - - // Test null inputs. - try { - dmp.patch_make(null); - assertEquals(Error, null); - } catch (e) { - // Exception expected. - } -} - -function testPatchSplitMax() { - // Assumes that dmp.Match_MaxBits is 32. - var patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz01234567890', 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0'); - dmp.patch_splitMax(patches); - assertEquals('@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n', dmp.patch_toText(patches)); - - patches = dmp.patch_make('abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 'abcdefuvwxyz'); - var oldToText = dmp.patch_toText(patches); - dmp.patch_splitMax(patches); - assertEquals(oldToText, dmp.patch_toText(patches)); - - patches = dmp.patch_make('1234567890123456789012345678901234567890123456789012345678901234567890', 'abc'); - dmp.patch_splitMax(patches); - assertEquals('@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n', dmp.patch_toText(patches)); - - patches = dmp.patch_make('abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1', 'abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1'); - dmp.patch_splitMax(patches); - assertEquals('@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n', dmp.patch_toText(patches)); -} - -function testPatchAddPadding() { - // Both edges full. - var patches = dmp.patch_make('', 'test'); - assertEquals('@@ -0,0 +1,4 @@\n+test\n', dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n', dmp.patch_toText(patches)); - - // Both edges partial. - patches = dmp.patch_make('XY', 'XtestY'); - assertEquals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n', dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n', dmp.patch_toText(patches)); - - // Both edges none. - patches = dmp.patch_make('XXXXYYYY', 'XXXXtestYYYY'); - assertEquals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); -} - -function testPatchApply() { - dmp.Match_Distance = 1000; - dmp.Match_Threshold = 0.5; - dmp.Patch_DeleteThreshold = 0.5; - // Null case. - var patches = dmp.patch_make('', ''); - var results = dmp.patch_apply(patches, 'Hello world.'); - assertEquivalent(['Hello world.', []], results); - - // Exact match. - patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'That quick brown fox jumped over a lazy dog.'); - results = dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); - assertEquivalent(['That quick brown fox jumped over a lazy dog.', [true, true]], results); - - // Partial match. - results = dmp.patch_apply(patches, 'The quick red rabbit jumps over the tired tiger.'); - assertEquivalent(['That quick red rabbit jumped over a tired tiger.', [true, true]], results); - - // Failed match. - results = dmp.patch_apply(patches, 'I am the very model of a modern major general.'); - assertEquivalent(['I am the very model of a modern major general.', [false, false]], results); - - // Big delete, small change. - patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); - results = dmp.patch_apply(patches, 'x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y'); - assertEquivalent(['xabcy', [true, true]], results); - - // Big delete, big change 1. - patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); - results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); - assertEquivalent(['xabc12345678901234567890---------------++++++++++---------------12345678901234567890y', [false, true]], results); - - // Big delete, big change 2. - dmp.Patch_DeleteThreshold = 0.6; - patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); - results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); - assertEquivalent(['xabcy', [true, true]], results); - dmp.Patch_DeleteThreshold = 0.5; - - // Compensate for failed patch. - dmp.Match_Threshold = 0.0; - dmp.Match_Distance = 0; - patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz--------------------1234567890', 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890'); - results = dmp.patch_apply(patches, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890'); - assertEquivalent(['ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890', [false, true]], results); - dmp.Match_Threshold = 0.5; - dmp.Match_Distance = 1000; - - // No side effects. - patches = dmp.patch_make('', 'test'); - var patchstr = dmp.patch_toText(patches); - dmp.patch_apply(patches, ''); - assertEquals(patchstr, dmp.patch_toText(patches)); - - // No side effects with major delete. - patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'Woof'); - patchstr = dmp.patch_toText(patches); - dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); - assertEquals(patchstr, dmp.patch_toText(patches)); - - // Edge exact match. - patches = dmp.patch_make('', 'test'); - results = dmp.patch_apply(patches, ''); - assertEquivalent(['test', [true]], results); - - // Near edge exact match. - patches = dmp.patch_make('XY', 'XtestY'); - results = dmp.patch_apply(patches, 'XY'); - assertEquivalent(['XtestY', [true]], results); - - // Edge partial match. - patches = dmp.patch_make('y', 'y123'); - results = dmp.patch_apply(patches, 'x'); - assertEquivalent(['x123', [true]], results); -}