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

fix Arb.map to honor minSize parameter in both generation and shrinks #2890

Merged
merged 1 commit into from Mar 22, 2022
Merged
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
Expand Up @@ -14,15 +14,37 @@ import io.kotest.property.Shrinker
* removing elements from the failed case until it is empty.
*
* @see MapShrinker
*
* @param arb the arbitrary to populate the map entries
* @param minSize the desired minimum size of the generated map
* @param maxSize the desired maximum size of the generated map
* @param slippage when generating keys, we may have repeats if the underlying gen is random.
* The slippage factor determines how many times we continue after retrieving a duplicate key.
* The total acceptable number of misses is the slippage factor multiplied by the target set size.
* If this value is not specified, then the default slippage value of 10 will be used.
*/
fun <K, V> Arb.Companion.map(
arb: Arb<Pair<K, V>>,
minSize: Int = 1,
maxSize: Int = 100
): Arb<Map<K, V>> = arbitrary(MapShrinker()) { random ->
val size = random.random.nextInt(minSize, maxSize)
val pairs = List(size) { arb.single(random) }
pairs.toMap()
maxSize: Int = 100,
slippage: Int = 10
): Arb<Map<K, V>> = arbitrary(MapShrinker(minSize)) { random ->
val targetSize = random.random.nextInt(minSize, maxSize)
val maxMisses = targetSize * slippage
val map = mutableMapOf<K, V>()
var iterations = 0
while (iterations < maxMisses && map.size < targetSize) {
val initialSize = map.size
val (key, value) = arb.single(random)
map[key] = value
if (map.size == initialSize) iterations++
}

require(map.size >= minSize) {
"the minimum size requirement of $minSize could not be satisfied after $iterations consecutive samples"
}

map
}

