Skip to content

Commit

Permalink
Improve Sequence.shouldContainExactly(...) (#3743)
Browse files Browse the repository at this point in the history
Previously, in the case of a failure, simply `toString()` on the involved sequences has been invoked to build the failure message. However, this just printed the (internal) sequence class name.

Now, the consumed and compared elements are collected in separate lists and then included in the failure message. So the failure contains all elements up the point, when a difference has been detected. This approach even works for infinite sequences.

Fixes #3742
  • Loading branch information
obecker authored Oct 21, 2023
1 parent 402ca06 commit 96e0125
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import io.kotest.assertions.eq.eq
import io.kotest.assertions.print.print
import io.kotest.matchers.*

private fun <T> Sequence<T>.toString(limit: Int = 10) = this.joinToString(", ", limit = limit)

/*
How should infinite sequences be detected, and how should they be dealt with?
Expand Down Expand Up @@ -85,33 +83,44 @@ fun <T> containExactly(vararg expected: T): Matcher<Sequence<T>?> = containExact
fun <T, C : Sequence<T>> containExactly(expected: C): Matcher<C?> = neverNullMatcher { value ->
val actualIterator = value.withIndex().iterator()
val expectedIterator = expected.withIndex().iterator()
val consumedActualValues = mutableListOf<IndexedValue<T>>()
val consumedExpectedValues = mutableListOf<IndexedValue<T>>()

fun IndexedValue<T>.printValue() = this.value.print().value
fun List<IndexedValue<T>>.printValues(hasNext: Boolean) = joinToString(postfix = if (hasNext) ", ..." else "") { it.printValue() }

var passed = true
var failMessage = "Sequence should contain exactly $expected but was $value."
var failDetails = ""
while (passed && actualIterator.hasNext() && expectedIterator.hasNext()) {
val actualElement = actualIterator.next()
consumedActualValues.add(actualElement)
val expectedElement = expectedIterator.next()
consumedExpectedValues.add(expectedElement)
if (eq(actualElement.value, expectedElement.value) != null) {
failMessage += " (expected ${expectedElement.value.print().value} at ${expectedElement.index} but found ${actualElement.value.print().value})"
failDetails = "\nExpected ${expectedElement.printValue()} at index ${expectedElement.index} but found ${actualElement.printValue()}."
passed = false
}
}

if (passed && actualIterator.hasNext()) {
failMessage += "\nActual sequence has more element than Expected sequence"
val actualElement = actualIterator.next()
consumedActualValues.add(actualElement)
failDetails = "\nActual sequence has more elements than expected sequence: found ${actualElement.printValue()} at index ${actualElement.index}."
passed = false
}

if (passed && expectedIterator.hasNext()) {
failMessage += "\nExpected sequence has more element than Actual sequence"
val expectedElement = expectedIterator.next()
consumedExpectedValues.add(expectedElement)
failDetails = "\nActual sequence has less elements than expected sequence: expected ${expectedElement.printValue()} at index ${expectedElement.index}."
passed = false
}

MatcherResult(
passed,
{ failMessage },
{ "Sequence should contain exactly ${consumedExpectedValues.printValues(expectedIterator.hasNext())} but was ${consumedActualValues.printValues(actualIterator.hasNext())}.$failDetails" },
{
"Sequence should not be exactly $expected"
"Sequence should not contain exactly ${consumedExpectedValues.printValues(expectedIterator.hasNext())}"
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.sksamuel.kotest.matchers.sequences

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.sequences.shouldContainExactly
import io.kotest.matchers.sequences.shouldNotContainExactly
import io.kotest.matchers.shouldBe

class SequenceMatchersTest : StringSpec({

"contain exactly" {
sequenceOf(1, 2, 3).shouldContainExactly(1, 2, 3)
sequenceOf(1).shouldContainExactly(1)
emptySequence<Any>().shouldContainExactly()

shouldThrow<AssertionError> {
sequenceOf(1, 2, 3).shouldContainExactly(1, 3, 5)
}.message shouldBe """
Sequence should contain exactly 1, 3, ... but was 1, 2, ....
Expected 3 at index 1 but found 2.""".trimIndent()

shouldThrow<AssertionError> {
sequenceOf(1, 2, 3).shouldContainExactly(1, 2, 3, 4)
}.message shouldBe """
Sequence should contain exactly 1, 2, 3, 4 but was 1, 2, 3.
Actual sequence has less elements than expected sequence: expected 4 at index 3.""".trimIndent()

shouldThrow<AssertionError> {
sequenceOf(1, 2, 3).shouldContainExactly(1, 2)
}.message shouldBe """
Sequence should contain exactly 1, 2 but was 1, 2, 3.
Actual sequence has more elements than expected sequence: found 3 at index 2.""".trimIndent()

shouldThrow<AssertionError> {
generateSequence(1) { it + 1 }.shouldContainExactly(1, 2, 3)
}.message shouldBe """
Sequence should contain exactly 1, 2, 3 but was 1, 2, 3, 4, ....
Actual sequence has more elements than expected sequence: found 4 at index 3.""".trimIndent()

shouldThrow<AssertionError> {
sequenceOf(1, 2, 3).shouldNotContainExactly(1, 2, 3)
}.message shouldBe "Sequence should not contain exactly 1, 2, 3"
}
})

0 comments on commit 96e0125

Please sign in to comment.