Skip to content

Commit

Permalink
Add support of Equality to all contain* matchers (#3662)
Browse files Browse the repository at this point in the history
* Add verifier parameter to all the following functions: containAll, containExactly, containExactlyInAnyOrder, containOnly

* :kotest-assertions-core:apiDump

* Fix test

* Overloads instead of additional parameters

---------

Co-authored-by: Sam <sam@sksamuel.com>
  • Loading branch information
pientaa and sksamuel committed Sep 1, 2023
1 parent 6355b52 commit d28b359
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ public final class io/kotest/matchers/collections/ContainAllIgnoringFieldsKt {

public final class io/kotest/matchers/collections/ContainAllKt {
public static final fun containAll (Ljava/util/Collection;)Lio/kotest/matchers/Matcher;
public static final fun containAll (Ljava/util/Collection;Lio/kotest/equals/Equality;)Lio/kotest/matchers/Matcher;
public static final fun containAll ([Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldContainAll (Ljava/lang/Iterable;Ljava/util/Collection;)V
public static final fun shouldContainAll (Ljava/lang/Iterable;[Ljava/lang/Object;)V
Expand All @@ -342,6 +343,7 @@ public final class io/kotest/matchers/collections/ContainAllKt {

public final class io/kotest/matchers/collections/ContainExactlyInAnyOrderKt {
public static final fun containExactlyInAnyOrder (Ljava/util/Collection;)Lio/kotest/matchers/Matcher;
public static final fun containExactlyInAnyOrder (Ljava/util/Collection;Lio/kotest/equals/Equality;)Lio/kotest/matchers/Matcher;
public static final fun containExactlyInAnyOrder ([Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldContainExactlyInAnyOrder (Ljava/util/Collection;Ljava/util/Collection;)Ljava/util/Collection;
public static final fun shouldContainExactlyInAnyOrder (Ljava/util/Collection;[Ljava/lang/Object;)Ljava/util/Collection;
Expand All @@ -352,7 +354,9 @@ public final class io/kotest/matchers/collections/ContainExactlyInAnyOrderKt {
}

public final class io/kotest/matchers/collections/ContainExactlyKt {
public static final fun appendMissingAndExtra (Ljava/lang/StringBuilder;Ljava/util/Collection;Ljava/util/Collection;)V
public static final fun containExactly (Ljava/util/Collection;)Lio/kotest/matchers/Matcher;
public static final fun containExactly (Ljava/util/Collection;Lio/kotest/equals/Equality;)Lio/kotest/matchers/Matcher;
public static final fun containExactly ([Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldContainExactly (Ljava/lang/Iterable;[Ljava/lang/Object;)V
public static final fun shouldContainExactly (Ljava/util/Collection;Ljava/util/Collection;)V
Expand Down Expand Up @@ -383,6 +387,7 @@ public final class io/kotest/matchers/collections/ContainKt {

public final class io/kotest/matchers/collections/ContainOnlyKt {
public static final fun containOnly (Ljava/util/Collection;)Lio/kotest/matchers/Matcher;
public static final fun containOnly (Ljava/util/Collection;Lio/kotest/equals/Equality;)Lio/kotest/matchers/Matcher;
public static final fun containOnly ([Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldContainOnly (Ljava/lang/Iterable;[Ljava/lang/Object;)V
public static final fun shouldContainOnly (Ljava/util/Collection;Ljava/util/Collection;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.kotest.matchers.collections

import io.kotest.assertions.print.print
import io.kotest.equals.Equality
import io.kotest.equals.types.byObjectEquality
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
Expand All @@ -21,16 +23,29 @@ infix fun <T> Array<T>.shouldNotContainAll(ts: Collection<T>) = asList().shouldN
infix fun <T> Collection<T>.shouldNotContainAll(ts: Collection<T>) = this shouldNot containAll(ts)

fun <T> containAll(vararg ts: T) = containAll(ts.asList())
fun <T> containAll(ts: Collection<T>): Matcher<Collection<T>> = object : Matcher<Collection<T>> {
override fun test(value: Collection<T>): MatcherResult {

val missing = ts.filterNot { value.contains(it) }
val passed = missing.isEmpty()
fun <T> containAll(ts: Collection<T>): Matcher<Collection<T>> =
containAll(ts, Equality.byObjectEquality(strictNumberEquality = true))

val failure =
{ "Collection should contain all of ${ts.print().value} but was missing ${missing.print().value}" }
val negFailure = { "Collection should not contain all of ${ts.print().value}" }
fun <T> containAll(
ts: Collection<T>,
verifier: Equality<T>,
): Matcher<Collection<T>> =
object : Matcher<Collection<T>> {
override fun test(value: Collection<T>): MatcherResult {

return MatcherResult(passed, failure, negFailure)
val missing = ts.filterNot { t ->
value.any { verifier.verify(it, t).areEqual() }
}
val passed = missing.isEmpty()

val failure =
{
"Collection should contain all of ${ts.print().value} based on ${verifier.name()} " +
"but was missing ${missing.print().value}"
}
val negFailure = { "Collection should not contain all of ${ts.print().value} based on ${verifier.name()}" }

return MatcherResult(passed, failure, negFailure)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import io.kotest.assertions.AssertionsConfig
import io.kotest.assertions.eq.IterableEq
import io.kotest.assertions.eq.eq
import io.kotest.assertions.print.print
import io.kotest.equals.Equality
import io.kotest.equals.types.byObjectEquality
import io.kotest.matchers.ComparableMatcherResult
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
Expand Down Expand Up @@ -56,44 +58,48 @@ fun <T> containExactly(vararg expected: T): Matcher<Collection<T>?> = containExa
/**
* Assert that a collection contains exactly, and only, the given elements, in the same order.
*/
fun <T, C : Collection<T>> containExactly(expected: C): Matcher<C?> = neverNullMatcher { actual ->
fun <T, C : Collection<T>> containExactly(expected: C): Matcher<C?> =
containExactly(expected, Equality.byObjectEquality(strictNumberEquality = true))

/**
* Assert that a collection contains exactly, and only, the given elements, in the same order.
*/
fun <T, C : Collection<T>> containExactly(
expected: C,
verifier: Equality<T>,
): Matcher<C?> = neverNullMatcher { actual ->
fun Throwable?.isDisallowedIterableComparisonFailure() =
this?.message?.startsWith(IterableEq.trigger) == true

val failureReason = eq(actual, expected, strictNumberEq = true)

val missing = expected.filterNot { t ->
actual.any { verifier.verify(it, t).areEqual() }
}
val extra = actual.filterNot { t ->
expected.any { verifier.verify(it, t).areEqual() }
}
val passed = failureReason == null

val failureMessage = {

val missing = expected.filterNot { actual.contains(it) }
val extra = actual.filterNot { expected.contains(it) }

val sb = StringBuilder()

if (failureReason.isDisallowedIterableComparisonFailure()) {
sb.append(failureReason?.message)
} else {
sb.append("Expecting: ${expected.print().value} but was: ${actual.print().value}")
}

sb.append("\n")
if (missing.isNotEmpty()) {
sb.append("Some elements were missing: ")
sb.append(missing.print().value)
if (extra.isNotEmpty()) {
sb.append(" and some elements were unexpected: ")
sb.append(extra.print().value)
buildString {
if (failureReason.isDisallowedIterableComparisonFailure()) {
append(failureReason?.message)
} else {
append(
"Collection should contain exactly: ${expected.print().value} based on ${verifier.name()}" +
" but was: ${actual.print().value}"
)
appendLine()
}
} else if (extra.isNotEmpty()) {
sb.append("Some elements were unexpected: ")
sb.append(extra.print().value)
}

sb.appendLine()
sb.toString()
appendMissingAndExtra(missing, extra)
appendLine()
}
}

val negatedFailureMessage = { "Collection should not contain exactly ${expected.print().value}" }
val negatedFailureMessage =
{ "Collection should not contain exactly: ${expected.print().value} based on ${verifier.name()}" }

if (
actual.size <= AssertionsConfig.maxCollectionEnumerateSize &&
Expand All @@ -117,7 +123,8 @@ fun <T, C : Collection<T>> containExactly(expected: C): Matcher<C?> = neverNullM
}

@JvmName("shouldNotContainExactly_iterable")
infix fun <T> Iterable<T>?.shouldNotContainExactly(expected: Iterable<T>) = this?.toList() shouldNot containExactly(expected.toList())
infix fun <T> Iterable<T>?.shouldNotContainExactly(expected: Iterable<T>) =
this?.toList() shouldNot containExactly(expected.toList())

@JvmName("shouldNotContainExactly_array")
infix fun <T> Array<T>?.shouldNotContainExactly(expected: Array<T>) = this?.asList() shouldNot containExactly(*expected)
Expand All @@ -127,3 +134,23 @@ fun <T> Array<T>?.shouldNotContainExactly(vararg expected: T) = this?.asList() s

infix fun <T, C : Collection<T>> C?.shouldNotContainExactly(expected: C) = this shouldNot containExactly(expected)
fun <T> Collection<T>?.shouldNotContainExactly(vararg expected: T) = this shouldNot containExactly(*expected)

fun StringBuilder.appendMissingAndExtra(missing: Collection<Any?>, extra: Collection<Any?>) {
if (missing.isNotEmpty()) {
append("Some elements were missing: ${missing.take(AssertionsConfig.maxCollectionPrintSize.value).print().value}")
}
if (missing.isNotEmpty() && extra.isNotEmpty()) {
append(
" and some elements were unexpected: ${
extra.take(AssertionsConfig.maxCollectionPrintSize.value).print().value
}"
)
}
if (missing.isEmpty() && extra.isNotEmpty()) {
append(
"Some elements were unexpected: ${
extra.take(AssertionsConfig.maxCollectionPrintSize.value).print().value
}"
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.kotest.matchers.collections

import io.kotest.assertions.print.print
import io.kotest.equals.Equality
import io.kotest.equals.types.byObjectEquality
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.neverNullMatcher
Expand Down Expand Up @@ -105,15 +107,42 @@ fun <T, C : Collection<T>> C?.shouldNotContainExactlyInAnyOrder(vararg expected:
}

/** Assert that a collection contains exactly the given values and nothing else, in any order. */
fun <T, C : Collection<T>> containExactlyInAnyOrder(expected: C): Matcher<C?> = neverNullMatcher { value ->
val valueGroupedCounts: Map<T, Int> = value.groupBy { it }.mapValues { it.value.size }
fun <T, C : Collection<T>> containExactlyInAnyOrder(expected: C) =
containExactlyInAnyOrder(expected, Equality.byObjectEquality(strictNumberEquality = true))

/** Assert that a collection contains exactly the given values and nothing else, in any order. */
fun <T, C : Collection<T>> containExactlyInAnyOrder(
expected: C,
verifier: Equality<T>,
): Matcher<C?> = neverNullMatcher { actual ->
val valueGroupedCounts: Map<T, Int> = actual.groupBy { it }.mapValues { it.value.size }
val expectedGroupedCounts: Map<T, Int> = expected.groupBy { it }.mapValues { it.value.size }
val passed = expectedGroupedCounts.size == valueGroupedCounts.size
&& expectedGroupedCounts.all { valueGroupedCounts[it.key] == it.value }
&& expectedGroupedCounts.all { (k, v) ->
valueGroupedCounts.filterKeys { verifier.verify(k, it).areEqual() }[k] == v
}

val missing = expected.filterNot { t ->
actual.any { verifier.verify(it, t).areEqual() }
}
val extra = actual.filterNot { t ->
expected.any { verifier.verify(it, t).areEqual() }
}

val failureMessage = {
buildString {
append("Collection should contain ${expected.print().value} based on ${verifier.name()} in any order, but was ${actual.print().value}")
appendLine()
appendMissingAndExtra(missing, extra)
appendLine()
}
}

val negatedFailureMessage = { "Collection should not contain exactly ${expected.print().value} in any order" }

MatcherResult(
passed,
{ "Collection should contain ${expected.print().value} in any order, but was ${value.print().value}" },
{ "Collection should not contain exactly ${expected.print().value} in any order" }
failureMessage,
negatedFailureMessage
)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package io.kotest.matchers.collections

import io.kotest.assertions.print.print
import io.kotest.equals.Equality
import io.kotest.equals.types.byObjectEquality
import io.kotest.matchers.*
import kotlin.jvm.JvmName
import io.kotest.assertions.AssertionsConfig.maxCollectionPrintSize as printSize

/**
* Assert that a collection contains only the given elements.
Expand All @@ -16,7 +17,7 @@ import io.kotest.assertions.AssertionsConfig.maxCollectionPrintSize as printSize
*/
@JvmName("shouldContainOnly_iterable")
infix fun <T> Iterable<T>?.shouldContainOnly(expected: Iterable<T>) =
this?.toList() should containOnly(expected.toList())
this?.toList() should containOnly(expected.toList())

/**
* Assert that an array contains only the given elements.
Expand All @@ -29,7 +30,7 @@ infix fun <T> Iterable<T>?.shouldContainOnly(expected: Iterable<T>) =
*/
@JvmName("shouldContainOnly_array")
infix fun <T> Array<T>?.shouldContainOnly(expected: Array<T>) =
this?.asList() should containOnly(*expected)
this?.asList() should containOnly(*expected)

/**
* Assert that a collection contains only the given elements.
Expand All @@ -41,7 +42,7 @@ infix fun <T> Array<T>?.shouldContainOnly(expected: Array<T>) =
* Note: Comparison is via the standard Java equals and hash code methods.
*/
fun <T> Iterable<T>?.shouldContainOnly(vararg expected: T) =
this?.toList() should containOnly(*expected)
this?.toList() should containOnly(*expected)

/**
* Assert that an array contains only the given elements.
Expand All @@ -53,7 +54,7 @@ fun <T> Iterable<T>?.shouldContainOnly(vararg expected: T) =
* Note: Comparison is via the standard Java equals and hash code methods.
*/
fun <T> Array<T>?.shouldContainOnly(vararg expected: T) =
this?.asList() should containOnly(*expected)
this?.asList() should containOnly(*expected)

/**
* Assert that a collection contains only the given elements.
Expand Down Expand Up @@ -85,30 +86,42 @@ fun <T> containOnly(vararg expected: T): Matcher<Collection<T>?> = containOnly(e
/**
* Assert that a collection contains only the given elements.
*/
fun <T, C : Collection<T>> containOnly(expectedCollection: C): Matcher<C?> = neverNullMatcher { actualCollection ->
val actualSet = actualCollection.toSet()
val expectedSet = expectedCollection.toSet()
val notExpectedSet = actualSet.minus(expectedSet)
val missingSet = expectedSet.minus(actualSet)

val passed = notExpectedSet.isEmpty() && missingSet.isEmpty()
val negatedFailureMessageSupplier = { "Collection should not have just ${expectedCollection.print().value}" }
val failureMessageSupplier = {
buildString {
if (missingSet.isNotEmpty()) {
append("Some elements were missing: ${missingSet.take(printSize.value).print().value}")
}
if (missingSet.isNotEmpty() && notExpectedSet.isNotEmpty()) {
append(" and some elements were unexpected: ${notExpectedSet.take(printSize.value).print().value}")
}
if (missingSet.isEmpty() && notExpectedSet.isNotEmpty()) {
append("Some elements were unexpected: ${notExpectedSet.take(printSize.value).print().value}")
}
appendLine()
}
}

MatcherResult(passed, failureMessageSupplier, negatedFailureMessageSupplier)
fun <T, C : Collection<T>> containOnly(expectedCollection: C) =
containOnly(expectedCollection, Equality.byObjectEquality(strictNumberEquality = true))

/**
* Assert that a collection contains only the given elements.
*/
fun <T, C : Collection<T>> containOnly(
expectedCollection: C,
verifier: Equality<T>,
): Matcher<C?> = neverNullMatcher { actualCollection ->
val actualSet = actualCollection.toSet()
val expectedSet = expectedCollection.toSet()

val notExpectedSet = actualSet.filterNot { actual ->
expectedSet.any { verifier.verify(it, actual).areEqual() }
}
val missingSet = expectedSet.filterNot { expected ->
actualSet.any { verifier.verify(it, expected).areEqual() }
}

val passed = notExpectedSet.isEmpty() && missingSet.isEmpty()
val negatedFailureMessageSupplier =
{ "Collection should not contain only ${expectedCollection.print().value} based on ${verifier.name()}" }
val failureMessageSupplier = {
buildString {
append(
"Collection should contain only: ${expectedCollection.print().value} based on ${verifier.name()}" +
" but was: ${actualCollection.print().value}"
)
appendLine()
appendMissingAndExtra(missingSet, notExpectedSet)
appendLine()
}
}

MatcherResult(passed, failureMessageSupplier, negatedFailureMessageSupplier)
}

/**
Expand All @@ -122,7 +135,7 @@ fun <T, C : Collection<T>> containOnly(expectedCollection: C): Matcher<C?> = nev
*/
@JvmName("shouldNotContainOnly_iterable")
infix fun <T> Iterable<T>?.shouldNotContainOnly(expected: Iterable<T>) =
this?.toList() shouldNot containOnly(expected.toList())
this?.toList() shouldNot containOnly(expected.toList())

/**
* Assert that an array should not have only the given elements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ExtractTest : WordSpec() {
shouldThrowAny {
extracting(persons) { name }
.shouldContainAll("<Some name that is wrong>")
}.message shouldBe """Collection should contain all of ["<Some name that is wrong>"] but was missing ["<Some name that is wrong>"]"""
}.message shouldBe """Collection should contain all of ["<Some name that is wrong>"] based on object equality but was missing ["<Some name that is wrong>"]"""
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ShouldContainAllTest : WordSpec() {
"print missing elements" {
shouldThrow<AssertionError> {
listOf<Number>(1, 2).shouldContainAll(listOf<Number>(1L, 2L))
}.shouldHaveMessage("""Collection should contain all of [1L, 2L] but was missing [1L, 2L]""")
}.shouldHaveMessage("""Collection should contain all of [1L, 2L] based on object equality but was missing [1L, 2L]""")
}
}
}
Expand Down

0 comments on commit d28b359

Please sign in to comment.