Skip to content

Commit fa777a1

Browse files
committed
Refactor MyersDiff and add unit tests
This commit refactors the `MyersDiff.kt` for improved clarity in the `mergeChangedEntries` method. It also introduces comprehensive unit tests for both `MyersDiff.kt` and `CharLevelDiff.kt` to ensure correctness and robustness of the diffing logic. Key changes: - Refactored `mergeChangedEntries` in `MyersDiff.kt` for better readability. - Added extensive test suite for `MyersDiff.kt` covering various scenarios including empty inputs, identical inputs, additions, deletions, changes, special characters, and large inputs. - Added extensive test suite for `CharLevelDiff.kt` covering cases like empty strings, identical strings, insertions, deletions, substitutions, and complex changes. - Updated `gradle.properties` to increase Gradle daemon JVM heap size to 4608m. - Updated `app/build.gradle.kts`: - Added ABI split configuration (disabled by default). - Added packaging options to exclude specific META-INF files. - Replaced `junit` and `androidx.ui.test.junit4` with `kotlin("test")`. - Added `junit-jupiter` to `libs.versions.toml`.
1 parent 99fcd3b commit fa777a1

6 files changed

Lines changed: 701 additions & 26 deletions

File tree

app/build.gradle.kts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ android {
1414
targetSdk = 35
1515
versionCode = 1
1616
versionName = "1.0"
17-
17+
splits {
18+
abi {
19+
isEnable = project.findProperty("splitApk")?.toString()?.toBoolean() ?: false
20+
}
21+
}
1822
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1923
}
2024

