Skip to content

Commit

Permalink
Improve integration tests to call sub-provider functions (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
serpro69 authored May 13, 2024
1 parent b4d9b16 commit aaa017c
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 67 deletions.
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/faker-lib-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ dependencies {
// we're shadowing these, so they need to be available for test runtime
testRuntimeOnly(libs.icu4j)
testRuntimeOnly(libs.rgxgen)
// needed to be able to run tests in intellij, no idea why... (gradle tests work fine from cli)
// clearly a bug with idea...
// maybe something related to https://youtrack.jetbrains.com/issue/IDEA-163411
testRuntimeOnly(libs.bundles.jackson)
}

val integrationTest by tasks.creating(Test::class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.github.serpro69.kfaker.provider.misc.RandomProvider
import io.github.serpro69.kfaker.provider.misc.StringProvider
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.core.spec.style.scopes.DescribeSpecContainerScope
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.KVisibility
Expand Down Expand Up @@ -69,6 +70,7 @@ abstract class AbstractIT : DescribeSpec {
)
}

@Suppress("FunctionName")
fun DescribeSpec.`describe all public generators`(
test: suspend DescribeSpecContainerScope.(Faker, KProperty<*>, KFunction<*>) -> Unit
) {
Expand All @@ -92,12 +94,68 @@ fun DescribeSpec.`describe all public generators`(
describe("all public functions in each provider") {
providerFunctions.forEach { (functions, provider) ->
functions.forEach {
context("result value for ${provider.name} ${it.name} is resolved correctly") {
context("result value for ${provider.name}#${it.name} is resolved correctly") {
test(this, faker, provider, it)
}
}
}
}
}


@Suppress("FunctionName")
fun DescribeSpec.`describe all public sub-providers`(
test: suspend DescribeSpecContainerScope.(FakeDataProvider, KProperty<*>, KFunction<*>) -> Unit
) {
val faker = Faker()

// Get a list of all publicly visible providers
val providers: List<KProperty<*>> = faker::class.declaredMemberProperties.filter {
it.visibility == KVisibility.PUBLIC
&& it.returnType.isSubtypeOf(FakeDataProvider::class.starProjectedType)
&& it.returnType.classifier != Money::class // Ignore Money provider as it's a special case
&& it.returnType.classifier != StringProvider::class // Ignore String provider
&& it.returnType.classifier != RandomProvider::class // Ignore String provider
}

// Get a list of all publicly visible sub-providers in each provider
val subProviders: List<Pair<FakeDataProvider, List<KProperty<*>>>> = providers.mapNotNull { p ->
val provider = p.getter.call(faker)!! as FakeDataProvider
val subs = provider::class.declaredMemberProperties.filter {
it.visibility == KVisibility.PUBLIC
&& it.returnType.isSubtypeOf(FakeDataProvider::class.starProjectedType)
&& it.returnType.classifier as KClass<*> != provider::class // exclude 'unique' properties
}
if (subs.isNotEmpty()) provider to subs else null
}

// Get a list of all publicly visible functions in each provider
val providerFunctions: GeneratorFunctions = providers.associateBy { provider ->
provider.getter.call(faker)!!::class.declaredMemberFunctions.filter {
it.visibility == KVisibility.PUBLIC && !it.annotations.any { ann -> ann is Deprecated }
}
}

// Get all publicly visible functions in each sub-provider
val subProviderFunctions = subProviders.map { (provider, subs) ->
provider to subs.associateBy { sub ->
sub.getter.call(provider)!!::class.declaredMemberFunctions.filter {
it.visibility == KVisibility.PUBLIC && !it.annotations.any { ann -> ann is Deprecated }
}
}
}

describe("all public functions in each sub-provider") {
subProviderFunctions.forEach { (provider, subs) ->
subs.forEach { (functions, sub) ->
functions.forEach {
context("result value for ${sub.name}#${it.name} is resolved correctly") {
test(this, provider, sub, it)
}
}
}
}
}
}

typealias GeneratorFunctions = Map<List<KFunction<*>>, KProperty<*>>
78 changes: 64 additions & 14 deletions core/src/integration/kotlin/io/github/serpro69/kfaker/FakerIT.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.serpro69.kfaker

import io.github.serpro69.kfaker.provider.FakeDataProvider
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
Expand All @@ -12,20 +13,7 @@ class FakerIT : AbstractIT({ t ->
`describe all public generators` { faker, provider: KProperty<*>, f: KFunction<*> ->
val regex = Regex("""#\{.*}|#++""")

val value = when (f.parameters.size) {
1 -> f.call(provider.getter.call(faker)).toString()
2 -> {
if (f.parameters[1].isOptional) { // optional params are enum typed (see functions in Dune, Finance or Tron, for example)
f.callBy(mapOf(f.parameters[0] to provider.getter.call(faker))).toString()
} else f.call(provider.getter.call(faker), "").toString()
}
3 -> {
if (f.parameters[1].isOptional && f.parameters[2].isOptional) {
f.callBy(mapOf(f.parameters[0] to provider.getter.call(faker))).toString()
} else f.call(provider.getter.call(faker), "", "").toString()
}
else -> throw IllegalArgumentException("")
}
val value = value(f, provider, faker)

it("resolved value should not contain yaml expression") {
if (
Expand Down Expand Up @@ -64,6 +52,48 @@ class FakerIT : AbstractIT({ t ->
}
}

`describe all public sub-providers` { provider: FakeDataProvider, sub: KProperty<*>, f: KFunction<*> ->
val regex = Regex("""#\{.*}|#++""")

val value = value(f, sub, provider)

it("resolved value should not contain yaml expression") {
if (
!value.contains("#chuck and #norris")
&& (sub.name != "markdown" && f.name != "headers")
&& value !in t.valuesWithHashKey
) {
if (value.contains(regex)) {
throw AssertionError("Value '$value' for '${sub.name} ${f.name}' should not contain regex '$regex'")
}
}
}

it("resolved value should not be empty string") {
if (value == "") {
throw AssertionError("Value for '${sub.name} ${f.name}' should not be empty string")
}
}

it("resolved value should not contain duplicates") {
val values = value.split(" ")

// Accounting for some exceptional cases where values are repeated
// in resolved expression
if (
(sub.name != "coffee" && f.name != "notes")
&& (sub.name != "onePiece" && f.name != "akumasNoMi")
&& (sub.name != "lorem" && f.name != "punctuation" && value != " ")
&& value !in t.duplicatedValues
) {
// Since there's no way to modify assertion message in KotlinTest it's better to throw a custom error
if (values.odds() == values.evens()) {
throw AssertionError("Value '$value' for '${sub.name} ${f.name}' should not contain duplicates")
}
}
}
}

describe("Faker instance is initialized with default locale") {
val faker = Faker()

Expand Down Expand Up @@ -123,6 +153,26 @@ class FakerIT : AbstractIT({ t ->
}
})

/**
* Return the value from the [f] member function of [p] property.
*
* @param args arguments to call [p] property getter
*/
private fun value(f: KFunction<*>, p: KProperty<*>, vararg args: Any?) = when (f.parameters.size) {
1 -> f.call(p.getter.call(*args)).toString()
2 -> {
if (f.parameters[1].isOptional) { // optional params are enum typed (see functions in Dune, Finance or Tron, for example)
f.callBy(mapOf(f.parameters[0] to p.getter.call(*args))).toString()
} else f.call(p.getter.call(*args), "").toString()
}
3 -> {
if (f.parameters[1].isOptional && f.parameters[2].isOptional) {
f.callBy(mapOf(f.parameters[0] to p.getter.call(*args))).toString()
} else f.call(p.getter.call(*args), "", "").toString()
}
else -> throw IllegalArgumentException("")
}

private fun List<String>.odds() = this.mapIndexedNotNull { index, s ->
if (index % 2 == 0) s else null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import io.github.serpro69.kfaker.provider.misc.RandomProvider
import io.github.serpro69.kfaker.provider.misc.StringProvider
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.core.spec.style.scopes.DescribeSpecContainerScope
import org.junit.jupiter.api.assertDoesNotThrow
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberFunctions
Expand All @@ -26,70 +29,96 @@ fun DescribeSpec.`every public function in each provider is invoked without exce
&& it.returnType.classifier != RandomProvider::class // Ignore String provider
}

// Get a list of all publicly visible sub-providers in each provider
val subProviders: List<Pair<FakeDataProvider, List<KProperty<*>>>> = providers.mapNotNull { p ->
val provider = p.getter.call(faker)!! as FakeDataProvider
val subs = provider::class.declaredMemberProperties.filter {
it.visibility == KVisibility.PUBLIC
&& it.returnType.isSubtypeOf(FakeDataProvider::class.starProjectedType)
&& it.returnType.classifier as KClass<*> != provider::class // exclude 'unique' properties
}
if (subs.isNotEmpty()) provider to subs else null
}

// Get a list of all publicly visible functions in each provider
val providerFunctions = providers.associateBy { provider ->
provider.getter.call(faker)!!::class.declaredMemberFunctions.filter {
it.visibility == KVisibility.PUBLIC && !it.annotations.any { ann -> ann is Deprecated }
}
}

// Get all publicly visible functions in each sub-provider
val subProviderFunctions = subProviders.map { (provider, subs) ->
provider to subs.associateBy { sub ->
sub.getter.call(provider)!!::class.declaredMemberFunctions.filter {
it.visibility == KVisibility.PUBLIC && !it.annotations.any { ann -> ann is Deprecated }
}
}
}

assertSoftly {
providerFunctions.forEach { (functions, provider) ->
functions.forEach {
context("result value for ${provider.name} ${it.name} is resolved correctly") {
val regex = Regex("""#\{.*}|#++""")

val value = when (it.parameters.size) {
1 -> it.call(provider.getter.call(faker)).toString()
2 -> {
if (it.parameters[1].isOptional) { // optional params are enum typed (see functions in Dune, Finance or Tron, for example)
it.callBy(mapOf(it.parameters[0] to provider.getter.call(faker))).toString()
} else it.call(provider.getter.call(faker), "").toString()
}
3 -> {
if (it.parameters[1].isOptional && it.parameters[2].isOptional) {
it.callBy(mapOf(it.parameters[0] to provider.getter.call(faker))).toString()
} else it.call(provider.getter.call(faker), "", "").toString()
}
else -> throw IllegalArgumentException("")
}
test(provider, functions, faker)
}

it("resolved value should not contain yaml expression") {
if (
!value.contains("#chuck and #norris")
&& (provider.name != "markdown" && it.name != "headers")
&& value !in valuesWithHashKey
) {
if (value.contains(regex)) {
throw AssertionError("Value '$value' for '${provider.name} ${it.name}' should not contain regex '$regex'")
}
}
}
subProviderFunctions.forEach { (provider, subs) ->
subs.forEach { (functions, sub) -> test(sub, functions, provider) }
}
}
}
}

it("resolved value should not be empty string") {
if (value == "") {
throw AssertionError("Value for '${provider.name} ${it.name}' should not be empty string")
}
}
private suspend fun DescribeSpecContainerScope.test(provider: KProperty<*>, functions: List<KFunction<*>>, vararg caller: Any?) = functions.forEach {
context("result value for ${provider.name}#${it.name} is resolved correctly") {
val regex = Regex("""#\{.*}|#++""")

it("resolved value should not contain duplicates") {
val values = value.split(" ")

// Accounting for some exceptional cases where values are repeated
// in resolved expression
if (
(provider.name != "coffee" && it.name != "notes")
&& (provider.name != "onePiece" && it.name != "akumasNoMi")
&& (provider.name != "lorem" && it.name != "punctuation" && value != " ")
&& value !in duplicatedValues
) {
// Since there's no way to modify assertion message in KotlinTest it's better to throw a custom error
if (values.odds() == values.evens()) {
throw AssertionError("Value '$value' for '${provider.name} ${it.name}' should not contain duplicates")
}
}
}
}
val value = when (it.parameters.size) {
1 -> it.call(provider.getter.call(*caller)).toString()
2 -> {
if (it.parameters[1].isOptional) { // optional params are enum typed (see functions in Dune, Finance or Tron, for example)
it.callBy(mapOf(it.parameters[0] to provider.getter.call(*caller))).toString()
} else it.call(provider.getter.call(*caller), "").toString()
}
3 -> {
if (it.parameters[1].isOptional && it.parameters[2].isOptional) {
it.callBy(mapOf(it.parameters[0] to provider.getter.call(*caller))).toString()
} else it.call(provider.getter.call(*caller), "", "").toString()
}
else -> throw IllegalArgumentException("")
}

it("resolved value should not contain yaml expression") {
if (
!value.contains("#chuck and #norris")
&& (provider.name != "markdown" && it.name != "headers")
&& value !in valuesWithHashKey
) {
if (value.contains(regex)) {
throw AssertionError("Value '$value' for '${provider.name} ${it.name}' should not contain regex '$regex'")
}
}
}

it("resolved value should not be empty string") {
if (value == "") {
throw AssertionError("Value for '${provider.name} ${it.name}' should not be empty string")
}
}

it("resolved value should not contain duplicates") {
val values = value.split(" ")

// Accounting for some exceptional cases where values are repeated
// in resolved expression
if (
(provider.name != "coffee" && it.name != "notes")
&& (provider.name != "onePiece" && it.name != "akumasNoMi")
&& (provider.name != "lorem" && it.name != "punctuation" && value != " ")
&& value !in duplicatedValues
) {
// Since there's no way to modify assertion message in KotlinTest it's better to throw a custom error
if (values.odds() == values.evens()) {
throw AssertionError("Value '$value' for '${provider.name} ${it.name}' should not contain duplicates")
}
}
}
Expand Down

0 comments on commit aaa017c

Please sign in to comment.