merge: EA-91241 - assert: DiffUtil.getMergeType
authorAleksey Pivovarov <AMPivovarov@gmail.com>
Mon, 14 Nov 2016 09:08:09 +0000 (12:08 +0300)
committerAleksey Pivovarov <AMPivovarov@gmail.com>
Mon, 14 Nov 2016 12:28:27 +0000 (15:28 +0300)
* ensure that "conflict" block is not "unchanged"
  this might've happen for trim/ignore policies

* add tests for three-side cases

platform/diff-impl/src/com/intellij/diff/comparison/ByWord.java
platform/diff-impl/src/com/intellij/diff/comparison/TrimUtil.java
platform/diff-impl/tests/com/intellij/diff/DiffTestCase.kt
platform/diff-impl/tests/com/intellij/diff/comparison/ComparisonUtilAutoTest.kt
platform/diff-impl/tests/com/intellij/diff/comparison/ComparisonUtilTestBase.kt
platform/diff-impl/tests/com/intellij/diff/comparison/MergeResolveUtilTest.kt
platform/diff-impl/tests/com/intellij/diff/comparison/WordComparisonUtilTest.kt
platform/diff-impl/tests/com/intellij/diff/comparison/WordMergeComparisonUtilTest.kt [new file with mode: 0644]

index eec26d3f1e25b0cef7caa292447efe883224fd5f..a685de3484a99b124adc461c4ce491994e0118c1 100644 (file)
@@ -600,7 +600,8 @@ public class ByWord {
         Range expanded = expandW(myText1, myText2, range);
         Range trimmed = trim(myText1, myText2, expanded);
 
-        if (!trimmed.isEmpty()) {
+        if (!trimmed.isEmpty() &&
+            !isEqualsIW(myText1, myText2, trimmed)) {
           myChanges.add(trimmed);
         }
       }
@@ -638,7 +639,8 @@ public class ByWord {
         MergeRange expanded = expandW(myText1, myText2, myText3, range);
         MergeRange trimmed = trim(myText1, myText2, myText3, expanded);
 
-        if (!trimmed.isEmpty()) {
+        if (!trimmed.isEmpty() &&
+            !isEqualsIW(myText1, myText2, myText3, trimmed)) {
           myChanges.add(trimmed);
         }
       }
@@ -690,7 +692,8 @@ public class ByWord {
 
         Range trimmed = new Range(start1, end1, start2, end2);
 
-        if (!trimmed.isEmpty()) {
+        if (!trimmed.isEmpty() &&
+            !isEquals(myText1, myText2, trimmed)) {
           myChanges.add(trimmed);
         }
       }
@@ -753,7 +756,8 @@ public class ByWord {
 
         MergeRange trimmed = new MergeRange(start1, end1, start2, end2, start3, end3);
 
-        if (!trimmed.isEmpty()) {
+        if (!trimmed.isEmpty() &&
+            !isEquals(myText1, myText2, myText3, trimmed)) {
           myChanges.add(trimmed);
         }
       }
index 7914771fad5cf05a79e8a22c3274a7fc3ed59b2f..48ed3629fc71b0f770bdb22b592d1b4f75dd9826 100644 (file)
@@ -384,4 +384,40 @@ public class TrimUtil {
   public static Range expandIW(@NotNull CharSequence text1, @NotNull CharSequence text2) {
     return expandIW(text1, text2, 0, 0, text1.length(), text2.length());
   }
+
+  //
+  // Equality
+  //
+
+  public static boolean isEquals(@NotNull CharSequence text1, @NotNull CharSequence text2,
+                                 @NotNull Range range) {
+    CharSequence sequence1 = text1.subSequence(range.start1, range.end1);
+    CharSequence sequence2 = text2.subSequence(range.start2, range.end2);
+    return ComparisonUtil.isEquals(sequence1, sequence2, ComparisonPolicy.DEFAULT);
+  }
+
+  public static boolean isEqualsIW(@NotNull CharSequence text1, @NotNull CharSequence text2,
+                                   @NotNull Range range) {
+    CharSequence sequence1 = text1.subSequence(range.start1, range.end1);
+    CharSequence sequence2 = text2.subSequence(range.start2, range.end2);
+    return ComparisonUtil.isEquals(sequence1, sequence2, ComparisonPolicy.IGNORE_WHITESPACES);
+  }
+
+  public static boolean isEquals(@NotNull CharSequence text1, @NotNull CharSequence text2, @NotNull CharSequence text3,
+                                 @NotNull MergeRange range) {
+    CharSequence sequence1 = text1.subSequence(range.start1, range.end1);
+    CharSequence sequence2 = text2.subSequence(range.start2, range.end2);
+    CharSequence sequence3 = text3.subSequence(range.start3, range.end3);
+    return ComparisonUtil.isEquals(sequence2, sequence1, ComparisonPolicy.DEFAULT) &&
+           ComparisonUtil.isEquals(sequence2, sequence3, ComparisonPolicy.DEFAULT);
+  }
+
+  public static boolean isEqualsIW(@NotNull CharSequence text1, @NotNull CharSequence text2, @NotNull CharSequence text3,
+                                   @NotNull MergeRange range) {
+    CharSequence sequence1 = text1.subSequence(range.start1, range.end1);
+    CharSequence sequence2 = text2.subSequence(range.start2, range.end2);
+    CharSequence sequence3 = text3.subSequence(range.start3, range.end3);
+    return ComparisonUtil.isEquals(sequence2, sequence1, ComparisonPolicy.IGNORE_WHITESPACES) &&
+           ComparisonUtil.isEquals(sequence2, sequence3, ComparisonPolicy.IGNORE_WHITESPACES);
+  }
 }
index 8145b2a415a715b47e25b8681e9a4d54863081ba..206d595853b463e2c0873374ac6276870b75e2d1 100644 (file)
@@ -21,7 +21,6 @@ import com.intellij.diff.util.ThreeSide
 import com.intellij.openapi.editor.Document
 import com.intellij.openapi.progress.DumbProgressIndicator
 import com.intellij.openapi.progress.ProgressIndicator
-import com.intellij.openapi.util.Couple
 import com.intellij.openapi.util.registry.Registry
 import com.intellij.openapi.util.text.StringUtil
 import com.intellij.testFramework.UsefulTestCase
@@ -66,6 +65,10 @@ abstract class DiffTestCase : UsefulTestCase() {
     assertTrue(message, actual)
   }
 
+  fun assertFalse(actual: Boolean, message: String = "") {
+    assertFalse(message, actual)
+  }
+
   fun assertEquals(expected: Any?, actual: Any?, message: String = "") {
     assertEquals(message, expected, actual)
   }
@@ -75,17 +78,33 @@ abstract class DiffTestCase : UsefulTestCase() {
   }
 
   fun assertEqualsCharSequences(chunk1: CharSequence, chunk2: CharSequence, ignoreSpaces: Boolean, skipLastNewline: Boolean) {
+    if (skipLastNewline && !ignoreSpaces) {
+      assertTrue(StringUtil.equals(chunk1, chunk2) ||
+                   StringUtil.equals(stripNewline(chunk1), chunk2) ||
+                   StringUtil.equals(chunk1, stripNewline(chunk2)))
+    }
+    else {
+      assertTrue(isEqualsCharSequences(chunk1, chunk2, ignoreSpaces))
+    }
+  }
+
+  fun assertNotEqualsCharSequences(chunk1: CharSequence, chunk2: CharSequence, ignoreSpaces: Boolean, skipLastNewline: Boolean) {
+    if (skipLastNewline && !ignoreSpaces) {
+      assertTrue(!StringUtil.equals(chunk1, chunk2) ||
+                   !StringUtil.equals(stripNewline(chunk1), chunk2) ||
+                   !StringUtil.equals(chunk1, stripNewline(chunk2)))
+    }
+    else {
+      assertFalse(isEqualsCharSequences(chunk1, chunk2, ignoreSpaces))
+    }
+  }
+
+  fun isEqualsCharSequences(chunk1: CharSequence, chunk2: CharSequence, ignoreSpaces: Boolean): Boolean {
     if (ignoreSpaces) {
-      assertTrue(StringUtil.equalsIgnoreWhitespaces(chunk1, chunk2))
-    } else {
-      if (skipLastNewline) {
-        if (StringUtil.equals(chunk1, chunk2)) return
-        if (StringUtil.equals(stripNewline(chunk1), chunk2)) return
-        if (StringUtil.equals(chunk1, stripNewline(chunk2))) return
-        assertTrue(false)
-      } else {
-        assertTrue(StringUtil.equals(chunk1, chunk2))
-      }
+      return StringUtil.equalsIgnoreWhitespaces(chunk1, chunk2)
+    }
+    else {
+      return StringUtil.equals(chunk1, chunk2)
     }
   }
 
@@ -202,7 +221,7 @@ abstract class DiffTestCase : UsefulTestCase() {
   // Helpers
   //
 
-  open class Trio<T : Any>(val data1: T, val data2: T, val data3: T) {
+  open class Trio<out T>(val data1: T, val data2: T, val data3: T) {
     companion object {
       fun <V : Any> from(f: (ThreeSide) -> V): Trio<V> = Trio(f(ThreeSide.LEFT), f(ThreeSide.BASE), f(ThreeSide.RIGHT))
     }
@@ -228,7 +247,11 @@ abstract class DiffTestCase : UsefulTestCase() {
     }
 
     override fun hashCode(): Int {
-      return data1.hashCode() * 37 * 37 + data2.hashCode() * 37 + data3.hashCode()
+      var h = 0
+      if (data1 != null) h = h * 31 + data1.hashCode()
+      if (data2 != null) h = h * 31 + data2.hashCode()
+      if (data3 != null) h = h * 31 + data3.hashCode()
+      return h
     }
   }
 }
\ No newline at end of file
index 4de112cd371bdaeb50eeee73dbc44f51e9514ec1..1422898a7d087af752b3041c93179a556fd98293 100644 (file)
@@ -26,28 +26,31 @@ import com.intellij.openapi.util.registry.Registry
 import com.intellij.openapi.util.text.StringUtil
 
 class ComparisonUtilAutoTest : DiffTestCase() {
+  val RUNS = 30
+  val MAX_LENGTH = 300
+
   fun testChar() {
-    doTestChar(System.currentTimeMillis(), 30, 30)
+    doTestChar(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   fun testWord() {
-    doTestWord(System.currentTimeMillis(), 30, 300)
+    doTestWord(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   fun testLine() {
-    doTestLine(System.currentTimeMillis(), 30, 300)
+    doTestLine(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   fun testLineSquashed() {
-    doTestLineSquashed(System.currentTimeMillis(), 30, 300)
+    doTestLineSquashed(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   fun testLineTrimSquashed() {
-    doTestLineTrimSquashed(System.currentTimeMillis(), 30, 300)
+    doTestLineTrimSquashed(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   fun testMerge() {
-    doTestMerge(System.currentTimeMillis(), 30, 300)
+    doTestMerge(System.currentTimeMillis(), RUNS, MAX_LENGTH)
   }
 
   private fun doTestLine(seed: Long, runs: Int, maxLength: Int) {
@@ -141,8 +144,8 @@ class ComparisonUtilAutoTest : DiffTestCase() {
         val chunk2 = DiffUtil.getLinesContent(text2, f.startLine2, f.endLine2)
         val chunk3 = DiffUtil.getLinesContent(text3, f.startLine3, f.endLine3)
 
-        val wordFragments = ByWord.compare(chunk1, chunk2, chunk3, policy, INDICATOR);
-        MergeLineFragmentImpl(f, wordFragments);
+        val wordFragments = ByWord.compare(chunk1, chunk2, chunk3, policy, INDICATOR)
+        MergeLineFragmentImpl(f, wordFragments)
       }
       debugData.put("Fragments", fineFragments)
 
@@ -200,18 +203,18 @@ class ComparisonUtilAutoTest : DiffTestCase() {
       }
     }
 
-    checkUnchanged(text1.charsSequence, text2.charsSequence, fragments, policy, true)
+    checkValidRanges(text1.charsSequence, text2.charsSequence, fragments, policy, true)
     checkCantTrimLines(text1, text2, fragments, policy, allowNonSquashed)
   }
 
   private fun checkResultWord(text1: CharSequence, text2: CharSequence, fragments: List<DiffFragment>, policy: ComparisonPolicy) {
     checkDiffConsistency(fragments)
-    checkUnchanged(text1, text2, fragments, policy, false)
+    checkValidRanges(text1, text2, fragments, policy, false)
   }
 
   private fun checkResultChar(text1: CharSequence, text2: CharSequence, fragments: List<DiffFragment>, policy: ComparisonPolicy) {
     checkDiffConsistency(fragments)
-    checkUnchanged(text1, text2, fragments, policy, false)
+    checkValidRanges(text1, text2, fragments, policy, false)
   }
 
   private fun checkResultMerge(text1: Document, text2: Document, text3: Document, fragments: List<MergeLineFragment>, policy: ComparisonPolicy) {
@@ -223,10 +226,10 @@ class ComparisonUtilAutoTest : DiffTestCase() {
       val chunk3 = DiffUtil.getLinesContent(text3, f.startLine3, f.endLine3)
 
       checkDiffConsistency3(f.innerFragments!!)
-      checkUnchanged3(chunk1, chunk2, chunk3, f.innerFragments!!, policy)
+      checkValidRanges3(chunk1, chunk2, chunk3, f.innerFragments!!, policy)
     }
 
-    checkUnchanged3(text1, text2, text3, fragments, policy)
+    checkValidRanges3(text1, text2, text3, fragments, policy)
     checkCantTrimLines3(text1, text2, text3, fragments, policy)
   }
 
@@ -374,28 +377,39 @@ class ComparisonUtilAutoTest : DiffTestCase() {
     }
   }
 
-  private fun checkUnchanged(text1: CharSequence, text2: CharSequence, fragments: List<DiffFragment>, policy: ComparisonPolicy, skipNewline: Boolean) {
+  private fun checkValidRanges(text1: CharSequence, text2: CharSequence, fragments: List<DiffFragment>, policy: ComparisonPolicy, skipNewline: Boolean) {
     // TODO: better check for Trim spaces case ?
-    val ignoreSpaces = policy !== ComparisonPolicy.DEFAULT
+    val ignoreSpacesUnchanged = policy != ComparisonPolicy.DEFAULT
+    val ignoreSpacesChanged = policy == ComparisonPolicy.IGNORE_WHITESPACES
 
     var last1 = 0
     var last2 = 0
     for (fragment in fragments) {
-      val chunk1 = text1.subSequence(last1, fragment.startOffset1)
-      val chunk2 = text2.subSequence(last2, fragment.startOffset2)
+      val start1 = fragment.startOffset1
+      val start2 = fragment.startOffset2
+      val end1 = fragment.endOffset1
+      val end2 = fragment.endOffset2
 
-      assertEqualsCharSequences(chunk1, chunk2, ignoreSpaces, skipNewline)
+      val chunk1 = text1.subSequence(last1, start1)
+      val chunk2 = text2.subSequence(last2, start2)
+      assertEqualsCharSequences(chunk1, chunk2, ignoreSpacesUnchanged, skipNewline)
+
+      val chunkContent1 = text1.subSequence(start1, end1)
+      val chunkContent2 = text2.subSequence(start2, end2)
+      if (!skipNewline) {
+        assertNotEqualsCharSequences(chunkContent1, chunkContent2, ignoreSpacesChanged, skipNewline)
+      }
 
       last1 = fragment.endOffset1
       last2 = fragment.endOffset2
     }
     val chunk1 = text1.subSequence(last1, text1.length)
     val chunk2 = text2.subSequence(last2, text2.length)
-    assertEqualsCharSequences(chunk1, chunk2, ignoreSpaces, skipNewline)
+    assertEqualsCharSequences(chunk1, chunk2, ignoreSpacesUnchanged, skipNewline)
   }
 
-  private fun checkUnchanged3(text1: Document, text2: Document, text3: Document, fragments: List<MergeLineFragment>, policy: ComparisonPolicy) {
-    val ignoreSpaces = policy !== ComparisonPolicy.DEFAULT
+  private fun checkValidRanges3(text1: Document, text2: Document, text3: Document, fragments: List<MergeLineFragment>, policy: ComparisonPolicy) {
+    val ignoreSpaces = policy != ComparisonPolicy.DEFAULT
 
     var last1 = 0
     var last2 = 0
@@ -425,8 +439,9 @@ class ComparisonUtilAutoTest : DiffTestCase() {
     assertEqualsCharSequences(content2, content3, ignoreSpaces, false)
   }
 
-  private fun checkUnchanged3(text1: CharSequence, text2: CharSequence, text3: CharSequence, fragments: List<MergeWordFragment>, policy: ComparisonPolicy) {
-    val ignoreSpaces = policy !== ComparisonPolicy.DEFAULT
+  private fun checkValidRanges3(text1: CharSequence, text2: CharSequence, text3: CharSequence, fragments: List<MergeWordFragment>, policy: ComparisonPolicy) {
+    val ignoreSpacesUnchanged = policy != ComparisonPolicy.DEFAULT
+    val ignoreSpacesChanged = policy == ComparisonPolicy.IGNORE_WHITESPACES
 
     var last1 = 0
     var last2 = 0
@@ -435,13 +450,21 @@ class ComparisonUtilAutoTest : DiffTestCase() {
       val start1 = fragment.startOffset1
       val start2 = fragment.startOffset2
       val start3 = fragment.startOffset3
+      val end1 = fragment.endOffset1
+      val end2 = fragment.endOffset2
+      val end3 = fragment.endOffset3
 
       val content1 = text1.subSequence(last1, start1)
       val content2 = text2.subSequence(last2, start2)
       val content3 = text3.subSequence(last3, start3)
+      assertEqualsCharSequences(content2, content1, ignoreSpacesUnchanged, false)
+      assertEqualsCharSequences(content2, content3, ignoreSpacesUnchanged, false)
 
-      assertEqualsCharSequences(content2, content1, ignoreSpaces, false)
-      assertEqualsCharSequences(content2, content3, ignoreSpaces, false)
+      val chunkContent1 = text1.subSequence(start1, end1)
+      val chunkContent2 = text2.subSequence(start2, end2)
+      val chunkContent3 = text3.subSequence(start3, end3)
+      assertFalse(isEqualsCharSequences(chunkContent2, chunkContent1, ignoreSpacesChanged) &&
+                    isEqualsCharSequences(chunkContent2, chunkContent3, ignoreSpacesChanged))
 
       last1 = fragment.endOffset1
       last2 = fragment.endOffset2
@@ -452,8 +475,8 @@ class ComparisonUtilAutoTest : DiffTestCase() {
     val content2 = text2.subSequence(last2, text2.length)
     val content3 = text3.subSequence(last3, text3.length)
 
-    assertEqualsCharSequences(content2, content1, ignoreSpaces, false)
-    assertEqualsCharSequences(content2, content3, ignoreSpaces, false)
+    assertEqualsCharSequences(content2, content1, ignoreSpacesUnchanged, false)
+    assertEqualsCharSequences(content2, content3, ignoreSpacesUnchanged, false)
   }
 
   private fun checkCantTrimLines(text1: Document, text2: Document, fragments: List<LineFragment>, policy: ComparisonPolicy, allowNonSquashed: Boolean) {
index ed4e081b95418cd8a49158f178a32f57758140b6..ec32651c510b200bf517810f643b82fc90bd5d08 100644 (file)
@@ -18,6 +18,9 @@ package com.intellij.diff.comparison
 import com.intellij.diff.DiffTestCase
 import com.intellij.diff.fragments.DiffFragment
 import com.intellij.diff.fragments.LineFragment
+import com.intellij.diff.fragments.MergeWordFragment
+import com.intellij.diff.util.IntPair
+import com.intellij.diff.util.ThreeSide
 import com.intellij.openapi.editor.Document
 import com.intellij.openapi.editor.impl.DocumentImpl
 import com.intellij.openapi.util.Couple
@@ -25,14 +28,18 @@ import com.intellij.util.containers.ContainerUtil
 import java.util.*
 
 abstract class ComparisonUtilTestBase : DiffTestCase() {
-  private fun doLineTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
+  private fun doLineTest(text: Couple<Document>, matchings: Couple<BitSet>?, expected: List<Couple<IntPair>>?, policy: ComparisonPolicy) {
+    val before = text.first
+    val after = text.second
     val fragments = MANAGER.compareLines(before.charsSequence, after.charsSequence, policy, INDICATOR)
     checkConsistency(fragments, before, after)
     if (matchings != null) checkLineMatching(fragments, matchings)
     if (expected != null) checkLineChanges(fragments, expected)
   }
 
-  private fun doLineInnerTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
+  private fun doLineInnerTest(text: Couple<Document>, matchings: Couple<BitSet>?, expected: List<Couple<IntPair>>?, policy: ComparisonPolicy) {
+    val before = text.first
+    val after = text.second
     val rawFragments = MANAGER.compareLinesInner(before.charsSequence, after.charsSequence, policy, INDICATOR)
     val fragments = MANAGER.squash(rawFragments)
     checkConsistencyLineInner(fragments, before, after)
@@ -42,7 +49,9 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
     if (expected != null) checkDiffChanges(diffFragments, expected)
   }
 
-  private fun doWordTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
+  private fun doWordTest(text: Couple<Document>, matchings: Couple<BitSet>?, expected: List<Couple<IntPair>>?, policy: ComparisonPolicy) {
+    val before = text.first
+    val after = text.second
     val fragments = MANAGER.compareWords(before.charsSequence, after.charsSequence, policy, INDICATOR)
     checkConsistency(fragments, before, after)
 
@@ -50,14 +59,33 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
     if (expected != null) checkDiffChanges(fragments, expected)
   }
 
-  private fun doCharTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
+  private fun doWordTest(text: Trio<Document>, matchings: Trio<BitSet>?, expected: List<Trio<IntPair>>?, policy: ComparisonPolicy) {
+    val before = text.data1
+    val base = text.data2
+    val after = text.data3
+    val fragments = ByWord.compare(before.charsSequence, base.charsSequence, after.charsSequence, policy, INDICATOR)
+    checkConsistency(fragments)
+
+    if (matchings != null) checkMergeMatching(fragments, matchings)
+    if (expected != null) checkMergeChanges(fragments, expected)
+  }
+
+  private fun doCharTest(text: Couple<Document>, matchings: Couple<BitSet>?, expected: List<Couple<IntPair>>?, policy: ComparisonPolicy) {
+    val before = text.first
+    val after = text.second
     val fragments = MANAGER.compareChars(before.charsSequence, after.charsSequence, policy, INDICATOR)
     checkConsistency(fragments, before, after)
     if (matchings != null) checkDiffMatching(fragments, matchings)
     if (expected != null) checkDiffChanges(fragments, expected)
   }
 
-  private fun doSplitterTest(before: Document, after: Document, squash: Boolean, trim: Boolean, expected: List<Change>?, policy: ComparisonPolicy) {
+  private fun doSplitterTest(text: Couple<Document>,
+                             squash: Boolean,
+                             trim: Boolean,
+                             expected: List<Couple<IntPair>>?,
+                             policy: ComparisonPolicy) {
+    val before = text.first
+    val after = text.second
     val text1 = before.charsSequence
     val text2 = after.charsSequence
 
@@ -110,16 +138,33 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
     }
   }
 
-  private fun checkLineChanges(fragments: List<LineFragment>, expected: List<Change>) {
+  private fun checkConsistency(fragments: List<MergeWordFragment>) {
+    for (fragment in fragments) {
+      assertTrue(fragment.getStartOffset(ThreeSide.LEFT) <= fragment.getEndOffset(ThreeSide.LEFT))
+      assertTrue(fragment.getStartOffset(ThreeSide.BASE) <= fragment.getEndOffset(ThreeSide.BASE))
+      assertTrue(fragment.getStartOffset(ThreeSide.RIGHT) <= fragment.getEndOffset(ThreeSide.RIGHT))
+
+      assertTrue(fragment.getStartOffset(ThreeSide.LEFT) != fragment.getEndOffset(ThreeSide.LEFT) ||
+                   fragment.getStartOffset(ThreeSide.BASE) != fragment.getEndOffset(ThreeSide.BASE) ||
+                   fragment.getStartOffset(ThreeSide.RIGHT) != fragment.getEndOffset(ThreeSide.RIGHT))
+    }
+  }
+
+  private fun checkLineChanges(fragments: List<LineFragment>, expected: List<Couple<IntPair>>) {
     val changes = convertLineFragments(fragments)
     assertOrderedEquals(changes, expected)
   }
 
-  private fun checkDiffChanges(fragments: List<DiffFragment>, expected: List<Change>) {
+  private fun checkDiffChanges(fragments: List<DiffFragment>, expected: List<Couple<IntPair>>) {
     val changes = convertDiffFragments(fragments)
     assertOrderedEquals(changes, expected)
   }
 
+  private fun checkMergeChanges(fragments: List<MergeWordFragment>, expected: List<Trio<IntPair>>) {
+    val changes = convertMergeFragments(fragments)
+    assertOrderedEquals(changes, expected)
+  }
+
   private fun checkLineMatching(fragments: List<LineFragment>, matchings: Couple<BitSet>) {
     val set1 = BitSet()
     val set2 = BitSet()
@@ -128,8 +173,8 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
       set2.set(fragment.startLine2, fragment.endLine2)
     }
 
-    assertEquals(matchings.first, set1)
-    assertEquals(matchings.second, set2)
+    assertEquals(matchings.first, set1, "Before")
+    assertEquals(matchings.second, set2, "After")
   }
 
   private fun checkDiffMatching(fragments: List<DiffFragment>, matchings: Couple<BitSet>) {
@@ -140,16 +185,39 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
       set2.set(fragment.startOffset2, fragment.endOffset2)
     }
 
-    assertEquals(matchings.first, set1)
-    assertEquals(matchings.second, set2)
+    assertEquals(matchings.first, set1, "Before")
+    assertEquals(matchings.second, set2, "After")
+  }
+
+  private fun checkMergeMatching(fragments: List<MergeWordFragment>, matchings: Trio<BitSet>) {
+    val set1 = BitSet()
+    val set2 = BitSet()
+    val set3 = BitSet()
+    for (fragment in fragments) {
+      set1.set(fragment.getStartOffset(ThreeSide.LEFT), fragment.getEndOffset(ThreeSide.LEFT))
+      set2.set(fragment.getStartOffset(ThreeSide.BASE), fragment.getEndOffset(ThreeSide.BASE))
+      set3.set(fragment.getStartOffset(ThreeSide.RIGHT), fragment.getEndOffset(ThreeSide.RIGHT))
+    }
+
+    assertEquals(matchings.data1, set1, "Before")
+    assertEquals(matchings.data2, set2, "Base")
+    assertEquals(matchings.data3, set3, "After")
+  }
+
+  private fun convertDiffFragments(fragments: List<DiffFragment>): List<Couple<IntPair>> {
+    return fragments.map { Couple(IntPair(it.startOffset1, it.endOffset1), IntPair(it.startOffset2, it.endOffset2)) }
   }
 
-  private fun convertDiffFragments(fragments: List<DiffFragment>): List<Change> {
-    return fragments.map { Change(it.startOffset1, it.endOffset1, it.startOffset2, it.endOffset2) }
+  private fun convertLineFragments(fragments: List<LineFragment>): List<Couple<IntPair>> {
+    return fragments.map { Couple(IntPair(it.startLine1, it.endLine1), IntPair(it.startLine2, it.endLine2)) }
   }
 
-  private fun convertLineFragments(fragments: List<LineFragment>): List<Change> {
-    return fragments.map { Change(it.startLine1, it.endLine1, it.startLine2, it.endLine2) }
+  private fun convertMergeFragments(fragments: List<MergeWordFragment>): List<Trio<IntPair>> {
+    return fragments.map {
+      Trio(IntPair(it.getStartOffset(ThreeSide.LEFT), it.getEndOffset(ThreeSide.LEFT)),
+           IntPair(it.getStartOffset(ThreeSide.BASE), it.getEndOffset(ThreeSide.BASE)),
+           IntPair(it.getStartOffset(ThreeSide.RIGHT), it.getEndOffset(ThreeSide.RIGHT)))
+    }
   }
 
   private fun checkLineOffsets(fragment: LineFragment, before: Document, after: Document) {
@@ -176,6 +244,37 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
   // Test Builder
   //
 
+  private fun parseLineMatching(matching: String, document: Document): BitSet {
+    assertEquals(matching.length, document.textLength)
+
+    val lines1 = matching.split('_', '*')
+    val lines2 = document.charsSequence.split('\n')
+    assertEquals(lines1.size, lines2.size)
+    for (i in 0..lines1.size - 1) {
+      assertEquals(lines1[i].length, lines2[i].length, "line $i")
+    }
+
+
+    val set = BitSet()
+
+    var index = 0
+    var lineNumber = 0
+    while (index < matching.length) {
+      var end = matching.indexOfAny(listOf("_", "*"), index) + 1
+      if (end == 0) end = matching.length
+
+      val line = matching.subSequence(index, end)
+      if (line.find { it != ' ' && it != '_' } != null) {
+        assert(!line.contains(' '))
+        set.set(lineNumber)
+      }
+      lineNumber++
+      index = end
+    }
+
+    return set
+  }
+
   internal enum class TestType {
     LINE, LINE_INNER, WORD, CHAR, SPLITTER
   }
@@ -183,32 +282,13 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
   internal inner class TestBuilder(private val type: TestType) {
     private var isExecuted: Boolean = false
 
-    private var before: Document? = null
-    private var after: Document? = null
-
-    private var defaultChanges: List<Change>? = null
-    private var trimChanges: List<Change>? = null
-    private var ignoreChanges: List<Change>? = null
-
-    private var defaultMatching: Couple<BitSet>? = null
-    private var trimMatching: Couple<BitSet>? = null
-    private var ignoreMatching: Couple<BitSet>? = null
+    private var text: Data<Document> = Data()
+    private var changes: PolicyData<List<Data<IntPair>>> = PolicyData()
+    private var matchings: PolicyData<Data<BitSet>> = PolicyData()
 
     private var shouldSquash: Boolean = false
     private var shouldTrim: Boolean = false
 
-    private fun changes(policy: ComparisonPolicy): List<Change>? = when (policy) {
-      ComparisonPolicy.IGNORE_WHITESPACES -> ignoreChanges ?: trimChanges ?: defaultChanges
-      ComparisonPolicy.TRIM_WHITESPACES -> trimChanges ?: defaultChanges
-      ComparisonPolicy.DEFAULT -> defaultChanges
-    }
-
-    private fun matchings(policy: ComparisonPolicy): Couple<BitSet>? = when (policy) {
-      ComparisonPolicy.IGNORE_WHITESPACES -> ignoreMatching ?: trimMatching ?: defaultMatching
-      ComparisonPolicy.TRIM_WHITESPACES -> trimMatching ?: defaultMatching
-      ComparisonPolicy.DEFAULT -> defaultMatching
-    }
-
     fun assertExecuted() {
       assertTrue(isExecuted)
     }
@@ -217,21 +297,36 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
       try {
         isExecuted = true
 
-        val change = changes(policy)
-        val matchings = matchings(policy)
-        assertTrue(change != null || matchings != null)
-
-        when (type) {
-          TestType.LINE -> doLineTest(before!!, after!!, matchings, change, policy)
-          TestType.LINE_INNER -> {
-            doLineInnerTest(before!!, after!!, matchings, change, policy)
-            doWordTest(before!!, after!!, matchings, change, policy)
+        if (text.isTwoSide()) {
+          val text = text.asCouple()
+          val changes = changes.get(policy)?.map { it.asCouple() }
+          val matchings = matchings.get(policy)?.asCouple()
+          assertTrue(changes != null || matchings != null)
+
+          when (type) {
+            TestType.LINE -> doLineTest(text, matchings, changes, policy)
+            TestType.LINE_INNER -> {
+              doLineInnerTest(text, matchings, changes, policy)
+              doWordTest(text, matchings, changes, policy)
+            }
+            TestType.WORD -> doWordTest(text, matchings, changes, policy)
+            TestType.CHAR -> doCharTest(text, matchings, changes, policy)
+            TestType.SPLITTER -> {
+              assertNull(matchings)
+              doSplitterTest(text, shouldSquash, shouldTrim, changes, policy)
+            }
+            else -> assert(false)
           }
-          TestType.WORD -> doWordTest(before!!, after!!, matchings, change, policy)
-          TestType.CHAR -> doCharTest(before!!, after!!, matchings, change, policy)
-          TestType.SPLITTER -> {
-            assertNull(matchings)
-            doSplitterTest(before!!, after!!, shouldSquash, shouldTrim, change, policy)
+        }
+        else {
+          val text = text.asTrio()
+          val changes = changes.get(policy)?.map { it.asTrio() }
+          val matchings = matchings.get(policy)?.asTrio()
+          assertTrue(changes != null || matchings != null)
+
+          when (type) {
+            TestType.WORD -> doWordTest(text, matchings, changes, policy)
+            else -> assert(false)
           }
         }
       }
@@ -266,102 +361,84 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
       return Helper(this, v)
     }
 
-    inner class Helper(val before: String, val after: String) {
+    operator fun Helper.minus(v: String): Helper {
+      return Helper(before, v, after)
+    }
+
+    inner class Helper(val before: String, val after: String, val base: String? = null) {
       init {
         val builder = this@TestBuilder
-        if (builder.before == null && builder.after == null) {
-          builder.before = DocumentImpl(parseSource(before))
-          builder.after = DocumentImpl(parseSource(after))
+        if (builder.text.before == null && builder.text.after == null ||
+          base != null && builder.text.base == null) {
+          builder.text.before = DocumentImpl(parseSource(before))
+          builder.text.after = DocumentImpl(parseSource(after))
+          if (base != null) builder.text.base = DocumentImpl(parseSource(base))
         }
       }
 
       fun plainSource() {
         val builder = this@TestBuilder
-        builder.before = DocumentImpl(before)
-        builder.after = DocumentImpl(after)
+        builder.text.before = DocumentImpl(before)
+        builder.text.after = DocumentImpl(after)
+        if (base != null) {
+          builder.text.base = DocumentImpl(base)
+        }
       }
 
       fun default() {
-        defaultMatching = parseMatching(before, after)
+        matchings.default = parseMatching(before, after, base)
       }
 
       fun trim() {
-        trimMatching = parseMatching(before, after)
+        matchings.trim = parseMatching(before, after, base)
       }
 
       fun ignore() {
-        ignoreMatching = parseMatching(before, after)
+        matchings.ignore = parseMatching(before, after, base)
       }
 
-      private fun parseMatching(before: String, after: String): Couple<BitSet> {
+      private fun parseMatching(before: String, after: String, base: String?): Data<BitSet> {
         if (type == TestType.LINE) {
           val builder = this@TestBuilder
-          return Couple.of(parseLineMatching(before, builder.before!!), parseLineMatching(after, builder.after!!))
+          return Data(parseLineMatching(before, builder.text.before!!),
+                      if (base != null) parseLineMatching(base, builder.text.base!!) else null,
+                      parseLineMatching(after, builder.text.after!!))
         }
         else {
-          return Couple.of(parseMatching(before), parseMatching(after))
-        }
-      }
-
-      fun parseLineMatching(matching: String, document: Document): BitSet {
-        assertEquals(matching.length, document.textLength)
-
-        val lines1 = matching.split('_', '*')
-        val lines2 = document.charsSequence.split('\n')
-        assertEquals(lines1.size, lines2.size)
-        for (i in 0..lines1.size - 1) {
-          assertEquals(lines1[i].length, lines2[i].length, "line $i")
+          return Data(parseMatching(before),
+                      if (base != null) parseMatching(base) else null,
+                      parseMatching(after))
         }
-
-
-        val set = BitSet()
-
-        var index = 0
-        var lineNumber = 0
-        while (index < matching.length) {
-          var end = matching.indexOfAny(listOf("_", "*"), index) + 1
-          if (end == 0) end = matching.length
-
-          val line = matching.subSequence(index, end)
-          if (line.find { it != ' ' && it != '_' } != null) {
-            assert(!line.contains(' '))
-            set.set(lineNumber)
-          }
-          lineNumber++
-          index = end
-        }
-
-        return set
       }
     }
 
 
-    fun default(vararg expected: Change): Unit {
-      defaultChanges = ContainerUtil.list(*expected)
+    fun default(vararg expected: Couple<IntPair>): Unit {
+      changes.default = ContainerUtil.list(*expected).map { Data(it.first, it.second) }
     }
 
-    fun trim(vararg expected: Change): Unit {
-      trimChanges = ContainerUtil.list(*expected)
+    fun trim(vararg expected: Couple<IntPair>): Unit {
+      changes.trim = ContainerUtil.list(*expected).map { Data(it.first, it.second) }
     }
 
-    fun ignore(vararg expected: Change): Unit {
-      ignoreChanges = ContainerUtil.list(*expected)
+    fun ignore(vararg expected: Couple<IntPair>): Unit {
+      changes.ignore = ContainerUtil.list(*expected).map { Data(it.first, it.second) }
     }
 
-    fun mod(line1: Int, line2: Int, count1: Int, count2: Int): Change {
+    fun mod(line1: Int, line2: Int, count1: Int, count2: Int): Couple<IntPair> {
       assert(count1 != 0)
       assert(count2 != 0)
-      return Change(line1, line1 + count1, line2, line2 + count2)
+      return Couple(IntPair(line1, line1 + count1), IntPair(line2, line2 + count2))
     }
 
-    fun del(line1: Int, line2: Int, count1: Int): Change {
+    fun del(line1: Int, line2: Int, count1: Int): Couple<IntPair> {
       assert(count1 != 0)
-      return Change(line1, line1 + count1, line2, line2)
+      return Couple(IntPair(line1, line1 + count1), IntPair(line2, line2))
     }
 
-    fun ins(line1: Int, line2: Int, count2: Int): Change {
+    fun ins(line1: Int, line2: Int, count2: Int): Couple<IntPair> {
       assert(count2 != 0)
-      return Change(line1, line1, line2, line2 + count2)
+      return Couple(IntPair(line1, line1), IntPair(line2, line2 + count2))
     }
 
 
@@ -396,9 +473,28 @@ abstract class ComparisonUtilTestBase : DiffTestCase() {
   // Helpers
   //
 
-  data class Change(val start1: Int, val end1: Int, val start2: Int, val end2: Int) {
-    override fun toString(): String {
-      return "($start1, $end1) - ($start2, $end2)"
+  private data class Data<T>(var before: T?, var base: T?, var after: T?) {
+    constructor() : this(null, null, null)
+    constructor(before: T?, after : T?) : this(before, null, after)
+    fun isTwoSide(): Boolean = before != null && after != null && base == null
+    fun isThreeSide(): Boolean = before != null && after != null && base != null
+    fun asCouple(): Couple<T> {
+      assert(isTwoSide())
+      return Couple(before!!, after!!)
+    }
+
+    fun asTrio(): Trio<T> {
+      assert(isThreeSide())
+      return Trio(before!!, base!!, after!!)
     }
   }
+
+  private data class PolicyData<T>(var default: T? = null, var trim: T? = null, var ignore: T? = null) {
+    fun get(policy: ComparisonPolicy): T? =
+      when (policy) {
+        ComparisonPolicy.IGNORE_WHITESPACES -> ignore ?: trim ?: default
+        ComparisonPolicy.TRIM_WHITESPACES -> trim ?: default
+        ComparisonPolicy.DEFAULT -> default
+      }
+  }
 }
index c5c299bd593a463d1e0c1f2970e95dd8779fa1c5..ef427342cbdba98fb529f55056747d7900402623 100644 (file)
@@ -187,25 +187,31 @@ class MergeResolveUtilTest : DiffTestCase() {
     )
   }
 
+  fun testRegressions() {
+    test(
+      "i\n",
+      "i",
+      "\ni",
+      "i\n",
+      "i"
+    )
+  }
+
   private fun testGreedy(base: String, left: String, right: String, expected: String?) {
     test(base, left, right, expected, true);
   }
 
   private fun test(base: String, left: String, right: String, expected: String?, isGreedy: Boolean = false) {
+    val expectedSimple = if (isGreedy) null else expected
+    val expectedGreedy = expected
+    test(base, left, right, expectedSimple, expectedGreedy)
+  }
+
+  private fun test(base: String, left: String, right: String, expectedSimple: String?, expectedGreedy: String?) {
     val simpleResult = MergeResolveUtil.tryResolve(left, base, right)
-    val magicResult = MergeResolveUtil.tryGreedyResolve(left, base, right);
-
-    if (expected == null) {
-      assertNull(simpleResult)
-      assertNull(magicResult)
-    }
-    else if (isGreedy) {
-      assertNull(simpleResult)
-      assertEquals(expected, magicResult)
-    }
-    else {
-      assertEquals(expected, simpleResult)
-      assertEquals(expected, magicResult)
-    }
+    val greedyResult = MergeResolveUtil.tryGreedyResolve(left, base, right);
+
+    assertEquals(expectedSimple, simpleResult, "Simple")
+    assertEquals(expectedGreedy, greedyResult, "Greedy")
   }
 }
index 99d224a836deec38af6f309e88d77b3cc8d2e969..c6c1e115a1ed949e08ea43a067df8ca6485c2f9e 100644 (file)
@@ -188,6 +188,37 @@ class WordComparisonUtilTest : ComparisonUtilTestBase() {
       ("   " - "     -     ").ignore()
       testAll()
     }
+
+    words {
+      ("_i" - "i_")
+      ("- " - " -").default()
+      ("  " - "  ").trim()
+      testAll()
+    }
+
+    words {
+      ("i_" - "_i")
+      ("- " - " -").default() // TODO
+      testAll()
+    }
+
+    words {
+      ("x_y" - "xy")
+      ("   " - "  ").ignore()
+      testIgnore()
+    }
+
+    words {
+      ("A x_y B" - "a xy b")
+      ("-------" - "------").ignore()
+      testIgnore()
+    }
+
+    words {
+      ("A xy B" - "a xy b")
+      ("-    -" - "-    -").ignore()
+      testIgnore()
+    }
   }
 
   fun testFixedBugs() {
@@ -233,7 +264,7 @@ class WordComparisonUtilTest : ComparisonUtilTestBase() {
 
     lines_inner {
       ("x .. z" - "x y .. z")
-      ("      " - " --     ").default() // TODO: looks wrong
+      ("      " - " --     ").default()
       ("      " - "  -     ").ignore()
       testAll()
     }
diff --git a/platform/diff-impl/tests/com/intellij/diff/comparison/WordMergeComparisonUtilTest.kt b/platform/diff-impl/tests/com/intellij/diff/comparison/WordMergeComparisonUtilTest.kt
new file mode 100644 (file)
index 0000000..9268734
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * 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.
+ */
+package com.intellij.diff.comparison
+
+class WordMergeComparisonUtilTest : ComparisonUtilTestBase() {
+  fun testSimple() {
+    words {
+      ("" - "" - "")
+      ("" - "" - "").default()
+      testAll()
+    }
+
+    words {
+      ("" - "X" - "")
+      ("" - "-" - "").default()
+      testAll()
+    }
+
+    words {
+      ("X" - "" - "")
+      ("-" - "" - "").default()
+      testAll()
+    }
+
+    words {
+      ("a b" - "a b" - "a b")
+      ("   " - "   " - "   ").default()
+      testAll()
+    }
+
+    words {
+      ("A b c" - "a b c" - "a b C")
+      ("-   -" - "-   -" - "-   -").default()
+      testAll()
+    }
+
+    words {
+      ("a c" - "a c" - "a X c")
+      ("   " - "   " - " --  ").default()
+      ("   " - "   " - "  -  ").ignore()
+      testAll()
+    }
+
+    words {
+      ("a X c" - "a X c" - "a c")
+      (" --  " - " --  " - "   ").default()
+      ("  -  " - "  -  " - "   ").ignore()
+      testAll()
+    }
+
+    words {
+      ("a X c" - "a c" - "a Y c")
+      (" --  " - "   " - " --  ").default()
+      ("  -  " - "   " - "  -  ").ignore()
+      testAll()
+    }
+
+    words {
+      ("a c" - "a X c" - "a Y c")
+      ("   " - " --  " - " --  ").default()
+      ("   " - "  -  " - "  -  ").ignore()
+      testAll()
+    }
+  }
+
+  fun testNewlines() {
+    words {
+      ("i" - "i_" - "_i")
+      ("-" - "--" - "--").default() // TODO
+      (" " - "  " - "  ").trim()
+      testAll()
+    }
+
+    words {
+      ("_i" - "i_" - "i")
+      ("--" - "--" - "-").default()
+      ("  " - "  " - " ").trim()
+      testAll()
+    }
+
+    words {
+      ("i" - "_i" - "i_")
+      (" " - "- " - " -").default()
+      (" " - "  " - "  ").trim()
+      testAll()
+    }
+
+    words {
+      ("_i" - "i" - "i_")
+      ("- " - " " - " -").default()
+      ("  " - " " - "  ").trim()
+      testAll()
+    }
+  }
+}
\ No newline at end of file