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

Add anyForType() dedicated to sealed classes and interfaces in Kotlin module: anyForSubtypeOf() #555

Merged
merged 7 commits into from
Mar 5, 2024
44 changes: 44 additions & 0 deletions documentation/src/docs/include/kotlin-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,34 @@ Getting a type-based generator using the Java API looks a bit awkward in Kotlin:
`Arbitraries.forType(MyType::class.java)`.
There's a more Kotlinish way to do the same: `anyForType<MyType>()`.

`anyForType<MyType>()` is limited to concrete classes. For example, it cannot
handle sealed class or interface by looking for sealed subtypes.
`anyForSubtypeOf<MyInterface>()` exists for such situation.

```kotlin
sealed interface Character
sealed interface Hero : Character
class Knight(val name: String) : Hero
class Wizard(val name: String) : Character

val arbitrary = anyForSubtypeOf<Character>()
```

In the previous example, the created arbitrary provides arbitrarily any instances of `Knight` or `Wizard`.
The arbitrary is recursively based on any sealed subtype.
Under the hood, it uses `anyForType<Subtype>()` for each subtype.
However, this can be customized subtype by subtype, by providing a custom arbitrary:

```kotlin
anyForSubtypeOf<Character> {
provide<Wizard> { Arbitraries.of(Wizard("Merlin"),Wizard("Élias de Kelliwic’h")) }
}
```

More over, like `anyForType<>()`, `anyForSubtypeOf<>()` can be applied recursively (default is false):
`anyForSubtypeOf<SealedClass>(enableArbitraryRecursion = true)`.


##### Diverse Convenience Functions

