Skip to content

Commit

Permalink
fix Arb.map to honor minSize parameter in both generation and shrinks (
Browse files Browse the repository at this point in the history
  • Loading branch information
myuwono committed Mar 22, 2022
1 parent df79e60 commit 9729b7e
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 15 deletions.
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))
}
}
}
})

0 comments on commit 9729b7e

Please sign in to comment.