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

#152 support value classes #849

Merged
merged 22 commits into from Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4c936b1
#152 add failing tests for value classes
aSemy Jul 20, 2022
b6670b7
Merge branch 'kotlin-1-7' into fix/152-value-classes
aSemy Jul 20, 2022
7da1ad8
Merge branch 'master' into fix/152-value-classes
aSemy Jul 21, 2022
514e014
add support for value class any() matcher
aSemy Jul 22, 2022
cd368da
fix value classes & slots
aSemy Jul 22, 2022
32ade90
update 'inline class' to 'value class'
aSemy Jul 22, 2022
0ffba57
update TODO, apply some auto-fixes to remove warnings
aSemy Jul 22, 2022
f2e6c7a
refactor mock-dsl-jvm to re-use ValueClassSupport code, and add TODOs…
aSemy Jul 23, 2022
1c5e442
test for #729
aSemy Jul 23, 2022
9cc892f
test #729, extension fun with UInt return
aSemy Jul 23, 2022
bcd427e
rm 'actual' from 'expect' object
aSemy Jul 23, 2022
201b43d
fix kdoc typo
aSemy Jul 23, 2022
65e8643
fix kdoc typo
aSemy Jul 23, 2022
5e7851b
refactor: simplify ValueClassSupport
qoomon Jul 23, 2022
ed8ab0c
Merge remote-tracking branch 'aSemy/mockk/fix/152-value-classes' into…
aSemy Jul 24, 2022
aafe7af
more testing for value classes - pre-merge in https://github.com/aSem…
aSemy Jul 24, 2022
fd6b24e
Merge pull request #1 from qoomon/fix/152-value-classes
aSemy Jul 24, 2022
360b08e
Merge remote-tracking branch 'aSemy/fix/152-value-classes' into fix/1…
aSemy Jul 24, 2022
af349e7
disable wrapped value class tests, rename tests for consistency
aSemy Jul 24, 2022
592bdaf
rename file to match obj name
aSemy Jul 24, 2022
fcaef2e
added timeout for Gradle test tasks
aSemy Jul 24, 2022
8b8017c
bump test timeout
aSemy Jul 24, 2022
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
100 changes: 40 additions & 60 deletions agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt
Expand Up @@ -6,80 +6,60 @@ import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible

private val valueClassFieldCache = mutableMapOf<KClass<out Any>, KProperty1<out Any, *>>()
// TODO this class is copy-pasted and should be de-duplicated
// see https://github.com/mockk/mockk/issues/857

/**
* Get boxed value of any value class
* Underlying property value of a **`value class`** or self.
*
* @return boxed value of value class, if this is value class, else just itself
* The type of the return might also be a `value class`!
*/
fun <T : Any> T.boxedValue(): Any? {
if (!this::class.isValueClass()) return this

// get backing field
val backingField = this::class.valueField()

// get boxed value
val <T : Any> T.boxedValue: Any?
@Suppress("UNCHECKED_CAST")
return (backingField as KProperty1<T, *>).get(this)
}
get() = if (!this::class.isValue_safe) {
this
} else {
(this::class as KClass<T>).boxedProperty.get(this)
}

/**
* Get class of boxed value of any value class
* Underlying property class of a **`value class`** or self.
*
* @return class of boxed value, if this is value class, else just class of itself
* The returned class might also be a `value class`!
*/
fun <T : Any> T.boxedClass(): KClass<*> {
return this::class.boxedClass()
}
val KClass<*>.boxedClass: KClass<*>
get() = if (!this.isValue_safe) {
this
} else {
this.boxedProperty.returnType.classifier as KClass<*>
}

private val valueClassFieldCache = mutableMapOf<KClass<out Any>, KProperty1<out Any, *>>()

/**
* Get the KClass of boxed value if this is a value class.
* Underlying property of a **`value class`**.
*
* @return class of boxed value, if this is value class, else just class of itself
* The underlying property might also be a `value class`!
*/
fun KClass<*>.boxedClass(): KClass<*> {
if (!this.isValueClass()) return this

// get backing field
val backingField = this.valueField()

// get boxed value
return backingField.returnType.classifier as KClass<*>
}


private fun <T : Any> KClass<T>.valueField(): KProperty1<out T, *> {
@Suppress("UNCHECKED_CAST")
return valueClassFieldCache.getOrPut(this) {
require(isValue) { "$this is not a value class" }

// value classes always have a primary constructor...
val constructor = primaryConstructor!!
// ...and exactly one constructor parameter
val constructorParameter = constructor.parameters.first()
// ...with a backing field
val backingField = declaredMemberProperties
.first { it.name == constructorParameter.name }
.apply { isAccessible = true }

backingField
} as KProperty1<out T, *>
}

