Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add-similarity-to-containExactly (#45) #3910

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import io.kotest.matchers.MatcherResult
import io.kotest.matchers.neverNullMatcher
import io.kotest.matchers.should
import io.kotest.matchers.shouldNot
import io.kotest.similarity.possibleMatchesDescription
import io.kotest.similarity.possibleMatchesDescriptions
import kotlin.jvm.JvmName

/**
Expand Down Expand Up @@ -93,6 +95,7 @@ fun <T, C : Collection<T>> containExactly(

appendMissingAndExtra(missing, extra)
appendLine()
appendPossibleMatches(missing, expected)
}
}

Expand All @@ -112,7 +115,7 @@ fun <T, C : Collection<T>> containExactly(
MatcherResult(
passed,
{
failureMessage() + "(set the 'kotest.assertions.collection.enumerate.size' JVM property to see full output)"
failureMessage() + "(set the 'kotest.assertions.collection.enumerate.size' JVM property to see full output for collection)"
},
{
negatedFailureMessage() + "(set the 'kotest.assertions.collection.enumerate.size' JVM property to see full output)"
Expand Down Expand Up @@ -161,3 +164,16 @@ fun StringBuilder.appendMissingAndExtra(missing: Collection<Any?>, extra: Collec
)
}
}

internal fun<T> StringBuilder.appendPossibleMatches(missing: Collection<T>, expected: Collection<T>) {
val expectedAsSet = expected.toSet()
val possibleMatches = missing.flatMap {
possibleMatchesDescriptions(expectedAsSet, it)
}.filter { it.isNotEmpty() }
if(possibleMatches.isNotEmpty()) {
append("\nPossible matches:\n${possibleMatches.take(AssertionsConfig.maxSimilarityPrintSize.value).joinToString("\n\n")}")
}
if(AssertionsConfig.maxSimilarityPrintSize.value < possibleMatches.size) {
append("\nPrinted first ${AssertionsConfig.maxSimilarityPrintSize.value} similarities out of ${possibleMatches.size}, (set the 'kotest.assertions.similarity.print.size' JVM property to see full output for similarity)\n")
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.sksamuel.kotest.matchers.collections

import io.kotest.assertions.assertSoftly
import io.kotest.assertions.shouldFailWithMessage
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.WordSpec
Expand All @@ -15,6 +16,9 @@ import io.kotest.matchers.collections.shouldNotContainExactlyInAnyOrder
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNot
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldEndWith
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.throwable.shouldHaveMessage
import io.kotest.property.Arb
import io.kotest.property.Exhaustive
Expand Down Expand Up @@ -172,20 +176,35 @@ class ShouldContainExactlyTest : WordSpec() {
}

"include extras when too many" {
shouldThrow<AssertionError> {
val message = shouldThrow<AssertionError> {
listOf(
Blonde("foo", true, 23423, inputPath)
).shouldContainExactly(
Blonde("foo", true, 23423, inputPath),
Blonde("woo", true, 97821, inputPath)
)
}.message?.trim() shouldBe
}.message?.trim()
message.shouldContain(
"""
|Collection should contain exactly: [Blonde(a=foo, b=true, c=23423, p=$expectedPath), Blonde(a=woo, b=true, c=97821, p=$expectedPath)] but was: [Blonde(a=foo, b=true, c=23423, p=$expectedPath)]
|Some elements were missing: [Blonde(a=woo, b=true, c=97821, p=$expectedPath)]
|
""".trimMargin()
)
message.shouldContain(
"""
|Possible matches:
| expected: Blonde(a=foo, b=true, c=23423, p=a/b/c),
| but was: Blonde(a=woo, b=true, c=97821, p=a/b/c),
| The following fields did not match:
| "a" expected: <"foo">, but was: <"woo">
| "c" expected: <23423>, but was: <97821>
""".trimMargin()
)
message.shouldContain(
"""
|expected:<[Blonde(a=foo, b=true, c=23423, p=$expectedPath), Blonde(a=woo, b=true, c=97821, p=$expectedPath)]> but was:<[Blonde(a=foo, b=true, c=23423, p=$expectedPath)]>
""".trimMargin()
)
}

"include missing when too few" {
Expand All @@ -207,25 +226,42 @@ class ShouldContainExactlyTest : WordSpec() {
}

"include missing and extras when not the right amount" {
shouldThrow<AssertionError> {
val message = shouldThrow<AssertionError> {
listOf(
Blonde("foo", true, 23423, inputPath),
Blonde("hoo", true, 96915, inputPath)
).shouldContainExactly(
Blonde("woo", true, 97821, inputPath),
Blonde("goo", true, 51984, inputPath)
)
}.message?.trim() shouldBe
}.message?.trim()
message shouldStartWith
"""
|Collection should contain exactly: [Blonde(a=woo, b=true, c=97821, p=$expectedPath), Blonde(a=goo, b=true, c=51984, p=$expectedPath)] but was: [Blonde(a=foo, b=true, c=23423, p=$expectedPath), Blonde(a=hoo, b=true, c=96915, p=$expectedPath)]
|Some elements were missing: [Blonde(a=woo, b=true, c=97821, p=$expectedPath), Blonde(a=goo, b=true, c=51984, p=$expectedPath)] and some elements were unexpected: [Blonde(a=foo, b=true, c=23423, p=$expectedPath), Blonde(a=hoo, b=true, c=96915, p=$expectedPath)]
""".trimMargin()
message shouldContain """
|Possible matches:
| expected: Blonde(a=goo, b=true, c=51984, p=a/b/c),
| but was: Blonde(a=woo, b=true, c=97821, p=a/b/c),
| The following fields did not match:
| "a" expected: <"goo">, but was: <"woo">
| "c" expected: <51984>, but was: <97821>
|
| expected: Blonde(a=woo, b=true, c=97821, p=a/b/c),
| but was: Blonde(a=goo, b=true, c=51984, p=a/b/c),
| The following fields did not match:
| "a" expected: <"woo">, but was: <"goo">
| "c" expected: <97821>, but was: <51984>
""".trimMargin()
message shouldContain
"""
|expected:<[Blonde(a=woo, b=true, c=97821, p=$expectedPath), Blonde(a=goo, b=true, c=51984, p=$expectedPath)]> but was:<[Blonde(a=foo, b=true, c=23423, p=$expectedPath), Blonde(a=hoo, b=true, c=96915, p=$expectedPath)]>
""".trimMargin()
}

"exclude full print with warning on large collections" {
shouldThrow<AssertionError> {
val thrown = shouldThrow<AssertionError> {
listOf(
Blonde("foo", true, 1, inputPath),
Blonde("foo", true, 2, inputPath),
Expand Down Expand Up @@ -271,12 +307,27 @@ class ShouldContainExactlyTest : WordSpec() {
Blonde("foo", true, 20, inputPath),
Blonde("foo", true, 21, inputPath),
)
}.message?.trim() shouldBe
"""
}
val message = thrown.message?.trim()
assertSoftly {
message shouldContain
"""
|Collection should contain exactly: [Blonde(a=foo, b=true, c=77, p=a/b/c), Blonde(a=foo, b=true, c=2, p=a/b/c), Blonde(a=foo, b=true, c=3, p=a/b/c), Blonde(a=foo, b=true, c=4, p=a/b/c), Blonde(a=foo, b=true, c=5, p=a/b/c), Blonde(a=foo, b=true, c=6, p=a/b/c), Blonde(a=foo, b=true, c=7, p=a/b/c), Blonde(a=foo, b=true, c=8, p=a/b/c), Blonde(a=foo, b=true, c=9, p=a/b/c), Blonde(a=foo, b=true, c=10, p=a/b/c), Blonde(a=foo, b=true, c=11, p=a/b/c), Blonde(a=foo, b=true, c=12, p=a/b/c), Blonde(a=foo, b=true, c=13, p=a/b/c), Blonde(a=foo, b=true, c=14, p=a/b/c), Blonde(a=foo, b=true, c=15, p=a/b/c), Blonde(a=foo, b=true, c=16, p=a/b/c), Blonde(a=foo, b=true, c=17, p=a/b/c), Blonde(a=foo, b=true, c=18, p=a/b/c), Blonde(a=foo, b=true, c=19, p=a/b/c), Blonde(a=foo, b=true, c=20, p=a/b/c), ...and 1 more (set the 'kotest.assertions.collection.print.size' JVM property to see more / less items)] but was: [Blonde(a=foo, b=true, c=1, p=a/b/c), Blonde(a=foo, b=true, c=2, p=a/b/c), Blonde(a=foo, b=true, c=3, p=a/b/c), Blonde(a=foo, b=true, c=4, p=a/b/c), Blonde(a=foo, b=true, c=5, p=a/b/c), Blonde(a=foo, b=true, c=6, p=a/b/c), Blonde(a=foo, b=true, c=7, p=a/b/c), Blonde(a=foo, b=true, c=8, p=a/b/c), Blonde(a=foo, b=true, c=9, p=a/b/c), Blonde(a=foo, b=true, c=10, p=a/b/c), Blonde(a=foo, b=true, c=11, p=a/b/c), Blonde(a=foo, b=true, c=12, p=a/b/c), Blonde(a=foo, b=true, c=13, p=a/b/c), Blonde(a=foo, b=true, c=14, p=a/b/c), Blonde(a=foo, b=true, c=15, p=a/b/c), Blonde(a=foo, b=true, c=16, p=a/b/c), Blonde(a=foo, b=true, c=17, p=a/b/c), Blonde(a=foo, b=true, c=18, p=a/b/c), Blonde(a=foo, b=true, c=19, p=a/b/c), Blonde(a=foo, b=true, c=20, p=a/b/c), ...and 1 more (set the 'kotest.assertions.collection.print.size' JVM property to see more / less items)]
|Some elements were missing: [Blonde(a=foo, b=true, c=77, p=a/b/c)] and some elements were unexpected: [Blonde(a=foo, b=true, c=1, p=a/b/c)]
|(set the 'kotest.assertions.collection.enumerate.size' JVM property to see full output)
""".trimMargin()
message shouldContain
"""
| expected: Blonde(a=foo, b=true, c=2, p=a/b/c),
| but was: Blonde(a=foo, b=true, c=77, p=a/b/c),
| The following fields did not match:
| "c" expected: <2>, but was: <77>
""".trimMargin()
message shouldContain "Printed first 5 similarities out of 20, (set the 'kotest.assertions.similarity.print.size' JVM property to see full output for similarity)"
message shouldContain
"""
|(set the 'kotest.assertions.collection.enumerate.size' JVM property to see full output for collection)
""".trimMargin()
}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public final class io/kotest/assertions/AssertionsConfig {
public final fun getMaxCollectionEnumerateSize ()I
public final fun getMaxCollectionPrintSize ()Lio/kotest/assertions/ConfigValue;
public final fun getMaxErrorsOutput ()I
public final fun getMaxSimilarityPrintSize ()Lio/kotest/assertions/ConfigValue;
public final fun getMultiLineDiff ()Ljava/lang/String;
public final fun getShowDataClassDiff ()Z
}
Expand Down Expand Up @@ -3062,6 +3063,10 @@ public final class io/kotest/matchers/ShouldKt {
public static final fun shouldNotHave (Ljava/lang/Object;Lio/kotest/matchers/Matcher;)V
}

public final class io/kotest/similarity/PossibleMatchesDescriptionsKt {
public static final fun possibleMatchesDescriptions (Ljava/util/Set;Ljava/lang/Object;)Ljava/util/List;
}

public final class io/kotest/similarity/PossibleMatchesForSetKt {
public static final fun possibleMatchesForSet (ZLjava/util/Set;Ljava/util/Set;Lio/kotest/equals/Equality;)Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ object AssertionsConfig {

val maxCollectionPrintSize: ConfigValue<Int> =
EnvironmentConfigValue<Int>("kotest.assertions.collection.print.size", 20, String::toInt)

val maxSimilarityPrintSize: ConfigValue<Int> =
EnvironmentConfigValue<Int>("kotest.assertions.similarity.print.size", 5, String::toInt)
}

interface ConfigValue<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.kotest.similarity

expect fun<T> possibleMatchesDescriptions(expected: Set<T>, actual: T): List<String>

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.kotest.similarity

actual fun<T> possibleMatchesDescriptions(expected: Set<T>, actual: T): List<String> = listOf()

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.kotest.similarity

actual fun<T> possibleMatchesDescriptions(expected: Set<T>, actual: T): List<String> = listOf()

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal data class MismatchByField(
override fun description() = """$field expected: $expected,
| but was: $actual,
| The following fields did not match:
|${comparisonResults.filter{ !it.match }.joinToString("\n ") { it.description() }}""".trimMargin()
|${comparisonResults.filter{ !it.match }.joinToString("\n") { it.description() }}""".trimMargin()
override val match: Boolean
get() = comparisonResults.all { it.match }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.kotest.similarity

actual fun<T> possibleMatchesDescriptions(expected: Set<T>, actual: T): List<String> {
val possibleMatches = closestMatches(expected, actual)
return possibleMatches.map {
it.comparisonResult.description()
}.filter { it.isNotEmpty() }
}