Skip to content

Commit

Permalink
Handling generic data classes when determining identifier stability
Browse files Browse the repository at this point in the history
By inspecting the stability of the actual values, we can see beyond the
generics of the data class.

Fixes #3947
  • Loading branch information
Kantis committed May 9, 2024
1 parent 3ab9358 commit b720e61
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 7 deletions.
22 changes: 17 additions & 5 deletions kotest-common/src/commonMain/kotlin/io/kotest/mpp/stable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ fun isStable(type: KType) = when (val classifier = type.classifier) {
* - a data class that only contains stable classes as members
* - a stable type on the executing platform
*/
fun isStable(kclass: KClass<*>): Boolean {
fun isStable(kclass: KClass<*>, t: Any? = null): Boolean {
return when {
allPlatformStableTypes.contains(kclass) -> true
reflection.isEnumClass(kclass) -> true
reflection.isDataClass(kclass) && hasStableMembers(kclass) -> true
reflection.isDataClass(kclass) && hasStableMembers(kclass, t) -> true
isPlatformStable(kclass) -> true
else -> {
println("Warning, type $kclass used in data testing does not have a stable toString()")
Expand All @@ -38,9 +38,21 @@ expect fun isPlatformStable(kclass: KClass<*>): Boolean
/**
* Returns true if all members of this class are "stable".
*/
private fun hasStableMembers(kclass: KClass<*>) = reflection.primaryConstructorMembers(kclass).let { members ->
members.isNotEmpty() && members.all { isStable(it.type) }
}
private fun hasStableMembers(kclass: KClass<*>, t: Any? = null) =
reflection.primaryConstructorMembers(kclass).let { members ->
members.isNotEmpty() && members.all { getter ->
val typeIsStable = isStable(getter.type)
if (t == null) {
typeIsStable
} else {
val valueIsStable = getter.call(t)?.let { memberValue ->
isStable(memberValue::class, memberValue)
} ?: false

typeIsStable || valueIsStable
}
}
}

private val allPlatformStableTypes = setOf(
String::class,
Expand Down
21 changes: 20 additions & 1 deletion kotest-common/src/jvmTest/kotlin/io/kotest/mpp/StableTest.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.kotest.mpp

import io.kotest.core.spec.style.FreeSpec
import io.kotest.core.spec.style.StringSpec
import io.kotest.data.Row3
import io.kotest.matchers.shouldBe

class StableTest : StringSpec({
class StableTest : FreeSpec({
"class should be correctly identified as stable" {
data class StableClass(
val string: String,
Expand All @@ -29,4 +31,21 @@ class StableTest : StringSpec({
isStable(Unstable::class) shouldBe false
isStable(UnstableDataClass::class) shouldBe false
}

"Given a data class with generics" - {
"When all members are stable, then the class should be considered stable" {
val x: Row3<String, Int, Double> = Row3("x", 1, 2.0)
isStable(x::class, x) shouldBe true
}

"When isStable does not have access to actual values, it cannot determine the stability of the class" {
val x: Row3<String, Int, Double> = Row3("x", 1, 2.0)
isStable(x::class, t = null) shouldBe false
}

"When an unstable type is included, the entire data class is considered unstable, even when including values" {
val y: Row3<String, Int, Array<Int>> = Row3("x", 1, arrayOf(1, 2))
isStable(y::class, y) shouldBe false
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object StableIdentifiers {
* Note: If the user has overridden `toString()` and the returned value is not stable, tests may not appear.
*/
fun stableIdentifier(t: Any): String {
return if (isStable(t::class)) {
return if (isStable(t::class, t)) {
t.toString()
} else {
t::class.bestName()
Expand Down

0 comments on commit b720e61

Please sign in to comment.