@@ -27,6 +31,15 @@ android {
2731
)
2832
}
2933
}
34+
packaging {
35+
resources {
36+
excludes += setOf(
37+
"META-INF/LICENSE.md",
38+
"META-INF/LICENSE-notice.md"
39+
)
40+
}
41+
}
42+
3043
compileOptions {
3144
sourceCompatibility = JavaVersion.VERSION_11
3245
targetCompatibility = JavaVersion.VERSION_11
@@ -49,11 +62,9 @@ dependencies {
4962
implementation(libs.androidx.ui.graphics)
5063
implementation(libs.androidx.ui.tooling.preview)
5164
implementation(libs.androidx.material3)
52-
testImplementation(libs.junit)
53-
androidTestImplementation(libs.androidx.junit)
5465
androidTestImplementation(libs.androidx.espresso.core)
5566
androidTestImplementation(platform(libs.androidx.compose.bom))
56-
androidTestImplementation(libs.androidx.ui.test.junit4)
5767
debugImplementation(libs.androidx.ui.tooling)
5868
debugImplementation(libs.androidx.ui.test.manifest)
69+
testImplementation(kotlin("test"))
5970
}

app/src/main/java/dev/jahidhasanco/diffly/domain/util/MyersDiff.kt

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ class MyersDiff {
88

99
private val charLevelDiff = CharLevelDiff()
1010

11-
fun calculateDiff(oldLines: List<String>, newLines: List<String>): List<DiffEntry> {
11+
fun calculateDiff(
12+
oldLines: List<String>,
13+
newLines: List<String>
14+
): List<DiffEntry> {
1215
val n = oldLines.size
1316
val m = newLines.size
1417
val max = n + m
@@ -132,31 +135,19 @@ class MyersDiff {
132135
val merged = mutableListOf<DiffEntry>()
133136
var i = 0
134137
while (i < diffEntries.size) {
135-
val current = diffEntries[i]
136-
137-
// Check if next entry is an inserted line
138-
if (current.type == DiffType.DELETED && i + 1 < diffEntries.size) {
138+
if (i < diffEntries.size - 1) {
139+
val curr = diffEntries[i]
139140
val next = diffEntries[i + 1]
140-
if (next.type == DiffType.ADDED) {
141-
// Merge as changed line with char diff
142-
val oldLine = current.oldLine ?: ""
141+
if (curr.type == DiffType.DELETED && next.type == DiffType.ADDED) {
142+
val oldLine = curr.oldLine ?: ""
143143
val newLine = next.newLine ?: ""
144-
val charDiffs = CharLevelDiff().diff(oldLine, newLine)
145-
146-
merged.add(
147-
DiffEntry(
148-
oldLine = oldLine,
149-
newLine = newLine,
150-
type = DiffType.CHANGED,
151-
charDiffs = charDiffs
152-
)
153-
)
144+
val charDiffs = charLevelDiff.diff(oldLine, newLine)
145+
merged.add(DiffEntry(oldLine, newLine, DiffType.CHANGED, charDiffs))
154146
i += 2
155147
continue
156148
}
157149
}
158-
// Otherwise add as-is
159-
merged.add(current)
150+
merged.add(diffEntries[i])
160151
i++
161152
}
162153
return merged
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package dev.jahidhasanco.diffly.domain.util
2+
3+
import dev.jahidhasanco.diffly.domain.model.CharDiffType
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
7+
8+
class CharLevelDiffTest {
9+
10+
private val diffUtil = CharLevelDiff()
11+
12+
@Test
13+
fun test_emptyStrings() {
14+
val result = diffUtil.diff("", "")
15+
assertEquals(emptyList(), result)
16+
}
17+
18+
@Test
19+
fun test_identicalStrings() {
20+
val input = "hello world"
21+
val result = diffUtil.diff(input, input)
22+
assertEquals(input.length, result.size)
23+
assert(result.all { it.type == CharDiffType.UNCHANGED })
24+
}
25+
26+
@Test
27+
fun test_insertionAtEnd() {
28+
val oldStr = "hello"
29+
val newStr = "hello!"
30+
val result = diffUtil.diff(oldStr, newStr)
31+
32+
val expectedTypes = listOf(
33+
CharDiffType.UNCHANGED,
34+
CharDiffType.UNCHANGED,
35+
CharDiffType.UNCHANGED,
36+
CharDiffType.UNCHANGED,
37+
CharDiffType.UNCHANGED,
38+
CharDiffType.INSERTED
39+
)
40+
assertEquals(expectedTypes, result.map { it.type })
41+
assertEquals(newStr, result.map { it.char }.joinToString(""))
42+
}
43+
44+
@Test
45+
fun test_deletionAtEnd() {
46+
val oldStr = "hello!"
47+
val newStr = "hello"
48+
val result = diffUtil.diff(oldStr, newStr)
49+
50+
val expectedTypes = listOf(
51+
CharDiffType.UNCHANGED,
52+
CharDiffType.UNCHANGED,
53+
CharDiffType.UNCHANGED,
54+
CharDiffType.UNCHANGED,
55+
CharDiffType.UNCHANGED,
56+
CharDiffType.DELETED
57+
)
58+
assertEquals(expectedTypes, result.map { it.type })
59+
assertEquals(oldStr, result.map { it.char }.joinToString(""))
60+
}
61+
62+
@Test
63+
fun test_insertionInMiddle() {
64+
val oldStr = "helo"
65+
val newStr = "hello"
66+
val result = diffUtil.diff(oldStr, newStr)
67+
68+
val expectedTypes = listOf(
69+
CharDiffType.UNCHANGED,
70+
CharDiffType.UNCHANGED,
71+
CharDiffType.UNCHANGED,
72+
CharDiffType.INSERTED,
73+
CharDiffType.UNCHANGED
74+
)
75+
assertEquals(expectedTypes, result.map { it.type })
76+
assertEquals("hello", result.map { it.char }.joinToString(""))
77+
}
78+
79+
@Test
80+
fun test_deletionInMiddle() {
81+
val oldStr = "hello"
82+
val newStr = "helo"
83+
val result = diffUtil.diff(oldStr, newStr)
84+
85+
val expectedTypes = listOf(
86+
CharDiffType.UNCHANGED,
87+
CharDiffType.UNCHANGED,
88+
CharDiffType.UNCHANGED,
89+
CharDiffType.DELETED,
90+
CharDiffType.UNCHANGED
91+
)
92+
assertEquals(expectedTypes, result.map { it.type })
93+
assertEquals("helo",
94+
result.filter { it.type != CharDiffType.DELETED }.map { it.char }
95+
.joinToString("")
96+
)
97+
}
98+
99+
@Test
100+
fun test_substitution() {
101+
val oldStr = "hello"
102+
val newStr = "hallo"
103+
val result = diffUtil.diff(oldStr, newStr)
104+
105+
// Deletion followed by insertion for 'e' -> 'a'
106+
val types = result.map { it.type }
107+
assert(types.contains(CharDiffType.DELETED))
108+
assert(types.contains(CharDiffType.INSERTED))
109+
110+
val filteredNew =
111+
result.filter { it.type != CharDiffType.DELETED }.map { it.char }
112+
.joinToString("")
113+
assertEquals(newStr, filteredNew)
114+
}
115+
116+
@Test
117+
fun test_complexChange() {
118+
val oldStr = "kitten"
119+
val newStr = "sitting"
120+
val result = diffUtil.diff(oldStr, newStr)
121+
122+
val filteredNew =
123+
result.filter { it.type != CharDiffType.DELETED }.map { it.char }
124+
.joinToString("")
125+
assertEquals(newStr, filteredNew)
126+
}
127+
128+
@Test
129+
fun test_completelyDifferentStrings() {
130+
val oldStr = "abc"
131+
val newStr = "xyz"
132+
val result = diffUtil.diff(oldStr, newStr)
133+
134+
assertEquals(oldStr.length + newStr.length, result.size)
135+
assertEquals(
136+
listOf(
137+
CharDiffType.DELETED, CharDiffType.DELETED, CharDiffType.DELETED,
138+
CharDiffType.INSERTED, CharDiffType.INSERTED, CharDiffType.INSERTED
139+
),
140+
result.map { it.type }
141+
)
142+
val filteredNew = result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString("")
143+
assertEquals(newStr, filteredNew)
144+
}
145+
146+
@Test
147+
fun test_newStringEmpty() {
148+
val oldStr = "delete me"
149+
val newStr = ""
150+
val result = diffUtil.diff(oldStr, newStr)
151+
152+
assertEquals(oldStr.length, result.size)
153+
assert(result.all { it.type == CharDiffType.DELETED })
154+
}
155+
156+
@Test
157+
fun test_oldStringEmpty() {
158+
val oldStr = ""
159+
val newStr = "insert me"
160+
val result = diffUtil.diff(oldStr, newStr)
161+
162+
assertEquals(newStr.length, result.size)
163+
assert(result.all { it.type == CharDiffType.INSERTED })
164+
}
165+
166+
@Test
167+
fun test_singleCharSame() {
168+
val oldStr = "a"
169+
val newStr = "a"
170+
val result = diffUtil.diff(oldStr, newStr)
171+
172+
assertEquals(1, result.size)
173+
assertEquals(CharDiffType.UNCHANGED, result[0].type)
174+
}
175+
176+
@Test
177+
fun test_singleCharDifferent() {
178+
val oldStr = "a"
179+
val newStr = "b"
180+
val result = diffUtil.diff(oldStr, newStr)
181+
182+
assertEquals(2, result.size)
183+
assertEquals(listOf(CharDiffType.DELETED, CharDiffType.INSERTED), result.map { it.type })
184+
}
185+
186+
@Test
187+
fun test_repeatingCharactersChange() {
188+
val oldStr = "aaabbb"
189+
val newStr = "aaaabbb"
190+
val result = diffUtil.diff(oldStr, newStr)
191+
192+
val types = result.map { it.type }
193+
assert(types.contains(CharDiffType.INSERTED))
194+
assertEquals(newStr, result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString(""))
195+
}
196+
197+
@Test
198+
fun test_insertionAtStart() {
199+
val oldStr = "world"
200+
val newStr = "hello world"
201+
val result = diffUtil.diff(oldStr, newStr)
202+
203+
val types = result.map { it.type }
204+
// Should begin with inserted 'hello '
205+
assert(types.take(6).all { it == CharDiffType.INSERTED })
206+
assertEquals(newStr, result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString(""))
207+
}
208+
209+
@Test
210+
fun test_deletionAtStart() {
211+
val oldStr = "hello world"
212+
val newStr = "world"
213+
val result = diffUtil.diff(oldStr, newStr)
214+
215+
val types = result.map { it.type }
216+
// Should begin with deleted 'hello '
217+
assert(types.take(6).all { it == CharDiffType.DELETED })
218+
assertEquals(newStr, result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString(""))
219+
}
220+
221+
@Test
222+
fun test_largeInputPerformance() {
223+
val oldStr = "a".repeat(1000)
224+
val newStr = "a".repeat(999) + "b"
225+
val result = diffUtil.diff(oldStr, newStr)
226+
227+
// Expect 1001 results: 999 unchanged + delete + insert
228+
assertEquals(1001, result.size)
229+
230+
// Verify first 999 are unchanged
231+
assert(result.take(999).all { it.type == CharDiffType.UNCHANGED })
232+
233+
// Verify deletion and insertion
234+
assert(result[999].type == CharDiffType.DELETED)
235+
assert(result[1000].type == CharDiffType.INSERTED)
236+
237+
// Verify reconstructed new string
238+
val filteredNew = result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString("")
239+
assertEquals(newStr, filteredNew)
240+
}
241+
242+
243+
@Test
244+
fun test_mixedInsertionsAndDeletions() {
245+
val oldStr = "abcdef"
246+
val newStr = "abXYef"
247+
val result = diffUtil.diff(oldStr, newStr)
248+
249+
val filteredNew = result.filter { it.type != CharDiffType.DELETED }.map { it.char }.joinToString("")
250+
assertEquals(newStr, filteredNew)
251+
}
252+
253+
}

0 commit comments

Comments
 (0)