private fun <T : Any> KClass<T>.isValueClass() = try {
this.isValue
} catch (_: Throwable) {
false
}
private val <T : Any> KClass<T>.boxedProperty: KProperty1<T, *>
get() = if (!this.isValue_safe) {
throw UnsupportedOperationException("$this is not a value class")
} else {
// value classes always have exactly one property
@Suppress("UNCHECKED_CAST")
valueClassFieldCache.getOrPut(this) {
this.declaredMemberProperties.first().apply { isAccessible = true }
} as KProperty1<T, *>
}

/**
* POLYFILL for kotlin version < 1.5
* will be shadowed by implementation in kotlin SDK 1.5+
* Returns `true` if calling [KClass.isValue] is safe.
*
* @return true if this is an inline class, else false
* (In some instances [KClass.isValue] can throw an exception.)
*/
private val <T : Any> KClass<T>.isValue: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" }
private val <T : Any> KClass<T>.isValue_safe: Boolean
get() = try {
this.isValue
} catch (_: UnsupportedOperationException) {
false
}
Expand Up @@ -81,7 +81,7 @@ internal class Advice(
superMethodCall,
arguments
)
?.boxedValue() // unbox value class objects
?.boxedValue // unbox value class objects
}
}

Expand Down
100 changes: 40 additions & 60 deletions agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt
Expand Up @@ -6,80 +6,60 @@ import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible

private val valueClassFieldCache = mutableMapOf<KClass<out Any>, KProperty1<out Any, *>>()
// TODO this class is copy-pasted and should be de-duplicated
// see https://github.com/mockk/mockk/issues/857

/**
* Get boxed value of any value class
* Underlying property value of a **`value class`** or self.
*
* @return boxed value of value class, if this is value class, else just itself
* The type of the return might also be a `value class`!
*/
fun <T : Any> T.boxedValue(): Any? {
if (!this::class.isValueClass()) return this

// get backing field
val backingField = this::class.valueField()

// get boxed value
val <T : Any> T.boxedValue: Any?
@Suppress("UNCHECKED_CAST")
return (backingField as KProperty1<T, *>).get(this)
}
get() = if (!this::class.isValue_safe) {
this
} else {
(this::class as KClass<T>).boxedProperty.get(this)
}

/**
* Get class of boxed value of any value class
* Underlying property class of a **`value class`** or self.
*
* @return class of boxed value, if this is value class, else just class of itself
* The returned class might also be a `value class`!
*/
fun <T : Any> T.boxedClass(): KClass<*> {
return this::class.boxedClass()
}
val KClass<*>.boxedClass: KClass<*>
get() = if (!this.isValue_safe) {
this
} else {
this.boxedProperty.returnType.classifier as KClass<*>
}

private val valueClassFieldCache = mutableMapOf<KClass<out Any>, KProperty1<out Any, *>>()

/**
* Get the KClass of boxed value if this is a value class.
* Underlying property of a **`value class`**.
*
* @return class of boxed value, if this is value class, else just class of itself
* The underlying property might also be a `value class`!
*/
fun KClass<*>.boxedClass(): KClass<*> {
if (!this.isValueClass()) return this

// get backing field
val backingField = this.valueField()

// get boxed value
return backingField.returnType.classifier as KClass<*>
}


private fun <T : Any> KClass<T>.valueField(): KProperty1<out T, *> {
@Suppress("UNCHECKED_CAST")
return valueClassFieldCache.getOrPut(this) {
require(isValue) { "$this is not a value class" }

// value classes always have a primary constructor...
val constructor = primaryConstructor!!
// ...and exactly one constructor parameter
val constructorParameter = constructor.parameters.first()
// ...with a backing field
val backingField = declaredMemberProperties
.first { it.name == constructorParameter.name }
.apply { isAccessible = true }

backingField
} as KProperty1<out T, *>
}

private fun <T : Any> KClass<T>.isValueClass() = try {
this.isValue
} catch (_: Throwable) {
false
}
private val <T : Any> KClass<T>.boxedProperty: KProperty1<T, *>
get() = if (!this.isValue_safe) {
throw UnsupportedOperationException("$this is not a value class")
} else {
// value classes always have exactly one property
@Suppress("UNCHECKED_CAST")
valueClassFieldCache.getOrPut(this) {
this.declaredMemberProperties.first().apply { isAccessible = true }
} as KProperty1<T, *>
}

