Skip to content

Commit

Permalink
adding filterMatching (#4658)
Browse files Browse the repository at this point in the history
<!-- 
If this PR updates documentation, please update all relevant versions of
the docs, see:
https://github.com/kotest/kotest/tree/master/documentation/versioned_docs
The documentation at
https://github.com/kotest/kotest/tree/master/documentation/docs is the
documentation for the next minor or major version _TO BE RELEASED_
-->

---------

Co-authored-by: Alex Kuznetsov <[email protected]>
  • Loading branch information
Kantis and AlexCue987 authored Jan 12, 2025
1 parent 8707885 commit f3a7739
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 0 deletions.
10 changes: 10 additions & 0 deletions documentation/docs/assertions/inspectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ xs.forNone {
}
```

If you want to filter a collection to only elements that pass the assertions, you can use the `filterMatching` method:

```kotlin
xs.filterMatching {
it.shouldContain("x")
it.shouldStartWith("bb")
}.shouldBeEmpty()
```

The full list of inspectors are:

* `forAll` which asserts every element passes the assertions
Expand All @@ -42,6 +51,7 @@ The full list of inspectors are:
* `forAny` which is an alias for `forAtLeastOne`
* `forSome` which asserts that between 1 and n-1 elements passed. Ie, if NONE pass or ALL pass then we consider that a failure.
* `forExactly(k)` which is a generalization that exactly k elements passed. This is the basis for the implementation of the other methods
* `filterMatching` which filters the collection to only include elements that pass the assertions



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.sksamuel.kotest.inspectors

import io.kotest.assertions.assertSoftly
import io.kotest.assertions.shouldFail
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.FunSpec
import io.kotest.inspectors.filterMatching
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.ints.shouldBeEven
import io.kotest.matchers.sequences.shouldBeEmpty
import io.kotest.matchers.sequences.shouldContainExactly
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldContainADigit
import io.kotest.matchers.string.shouldContainInOrder
import io.kotest.matchers.string.shouldHaveLength
import io.kotest.matchers.string.shouldNotContain
import io.kotest.matchers.types.shouldBeInstanceOf

class FilterMatchingTest : FunSpec({
test("Filtering empty list or array should return empty list") {
emptyList<String>().filterMatching { it shouldBe "foo" } shouldBe emptyList()
emptyArray<String>().filterMatching { it shouldBe "foo" } shouldBe emptyList()
}

test("Filtering empty sequence should return an empty sequence") {
emptySequence<String>()
.filterMatching { it shouldBe "foo" }
.shouldBeInstanceOf<Sequence<String>>()
.shouldBeEmpty()
}

test("Filtering should only return matching elements") {
arrayOf("a", "bb", "ccc", "dddd").filterMatching { it shouldHaveLength 1 } shouldBe listOf("a")
listOf("a", "bb", "ccc", "dddd").filterMatching { it shouldHaveLength 1 } shouldBe listOf("a")

sequenceOf("a", "bb", "ccc", "dddd")
.filterMatching { it shouldHaveLength 1 }
.shouldBeInstanceOf<Sequence<String>>()
.shouldContainExactly("a")
}

test("Filtering applies all assertions to each element") {
val assertion: (String) -> Unit = {
it shouldHaveLength 1
it shouldNotContain "b"
}

arrayOf("a", "b", "bb", "ccc", "dddd").filterMatching(assertion) shouldBe listOf("a")
listOf("a", "b", "bb", "ccc", "dddd").filterMatching(assertion) shouldBe listOf("a")

sequenceOf("a", "b", "bb", "ccc", "dddd")
.filterMatching(assertion)
.shouldBeInstanceOf<Sequence<String>>()
.shouldContainExactly("a")
}

test("Filtering should work in assertSoftly context when all assertions pass") {
shouldNotThrowAny {
assertSoftly {
arrayOf(1, 2, 3).filterMatching { it.shouldBeEven() } shouldBe listOf(2)
listOf(1, 2, 3).filterMatching { it.shouldBeEven() } shouldBe listOf(2)

sequenceOf(1, 2, 3)
.filterMatching { it.shouldBeEven() }
.shouldBeInstanceOf<Sequence<Int>>()
.shouldContainExactly(2)
}
}
}

test("Filtering in assertSoftly block should work when assertions fail") {
shouldFail {
assertSoftly {
arrayOf(1, 2, 3).filterMatching { it.shouldBeEven() }.shouldBeEmpty()
listOf(1, 2, 3).filterMatching { it.shouldBeEven() }.shouldBeEmpty()

sequenceOf(1, 2, 3)
.filterMatching { it.shouldBeEven() }
.shouldBeInstanceOf<Sequence<Int>>()
.shouldBeEmpty()
}
}.message.shouldContainInOrder(
"The following 3 assertions failed:",
"1) List should be empty but has 1 elements, first being: 2",
"2) List should be empty but has 1 elements, first being: 2",
"3) Sequence should be empty but has at least one element, first being: 2"
)
}

test("Filtering all elements should return empty list") {
listOf("foo", "bar").filterMatching {
it shouldBe "baz"
} shouldBe emptyList()
}

test("Filtering sequences should be done lazily") {
shouldNotThrowAny {
sequence<Int> {
(1..100).forEach { yield(it) }
error("Should not be evaluated") // If the filtering is not done lazily, this will throw
}.filterMatching { it.shouldBeEven() }
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,9 @@ public final class io/kotest/inspectors/InspectorAliasesKt {
}

public final class io/kotest/inspectors/InspectorsKt {
public static final fun filterMatching (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun filterMatching (Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence;
public static final fun filterMatching ([Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun forAll (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Ljava/lang/CharSequence;
public static final fun forAll (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/util/Collection;
public static final fun forAll (Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Ljava/util/Map;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.kotest.inspectors

import io.kotest.assertions.failure
import io.kotest.common.ExperimentalKotest

inline fun <K, V, C : Map<K, V>> C.forAllValues(fn: (V) -> Unit): C = apply { values.forAll(fn) }
inline fun <K, V, C : Map<K, V>> C.forAllKeys(fn: (K) -> Unit): C = apply { keys.forAll(fn) }
Expand Down Expand Up @@ -203,3 +204,24 @@ fun <T, C : Collection<T>> C.forSingle(f: (T) -> Unit): T = run {
else -> buildAssertionError("Expected a single element in the collection, but found ${results.size}.", results)
}
}

/**
* Filters the [Collection], excluding all elements which fail the given assertion block [f]
*/
@ExperimentalKotest
inline fun <T, C : Collection<T>> C.filterMatching(f: (T) -> Unit): List<T> =
filterIndexed { i, element -> runTest(i, element, f) is ElementPass<T> }

/**
* Filters the [Collection], excluding all elements which fail the given assertion block [f]
*/
@ExperimentalKotest
inline fun <T> Array<T>.filterMatching(f: (T) -> Unit): List<T> =
filterIndexed { i, element -> runTest(i, element, f) is ElementPass<T> }

/**
* Filters the [Sequence], excluding all elements which fail the given assertion block [f]
*/
@ExperimentalKotest
inline fun <T> Sequence<T>.filterMatching(crossinline f: (T) -> Unit): Sequence<T> =
filterIndexed { i, element -> runTest(i, element, f) is ElementPass<T> }

0 comments on commit f3a7739

Please sign in to comment.