Skip to content

Commit 1938d93

Browse files
authored
Improved diffing for containsExactly (#84)
* Migrated Collection,List,Iterable tests to junit5 * Collect assertion test * Improved diffing for containsExactly * Update detekt
1 parent 928bba1 commit 1938d93

File tree

13 files changed

+446
-665
lines changed

13 files changed

+446
-665
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ consistent and better mirrors `assertAll { ... }` which has a similar behavior.
2424

2525
### Breaking Changes
2626
- Because of the above, `Assert<Collection>.all` and `Assert<Array>.all` have both been renamed to `each`.
27+
- `Assert<Collection>.containsExactly` is now `Assert<List>.containsExactly` as ordering does not matter on all
28+
collections (ex: `Set`).
2729

2830
### Deprecated
2931
src/main/kotlin/assertk/assert.kt

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version '0.10-SNAPSHOT'
44
buildscript {
55
ext.kotlin_version = '1.1.50'
66
ext.dokka_version = '0.9.14'
7-
ext.detekt_version = '1.0.0.RC4-3'
7+
ext.detekt_version = '1.0.0.RC5'
88

99
repositories {
1010
jcenter()

detekt-test.yml

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,2 @@
1-
complexity:
2-
TooManyFunctions:
3-
active: false
4-
5-
comments:
6-
active: true
7-
UndocumentedPublicClass:
8-
active: true
9-
searchInNestedClass: true
10-
searchInInnerClass: true
11-
searchInInnerObject: true
12-
searchInInnerInterface: true
13-
UndocumentedPublicFunction:
14-
active: true
1+
build:
2+
maxIssues: 0

detekt.yml

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,2 @@
11
build:
2-
failThreshold: 1
3-
4-
complexity:
5-
StringLiteralDuplication:
6-
active: false
7-
TooManyFunctions:
8-
active: false
9-
10-
performance:
11-
SpreadOperator:
12-
active: false
13-
14-
style:
15-
VariableMinLength:
16-
active: false
17-
ReturnCount:
18-
active: false
19-
UseDataClass:
20-
active: false
21-
ClassNaming:
22-
active: true
23-
classPattern: ^[A-Z$][a-zA-Z0-9$]*$
2+
maxIssues: 0

src/main/kotlin/assertk/assertions/collection.kt

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ fun <T : Collection<*>> Assert<T>.containsNone(vararg elements: Any?) {
5959
}
6060

6161
val notExpected = elements.filter { it in actual }
62-
expected("to contain none of:${show(elements)} but was:${show(actual)}" +
63-
" some elements were not expected:${show(notExpected)}")
62+
expected("to contain none of:${show(elements)} some elements were not expected:${show(notExpected)}")
6463
}
6564

6665
/**
@@ -75,46 +74,5 @@ fun <T : Collection<*>> Assert<T>.containsAll(vararg elements: Any?) {
7574
}
7675

7776
val notFound = elements.filterNot { it in actual }
78-
expected("to contain all:${show(elements)} but was:${show(actual)}" +
79-
" some elements were not found:${show(notFound)}")
80-
}
81-
82-
/**
83-
* Asserts the collection contains exactly the expected elements. They must be in the same order and
84-
* there must not be any extra elements.
85-
* @see [containsAll]
86-
*/
87-
fun <T : Collection<*>> Assert<T>.containsExactly(vararg elements: Any?) {
88-
val actualAsList = actual.toList()
89-
if (actualAsList == elements.toList()) {
90-
return
91-
}
92-
93-
val firstDifferingIndex = (elements zip actualAsList).indexOfFirst { it.first != it.second }
94-
val notExpected = actualAsList.toMutableList()
95-
val notFound = mutableListOf<Any?>()
96-
97-
for (element in elements) {
98-
if (element in notExpected) {
99-
notExpected.removeAt(notExpected.indexOfFirst { it == element })
100-
} else {
101-
notFound += element
102-
}
103-
}
104-
105-
if (!notExpected.isEmpty() && !notFound.isEmpty()) {
106-
expected("to contain exactly:${show(elements)} but was:${show(actual)}" +
107-
" some elements were not found:${show(notFound)}" +
108-
" some elements were not expected:${show(notExpected)}")
109-
} else if (!notFound.isEmpty()) {
110-
expected("to contain exactly:${show(elements)} but was:${show(actual)}" +
111-
" some elements were not found:${show(notFound)}")
112-
} else if (!notExpected.isEmpty()) {
113-
expected("to contain exactly:${show(elements)} but was:${show(actual)}" +
114-
" some elements were not expected:${show(notExpected)}")
115-
} else {
116-
expected("to contain exactly:${show(elements)} but was:${show(actual)}" +
117-
" first difference at index $firstDifferingIndex" +
118-
" expected:${show(elements[firstDifferingIndex])} but was:${show(actualAsList[firstDifferingIndex])}")
119-
}
77+
expected("to contain all:${show(elements)} some elements were not found:${show(notFound)}")
12078
}

src/main/kotlin/assertk/assertions/iterable.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package assertk.assertions
22

33
import assertk.Assert
4+
import assertk.assertAll
45
import assertk.assertions.support.expected
56
import assertk.assertions.support.show
67

@@ -32,7 +33,9 @@ fun <T : Iterable<*>> Assert<T>.doesNotContain(element: Any?) {
3233
* ```
3334
*/
3435
fun <E, T : Iterable<E>> Assert<T>.each(f: (Assert<E>) -> Unit) {
35-
actual.forEachIndexed { index, item ->
36-
f(assert(item, "${name ?: ""}${show(index, "[]")}"))
36+
assertAll {
37+
actual.forEachIndexed { index, item ->
38+
f(assert(item, "${name ?: ""}${show(index, "[]")}"))
39+
}
3740
}
3841
}

src/main/kotlin/assertk/assertions/list.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package assertk.assertions
22

33
import assertk.Assert
4-
import assertk.assertions.support.expected
5-
import assertk.assertions.support.fail
6-
import assertk.assertions.support.show
4+
import assertk.assertions.support.*
75

86
/**
97
* Returns an assert that assertion on the value at the given index in the list.
@@ -20,3 +18,22 @@ fun <T> Assert<List<T>>.index(index: Int, f: (Assert<T>) -> Unit) {
2018
}
2119
}
2220

21+
/**
22+
* Asserts the list contains exactly the expected elements. They must be in the same order and
23+
* there must not be any extra elements.
24+
* @see [containsAll]
25+
*/
26+
fun <T : List<*>> Assert<T>.containsExactly(vararg elements: Any?) {
27+
if (actual == elements.asList()) return
28+
29+
val diff = ListDiffer.diff(elements.asList(), actual)
30+
.filterNot { it is ListDiffer.Edit.Eq }
31+
32+
expected(diff.joinToString(prefix = "to contain exactly:\n", separator = "\n") { edit ->
33+
when (edit) {
34+
is ListDiffer.Edit.Del -> " at index:${edit.oldIndex} expected:${show(edit.oldValue)}"
35+
is ListDiffer.Edit.Ins -> " at index:${edit.newIndex} unexpected:${show(edit.newValue)}"
36+
else -> throw IllegalStateException()
37+
}
38+
})
39+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package assertk.assertions.support
2+
3+
/**
4+
* List diffing using the Myers diff algorithm.
5+
*/
6+
internal object ListDiffer {
7+
8+
fun diff(a: List<*>, b: List<*>): List<Edit> {
9+
val diff = mutableListOf<Edit>()
10+
backtrack(a, b) { prevX, prevY, x, y ->
11+
diff.add(
12+
0, when {
13+
x == prevX -> Edit.Ins(prevY, b[prevY])
14+
y == prevY -> Edit.Del(prevX, a[prevX])
15+
else -> Edit.Eq(prevX, a[prevX], prevY, b[prevY])
16+
}
17+
)
18+
}
19+
return diff
20+
}
21+
22+
sealed class Edit {
23+
data class Ins(val newIndex: Int, val newValue: Any?) : Edit()
24+
data class Del(val oldIndex: Int, val oldValue: Any?) : Edit()
25+
data class Eq(val oldIndex: Int, val oldValue: Any?, val newIndex: Int, val newValue: Any?) : Edit()
26+
}
27+
28+
private fun shortestEdit(a: List<*>, b: List<*>): List<IntArray> {
29+
val n = a.size
30+
val m = b.size
31+
val max = n + m
32+
if (max == 0) {
33+
return emptyList()
34+
}
35+
val v = IntArray(2 * max + 1)
36+
val trace = mutableListOf<IntArray>()
37+
38+
for (d in 0..max) {
39+
trace += v.copyOf()
40+
41+
for (k in -d..d step 2) {
42+
var x = if (k == -d || (k != d && v.ringIndex(k - 1) < v.ringIndex(k + 1))) {
43+
v.ringIndex(k + 1)
44+
} else {
45+
v.ringIndex(k - 1) + 1
46+
}
47+
var y = x - k
48+
49+
while (x < n && y < m && a[x] == b[y]) {
50+
x += 1
51+
y += 1
52+
}
53+
54+
v.ringIndexAssign(k, x)
55+
56+
if (x >= n && y >= m) {
57+
return trace
58+
}
59+
}
60+
}
61+
62+
return trace
63+
}
64+
65+
private fun IntArray.ringIndex(index: Int): Int = this[if (index < 0) size + index else index]
66+
67+
private fun IntArray.ringIndexAssign(index: Int, value: Int) {
68+
this[if (index < 0) size + index else index] = value
69+
}
70+
71+
private fun backtrack(a: List<*>, b: List<*>, yield: (Int, Int, Int, Int) -> Unit) {
72+
var x = a.size
73+
var y = b.size
74+
75+
val shortestEdit = shortestEdit(a, b)
76+
for (d in shortestEdit.size - 1 downTo 0) {
77+
val v = shortestEdit[d]
78+
79+
val k = x - y
80+
81+
val prevK = if (k == -d || (k != d && v.ringIndex(k - 1) < v.ringIndex(k + 1))) {
82+
k + 1
83+
} else {
84+
k - 1
85+
}
86+
87+
val prevX = v[prevK]
88+
val prevY = prevX - prevK
89+
90+
while (x > prevX && y > prevY) {
91+
yield(x - 1, y - 1, x, y)
92+
x -= 1
93+
y -= 1
94+
}
95+
if (d > 0) {
96+
yield(prevX, prevY, x, y)
97+
}
98+
x = prevX
99+
y = prevY
100+
}
101+
}
102+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package test.assertk
2+
3+
import assertk.assertions.support.ListDiffer
4+
import org.junit.Test
5+
import kotlin.test.assertEquals
6+
7+
class ListDifferTest {
8+
@Test
9+
fun empty_diff() {
10+
val diff = ListDiffer.diff(emptyList<Any>(), emptyList<Any>())
11+
12+
assertEquals(emptyList(), diff)
13+
}
14+
15+
@Test
16+
fun single_item_no_change() {
17+
val diff = ListDiffer.diff(listOf(1), listOf(1))
18+
19+
assertEquals(listOf(ListDiffer.Edit.Eq(oldIndex = 0, oldValue = 1, newIndex = 0, newValue = 1)), diff)
20+
}
21+
22+
@Test
23+
fun singe_insert() {
24+
val diff = ListDiffer.diff(emptyList<Int>(), listOf(1))
25+
26+
assertEquals(listOf(ListDiffer.Edit.Ins(newIndex = 0, newValue = 1)), diff)
27+
}
28+
29+
@Test
30+
fun singe_delete() {
31+
val diff = ListDiffer.diff(listOf(1), emptyList<Int>())
32+
33+
assertEquals(listOf(ListDiffer.Edit.Del(oldIndex = 0, oldValue = 1)), diff)
34+
}
35+
36+
@Test
37+
fun single_insert_middle() {
38+
val diff = ListDiffer.diff(listOf(1, 3), listOf(1, 2, 3))
39+
40+
assertEquals(
41+
listOf(
42+
ListDiffer.Edit.Eq(oldIndex = 0, oldValue = 1, newIndex = 0, newValue = 1),
43+
ListDiffer.Edit.Ins(newIndex = 1, newValue = 2),
44+
ListDiffer.Edit.Eq(oldIndex = 1, oldValue = 3, newIndex = 2, newValue = 3)
45+
), diff
46+
)
47+
}
48+
49+
@Test
50+
fun singe_delete_middle() {
51+
val diff = ListDiffer.diff(listOf(1, 2, 3), listOf(1, 3))
52+
53+
assertEquals(
54+
listOf(
55+
ListDiffer.Edit.Eq(oldIndex = 0, oldValue = 1, newIndex = 0, newValue = 1),
56+
ListDiffer.Edit.Del(oldIndex = 1, oldValue = 2),
57+
ListDiffer.Edit.Eq(oldIndex = 2, oldValue = 3, newIndex = 1, newValue = 3)
58+
), diff
59+
)
60+
}
61+
62+
@Test
63+
fun single_delete_multiple_inserts() {
64+
val diff = ListDiffer.diff(listOf(3), listOf(1, 2))
65+
66+
assertEquals(
67+
listOf(
68+
ListDiffer.Edit.Del(oldIndex = 0, oldValue = 3),
69+
ListDiffer.Edit.Ins(newIndex = 0, newValue = 1),
70+
ListDiffer.Edit.Ins(newIndex = 1, newValue = 2)
71+
), diff
72+
)
73+
}
74+
}

0 commit comments

Comments
 (0)