-
Notifications
You must be signed in to change notification settings - Fork 624
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
When keys are data classes with several fields, it really helps to find similarities between actual and expected keys, as shown in the section "Possible matches for missing keys" of the following output, where the matcher finds the most similar key: ``` val sweetGreenApple = Fruit("apple", "green", "sweet") val sweetGreenPear = Fruit("pear", "green", "sweet") val e = shouldThrow<AssertionError> { mapOf( sweetGreenApple to 1 ) should containExactly(mapOf(sweetGreenPear to 1)) } e.message shouldBe """ | |Expected: | mapOf(Fruit(name=apple, color=green, taste=sweet) to 1) |should be equal to: | mapOf(Fruit(name=pear, color=green, taste=sweet) to 1) |but differs by: | missing keys: | Fruit(name=pear, color=green, taste=sweet) | extra keys: | Fruit(name=apple, color=green, taste=sweet) | |Possible matches for missing keys: | | expected: Fruit(name=pear, color=green, taste=sweet), | but was: Fruit(name=apple, color=green, taste=sweet), | distance: 0.67, | fields: | name expected: "pear", but was: "apple" | color = "green" | taste = "sweet" """.trimMargin() ``` --------- Co-authored-by: Emil Kantis <emil.kantis@protonmail.com>
- Loading branch information
1 parent
968653b
commit cd60ed6
Showing
25 changed files
with
692 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
...s/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/maps/TestData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.sksamuel.kotest.matchers.maps | ||
|
||
data class Thing( | ||
val color: String, | ||
val shape: String, | ||
) | ||
|
||
val redCircle = Thing("red", "circle") | ||
val blueCircle = Thing("blue", "circle") | ||
val redTriangle = Thing("red", "triangle") | ||
val blueTriangle = Thing("blue", "triangle") | ||
|
||
data class OtherThing( | ||
val color: String, | ||
val shape: String, | ||
) | ||
|
||
val otherRedCircle = OtherThing("red", "circle") | ||
|
||
data class ThingWithPrivateField( | ||
val color: String, | ||
private val shape: String, | ||
) | ||
|
||
val redCircleWithPrivateField = ThingWithPrivateField("red", "circle") | ||
|
||
data class CountedName( | ||
val name: String, | ||
val count: Int | ||
) | ||
|
||
val oneApple = CountedName("apple", 1) | ||
val twoApples = CountedName("apple", 2) | ||
val oneOrange = CountedName("orange", 1) | ||
val twoOranges = CountedName("orange", 2) | ||
val threeLemons = CountedName("lemon", 3) | ||
|
||
data class Fruit( | ||
val name: String, | ||
val color: String, | ||
val taste: String | ||
) | ||
|
||
val sweetGreenApple = Fruit("apple", "green", "sweet") | ||
val sweetRedApple = Fruit("apple", "red", "sweet") | ||
val sweetGreenPear = Fruit("pear", "green", "sweet") | ||
val sourYellowLemon = Fruit("lemon", "yellow", "sour") | ||
val tartRedCherry = Fruit("cherry", "red", "tart") | ||
val bitterPurplePlum = Fruit("plum", "purple", "bitter") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
...ns/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/similarity/PossibleMatches.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package io.kotest.similarity | ||
|
||
expect fun<T> possibleMatchesDescription(expected: Set<T>, actual: T): String | ||
|
4 changes: 4 additions & 0 deletions
4
...s/kotest-assertions-shared/src/desktopMain/kotlin/io/kotest/similarity/PossibleMatches.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package io.kotest.similarity | ||
|
||
actual fun<T> possibleMatchesDescription(expected: Set<T>, actual: T): String = "" | ||
|
4 changes: 4 additions & 0 deletions
4
...rtions/kotest-assertions-shared/src/jsMain/kotlin/io/kotest/similarity/PossibleMatches.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package io.kotest.similarity | ||
|
||
actual fun<T> possibleMatchesDescription(expected: Set<T>, actual: T): String = "" | ||
|
42 changes: 42 additions & 0 deletions
42
...tions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/BestMatchFinder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package io.kotest.similarity | ||
|
||
import java.math.BigDecimal | ||
|
||
internal fun findBestMatches(element: Any?, candidates: List<Any?>): List<IndexedComparisonResult> { | ||
val comparisonResults = candidates.mapIndexed { index, candidate -> | ||
IndexedComparisonResult( | ||
index, | ||
VanillaDistanceCalculator.compare("", candidate, element) | ||
) | ||
} | ||
|
||
val (completeMatches, partialMatches) = comparisonResults.filter { | ||
it.comparisonResult !is AtomicMismatch | ||
}.partition { it.comparisonResult is Match } | ||
|
||
if(completeMatches.isNotEmpty()) return completeMatches | ||
|
||
return partialMatches.asSequence() | ||
.mapNotNull { | ||
when(it.comparisonResult) { | ||
is MismatchByField -> IndexedMismatchByField(it.index, it.comparisonResult) | ||
else -> null | ||
} | ||
} | ||
.filter { it.comparisonResult.distance.distance > BigDecimal.ZERO } | ||
.topWithTiesBy { | ||
it.comparisonResult.distance.distance | ||
}.map { | ||
IndexedComparisonResult(it.index, it.comparisonResult) | ||
} | ||
} | ||
|
||
internal data class IndexedComparisonResult( | ||
val index: Int, | ||
val comparisonResult: ComparisonResult | ||
) | ||
|
||
internal data class IndexedMismatchByField( | ||
val index: Int, | ||
val comparisonResult: MismatchByField | ||
) |
19 changes: 19 additions & 0 deletions
19
...rtions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/ClosestMatches.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.kotest.similarity | ||
|
||
internal fun<T> closestMatches(expected: Set<T>, actual: T): List<PairComparison<T>> { | ||
return expected.asSequence().mapNotNull { candidate -> | ||
val comparisonResult = VanillaDistanceCalculator.compare("", candidate, actual) | ||
if (comparisonResult is MismatchByField && | ||
comparisonResult.distance.distance > Distance.COMPLETE_MISMATCH_VALUE) { | ||
PairComparison(actual, candidate, comparisonResult) | ||
} else null | ||
}.topWithTiesBy { | ||
it.comparisonResult.distance.distance | ||
} | ||
} | ||
|
||
internal data class PairComparison<T>( | ||
val value: T, | ||
val possibleMatch: T, | ||
val comparisonResult: MismatchByField | ||
) |
69 changes: 69 additions & 0 deletions
69
...assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/Comparison.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package io.kotest.similarity | ||
|
||
import io.kotest.assertions.print.print | ||
import io.kotest.similarity.Distance.Companion.CompleteMatch | ||
import io.kotest.similarity.Distance.Companion.CompleteMismatch | ||
import java.math.BigDecimal | ||
|
||
internal sealed interface ComparisonResult { | ||
val match: Boolean | ||
fun description(): String | ||
} | ||
|
||
internal data class Match( | ||
val field: String, | ||
val value: Any? | ||
): ComparisonResult { | ||
val distance | ||
get() = CompleteMatch | ||
override fun description() = " $field = ${value.print().value}" | ||
override val match: Boolean | ||
get() = true | ||
} | ||
|
||
internal data class AtomicMismatch( | ||
val field: String, | ||
val expected: Any?, | ||
val actual: Any?, | ||
val distance: Distance = CompleteMismatch | ||
): ComparisonResult { | ||
override fun description() = " \"$field\" expected: <${expected.print().value}>, but was: <${actual.print().value}>" | ||
override val match: Boolean | ||
get() = false | ||
} | ||
|
||
internal data class MismatchByField( | ||
val field: String, | ||
val expected: Any, | ||
val actual: Any, | ||
val comparisonResults: List<ComparisonResult>, | ||
val distance: Distance | ||
): ComparisonResult { | ||
override fun description() = """$field expected: $expected, | ||
| but was: $actual, | ||
| The following fields did not match: | ||
|${comparisonResults.filter{ !it.match }.joinToString("\n ") { it.description() }}""".trimMargin() | ||
override val match: Boolean | ||
get() = comparisonResults.all { it.match } | ||
} | ||
|
||
internal fun possibleMatchDescription(possibleMatch: PossibleMatch): String = when(possibleMatch.comparisonResult) { | ||
is Match -> "actual[${possibleMatch.actual.index}] == expected[${possibleMatch.matchInExpected.index}], is: ${possibleMatch.actual.element}" | ||
is AtomicMismatch -> "actual[${possibleMatch.actual.index}] = ${possibleMatch.actual.element} is similar to\nexpected[${possibleMatch.matchInExpected.index}] = ${possibleMatch.matchInExpected.element}\n" | ||
is MismatchByField -> possibleMismatchByFieldDescription(possibleMatch) | ||
} | ||
|
||
internal fun possibleMismatchByFieldDescription(possibleMatch: PossibleMatch): String { | ||
val mismatchByField = (possibleMatch.comparisonResult as MismatchByField) | ||
val header = "actual[${possibleMatch.actual.index}] = ${possibleMatch.actual.element} is similar to\nexpected[${possibleMatch.matchInExpected.index}] = ${possibleMatch.matchInExpected.element}\n" | ||
val fields = mismatchByField.comparisonResults.joinToString("\n") { | ||
comparisonResultDescription(it) | ||
} | ||
return "$header\n$fields" | ||
} | ||
|
||
internal fun comparisonResultDescription(comparisonResult: ComparisonResult): String = when(comparisonResult) { | ||
is Match -> "\"${comparisonResult.field}\" = ${comparisonResult.value}" | ||
is AtomicMismatch -> "\"${comparisonResult.field}\" expected: <${comparisonResult.expected}>,\n but was: <${comparisonResult.actual}>" | ||
else -> "Unknown $comparisonResult" | ||
} |
35 changes: 35 additions & 0 deletions
35
...sertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/FieldsReader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package io.kotest.similarity | ||
|
||
import kotlin.reflect.full.memberProperties | ||
import kotlin.reflect.full.primaryConstructor | ||
import kotlin.reflect.KProperty1 | ||
import kotlin.reflect.jvm.isAccessible | ||
|
||
internal class FieldsReader { | ||
fun fieldsOf(instance: Any): List<FieldAndValue> { | ||
require(instance::class.isData) { | ||
"Data classes expected, was: ${instance::class.qualifiedName}" | ||
} | ||
val fields = getFields(instance) | ||
return fields.asSequence() | ||
.map { | ||
it.getter.isAccessible = true | ||
val value = it.getter.call(instance) | ||
FieldAndValue(it.name, value) | ||
} | ||
.toList() | ||
} | ||
|
||
fun getFields(instance: Any): Collection<KProperty1<out Any, *>> { | ||
val fields = instance::class.memberProperties | ||
val klass = instance.javaClass.kotlin | ||
val primaryConstructor = klass.primaryConstructor | ||
val params = primaryConstructor!!.parameters | ||
return params.map { param -> fields.first { field -> field.name == param.name} } | ||
} | ||
} | ||
|
||
internal data class FieldAndValue( | ||
val name: String, | ||
val value: Any? | ||
) |
25 changes: 25 additions & 0 deletions
25
...s/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/IDistanceCalculator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package io.kotest.similarity | ||
|
||
import java.math.BigDecimal | ||
|
||
internal interface IDistanceCalculator { | ||
fun compare(field: String, expected: Any?, actual: Any?): ComparisonResult | ||
} | ||
|
||
internal data class Distance( | ||
val distance: BigDecimal | ||
) { | ||
init { | ||
require(distance in COMPLETE_MISMATCH_VALUE..COMPLETE_MATCH_VALUE) { | ||
"Distance must be between 0 and 1, was: $distance" | ||
} | ||
} | ||
companion object { | ||
val COMPLETE_MISMATCH_VALUE: BigDecimal = BigDecimal.ZERO | ||
val COMPLETE_MATCH_VALUE: BigDecimal = BigDecimal.ONE | ||
|
||
val CompleteMatch = Distance(COMPLETE_MATCH_VALUE) | ||
val CompleteMismatch = Distance(COMPLETE_MISMATCH_VALUE) | ||
} | ||
} | ||
|
22 changes: 22 additions & 0 deletions
22
...ertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/MatchByFields.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package io.kotest.similarity | ||
|
||
import java.math.BigDecimal | ||
import java.math.MathContext | ||
|
||
internal fun matchByFields(field: String, expected: Any, actual: Any): ComparisonResult { | ||
require(expected::class == actual::class) { | ||
"Expected and Actual should be of same type, were ${expected::class.qualifiedName} and ${actual::class.qualifiedName}" | ||
} | ||
val fieldsReader = FieldsReader() | ||
val expectedFields = fieldsReader.fieldsOf(expected) | ||
val actualFields = fieldsReader.fieldsOf(actual) | ||
val comparisons = expectedFields.mapIndexed{ | ||
index, expectedField -> | ||
val actualField = actualFields[index] | ||
VanillaDistanceCalculator.compare(expectedField.name, expectedField.value, actualField.value) | ||
} | ||
val matches = comparisons.count { it is Match } | ||
val distance = Distance(BigDecimal(matches) | ||
.divide(BigDecimal(expectedFields.size), MathContext(2))) | ||
return MismatchByField(field, expected, actual, comparisons, distance) | ||
} |
13 changes: 13 additions & 0 deletions
13
...ertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/PossibleMatch.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package io.kotest.similarity | ||
|
||
internal data class IndexedElement( | ||
val index: Int, | ||
val element: Any? | ||
) | ||
|
||
internal data class PossibleMatch( | ||
val matchInExpected: IndexedElement, | ||
val actual: IndexedElement, | ||
val comparisonResult: ComparisonResult | ||
) | ||
|
10 changes: 10 additions & 0 deletions
10
...tions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/similarity/PossibleMatches.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package io.kotest.similarity | ||
|
||
actual fun<T> possibleMatchesDescription(expected: Set<T>, actual: T): String { | ||
val possibleMatches = closestMatches(expected, actual) | ||
return if(possibleMatches.isEmpty()) "" | ||
else { | ||
"\n${possibleMatches.joinToString("\n\n"){it.comparisonResult.description()}}" | ||
} | ||
} | ||
|
Oops, something went wrong.