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 5 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
11 changes: 0 additions & 11 deletions agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt
Expand Up @@ -72,14 +72,3 @@ private fun <T : Any> KClass<T>.isValueClass() = try {
} catch (_: Throwable) {
false
}

/**
* POLYFILL for kotlin version < 1.5
* will be shadowed by implementation in kotlin SDK 1.5+
*
* @return true if this is an inline class, else false
*/
private val <T : Any> KClass<T>.isValue: Boolean
get() = !isData &&
primaryConstructor?.parameters?.size == 1 &&
java.declaredMethods.any { it.name == "box-impl" }
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`es (of type [cls]) it will construct a new instance of the
* class, and set [arg] as the value.
*/
actual 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) }
}
}

50 changes: 49 additions & 1 deletion dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt
@@ -1,6 +1,5 @@
package io.mockk

import kotlinx.coroutines.runBlocking
import java.lang.reflect.AccessibleObject
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
Expand All @@ -13,10 +12,13 @@ import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.functions
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaMethod
import kotlinx.coroutines.runBlocking

actual object InternalPlatformDsl {
actual fun identityHashCode(obj: Any): Int = System.identityHashCode(obj)
Expand Down Expand Up @@ -214,6 +216,29 @@ actual object InternalPlatformDsl {
}

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

actual fun unboxClass(cls: KClass<*>): KClass<*> {
if (!cls.isValue) return cls

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

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

@Suppress("UNCHECKED_CAST")
actual fun <T : Any> boxCast(
cls: KClass<*>,
arg: Any,
): T {
return if (cls.isValue) {
val constructor = cls.primaryConstructor!!.apply { isAccessible = true }
constructor.call(arg) as T
} else {
arg as T
}
}
}

class JvmCoroutineCall<T>(private val lambda: suspend () -> T) : CoroutineCall<T> {
Expand All @@ -232,3 +257,26 @@ class JvmCoroutineCall<T>(private val lambda: suspend () -> T) : CoroutineCall<T
}
}
}

// TODO this is copy-pasted from ValueClassSupport
// I will try to move that class so it's available here
aSemy marked this conversation as resolved.
Show resolved Hide resolved

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

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!!.apply { isAccessible = true }
// ...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, *>
}
Copy link
Contributor Author

@aSemy aSemy Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to resolve this

TODO this is from ValueClassSupport.kt - try and refactor to avoid copy-pasting

  • ValueClassSupport.kt is in project mockk-agent-jvm
  • InternalPlatformDsl is in project mockk-dsl-jvm

I'm looking for suggestions. Maybe it's okay just to have it copy and pasted?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep i think if they are in those two separate modules there's no better way and we can live with it being copypasted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a comment where the same functionality is implemented would help, i case that code ever needs to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, and I've made an issue to try and de-duplicated it later #857

@@ -1,7 +1,13 @@
package io.mockk.impl.recording

import io.mockk.impl.instantiation.AbstractInstantiator
import io.mockk.impl.instantiation.AnyValueGenerator
import kotlin.reflect.KClass

interface SignatureValueGenerator {
fun <T : Any> signatureValue(cls: KClass<T>, orInstantiateVia: () -> T): T
}
fun <T : Any> signatureValue(
cls: KClass<T>,
anyValueGeneratorProvider: () -> AnyValueGenerator,
instantiator: AbstractInstantiator,
): T
}
Expand Up @@ -42,15 +42,17 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState
recorder.calls.addAll(detector.calls)
}

@Suppress("UNCHECKED_CAST")
override fun <T : Any> matcher(matcher: Matcher<*>, cls: KClass<T>): T {
val signatureValue = recorder.signatureValueGenerator.signatureValue(cls) {
recorder.anyValueGenerator().anyValue(cls, isNullable = false) {
recorder.instantiator.instantiate(cls)
} as T
}
val signatureValue = recorder.signatureValueGenerator.signatureValue(
cls,
recorder.anyValueGenerator,
recorder.instantiator,
)

val packRef: Any = InternalPlatform.packRef(signatureValue)
?: error("null packRef for $cls signature $signatureValue")

builder().addMatcher(matcher, InternalPlatform.packRef(signatureValue)!!)
builder().addMatcher(matcher, packRef)

return signatureValue
}
Expand Down
51 changes: 49 additions & 2 deletions mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt
@@ -1,6 +1,8 @@
package io.mockk.it

import io.mockk.*
import kotlin.jvm.JvmInline
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals

Expand Down Expand Up @@ -43,14 +45,59 @@ class ValueClassTest {

verify { mock.processValue(DummyValue(1)) }
}

@Test
fun `any matcher for value class`() {
val mock = mockk<ValueServiceDummy>(relaxed = true)
val givenResult = 1
every { mock.doSomething(any()) } returns givenResult

val result = mock.doSomething(ValueDummy("moin"))

assertEquals(givenResult, result)
}

@Test
fun `slot for value class`() {
val mock = mockk<ValueServiceDummy>(relaxed = true)
val slot = slot<ValueDummy>()
val givenResult = 1
every { mock.doSomething(capture(slot)) } returns givenResult

val givenParameter = ValueDummy("s")

val result = mock.doSomething(givenParameter)

assertEquals(givenResult, result)
assertEquals(givenParameter, slot.captured)
}

@Test
fun `value class as return value`() {
val mock = mockk<ValueServiceDummy>(relaxed = true)
val givenResult = ValueDummy("moin")
every { mock.getSomething() } returns givenResult

val result = mock.getSomething()

assertEquals(givenResult, result)
}
}

// TODO should be value class in kotlin 1.5+
private inline class DummyValue(val value: Int)
inline class DummyValue(val value: Int)

private class DummyService {
class DummyService {

fun requestValue() = DummyValue(0)

fun processValue(value: DummyValue) = DummyValue(0)
}

@JvmInline
value class ValueDummy(val value: String)

interface ValueServiceDummy {
fun doSomething(value: ValueDummy): Int
fun getSomething(): ValueDummy
}
@@ -1,11 +1,29 @@
package io.mockk.impl.recording

import java.util.*
import io.mockk.InternalPlatformDsl
import io.mockk.impl.instantiation.AbstractInstantiator
import io.mockk.impl.instantiation.AnyValueGenerator
import java.util.Random
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible

class JvmSignatureValueGenerator(val rnd: Random) : SignatureValueGenerator {
override fun <T : Any> signatureValue(cls: KClass<T>, orInstantiateVia: () -> T): T {
override fun <T : Any> signatureValue(
cls: KClass<T>,
anyValueGeneratorProvider: () -> AnyValueGenerator,
instantiator: AbstractInstantiator,
): T {

if (cls.isValue) {
val valueCls = InternalPlatformDsl.unboxClass(cls)
val valueSig = signatureValue(valueCls, anyValueGeneratorProvider, instantiator)

val constructor = cls.primaryConstructor!!.apply { isAccessible = true }
return constructor.call(valueSig)
}

return cls.cast(
when (cls) {
java.lang.Boolean::class -> rnd.nextBoolean()
Expand All @@ -17,8 +35,13 @@ class JvmSignatureValueGenerator(val rnd: Random) : SignatureValueGenerator {
java.lang.Float::class -> rnd.nextFloat()
java.lang.Double::class -> rnd.nextDouble()
java.lang.String::class -> rnd.nextLong().toString(16)
else -> orInstantiateVia()

else ->
@Suppress("UNCHECKED_CAST")
anyValueGeneratorProvider().anyValue(cls, isNullable = false) {
instantiator.instantiate(cls)
} as T
}
)
}
}
}