- `combine(a1: Arbitrary<T1>, ..., (v1: T1, ...) -> R)` can replace all
Expand Down Expand Up @@ -547,3 +575,19 @@ combine {
you have to generate values of the _inlined class_ instead,
which would be `String` in the example above.
[Create an issue](https://github.com/jqwik-team/jqwik/issues/new) if that bothers you too much.

- `anyForSubtypeOf<>()` does not work as expected when a sealed subtype requires
a concrete class to be created, which requires a sealed class or interface.
The following example demonstrate the issue:

```kotlin
sealed interface Character
class Knight(val kingdom: Kingdom) : Character
class Kingdom(val army: Army)
sealed interface Army

val arbitrary = anyForSubtypeOf<Character>() // this arbitrary will fail during generation
```

However, the workaround consist on the registration of an arbitrary dedicated
to involved sealed class or interface, `Army` in the example above.
70 changes: 70 additions & 0 deletions kotlin/src/main/kotlin/net/jqwik/kotlin/api/AnyForSubtypeOfDsl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.jqwik.kotlin.api

import net.jqwik.api.Arbitraries
import net.jqwik.api.Arbitrary
import net.jqwik.api.arbitraries.TypeArbitrary
import kotlin.reflect.KClass


/**
* Creates [Arbitrary] with subtypes of a sealed class or interface [T].
* If a subtype is a sealed class or interface, its subtypes are used to create [Arbitrary]. This is done recursively.
* [TypeArbitrary] are created by default under the hood, but this can be customized, for each subtype, with [SubtypeScope.provide] .
* ```kotlin
* anyForSubtypeOf<MyInterface> {
* provide<MyImplementation1> { customArbitrary1() }
* provide<MyImplementation2> { customArbitrary2() }
* }
* ```
* @param enableArbitraryRecursion is applied to all created [TypeArbitrary].
*/
inline fun <reified T> anyForSubtypeOf(
enableArbitraryRecursion: Boolean = false,
crossinline subtypeScope: SubtypeScope<T>.() -> Unit = {}
): Arbitrary<T> where T : Any {
val scope = SubtypeScope<T>().apply(subtypeScope)
return Arbitraries.of(T::class.allSealedSubclasses).flatMap {
scope.getProviderFor(it)
?: Arbitraries.forType(it.java as Class<T>).run {
if (enableArbitraryRecursion) {
enableRecursion()
} else {
this
}
}
}.map { obj -> obj as T }
}

/**
* All sealed subclasses, recursively.
*/
val <T : Any> KClass<T>.allSealedSubclasses: List<KClass<out T>>
get() = sealedSubclasses.flatMap {
if (it.isSealed) {
it.allSealedSubclasses
} else {
listOf(it)
}
}

class SubtypeScope<T> {
val customProviders = mutableListOf<CustomProvider<T>>()

/**
* Registers a custom provider for subtype [U], instead of default one created by [anyForSubtypeOf].
*/
inline fun <reified U> provide(noinline customProvider: () -> Arbitrary<U>) where U : T {
customProviders.add(CustomProvider(U::class as KClass<Any>, customProvider) as CustomProvider<T>)
}

/**
* @return custom provider registered with [provide], or null.
*/
fun getProviderFor(targetType: KClass<*>) =
customProviders.firstOrNull { it.targetType == targetType }?.arbitraryFactory?.invoke()

class CustomProvider<T>(
val targetType: KClass<Any>,
val arbitraryFactory: () -> Arbitrary<T>
)
}
68 changes: 68 additions & 0 deletions kotlin/src/test/kotlin/net/jqwik/kotlin/AnyForSubtypeOfDslTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package net.jqwik.kotlin

import net.jqwik.api.Arbitraries
import net.jqwik.api.Example
import net.jqwik.api.ForAll
import net.jqwik.api.Property
import net.jqwik.api.Provide
import net.jqwik.kotlin.api.anyForSubtypeOf
import net.jqwik.testing.TestingSupport
import org.assertj.core.api.Assertions.assertThat
import java.util.*

class AnyForSubtypeOfDslTests {

sealed interface Interface
class Implementation(val value: String) : Interface

@Example
fun `anyForSubtypeOf() returns type arbitrary for any implementations of given sealed interface`(@ForAll random: Random) {
val subtypes = anyForSubtypeOf<Interface>()
TestingSupport.checkAllGenerated(subtypes, random) { it is Implementation }
}

sealed class Parent
class Child(val value: String) : Parent()

@Example
fun `anyForSubtypeOf() returns type arbitrary for any implementations of given sealed class`(@ForAll random: Random) {
val subtypes = anyForSubtypeOf<Parent>()
TestingSupport.checkAllGenerated(subtypes, random) { it is Child }
}

sealed class ParentWithRecursion
class ChildWithCustomType(val customType: CustomType) : ParentWithRecursion()
class CustomType(val value: String)

@Example
fun `anyForSubtypeOf() with arbitrary recursion`(@ForAll random: Random) {
val subtypes = anyForSubtypeOf<ParentWithRecursion>(enableArbitraryRecursion = true)
TestingSupport.checkAllGenerated(subtypes, random) { it is ChildWithCustomType }
}

sealed interface ParentInterface
sealed interface ChildInterface : ParentInterface
sealed class ChildClass : ParentInterface
class ChildInterfaceImpl(val value: String) : ChildInterface
class ChildClassImpl(val value: String) : ChildClass()

@Provide
fun parentInterface() = anyForSubtypeOf<ParentInterface>()

@Property
fun `anyForSubtypeOf() returns type arbitrary for any concrete subtype of a given sealed class or interface, even nested`(
@ForAll(
"parentInterface"
) parent: ParentInterface
) {
assertThat(parent).matches { it is ChildInterfaceImpl || it is ChildClassImpl }
}

@Example
fun `anyForSubtypeOf() with type arbitrary customization`(@ForAll random: Random) {
val subtypes = anyForSubtypeOf<Interface> {
provide<Implementation> { Arbitraries.of(Implementation("custom arbitrary")) }
}
TestingSupport.checkAllGenerated(subtypes, random) { it is Implementation && it.value == "custom arbitrary" }
}
}