From 304c52b84041d6bd277f8a66d86cf87ea4919a02 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Thu, 27 May 2021 09:53:43 +0200 Subject: [PATCH 1/9] fix: fix kotlin 1.5 incompatibilities --- build.gradle | 3 +-- client-tests/jvm/build.gradle | 7 ------- dsl/common/build.gradle | 10 ---------- gradle/jacoco.gradle | 2 +- mockk/common/build.gradle | 7 ------- .../io/mockk/impl/recording/states/RecordingState.kt | 4 ++-- .../kotlin/io/mockk/impl/verify/VerificationHelpers.kt | 2 +- 7 files changed, 5 insertions(+), 30 deletions(-) diff --git a/build.gradle b/build.gradle index 537f40ca2..590605d8a 100644 --- a/build.gradle +++ b/build.gradle @@ -36,11 +36,10 @@ subprojects { subProject -> jcenter() } - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" - languageVersion = "1.3" + languageVersion = kotlin_version.replaceAll( /^([^.]+\.[^.]+).*/, '$1' ) useIR = findProperty("kotlin.ir.enabled")?.toBoolean() == true } } diff --git a/client-tests/jvm/build.gradle b/client-tests/jvm/build.gradle index 671791949..8a8b09f7c 100644 --- a/client-tests/jvm/build.gradle +++ b/client-tests/jvm/build.gradle @@ -17,13 +17,6 @@ apply plugin: 'kotlin' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 -compileTestKotlin { - kotlinOptions { - apiVersion = '1.3' - languageVersion = '1.3' - } -} - configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds' diff --git a/dsl/common/build.gradle b/dsl/common/build.gradle index c6efe601c..17c431fed 100644 --- a/dsl/common/build.gradle +++ b/dsl/common/build.gradle @@ -10,13 +10,3 @@ apply from: "${gradles}/upload.gradle" sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 -compileKotlinCommon { - kotlinOptions { - apiVersion = '1.3' - languageVersion = '1.3' - } -} - -jar { -} - diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index 7ed4e5fa4..6a0c27d18 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -1,7 +1,7 @@ apply plugin: 'jacoco' jacoco { - toolVersion = "0.8.6" + toolVersion = "0.8.7" } afterEvaluate { diff --git a/mockk/common/build.gradle b/mockk/common/build.gradle index 5dfb07f45..d069b6dcf 100644 --- a/mockk/common/build.gradle +++ b/mockk/common/build.gradle @@ -13,10 +13,3 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { api project(":mockk-dsl") } - -compileKotlinCommon { - kotlinOptions { - apiVersion = '1.3' - languageVersion = '1.3' - } -} diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt index 401ae710f..a407eb769 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt @@ -144,7 +144,7 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState .flatMap { it.args } .filterNotNull() .map(this::typeEstimation) - .max() ?: 1 + .maxOrNull() ?: 1 val varargArguments = builder().signedCalls .mapNotNull { @@ -154,7 +154,7 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState null } }.map(this::varArgTypeEstimation) - .max() ?: 1 + .maxOrNull() ?: 1 return max(regularArguments, varargArguments) } diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt b/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt index 0dcb884e2..e00b61b6e 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt @@ -26,7 +26,7 @@ object VerificationHelpers { fun stackTrace(prefix: Int, stackTrace: List): String { fun columnSize(block: StackElement.() -> String) = - stackTrace.map(block).map { it.length }.max() ?: 0 + stackTrace.map(block).map { it.length }.maxOrNull() ?: 0 fun StackElement.fileLine() = "($fileName:$line)${if (nativeMethod) "N" else ""}" From 8d4392780cd18aacd8dd425a9e117bdd1e52375d Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Thu, 27 May 2021 14:02:51 +0200 Subject: [PATCH 2/9] feat: add support for value/inline classes --- agent/jvm/build.gradle | 2 + .../io/mockk/proxy/jvm/advice/Interceptor.kt | 1 + .../proxy/jvm/advice/ValueClassSupport.kt | 69 +++++++++++++++++++ gradle.properties | 1 + .../test/kotlin/io/mockk/it/ValueClassTest.kt | 57 +++++++++++++++ .../kotlin/io/mockk/impl/InternalPlatform.kt | 11 +-- 6 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt create mode 100644 mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt diff --git a/agent/jvm/build.gradle b/agent/jvm/build.gradle index 8b4d1cac6..6f5abaa7d 100644 --- a/agent/jvm/build.gradle +++ b/agent/jvm/build.gradle @@ -16,6 +16,8 @@ dependencies { api "net.bytebuddy:byte-buddy:$byte_buddy_version" api "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" + + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } task copyMockKDispatcher(type: Copy) { diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt index b563ece75..c7bec9bea 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt @@ -18,6 +18,7 @@ internal class Interceptor( method ) return handler.invocation(self, method, callOriginalMethod, arguments) + ?.boxedValue() // unbox value class objects } } diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt new file mode 100644 index 000000000..c5dada36d --- /dev/null +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt @@ -0,0 +1,69 @@ +package io.mockk.proxy.jvm.advice + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible + +private val valueClassFieldCache = mutableMapOf, KProperty1>() + +/** + * @return boxed value of value class if this is value class, else just itself + */ +fun T.boxedValue(): Any? { + if (!this::class.isValueClass()) return this + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + @Suppress("UNCHECKED_CAST") + return (backingField as KProperty1).get(this) +} + +fun T.boxedClass(): KClass<*> { + if (!this::class.isValueClass()) return this::class + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + return backingField.returnType.classifier as KClass<*> +} + + +private fun KClass.valueField(): KProperty1 { + @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 +} + +fun KClass.isValueClass() = try { + this.isValue +} catch (ex: UnsupportedOperationException) { + 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 KClass.isValue: Boolean + get() = !isData && + primaryConstructor?.parameters?.size == 1 && + java.declaredMethods.any { it.name == "box-impl" } diff --git a/gradle.properties b/gradle.properties index 67d38280d..b3f3f6161 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ org.gradle.configureondemand=false org.gradle.jvmargs=-XX:MaxMetaspaceSize=768m localrepo=build/mockk-repo # localrepo=/Users/raibaz/.m2/repository +kotlin.version=1.5.10 \ No newline at end of file diff --git a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt new file mode 100644 index 000000000..08720c7bb --- /dev/null +++ b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt @@ -0,0 +1,57 @@ +package io.mockk.it + +import io.mockk.* +import kotlin.jvm.JvmInline +import kotlin.test.Test +import kotlin.test.assertEquals + +class ValueClassTest { + + private val mock = mockk() + + @Test + fun `value class object as return value`() { + every { mock.requestValue() } returns DummyValue(42) + + assertEquals(DummyValue(42), mock.requestValue()) + + verify { mock.requestValue() } + } + + @Test + fun `value class object as function argument and return value`() { + every { mock.processValue(DummyValue(1)) } returns DummyValue(42) + + assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + + verify { mock.processValue(DummyValue(1)) } + } + + @Test + fun `value class object as function argument and answer value`() { + every { mock.processValue(DummyValue(1)) } answers { DummyValue(42) } + + assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + + verify { mock.processValue(DummyValue(1)) } + } + + @Test + fun `any value class matcher as function argument and value class object as return value`() { + every { mock.processValue(any()) } returns DummyValue(42) + + assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + + verify { mock.processValue(DummyValue(1)) } + } +} + +@JvmInline +private value class DummyValue(val value: Int) + +private class DummyService { + + fun requestValue() = DummyValue(0) + + fun processValue(value: DummyValue) = DummyValue(0) +} \ No newline at end of file diff --git a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt index 785363a0d..24650f130 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt @@ -6,6 +6,8 @@ import io.mockk.StackElement import io.mockk.impl.platform.CommonIdentityHashMapOf import io.mockk.impl.platform.CommonRef import io.mockk.impl.platform.JvmWeakConcurrentMap +import io.mockk.proxy.jvm.advice.boxedClass +import io.mockk.proxy.jvm.advice.boxedValue import java.lang.ref.WeakReference import java.lang.reflect.Modifier import java.util.* @@ -58,10 +60,11 @@ actual object InternalPlatform { actual fun synchronizedMutableMap(): MutableMap = Collections.synchronizedMap(hashMapOf()) actual fun packRef(arg: Any?): Any? { - return if (arg == null || isPassedByValue(arg::class)) - arg - else - ref(arg) + return when { + arg == null -> null + isPassedByValue(arg.boxedClass()) -> arg.boxedValue() + else -> ref(arg) + } } actual fun prettifyRecordingException(ex: Throwable): Throwable { From 31a3c9d266e0373cd523f16166b7b9810e30b0c7 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Tue, 1 Jun 2021 21:56:59 +0200 Subject: [PATCH 3/9] Update ValueClassSupport.kt --- .../main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt index c5dada36d..407dd9da1 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt @@ -22,6 +22,9 @@ fun T.boxedValue(): Any? { return (backingField as KProperty1).get(this) } +/** + * @return class of boxed value of value class if this is value class, else just class of itself + */ fun T.boxedClass(): KClass<*> { if (!this::class.isValueClass()) return this::class From 9b5b0649b026daa8af9f525d734b9e558f8f47b7 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Tue, 1 Jun 2021 22:01:45 +0200 Subject: [PATCH 4/9] Update ValueClassSupport.kt --- .../kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt index 407dd9da1..77c84ca37 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt @@ -9,7 +9,9 @@ import kotlin.reflect.jvm.isAccessible private val valueClassFieldCache = mutableMapOf, KProperty1>() /** - * @return boxed value of value class if this is value class, else just itself + * Get boxed value of any value class + * + * @return boxed value of value class, if this is value class, else just itself */ fun T.boxedValue(): Any? { if (!this::class.isValueClass()) return this @@ -23,7 +25,9 @@ fun T.boxedValue(): Any? { } /** - * @return class of boxed value of value class if this is value class, else just class of itself + * Get class of boxed value of any value class + * + * @return class of boxed value, if this is value class, else just class of itself */ fun T.boxedClass(): KClass<*> { if (!this::class.isValueClass()) return this::class From e16f7e9b682b60850cbc33c1fa1cf115e1278e42 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Sun, 6 Jun 2021 15:52:44 +0200 Subject: [PATCH 5/9] refactor: adjust code to be compatible with kotlin 1.3 --- .../kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt | 2 +- build.gradle | 2 +- gradle.properties | 2 +- .../kotlin/io/mockk/impl/recording/states/RecordingState.kt | 5 +++-- .../main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt | 3 ++- mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt | 5 ++--- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt index 77c84ca37..074576058 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt @@ -60,7 +60,7 @@ private fun KClass.valueField(): KProperty1 { fun KClass.isValueClass() = try { this.isValue -} catch (ex: UnsupportedOperationException) { +} catch (_: UnsupportedOperationException) { false } diff --git a/build.gradle b/build.gradle index 590605d8a..c7f1761c2 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ subprojects { subProject -> tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" - languageVersion = kotlin_version.replaceAll( /^([^.]+\.[^.]+).*/, '$1' ) + apiVersion = "1.3" useIR = findProperty("kotlin.ir.enabled")?.toBoolean() == true } } diff --git a/gradle.properties b/gradle.properties index b3f3f6161..b819dccb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.configureondemand=false org.gradle.jvmargs=-XX:MaxMetaspaceSize=768m localrepo=build/mockk-repo # localrepo=/Users/raibaz/.m2/repository -kotlin.version=1.5.10 \ No newline at end of file +# kotlin.version=1.5.10 \ No newline at end of file diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt index a407eb769..09189bebb 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/RecordingState.kt @@ -139,12 +139,13 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState * * Max 40 calls looks like reasonable compromise */ + @Suppress("DEPRECATION_ERROR") override fun estimateCallRounds(): Int { val regularArguments = builder().signedCalls .flatMap { it.args } .filterNotNull() .map(this::typeEstimation) - .maxOrNull() ?: 1 + .max() ?: 1 val varargArguments = builder().signedCalls .mapNotNull { @@ -154,7 +155,7 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState null } }.map(this::varArgTypeEstimation) - .maxOrNull() ?: 1 + .max() ?: 1 return max(regularArguments, varargArguments) } diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt b/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt index e00b61b6e..041df875c 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/verify/VerificationHelpers.kt @@ -25,8 +25,9 @@ object VerificationHelpers { } fun stackTrace(prefix: Int, stackTrace: List): String { + @Suppress("DEPRECATION_ERROR") fun columnSize(block: StackElement.() -> String) = - stackTrace.map(block).map { it.length }.maxOrNull() ?: 0 + stackTrace.map(block).map { it.length }.max() ?: 0 fun StackElement.fileLine() = "($fileName:$line)${if (nativeMethod) "N" else ""}" diff --git a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt index 08720c7bb..6d9adb7db 100644 --- a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt +++ b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt @@ -1,7 +1,6 @@ package io.mockk.it import io.mockk.* -import kotlin.jvm.JvmInline import kotlin.test.Test import kotlin.test.assertEquals @@ -46,8 +45,8 @@ class ValueClassTest { } } -@JvmInline -private value class DummyValue(val value: Int) +// TODO should be value class in kotlin 1.5+ +private inline class DummyValue(val value: Int) private class DummyService { From fe07e444cb330d4a0a16c656c265c240ac5e830c Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Wed, 9 Jun 2021 15:08:39 +0200 Subject: [PATCH 6/9] Update ValueClassTest.kt --- .../src/test/kotlin/io/mockk/it/ValueClassTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt index 6d9adb7db..6acf64f9f 100644 --- a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt +++ b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt @@ -9,7 +9,7 @@ class ValueClassTest { private val mock = mockk() @Test - fun `value class object as return value`() { + fun valueClassObjectAsReturnValue() { every { mock.requestValue() } returns DummyValue(42) assertEquals(DummyValue(42), mock.requestValue()) @@ -18,7 +18,7 @@ class ValueClassTest { } @Test - fun `value class object as function argument and return value`() { + fun valueClassObjectAsFunctionArgumentAndReturnValue() { every { mock.processValue(DummyValue(1)) } returns DummyValue(42) assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) @@ -27,7 +27,7 @@ class ValueClassTest { } @Test - fun `value class object as function argument and answer value`() { + fun valueClassObjectAsFunctionArgumentAndAnswerValue() { every { mock.processValue(DummyValue(1)) } answers { DummyValue(42) } assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) @@ -36,7 +36,7 @@ class ValueClassTest { } @Test - fun `any value class matcher as function argument and value class object as return value`() { + fun anyValueClassMatcherAsFunctionArgumentAndValueClassObjectAsReturnValue() { every { mock.processValue(any()) } returns DummyValue(42) assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) @@ -53,4 +53,4 @@ private class DummyService { fun requestValue() = DummyValue(0) fun processValue(value: DummyValue) = DummyValue(0) -} \ No newline at end of file +} From cbce92567ac966498dfbb92bb8d8d25f69e05826 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Wed, 16 Jun 2021 11:12:46 +0200 Subject: [PATCH 7/9] refactor: dublicate ValueClassSupport to mockk-jvm --- .../main/kotlin/io/mockk/ValueClassSupport.kt | 76 +++++++++++++++++++ .../io/mockk/proxy/jvm/advice/Interceptor.kt | 1 + .../kotlin/io/mockk}/ValueClassSupport.kt | 4 +- .../kotlin/io/mockk/impl/InternalPlatform.kt | 6 +- 4 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt rename {agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice => mockk/jvm/src/main/kotlin/io/mockk}/ValueClassSupport.kt (96%) diff --git a/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt new file mode 100644 index 000000000..7e3beeec6 --- /dev/null +++ b/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -0,0 +1,76 @@ +package io.mockk.proxy.jvm + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible + +private val valueClassFieldCache = mutableMapOf, KProperty1>() + +/** + * Get boxed value of any value class + * + * @return boxed value of value class, if this is value class, else just itself + */ +fun T.boxedValue(): Any? { + if (!this::class.isValueClass()) return this + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + @Suppress("UNCHECKED_CAST") + return (backingField as KProperty1).get(this) +} + +/** + * Get class of boxed value of any value class + * + * @return class of boxed value, if this is value class, else just class of itself + */ +fun T.boxedClass(): KClass<*> { + if (!this::class.isValueClass()) return this::class + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + return backingField.returnType.classifier as KClass<*> +} + + +private fun KClass.valueField(): KProperty1 { + @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 +} + +private fun KClass.isValueClass() = try { + this.isValue +} catch (_: UnsupportedOperationException) { + 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 KClass.isValue: Boolean + get() = !isData && + primaryConstructor?.parameters?.size == 1 && + java.declaredMethods.any { it.name == "box-impl" } diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt index c7bec9bea..bcb1df19f 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt @@ -1,5 +1,6 @@ package io.mockk.proxy.jvm.advice +import io.mockk.proxy.jvm.boxedValue import io.mockk.proxy.MockKInvocationHandler import java.lang.reflect.Method import java.util.concurrent.Callable diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt b/mockk/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt similarity index 96% rename from agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt rename to mockk/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt index 074576058..77e432048 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/ValueClassSupport.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -1,4 +1,4 @@ -package io.mockk.proxy.jvm.advice +package io.mockk import kotlin.reflect.KClass import kotlin.reflect.KProperty1 @@ -58,7 +58,7 @@ private fun KClass.valueField(): KProperty1 { } as KProperty1 } -fun KClass.isValueClass() = try { +private fun KClass.isValueClass() = try { this.isValue } catch (_: UnsupportedOperationException) { false diff --git a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt index 24650f130..0f74d6083 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt @@ -1,13 +1,9 @@ package io.mockk.impl -import io.mockk.InternalPlatformDsl -import io.mockk.MockKException -import io.mockk.StackElement +import io.mockk.* import io.mockk.impl.platform.CommonIdentityHashMapOf import io.mockk.impl.platform.CommonRef import io.mockk.impl.platform.JvmWeakConcurrentMap -import io.mockk.proxy.jvm.advice.boxedClass -import io.mockk.proxy.jvm.advice.boxedValue import java.lang.ref.WeakReference import java.lang.reflect.Modifier import java.util.* From e198b320a0b943dae6f3bbeb9759781bf32baf71 Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Wed, 16 Jun 2021 11:29:46 +0200 Subject: [PATCH 8/9] feat: android support --- agent/android/build.gradle | 2 + .../main/kotlin/io/mockk/ValueClassSupport.kt | 76 +++++++++++++++++++ .../io/mockk/proxy/android/advice/Advice.kt | 2 + .../main/kotlin/io/mockk/ValueClassSupport.kt | 2 +- .../io/mockk/proxy/jvm/advice/Interceptor.kt | 2 +- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt diff --git a/agent/android/build.gradle b/agent/android/build.gradle index ffe7223a2..b27dc8875 100644 --- a/agent/android/build.gradle +++ b/agent/android/build.gradle @@ -85,4 +85,6 @@ dependencies { exclude group: 'com.android.support', module: 'support-annotations' }) androidTestImplementation 'junit:junit:4.13.1' + + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } diff --git a/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt b/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt new file mode 100644 index 000000000..77e432048 --- /dev/null +++ b/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -0,0 +1,76 @@ +package io.mockk + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible + +private val valueClassFieldCache = mutableMapOf, KProperty1>() + +/** + * Get boxed value of any value class + * + * @return boxed value of value class, if this is value class, else just itself + */ +fun T.boxedValue(): Any? { + if (!this::class.isValueClass()) return this + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + @Suppress("UNCHECKED_CAST") + return (backingField as KProperty1).get(this) +} + +/** + * Get class of boxed value of any value class + * + * @return class of boxed value, if this is value class, else just class of itself + */ +fun T.boxedClass(): KClass<*> { + if (!this::class.isValueClass()) return this::class + + // get backing field + val backingField = this::class.valueField() + + // get boxed value + return backingField.returnType.classifier as KClass<*> +} + + +private fun KClass.valueField(): KProperty1 { + @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 +} + +private fun KClass.isValueClass() = try { + this.isValue +} catch (_: UnsupportedOperationException) { + 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 KClass.isValue: Boolean + get() = !isData && + primaryConstructor?.parameters?.size == 1 && + java.declaredMethods.any { it.name == "box-impl" } diff --git a/agent/android/src/main/kotlin/io/mockk/proxy/android/advice/Advice.kt b/agent/android/src/main/kotlin/io/mockk/proxy/android/advice/Advice.kt index 0116f9701..e1b9e18ab 100644 --- a/agent/android/src/main/kotlin/io/mockk/proxy/android/advice/Advice.kt +++ b/agent/android/src/main/kotlin/io/mockk/proxy/android/advice/Advice.kt @@ -5,6 +5,7 @@ package io.mockk.proxy.android.advice +import io.mockk.boxedValue import io.mockk.proxy.MockKAgentException import io.mockk.proxy.android.AndroidMockKMap import io.mockk.proxy.android.MethodDescriptor @@ -80,6 +81,7 @@ internal class Advice( superMethodCall, arguments ) + ?.boxedValue() // unbox value class objects } } diff --git a/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt b/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt index 7e3beeec6..77e432048 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -1,4 +1,4 @@ -package io.mockk.proxy.jvm +package io.mockk import kotlin.reflect.KClass import kotlin.reflect.KProperty1 diff --git a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt index bcb1df19f..dcb8019c5 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/proxy/jvm/advice/Interceptor.kt @@ -1,6 +1,6 @@ package io.mockk.proxy.jvm.advice -import io.mockk.proxy.jvm.boxedValue +import io.mockk.boxedValue import io.mockk.proxy.MockKInvocationHandler import java.lang.reflect.Method import java.util.concurrent.Callable From c4d1bd73635c2e06e4bbfbc54339e69a30938eef Mon Sep 17 00:00:00 2001 From: Bengt Brodersen Date: Wed, 16 Jun 2021 20:31:33 +0200 Subject: [PATCH 9/9] refactor: reorganize imports --- mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt index 0f74d6083..13b72b310 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt @@ -1,6 +1,10 @@ package io.mockk.impl -import io.mockk.* +import io.mockk.InternalPlatformDsl +import io.mockk.MockKException +import io.mockk.StackElement +import io.mockk.boxedClass +import io.mockk.boxedValue import io.mockk.impl.platform.CommonIdentityHashMapOf import io.mockk.impl.platform.CommonRef import io.mockk.impl.platform.JvmWeakConcurrentMap