diff: separate tests for words and inner fragments
[idea/community.git] / platform / diff-impl / tests / com / intellij / diff / comparison / ComparisonUtilTestBase.kt
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.diff.comparison
17
18 import com.intellij.diff.DiffTestCase
19 import com.intellij.diff.fragments.DiffFragment
20 import com.intellij.diff.fragments.LineFragment
21 import com.intellij.openapi.editor.Document
22 import com.intellij.openapi.editor.impl.DocumentImpl
23 import com.intellij.openapi.util.Couple
24 import com.intellij.util.containers.ContainerUtil
25 import java.util.*
26
27 abstract class ComparisonUtilTestBase : DiffTestCase() {
28   private fun doLineTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
29     val fragments = MANAGER.compareLines(before.charsSequence, after.charsSequence, policy, INDICATOR)
30     checkConsistency(fragments, before, after)
31     if (matchings != null) checkLineMatching(fragments, matchings)
32     if (expected != null) checkLineChanges(fragments, expected)
33   }
34
35   private fun doLineInnerTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
36     val rawFragments = MANAGER.compareLinesInner(before.charsSequence, after.charsSequence, policy, INDICATOR)
37     val fragments = MANAGER.squash(rawFragments)
38     checkConsistencyLineInner(fragments, before, after)
39
40     val diffFragments = fragments[0].innerFragments!!
41     if (matchings != null) checkDiffMatching(diffFragments, matchings)
42     if (expected != null) checkDiffChanges(diffFragments, expected)
43   }
44
45   private fun doWordTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
46     val fragments = MANAGER.compareWords(before.charsSequence, after.charsSequence, policy, INDICATOR)
47     checkConsistency(fragments, before, after)
48
49     if (matchings != null) checkDiffMatching(fragments, matchings)
50     if (expected != null) checkDiffChanges(fragments, expected)
51   }
52
53   private fun doCharTest(before: Document, after: Document, matchings: Couple<BitSet>?, expected: List<Change>?, policy: ComparisonPolicy) {
54     val fragments = MANAGER.compareChars(before.charsSequence, after.charsSequence, policy, INDICATOR)
55     checkConsistency(fragments, before, after)
56     if (matchings != null) checkDiffMatching(fragments, matchings)
57     if (expected != null) checkDiffChanges(fragments, expected)
58   }
59
60   private fun doSplitterTest(before: Document, after: Document, squash: Boolean, trim: Boolean, expected: List<Change>?, policy: ComparisonPolicy) {
61     val text1 = before.charsSequence
62     val text2 = after.charsSequence
63
64     var fragments = MANAGER.compareLinesInner(text1, text2, policy, INDICATOR)
65     checkConsistency(fragments, before, after)
66
67     fragments = MANAGER.processBlocks(fragments, text1, text2, policy, squash, trim)
68     checkConsistency(fragments, before, after)
69
70     if (expected != null) checkLineChanges(fragments, expected)
71   }
72
73   private fun checkConsistencyLineInner(fragments: List<LineFragment>, before: Document, after: Document) {
74     assertTrue(fragments.size == 1)
75     val fragment = fragments[0]
76
77     assertTrue(fragment.startOffset1 == 0)
78     assertTrue(fragment.startOffset2 == 0)
79     assertTrue(fragment.endOffset1 == before.textLength)
80     assertTrue(fragment.endOffset2 == after.textLength)
81
82     // It could be null if there are no common words. We do not test such cases here.
83     checkConsistency(fragment.innerFragments!!, before, after)
84   }
85
86   private fun checkConsistency(fragments: List<DiffFragment>, before: Document, after: Document) {
87     for (fragment in fragments) {
88       assertTrue(fragment.startOffset1 <= fragment.endOffset1)
89       assertTrue(fragment.startOffset2 <= fragment.endOffset2)
90
91       if (fragment is LineFragment) {
92         assertTrue(fragment.startLine1 <= fragment.endLine1)
93         assertTrue(fragment.startLine2 <= fragment.endLine2)
94
95         assertTrue(fragment.startLine1 != fragment.endLine1 || fragment.startLine2 != fragment.endLine2)
96
97         assertTrue(fragment.startLine1 >= 0)
98         assertTrue(fragment.startLine2 >= 0)
99         assertTrue(fragment.endLine1 <= getLineCount(before))
100         assertTrue(fragment.endLine2 <= getLineCount(after))
101
102         checkLineOffsets(fragment, before, after)
103
104         val innerFragments = fragment.innerFragments
105         innerFragments?.let { checkConsistency(innerFragments, before, after) }
106       }
107       else {
108         assertTrue(fragment.startOffset1 != fragment.endOffset1 || fragment.startOffset2 != fragment.endOffset2)
109       }
110     }
111   }
112
113   private fun checkLineChanges(fragments: List<LineFragment>, expected: List<Change>) {
114     val changes = convertLineFragments(fragments)
115     assertOrderedEquals(changes, expected)
116   }
117
118   private fun checkDiffChanges(fragments: List<DiffFragment>, expected: List<Change>) {
119     val changes = convertDiffFragments(fragments)
120     assertOrderedEquals(changes, expected)
121   }
122
123   private fun checkLineMatching(fragments: List<LineFragment>, matchings: Couple<BitSet>) {
124     val set1 = BitSet()
125     val set2 = BitSet()
126     for (fragment in fragments) {
127       set1.set(fragment.startLine1, fragment.endLine1)
128       set2.set(fragment.startLine2, fragment.endLine2)
129     }
130
131     assertEquals(matchings.first, set1)
132     assertEquals(matchings.second, set2)
133   }
134
135   private fun checkDiffMatching(fragments: List<DiffFragment>, matchings: Couple<BitSet>) {
136     val set1 = BitSet()
137     val set2 = BitSet()
138     for (fragment in fragments) {
139       set1.set(fragment.startOffset1, fragment.endOffset1)
140       set2.set(fragment.startOffset2, fragment.endOffset2)
141     }
142
143     assertEquals(matchings.first, set1)
144     assertEquals(matchings.second, set2)
145   }
146
147   private fun convertDiffFragments(fragments: List<DiffFragment>): List<Change> {
148     return fragments.map { Change(it.startOffset1, it.endOffset1, it.startOffset2, it.endOffset2) }
149   }
150
151   private fun convertLineFragments(fragments: List<LineFragment>): List<Change> {
152     return fragments.map { Change(it.startLine1, it.endLine1, it.startLine2, it.endLine2) }
153   }
154
155   private fun checkLineOffsets(fragment: LineFragment, before: Document, after: Document) {
156     checkLineOffsets(before, fragment.startLine1, fragment.endLine1, fragment.startOffset1, fragment.endOffset1)
157
158     checkLineOffsets(after, fragment.startLine2, fragment.endLine2, fragment.startOffset2, fragment.endOffset2)
159   }
160
161   private fun checkLineOffsets(document: Document, startLine: Int, endLine: Int, startOffset: Int, endOffset: Int) {
162     if (startLine != endLine) {
163       assertEquals(document.getLineStartOffset(startLine), startOffset)
164       var offset = document.getLineEndOffset(endLine - 1)
165       if (offset < document.textLength) offset++
166       assertEquals(offset, endOffset)
167     }
168     else {
169       val offset = if (startLine == getLineCount(document)) document.textLength else document.getLineStartOffset(startLine)
170       assertEquals(offset, startOffset)
171       assertEquals(offset, endOffset)
172     }
173   }
174
175   //
176   // Test Builder
177   //
178
179   internal enum class TestType {
180     LINE, LINE_INNER, WORD, CHAR, SPLITTER
181   }
182
183   internal inner class TestBuilder(private val type: TestType) {
184     private var isExecuted: Boolean = false
185
186     private var before: Document? = null
187     private var after: Document? = null
188
189     private var defaultChanges: List<Change>? = null
190     private var trimChanges: List<Change>? = null
191     private var ignoreChanges: List<Change>? = null
192
193     private var defaultMatching: Couple<BitSet>? = null
194     private var trimMatching: Couple<BitSet>? = null
195     private var ignoreMatching: Couple<BitSet>? = null
196
197     private var shouldSquash: Boolean = false
198     private var shouldTrim: Boolean = false
199
200     private fun changes(policy: ComparisonPolicy): List<Change>? = when (policy) {
201       ComparisonPolicy.IGNORE_WHITESPACES -> ignoreChanges ?: trimChanges ?: defaultChanges
202       ComparisonPolicy.TRIM_WHITESPACES -> trimChanges ?: defaultChanges
203       ComparisonPolicy.DEFAULT -> defaultChanges
204     }
205
206     private fun matchings(policy: ComparisonPolicy): Couple<BitSet>? = when (policy) {
207       ComparisonPolicy.IGNORE_WHITESPACES -> ignoreMatching ?: trimMatching ?: defaultMatching
208       ComparisonPolicy.TRIM_WHITESPACES -> trimMatching ?: defaultMatching
209       ComparisonPolicy.DEFAULT -> defaultMatching
210     }
211
212     fun assertExecuted() {
213       assertTrue(isExecuted)
214     }
215
216     private fun run(policy: ComparisonPolicy) {
217       try {
218         isExecuted = true
219
220         val change = changes(policy)
221         val matchings = matchings(policy)
222         assertTrue(change != null || matchings != null)
223
224         when (type) {
225           TestType.LINE -> doLineTest(before!!, after!!, matchings, change, policy)
226           TestType.LINE_INNER -> {
227             doLineInnerTest(before!!, after!!, matchings, change, policy)
228             doWordTest(before!!, after!!, matchings, change, policy)
229           }
230           TestType.WORD -> doWordTest(before!!, after!!, matchings, change, policy)
231           TestType.CHAR -> doCharTest(before!!, after!!, matchings, change, policy)
232           TestType.SPLITTER -> {
233             assertNull(matchings)
234             doSplitterTest(before!!, after!!, shouldSquash, shouldTrim, change, policy)
235           }
236         }
237       }
238       catch (e: Throwable) {
239         println("Policy: " + policy.name)
240         throw e
241       }
242     }
243
244
245     fun testAll() {
246       testDefault()
247       testTrim()
248       testIgnore()
249     }
250
251     fun testDefault() {
252       run(ComparisonPolicy.DEFAULT)
253     }
254
255     fun testTrim() {
256       if (type == TestType.CHAR) return // not supported
257       run(ComparisonPolicy.TRIM_WHITESPACES)
258     }
259
260     fun testIgnore() {
261       run(ComparisonPolicy.IGNORE_WHITESPACES)
262     }
263
264
265     operator fun String.minus(v: String): Helper {
266       return Helper(this, v)
267     }
268
269     inner class Helper(val before: String, val after: String) {
270       init {
271         val builder = this@TestBuilder
272         if (builder.before == null && builder.after == null) {
273           builder.before = DocumentImpl(parseSource(before))
274           builder.after = DocumentImpl(parseSource(after))
275         }
276       }
277
278       fun plainSource() {
279         val builder = this@TestBuilder
280         builder.before = DocumentImpl(before)
281         builder.after = DocumentImpl(after)
282       }
283
284       fun default() {
285         defaultMatching = parseMatching(before, after)
286       }
287
288       fun trim() {
289         trimMatching = parseMatching(before, after)
290       }
291
292       fun ignore() {
293         ignoreMatching = parseMatching(before, after)
294       }
295
296       private fun parseMatching(before: String, after: String): Couple<BitSet> {
297         if (type == TestType.LINE) {
298           val builder = this@TestBuilder
299           return Couple.of(parseLineMatching(before, builder.before!!), parseLineMatching(after, builder.after!!))
300         }
301         else {
302           return Couple.of(parseMatching(before), parseMatching(after))
303         }
304       }
305
306       fun parseLineMatching(matching: String, document: Document): BitSet {
307         assertEquals(matching.length, document.textLength)
308
309         val lines1 = matching.split('_', '*')
310         val lines2 = document.charsSequence.split('\n')
311         assertEquals(lines1.size, lines2.size)
312         for (i in 0..lines1.size - 1) {
313           assertEquals(lines1[i].length, lines2[i].length, "line $i")
314         }
315
316
317         val set = BitSet()
318
319         var index = 0
320         var lineNumber = 0
321         while (index < matching.length) {
322           var end = matching.indexOfAny(listOf("_", "*"), index) + 1
323           if (end == 0) end = matching.length
324
325           val line = matching.subSequence(index, end)
326           if (line.find { it != ' ' && it != '_' } != null) {
327             assert(!line.contains(' '))
328             set.set(lineNumber)
329           }
330           lineNumber++
331           index = end
332         }
333
334         return set
335       }
336     }
337
338
339     fun default(vararg expected: Change): Unit {
340       defaultChanges = ContainerUtil.list(*expected)
341     }
342
343     fun trim(vararg expected: Change): Unit {
344       trimChanges = ContainerUtil.list(*expected)
345     }
346
347     fun ignore(vararg expected: Change): Unit {
348       ignoreChanges = ContainerUtil.list(*expected)
349     }
350
351     fun mod(line1: Int, line2: Int, count1: Int, count2: Int): Change {
352       assert(count1 != 0)
353       assert(count2 != 0)
354       return Change(line1, line1 + count1, line2, line2 + count2)
355     }
356
357     fun del(line1: Int, line2: Int, count1: Int): Change {
358       assert(count1 != 0)
359       return Change(line1, line1 + count1, line2, line2)
360     }
361
362     fun ins(line1: Int, line2: Int, count2: Int): Change {
363       assert(count2 != 0)
364       return Change(line1, line1, line2, line2 + count2)
365     }
366
367
368     fun postprocess(squash: Boolean, trim: Boolean): Unit {
369       shouldSquash = squash
370       shouldTrim = trim
371     }
372   }
373
374   internal fun lines(f: TestBuilder.() -> Unit): Unit = doTest(TestType.LINE, f)
375
376   internal fun lines_inner(f: TestBuilder.() -> Unit): Unit = doTest(TestType.LINE_INNER, f)
377
378   internal fun words(f: TestBuilder.() -> Unit): Unit = doTest(TestType.WORD, f)
379
380   internal fun chars(f: TestBuilder.() -> Unit): Unit = doTest(TestType.CHAR, f)
381
382   internal  fun splitter(squash: Boolean = false, trim: Boolean = false, f: TestBuilder.() -> Unit): Unit {
383     doTest(TestType.SPLITTER, {
384       postprocess(squash, trim)
385       f()
386     })
387   }
388
389   private fun doTest(type: TestType, f: TestBuilder.() -> Unit) {
390     val builder = TestBuilder(type)
391     builder.f()
392     builder.assertExecuted()
393   }
394
395   //
396   // Helpers
397   //
398
399   data class Change(val start1: Int, val end1: Int, val start2: Int, val end2: Int) {
400     override fun toString(): String {
401       return "($start1, $end1) - ($start2, $end2)"
402     }
403   }
404 }