Skip to content

Commit

Permalink
Contains in order more details draft (#43) (#3907)
Browse files Browse the repository at this point in the history
fix the following bug - the test below was passing:
```
 "fail to detect count mismatch" {
            sequenceOf("apple", "apple", "orange") should containAllInAnyOrder(
                sequenceOf("apple", "orange", "orange")
            )
        }
```
* also fix two tests named "for subset, same count with nulls" - they
were not validating correct behavior.
* also provide more details about what exactly did not match, such as:
```
Sequence should contain the values of [1, 1, 2] in any order, but was [1, 2, 2].
Count Mismatches: 
  For 1: expected count: <2>, but was: <1>
  For 2: expected count: <1>, but was: <2>
```
  • Loading branch information
AlexCue987 committed Mar 5, 2024
1 parent cd60ed6 commit cc6ae07
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.kotest.matchers.sequences

import io.kotest.assertions.print.print

internal data class UnorderedCollectionsDifference<T>(
val missingElements: Set<T>,
val extraElements: Set<T>,
val countMismatches: List<CountMismatch<T>>
) {
private val describedMismatches = sequenceOf(
DescribedMismatch(missingElements, "Missing Elements"),
DescribedMismatch(extraElements, "Extra Elements"),
DescribedMismatch(countMismatches, "Count Mismatches")
)

fun isMatch(): Boolean {
return describedMismatches.all { it.elements.isEmpty() }
}

override fun toString(): String {
return if (isMatch()) "" else "\n" + describedMismatches
.map { it.toString() }
.filter { it.isNotEmpty() }
.joinToString("\n")
}

private data class DescribedMismatch<T>(
val elements: Collection<T>,
val description: String
) {
override fun toString(): String {
return if (elements.isEmpty()) "" else
"$description:\n${elements.joinToString("\n") { it.print().value }}"
}
}

companion object {
fun<T> of(expected: List<T>, value: List<T>): UnorderedCollectionsDifference<T> {
val expectedCounts = expected.counted()
val valueCounts = value.counted()
return UnorderedCollectionsDifference(
missingElements = expectedCounts.keys - valueCounts.keys,
extraElements = valueCounts.keys - expectedCounts.keys,
countMismatches = expectedCounts.mapNotNull { expected ->
val valueCount = valueCounts[expected.key]
valueCount?.let {
if (expected.value == valueCount) null else
CountMismatch(expected.key, expected.value, valueCount)
}
}
)
}
}

internal data class CountMismatch<T>(
val value: T,
val expectedCount: Int,
val actualCount: Int
) {
init {
require(expectedCount != actualCount) { "Expected count should be different from actual, but both were: $expectedCount" }
}

override fun toString(): String = " For ${value.print().value}: expected count: <$expectedCount>, but was: <$actualCount>"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,16 @@ fun <T> containAllInAnyOrder(vararg expected: T): Matcher<Sequence<T>?> =
fun <T, C : Sequence<T>> containAllInAnyOrder(expected: C): Matcher<C?> = neverNullMatcher { value ->
val valueAsList = value.toList()
val expectedAsList = expected.toList()
val passed = valueAsList.size == expectedAsList.size && valueAsList.containsAll(expectedAsList)
val comparison = UnorderedCollectionsDifference.of(expectedAsList, valueAsList)
MatcherResult(
passed,
{ "Sequence should contain the values of $expectedAsList in any order, but was $valueAsList" },
comparison.isMatch(),
{ "Sequence should contain the values of $expectedAsList in any order, but was $valueAsList.${comparison}" },
{ "Sequence should not contain the values of $expectedAsList in any order" }
)
}

internal fun<T> List<T>.counted(): Map<T, Int> = this.groupingBy { it }.eachCount()

infix fun <T : Comparable<T>, C : Sequence<T>> C.shouldHaveUpperBound(t: T) = this should haveUpperBound(t)

fun <T : Comparable<T>, C : Sequence<T>> haveUpperBound(t: T) = object : Matcher<C> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ class SequenceMatchersTest : WordSpec() {
sampleData.countup.shouldContainAllInAnyOrder((5..15).asSequence())
}

succeed("for subset, same count with nulls") {
fail("for subset, same count with nulls") {
sampleData.sparse.shouldContainAllInAnyOrder(sampleData.nulls)
}

Expand All @@ -735,6 +735,17 @@ class SequenceMatchersTest : WordSpec() {
fail("for same, different count") {
sampleData.repeating.shouldContainAllInAnyOrder(sampleData.unique)
}

succeed("detect different count of individual elements in collections of same length") {
shouldThrowAny{
sequenceOf(1, 2, 2).shouldContainAllInAnyOrder(sequenceOf(1, 1, 2))
}.shouldHaveMessage("""
|Sequence should contain the values of [1, 1, 2] in any order, but was [1, 2, 2].
|Count Mismatches:
| For 1: expected count: <2>, but was: <1>
| For 2: expected count: <1>, but was: <2>
""".trimMargin())
}
}

"not contain in any order" should {
Expand All @@ -754,7 +765,7 @@ class SequenceMatchersTest : WordSpec() {
sampleData.countup.shouldNotContainAllInAnyOrder((5..15).asSequence())
}

fail("for subset, same count with nulls") {
succeed("for subset, same count with nulls") {
sampleData.sparse.shouldNotContainAllInAnyOrder(sampleData.nulls)
}

Expand All @@ -774,6 +785,9 @@ class SequenceMatchersTest : WordSpec() {
sampleData.repeating.shouldNotContainAllInAnyOrder(sampleData.unique)
}

succeed("detect different count of individual elements in sequences of same length") {
sequenceOf(1, 2, 2).shouldNotContainAllInAnyOrder(sequenceOf(1, 1, 2))
}
}

"contain in order" should {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.sksamuel.kotest.matchers.sequences

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.sequences.UnorderedCollectionsDifference
import io.kotest.matchers.shouldBe

class UnorderedCollectionsDifferenceTest: StringSpec() {
init {
"detect match" {
UnorderedCollectionsDifference.of(
expected = listOf("apple", "orange", "apple"),
value = listOf("apple", "apple", "orange"),
).isMatch() shouldBe true
}
"detect missing elements" {
UnorderedCollectionsDifference.of(
expected = listOf("apple", "orange", "apple", "banana"),
value = listOf("apple", "apple", "orange"),
) shouldBe UnorderedCollectionsDifference(
missingElements = setOf("banana"),
extraElements = setOf(),
countMismatches = listOf()
)
}
"detect extra elements" {
UnorderedCollectionsDifference.of(
expected = listOf("apple", "orange", "apple"),
value = listOf("apple", "apple", "orange", "banana"),
) shouldBe UnorderedCollectionsDifference(
missingElements = setOf(),
extraElements = setOf("banana"),
countMismatches = listOf()
)
}
"detect count mismatch" {
UnorderedCollectionsDifference.of(
expected = listOf("apple", "orange", "apple"),
value = listOf("apple", "apple", "orange", "orange"),
) shouldBe UnorderedCollectionsDifference(
missingElements = setOf(),
extraElements = setOf(),
countMismatches = listOf(
UnorderedCollectionsDifference.CountMismatch(
value = "orange",
expectedCount = 1,
actualCount = 2
)
)
)
}
}
}

0 comments on commit cc6ae07

Please sign in to comment.