/**
Expand All @@ -37,35 +59,56 @@ fun <K, V> Arb.Companion.map(
*
* @see MapShrinker
*
* @param keyArb the arbitrary to populate the keys
* @param valueArb the arbitrary to populate the values
* @param minSize the desired minimum size of the generated map
* @param maxSize the desired maximum size of the generated map
* @param slippage when generating keys, we may have repeats if the underlying gen is random.
* The slippage factor determines how many times we continue after retrieving a duplicate key.
* The total acceptable number of misses is the slippage factor multiplied by the target set size.
* If this value is not specified, then the default slippage value of 10 will be used.
*/
fun <K, V> Arb.Companion.map(
keyArb: Arb<K>,
valueArb: Arb<V>,
minSize: Int = 1,
maxSize: Int = 100
maxSize: Int = 100,
slippage: Int = 10
): Arb<Map<K, V>> {
require(minSize >= 0) { "minSize must be positive" }
require(maxSize >= 0) { "maxSize must be positive" }

return arbitrary(MapShrinker()) { random ->
val size = random.random.nextInt(minSize, maxSize)
val pairs = List(size) {
keyArb.single(random) to valueArb.single(random)
return arbitrary(MapShrinker(minSize)) { random ->
val targetSize = random.random.nextInt(minSize, maxSize)
val maxMisses = targetSize * slippage
val map = mutableMapOf<K, V>()
var iterations = 0
while (iterations < maxMisses && map.size < targetSize) {
val initialSize = map.size
map[keyArb.single(random)] = valueArb.single(random)
if (map.size == initialSize) iterations++
}
pairs.toMap()

require(map.size >= minSize) {
"the minimum size requirement of $minSize could not be satisfied after $iterations consecutive samples"
}

map
}
}

class MapShrinker<K, V> : Shrinker<Map<K, V>> {
class MapShrinker<K, V>(private val minSize: Int) : Shrinker<Map<K, V>> {
override fun shrink(value: Map<K, V>): List<Map<K, V>> {
return when (value.size) {
val shrinks = when (value.size) {
0 -> emptyList()
1 -> listOf(emptyMap())
else -> listOf(
value.toList().take(value.size / 2).toMap(),
value.toList().drop(1).toMap()
)
}

return shrinks.filter { it.size >= minSize }
}
}

Expand All @@ -74,8 +117,8 @@ class MapShrinker<K, V> : Shrinker<Map<K, V>> {
* Edgecases will be derived from [k] and [v].
*/
fun <K, V> Arb.Companion.pair(k: Arb<K>, v: Arb<V>): Arb<Pair<K, V>> {
val arbPairWithoutKeyEdges:Arb<Pair<K, V>> = Arb.bind(k.removeEdgecases(), v, ::Pair)
val arbPairWithoutValueEdges:Arb<Pair<K, V>> = Arb.bind(k, v.removeEdgecases(), ::Pair)
val arbPairWithoutKeyEdges: Arb<Pair<K, V>> = Arb.bind(k.removeEdgecases(), v, ::Pair)
val arbPairWithoutValueEdges: Arb<Pair<K, V>> = Arb.bind(k, v.removeEdgecases(), ::Pair)
val arbPair: Arb<Pair<K, V>> = Arb.bind(k, v, ::Pair)
return Arb.choice(arbPair, arbPairWithoutKeyEdges, arbPairWithoutValueEdges)
}
@@ -1,14 +1,19 @@
package com.sksamuel.kotest.property.arbitrary

import io.kotest.assertions.throwables.shouldThrowWithMessage
import io.kotest.core.spec.style.FunSpec
import io.kotest.inspectors.forAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.ints.shouldBeInRange
import io.kotest.property.Arb
import io.kotest.property.RandomSource
import io.kotest.property.arbitrary.Codepoint
import io.kotest.property.arbitrary.alphanumeric
import io.kotest.property.arbitrary.edgecases
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.map
import io.kotest.property.arbitrary.pair
import io.kotest.property.arbitrary.single
import io.kotest.property.arbitrary.string
import io.kotest.property.arbitrary.take
import io.kotest.property.arbitrary.withEdgecases
Expand Down Expand Up @@ -43,4 +48,59 @@ class MapsTest : FunSpec({
)
}
}

context("Arb.map with individual key arb and value arb") {
test("should generate map of a specified size") {
val arbMap = Arb.map(Arb.int(1..10), Arb.string(1..10, Codepoint.alphanumeric()), minSize = 5, maxSize = 10)
val maps = arbMap.take(1000, RandomSource.seeded(12345L)).toList()
maps.forAll { it.size shouldBeInRange 5..10 }
}

test("should produce shrinks that adhere to minimum size") {
val arbMap = Arb.map(Arb.int(1..10), Arb.string(1..10, Codepoint.alphanumeric()), minSize = 5, maxSize = 10)
val maps = arbMap.samples(RandomSource.seeded(12345L)).take(100).toList()
val shrinks = maps.flatMap { it.shrinks.children.value }
shrinks.forAll { it.value().size shouldBeInRange 5..10 }
}

test("should throw when the cardinality of the key arbitrary does not satisfy the required minimum size") {
val arbKey = Arb.int(1..3)
val arbMap = Arb.map(arbKey, Arb.string(1..10), minSize = 5, maxSize = 10)
shouldThrowWithMessage<IllegalArgumentException>(
"the minimum size requirement of 5 could not be satisfied after 90 consecutive samples"
) {
arbMap.single(RandomSource.seeded(1234L))
}
}
}


context("Arb.map with arb pair") {
test("should generate map of a specified size") {
val arbPair = Arb.pair(Arb.int(1..10), Arb.string(1..10, Codepoint.alphanumeric()))
val arbMap = Arb.map(arbPair, minSize = 5, maxSize = 10)
val maps = arbMap.take(100, RandomSource.seeded(12345L)).toList()
maps.forAll { it.size shouldBeInRange 5..10 }
}

test("should produce shrinks that adhere to minimum size") {
val arbPair = Arb.pair(Arb.int(1..10), Arb.string(1..10, Codepoint.alphanumeric()))
val arbMap = Arb.map(arbPair, minSize = 5, maxSize = 10)
val maps = arbMap.samples(RandomSource.seeded(12345L)).take(100).toList()
val shrinks = maps.flatMap { it.shrinks.children.value }
shrinks.forAll {
it.value().size shouldBeInRange 5..10
}
}

test("should throw when the cardinality of the key arbitrary does not satisfy the required minimum size") {
val arbPair = Arb.pair(Arb.int(1..3), Arb.string(1..10, Codepoint.alphanumeric()))
val arbMap = Arb.map(arbPair, minSize = 5, maxSize = 10)
shouldThrowWithMessage<IllegalArgumentException>(
"the minimum size requirement of 5 could not be satisfied after 90 consecutive samples"
) {
arbMap.single(RandomSource.seeded(1234L))
}
}
}
})