/**
* POLYFILL for kotlin version < 1.5
* will be shadowed by implementation in kotlin SDK 1.5+
* Returns `true` if calling [KClass.isValue] is safe.
*
* @return true if this is an inline class, else false
* (In some instances [KClass.isValue] can throw an exception.)
*/
private val <T : Any> KClass<T>.isValue: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" }
private val <T : Any> KClass<T>.isValue_safe: Boolean
get() = try {
this.isValue
} catch (_: UnsupportedOperationException) {
false
}
Expand Up @@ -19,7 +19,7 @@ internal class Interceptor(
method
)
return handler.invocation(self, method, callOriginalMethod, arguments)
?.boxedValue() // unbox value class objects
?.boxedValue // unbox value class objects
}

}
6 changes: 6 additions & 0 deletions build.gradle
@@ -1,3 +1,5 @@
import java.time.Duration

buildscript {
ext.kotlin_gradle_version = findProperty('kotlin.version')?.toString() ?: '1.6.0'
ext.android_gradle_version = '7.0.0'
Expand Down Expand Up @@ -44,4 +46,8 @@ subprojects { subProject ->
languageVersion = "1.5"
}
}

tasks.withType(Test).configureEach {
timeout.set(Duration.ofMinutes(5))
}
}
10 changes: 6 additions & 4 deletions dsl/common/src/main/kotlin/io/mockk/API.kt
Expand Up @@ -3557,11 +3557,13 @@ interface TypedMatcher {
val argumentType: KClass<*>

fun checkType(arg: Any?): Boolean {
if (argumentType.simpleName === null) {
return true
return when {
argumentType.simpleName === null -> true
else -> {
val unboxedClass = InternalPlatformDsl.unboxClass(argumentType)
return unboxedClass.isInstance(arg)
}
}

return argumentType.isInstance(arg)
}
}

Expand Down
23 changes: 22 additions & 1 deletion dsl/common/src/main/kotlin/io/mockk/InternalPlatformDsl.kt
@@ -1,6 +1,7 @@
package io.mockk

import kotlin.coroutines.Continuation
import kotlin.reflect.KClass

expect object InternalPlatformDsl {
fun identityHashCode(obj: Any): Int
Expand Down Expand Up @@ -35,6 +36,26 @@ expect object InternalPlatformDsl {
fun counter(): InternalCounter

fun <T> coroutineCall(lambda: suspend () -> T): CoroutineCall<T>

/**
* Get the [KClass] of the single value that a `value class` contains.
*
* The result might also be a value class! So check recursively, if necessary.
*
* @return [KClass] of boxed value, if this is `value class`, else [cls].
*/
fun unboxClass(cls: KClass<*>): KClass<*>

/**
* Normally this simply casts [arg] to `T`
*
* However, if `T` is a `value class` (of type [cls]) this will construct a new instance of the
* value class, and set [arg] as the value.
*/
fun <T : Any> boxCast(
cls: KClass<*>,
arg: Any,
): T
}

interface CoroutineCall<T> {
Expand All @@ -50,4 +71,4 @@ interface InternalCounter {
val value: Long

fun increment(): Long
}
}
6 changes: 2 additions & 4 deletions dsl/common/src/main/kotlin/io/mockk/Matchers.kt
Expand Up @@ -140,17 +140,16 @@ data class CaptureNullableMatcher<T : Any>(
*/
data class CapturingSlotMatcher<T : Any>(
val captureSlot: CapturingSlot<T>,
override val argumentType: KClass<*>
override val argumentType: KClass<*>,
) : Matcher<T>, CapturingMatcher, TypedMatcher, EquivalentMatcher {
override fun equivalent(): Matcher<Any> = ConstantMatcher<Any>(true)

@Suppress("UNCHECKED_CAST")
override fun capture(arg: Any?) {
if (arg == null) {
captureSlot.isNull = true
} else {
captureSlot.isNull = false
captureSlot.captured = arg as T
captureSlot.captured = InternalPlatformDsl.boxCast(argumentType, arg)
}
captureSlot.isCaptured = true
}
Expand Down Expand Up @@ -473,4 +472,3 @@ fun CompositeMatcher<*>.captureSubMatchers(arg: Any?) {
.forEach { it.capture(arg) }
}
}