diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 613965d21..aa2fea9db 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -6,56 +6,73 @@ on: pull_request: branches: [ master ] +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + jobs: tests: + runs-on: ubuntu-latest strategy: matrix: - java-version: [11, 12, 13, 14, 15, 16, 17] - kotlin-version: [1.6.21, 1.7.10] - kotlin-ir-enabled: [true, false] + java-version: [ 11, 17, 18 ] # test LTS versions, and the newest + kotlin-version: [ 1.5.31, 1.6.21, 1.7.10 ] + kotlin-ir-enabled: [ true, false ] # in case one JDK fails, we still want to see results from others fail-fast: false - runs-on: ubuntu-latest - + timeout-minutes: 30 steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-${{ matrix.java-version }}-${{ matrix.kotlin-version }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.java-version }}-gradle- - - name: Set up JDK ${{ matrix.java-version }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java-version }} - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Run tests with Gradle - uses: eskatos/gradle-command-action@v1 - with: - arguments: test --stacktrace -Pkotlin.version=${{ matrix.kotlin-version }} -Pkotlin.ir.enabled=${{ matrix.kotlin-ir-enabled }} + - uses: actions/checkout@v2 + + - name: Setup Gradle Dependencies Cache + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }} + + - name: Setup Gradle Wrapper Cache + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java-version }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run tests with Gradle + uses: eskatos/gradle-command-action@v1 + with: + arguments: test --stacktrace -Pkotlin.version=${{ matrix.kotlin-version }} -Pkotlin.ir.enabled=${{ matrix.kotlin-ir-enabled }} + android-instrumented-tests: runs-on: macos-latest strategy: matrix: api-level: [ 28, 29 ] + timeout-minutes: 30 steps: - uses: actions/setup-java@v2 with: distribution: 'adopt' java-version: '11' - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-${{ matrix.api-level }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.api-level }}-gradle- + + - name: Setup Gradle Dependencies Cache + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }} + + - name: Setup Gradle Wrapper Cache + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/README.md b/README.md index 1984f7237..04cd46526 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![mockk](doc/logo-site.png) ![kotlin](doc/kotlin-logo.png) [![Gitter](https://badges.gitter.im/mockk-io/Lobby.svg)](https://gitter.im/mockk-io/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) -[![Relase Version](https://img.shields.io/maven-central/v/io.mockk/mockk.svg?label=release)](http://search.maven.org/#search%7Cga%7C1%7Cmockk) +[![Relase Version](https://img.shields.io/maven-central/v/io.mockk/mockk.svg?label=release)](https://search.maven.org/#search%7Cga%7C1%7Cmockk) [![Change log](https://img.shields.io/badge/change%20log-%E2%96%A4-yellow.svg)](https://github.com/mockk/mockk/releases) [![codecov](https://codecov.io/gh/mockk/mockk/branch/master/graph/badge.svg)](https://codecov.io/gh/mockk/mockk) [![Android](https://img.shields.io/badge/android-support-green.svg)](https://mockk.io/ANDROID) @@ -1397,7 +1397,7 @@ This project exists thanks to all the people who contribute. To ask questions, please use Stack Overflow or Gitter. * Chat/Gitter: [https://gitter.im/mockk-io/Lobby](https://gitter.im/mockk-io/Lobby) -* Stack Overflow: [http://stackoverflow.com/questions/tagged/mockk](http://stackoverflow.com/questions/tagged/mockk) +* Stack Overflow: [http://stackoverflow.com/questions/tagged/mockk](https://stackoverflow.com/questions/tagged/mockk) To report bugs, please use the GitHub project. diff --git a/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt b/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt index 1781141b5..961634ead 100644 --- a/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt +++ b/agent/android/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -6,80 +6,60 @@ import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible -private val valueClassFieldCache = mutableMapOf, KProperty1>() +// 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.boxedValue(): Any? { - if (!this::class.isValueClass()) return this - - // get backing field - val backingField = this::class.valueField() - - // get boxed value +val T.boxedValue: Any? @Suppress("UNCHECKED_CAST") - return (backingField as KProperty1).get(this) -} + get() = if (!this::class.isValue_safe) { + this + } else { + (this::class as KClass).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.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, KProperty1>() /** - * 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 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 (_: Throwable) { - false -} +private val KClass.boxedProperty: KProperty1 + 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 + } /** - * 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 KClass.isValue: Boolean - get() = !isData && - primaryConstructor?.parameters?.size == 1 && - java.declaredMethods.any { it.name == "box-impl" } +private val KClass.isValue_safe: Boolean + get() = try { + this.isValue + } catch (_: UnsupportedOperationException) { + false + } 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 e1b9e18ab..4c4dd594f 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 @@ -81,7 +81,7 @@ internal class Advice( superMethodCall, arguments ) - ?.boxedValue() // unbox value class objects + ?.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 1781141b5..961634ead 100644 --- a/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt +++ b/agent/jvm/src/main/kotlin/io/mockk/ValueClassSupport.kt @@ -6,80 +6,60 @@ import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible -private val valueClassFieldCache = mutableMapOf, KProperty1>() +// 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.boxedValue(): Any? { - if (!this::class.isValueClass()) return this - - // get backing field - val backingField = this::class.valueField() - - // get boxed value +val T.boxedValue: Any? @Suppress("UNCHECKED_CAST") - return (backingField as KProperty1).get(this) -} + get() = if (!this::class.isValue_safe) { + this + } else { + (this::class as KClass).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.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, KProperty1>() /** - * 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 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 (_: Throwable) { - false -} +private val KClass.boxedProperty: KProperty1 + 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 + } /** - * 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 KClass.isValue: Boolean - get() = !isData && - primaryConstructor?.parameters?.size == 1 && - java.declaredMethods.any { it.name == "box-impl" } +private val KClass.isValue_safe: Boolean + get() = try { + this.isValue + } catch (_: UnsupportedOperationException) { + false + } 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 dcb8019c5..1ce1673cf 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 @@ -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 } } diff --git a/build.gradle b/build.gradle index 822e4976d..4f889e0a7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import java.time.Duration + buildscript { ext.kotlin_gradle_version = findProperty('kotlin.version')?.toString() ?: '1.7.10' ext.android_gradle_version = '7.2.1' @@ -8,7 +10,7 @@ buildscript { ext.objenesis_android_version = '3.2' ext.junit_jupiter_version = '5.6.2' ext.junit_vintage_version = '5.6.2' - ext.dokka_version = '1.7.0' + ext.dokka_version = '1.7.10' ext.gradles = project.projectDir.toString() + "/gradle" repositories { @@ -31,7 +33,7 @@ subprojects { subProject -> group = 'io.mockk' ext.kotlin_version = findProperty('kotlin.version')?.toString() ?: '1.7.10' - ext.kotlin_gradle_version = findProperty('kotlin.version')?.toString() ?: '1.7.10' + ext.kotlin_gradle_version = findProperty('kotlin.version')?.toString() ?: '1.6.0' repositories { mavenCentral() @@ -40,9 +42,13 @@ subprojects { subProject -> tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - apiVersion = "1.7" - jvmTarget = "17" - languageVersion = "1.7" + jvmTarget = "1.8" + apiVersion = "1.5" + languageVersion = "1.5" } } + + tasks.withType(Test).configureEach { + timeout.set(Duration.ofMinutes(5)) + } } diff --git a/doc/font.txt b/doc/font.txt deleted file mode 100644 index 20dfc42c9..000000000 --- a/doc/font.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://www.behance.net/gallery/17890579/AXIS-Typeface - diff --git a/doc/icon.svg b/doc/icon.svg index 16bf3da52..e82b4b26c 100644 --- a/doc/icon.svg +++ b/doc/icon.svg @@ -2,20 +2,20 @@ + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)" + sodipodi:docname="icon.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:window-width="1277" + inkscape:window-height="1528" + inkscape:window-x="1273" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:pagecheckerboard="0" + fit-margin-left="5" + fit-margin-top="5" + fit-margin-right="5" + fit-margin-bottom="5" /> @@ -44,65 +49,52 @@ image/svg+xml - - - - - - + id="layer1" + transform="translate(-63.497518,-64.382512)"> + + + + + + K diff --git a/doc/logo.svg b/doc/logo.svg index d6a6e18bf..018410b39 100644 --- a/doc/logo.svg +++ b/doc/logo.svg @@ -2,23 +2,23 @@ + inkscape:export-ydpi="99.209999" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:window-width="1277" + inkscape:window-height="1528" + inkscape:window-x="1273" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:pagecheckerboard="0" + units="px" /> @@ -47,7 +49,6 @@ image/svg+xml - @@ -55,57 +56,44 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> - - - - - + + + + + + MockK + x="7.7152562" + y="37.920082" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.1111px;font-family:Axis;-inkscape-font-specification:Axis;fill:#ffffff;stroke-width:0.264583">MockK diff --git a/doc/new.svg b/doc/new.svg index f40262541..cd7b59635 100644 --- a/doc/new.svg +++ b/doc/new.svg @@ -2,20 +2,20 @@ + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)" + sodipodi:docname="new.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:window-width="1277" + inkscape:window-height="1528" + inkscape:window-x="1273" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:pagecheckerboard="0" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> @@ -44,37 +49,27 @@ image/svg+xml - - - - NEW + id="layer1" + transform="translate(-52.967503,-77.017563)"> + + + + + diff --git a/doc/typefaces/Axis Extrabold.otf b/doc/typefaces/Axis Extrabold.otf new file mode 100644 index 000000000..9fe377638 Binary files /dev/null and b/doc/typefaces/Axis Extrabold.otf differ diff --git a/doc/typefaces/typefaces.md b/doc/typefaces/typefaces.md new file mode 100644 index 000000000..1006ded8f --- /dev/null +++ b/doc/typefaces/typefaces.md @@ -0,0 +1,7 @@ +### Axis + +https://freefontslab.com/axis-font-download-free/ + +Author: [Jean Wojciechowski](https://www.behance.net/jeanwoj). + +This font is free to use for personal and commercial purposes diff --git a/dsl/common/src/main/kotlin/io/mockk/API.kt b/dsl/common/src/main/kotlin/io/mockk/API.kt index 2c095eaff..0c802aceb 100644 --- a/dsl/common/src/main/kotlin/io/mockk/API.kt +++ b/dsl/common/src/main/kotlin/io/mockk/API.kt @@ -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) } } diff --git a/dsl/common/src/main/kotlin/io/mockk/InternalPlatformDsl.kt b/dsl/common/src/main/kotlin/io/mockk/InternalPlatformDsl.kt index c01d0bd17..e55b0d14d 100644 --- a/dsl/common/src/main/kotlin/io/mockk/InternalPlatformDsl.kt +++ b/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 @@ -35,6 +36,26 @@ expect object InternalPlatformDsl { fun counter(): InternalCounter fun coroutineCall(lambda: suspend () -> T): CoroutineCall + + /** + * 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 boxCast( + cls: KClass<*>, + arg: Any, + ): T } interface CoroutineCall { diff --git a/dsl/common/src/main/kotlin/io/mockk/Matchers.kt b/dsl/common/src/main/kotlin/io/mockk/Matchers.kt index c9666ee08..100cad605 100644 --- a/dsl/common/src/main/kotlin/io/mockk/Matchers.kt +++ b/dsl/common/src/main/kotlin/io/mockk/Matchers.kt @@ -140,17 +140,16 @@ data class CaptureNullableMatcher( */ data class CapturingSlotMatcher( val captureSlot: CapturingSlot, - override val argumentType: KClass<*> + override val argumentType: KClass<*>, ) : Matcher, CapturingMatcher, TypedMatcher, EquivalentMatcher { override fun equivalent(): Matcher = ConstantMatcher(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 } @@ -473,4 +472,3 @@ fun CompositeMatcher<*>.captureSubMatchers(arg: Any?) { .forEach { it.capture(arg) } } } - diff --git a/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt b/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt index 6854e59d9..1419a1367 100644 --- a/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt +++ b/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt @@ -1,6 +1,6 @@ package io.mockk -import kotlinx.coroutines.runBlocking +import io.mockk.ValueClassSupportDsl.boxedClass import java.lang.reflect.AccessibleObject import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method @@ -13,10 +13,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) @@ -44,7 +47,7 @@ actual object InternalPlatformDsl { kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -> "SUSPEND_MARKER" is Continuation<*> -> "continuation {}" is KClass<*> -> this.simpleName ?: "" - is Method -> name + "(" + parameterTypes.map { it.simpleName }.joinToString() + ")" + is Method -> name + "(" + parameterTypes.joinToString { it.simpleName } + ")" is Function<*> -> "lambda {}" else -> toString() } @@ -214,11 +217,26 @@ actual object InternalPlatformDsl { } actual fun coroutineCall(lambda: suspend () -> T): CoroutineCall = JvmCoroutineCall(lambda) + + actual fun unboxClass(cls: KClass<*>): KClass<*> = cls.boxedClass + + @Suppress("UNCHECKED_CAST") + actual fun 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(private val lambda: suspend () -> T) : CoroutineCall { companion object { - val callMethod = JvmCoroutineCall::class.java.getMethod("callCoroutine", Continuation::class.java) + val callMethod: Method = JvmCoroutineCall::class.java.getMethod("callCoroutine", Continuation::class.java) } suspend fun callCoroutine() = lambda() diff --git a/dsl/jvm/src/main/kotlin/io/mockk/ValueClassSupportDsl.kt b/dsl/jvm/src/main/kotlin/io/mockk/ValueClassSupportDsl.kt new file mode 100644 index 000000000..650c7aeca --- /dev/null +++ b/dsl/jvm/src/main/kotlin/io/mockk/ValueClassSupportDsl.kt @@ -0,0 +1,71 @@ +package io.mockk + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.jvm.isAccessible + +/** + * Provides value class support in the `mockk-dsl-jvm` subproject. + * + * This is marked as internal so that it won't clash with the other class in `mockk-agent-jvm`. + * + * TODO this class is copy-pasted and should be de-duplicated, see https://github.com/mockk/mockk/issues/857 + */ +internal object ValueClassSupportDsl { + + /** + * Underlying property value of a **`value class`** or self. + * + * The type of the return might also be a `value class`! + */ + val T.boxedValue: Any? + @Suppress("UNCHECKED_CAST") + get() = if (!this::class.isValue_safe) { + this + } else { + (this::class as KClass).boxedProperty.get(this) + } + + /** + * Underlying property class of a **`value class`** or self. + * + * The returned class might also be a `value class`! + */ + val KClass<*>.boxedClass: KClass<*> + get() = if (!this.isValue_safe) { + this + } else { + this.boxedProperty.returnType.classifier as KClass<*> + } + + private val valueClassFieldCache = mutableMapOf, KProperty1>() + + /** + * Underlying property of a **`value class`**. + * + * The underlying property might also be a `value class`! + */ + private val KClass.boxedProperty: KProperty1 + 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 + } + + /** + * Returns `true` if calling [KClass.isValue] is safe. + * + * (In some instances [KClass.isValue] can throw an exception.) + */ + private val KClass.isValue_safe: Boolean + get() = try { + this.isValue + } catch (_: UnsupportedOperationException) { + false + } +} diff --git a/gradle.properties b/gradle.properties index aed09b3f8..cc19a5769 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,12 @@ -android.useAndroidX=true -kotlin.version=1.7.10 -localrepo=/Users/hendr/.m2/repository +version=1.12.5-SNAPSHOT +# Enable Gradle build cache https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true org.gradle.configureondemand=false -org.gradle.jvmargs=-XX:MaxMetaspaceSize=768m org.gradle.parallel=true -version=1.13.0-SNAPSHOT +# disable annoying Gradle Welcome in CI/CD +org.gradle.welcome=never +org.gradle.jvmargs=-XX:MaxMetaspaceSize=768m +# localrepo=build/mockk-repo +localrepo=/Users/raibaz/.m2/repository +kotlin.version=1.7.10 +android.useAndroidX=true diff --git a/gradle/upload.gradle b/gradle/upload.gradle index 32fce7923..d128b0baf 100644 --- a/gradle/upload.gradle +++ b/gradle/upload.gradle @@ -28,7 +28,7 @@ afterEvaluate { pom { name = mavenName description = mavenDescription - url = 'http://mockk.io' + url = 'https://mockk.io' scm { connection = 'scm:git:git@github.com:mockk/mockk.git' @@ -52,7 +52,7 @@ afterEvaluate { licenses { license { name = 'Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' } } } diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/PermanentMocker.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/PermanentMocker.kt index a57a3e531..0203747cb 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/PermanentMocker.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/PermanentMocker.kt @@ -145,4 +145,4 @@ class PermanentMocker( return prefix + methodName + "(" + args.joinToString(", ") + ")" } -} \ No newline at end of file +} diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/SignatureValueGenerator.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/SignatureValueGenerator.kt index 53f21cc14..5605a413e 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/SignatureValueGenerator.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/SignatureValueGenerator.kt @@ -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 signatureValue(cls: KClass, orInstantiateVia: () -> T): T -} \ No newline at end of file + fun signatureValue( + cls: KClass, + anyValueGeneratorProvider: () -> AnyValueGenerator, + instantiator: AbstractInstantiator, + ): T +} 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 b58b45b23..e78bb1b94 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 @@ -42,15 +42,17 @@ abstract class RecordingState(recorder: CommonCallRecorder) : CallRecordingState recorder.calls.addAll(detector.calls) } - @Suppress("UNCHECKED_CAST") override fun matcher(matcher: Matcher<*>, cls: KClass): 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 } diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt index fef1eeb9e..6a0cdb9ee 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt @@ -72,4 +72,4 @@ class StubbingAwaitingAnswerState(recorder: CommonCallRecorder) : CallRecordingS } private fun String.toCamelCase() = if (isEmpty()) this else substring(0, 1).lowercase() + substring(1) -} \ No newline at end of file +} diff --git a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/VerifyingState.kt b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/VerifyingState.kt index b8327968f..b251833d7 100644 --- a/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/VerifyingState.kt +++ b/mockk/common/src/main/kotlin/io/mockk/impl/recording/states/VerifyingState.kt @@ -54,18 +54,13 @@ class VerifyingState( } private fun failIfNotPassed(outcome: VerificationResult, inverse: Boolean) { - if (inverse) { - when (outcome) { - is VerificationResult.OK -> { - val callsReport = VerificationHelpers.formatCalls(outcome.verifiedCalls) - throw AssertionError("Inverse verification failed.\n\nVerified calls:\n$callsReport") - } - else -> {} + when (outcome) { + is VerificationResult.OK -> if (inverse) { + val callsReport = VerificationHelpers.formatCalls(outcome.verifiedCalls) + throw AssertionError("Inverse verification failed.\n\nVerified calls:\n$callsReport") } - } else { - when (outcome) { - is VerificationResult.Failure -> throw AssertionError("Verification failed: ${outcome.message}") - else -> {} + is VerificationResult.Failure -> if (!inverse) { + throw AssertionError("Verification failed: ${outcome.message}") } } } @@ -88,17 +83,17 @@ class VerifyingState( } } - if (!calledStubs.isEmpty()) { + if (calledStubs.isNotEmpty()) { if (calledStubs.size == 1) { val calledStub = calledStubs[0] throw AssertionError(recorder.safeExec { "Verification failed: ${calledStub.toStr()} should not be called:\n" + - calledStub.allRecordedCalls().joinToString("\n") + calledStub.allRecordedCalls().joinToString("\n") }) } else { throw AssertionError(recorder.safeExec { - "Verification failed: ${calledStubs.map { it.toStr() }.joinToString(", ")} should not be called:\n" + - calledStubs.flatMap { it.allRecordedCalls() }.joinToString("\n") + "Verification failed: ${calledStubs.joinToString(", ") { it.toStr() }} should not be called:\n" + + calledStubs.flatMap { it.allRecordedCalls() }.joinToString("\n") }) } } @@ -107,4 +102,4 @@ class VerifyingState( companion object { val log = Logger() } -} \ No newline at end of file +} diff --git a/mockk/common/src/test/kotlin/io/mockk/it/SealedClassTest.kt b/mockk/common/src/test/kotlin/io/mockk/it/SealedClassTest.kt new file mode 100644 index 000000000..efa39cab2 --- /dev/null +++ b/mockk/common/src/test/kotlin/io/mockk/it/SealedClassTest.kt @@ -0,0 +1,49 @@ +package io.mockk.it + +import io.mockk.every +import io.mockk.mockk +import kotlin.test.Test +import kotlin.test.assertEquals + + +class SealedClassTest { + + @Test + fun serviceReturnsSealedClassImpl() { + val factory = mockk { + every { create() } returns Leaf(1) + } + + val result = factory.create() + + assertEquals(Leaf(1), result) + } + + @Test + fun serviceAnswersSealedClassImpl() { + val factory = mockk { + every { create() } answers { Leaf(1) } + } + + val result = factory.create() + + assertEquals(Leaf(1), result) + } + + companion object { + + sealed class Node + + data class Root(val id: Int) : Node() + data class Leaf(val id: Int) : Node() + + interface Factory { + fun create(): Node + } + + class FactoryImpl : Factory { + override fun create(): Node = Root(0) + } + + } +} diff --git a/mockk/common/src/test/kotlin/io/mockk/it/SealedInterfaceTest.kt b/mockk/common/src/test/kotlin/io/mockk/it/SealedInterfaceTest.kt new file mode 100644 index 000000000..91f727abf --- /dev/null +++ b/mockk/common/src/test/kotlin/io/mockk/it/SealedInterfaceTest.kt @@ -0,0 +1,49 @@ +package io.mockk.it + +import io.mockk.every +import io.mockk.mockk +import kotlin.test.Test +import kotlin.test.assertEquals + + +class SealedInterfaceTest { + + @Test + fun serviceReturnsSealedClassImpl() { + val factory = mockk { + every { create() } returns Leaf(1) + } + + val result = factory.create() + + assertEquals(Leaf(1), result) + } + + @Test + fun serviceAnswersSealedClassImpl() { + val factory = mockk { + every { create() } answers { Leaf(1) } + } + + val result = factory.create() + + assertEquals(Leaf(1), result) + } + + companion object { + + sealed interface Node + + data class Root(val id: Int) : Node + data class Leaf(val id: Int) : Node + + interface Factory { + fun create(): Node + } + + class FactoryImpl : Factory { + override fun create(): Node = Root(0) + } + + } +} 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 c93c8dc70..b95d04231 100644 --- a/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt +++ b/mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt @@ -2,56 +2,507 @@ package io.mockk.it import io.mockk.* import kotlin.jvm.JvmInline +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals class ValueClassTest { - private val mock = mockk() + private val dummyValueWrapperArg get() = DummyValueWrapper(DummyValue(42)) + private val dummyValueWrapperReturn get() = DummyValueWrapper(DummyValue(99)) + private val dummyValueClassArg get() = DummyValue(101) + private val dummyValueClassReturn get() = DummyValue(202) + + // @Test - fun valueClassObjectAsReturnValue() { - every { mock.requestValue() } returns DummyValue(42) + fun `arg is ValueClass, returns ValueClass`() { + val mock = mockk { + every { argValueClassReturnValueClass(dummyValueClassArg) } returns dummyValueClassReturn + } - assertEquals(DummyValue(42), mock.requestValue()) + assertEquals(dummyValueClassReturn, mock.argValueClassReturnValueClass(dummyValueClassArg)) - verify { mock.requestValue() } + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } } @Test - fun valueClassObjectAsFunctionArgumentAndReturnValue() { - every { mock.processValue(DummyValue(1)) } returns DummyValue(42) + fun `arg is any(ValueClass), returns ValueClass`() { + val mock = mockk { + every { argValueClassReturnValueClass(any()) } returns dummyValueClassReturn + } - assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + assertEquals(dummyValueClassReturn, mock.argValueClassReturnValueClass(dummyValueClassArg)) - verify { mock.processValue(DummyValue(1)) } + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } } @Test - fun valueClassObjectAsFunctionArgumentAndAnswerValue() { - every { mock.processValue(DummyValue(1)) } answers { DummyValue(42) } + fun `arg is slot(ValueClass), returns ValueClass`() { + val slot = slot() + val mock = mockk { + every { argValueClassReturnValueClass(capture(slot)) } returns dummyValueClassReturn + } + + val result = mock.argValueClassReturnValueClass(dummyValueClassArg) + + assertEquals(dummyValueClassReturn, result) - assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + assertEquals(dummyValueClassArg, slot.captured) - verify { mock.processValue(DummyValue(1)) } + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } } @Test - fun anyValueClassMatcherAsFunctionArgumentAndValueClassObjectAsReturnValue() { - every { mock.processValue(any()) } returns DummyValue(42) + fun `arg is ValueClass, answers ValueClass`() { + val mock = mockk { + every { argValueClassReturnValueClass(dummyValueClassArg) } answers { dummyValueClassReturn } + } - assertEquals(DummyValue(42), mock.processValue(DummyValue(1))) + assertEquals(dummyValueClassReturn, mock.argValueClassReturnValueClass(dummyValueClassArg)) - verify { mock.processValue(DummyValue(1)) } + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } } -} -@JvmInline -private value class DummyValue(val value: Int) + @Test + fun `arg is any(ValueClass), answers ValueClass`() { + val mock = mockk { + every { argValueClassReturnValueClass(any()) } answers { dummyValueClassReturn } + } + + assertEquals(dummyValueClassReturn, mock.argValueClassReturnValueClass(dummyValueClassArg)) + + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } + } + + @Test + fun `arg is slot(ValueClass), answers ValueClass`() { + val slot = slot() + + val mock = mockk { + every { argValueClassReturnValueClass(capture(slot)) } answers { dummyValueClassReturn } + } + + val result = mock.argValueClassReturnValueClass(dummyValueClassArg) + + assertEquals(dummyValueClassReturn, result) + + assertEquals(dummyValueClassArg, slot.captured) + + verify { mock.argValueClassReturnValueClass(dummyValueClassArg) } + } + // + + // + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is ValueClass, returns Wrapper`() { + val mock = mockk { + every { argValueClassReturnWrapper(dummyValueClassArg) } returns dummyValueWrapperReturn + } + + assertEquals(dummyValueWrapperReturn, mock.argValueClassReturnWrapper(dummyValueClassArg)) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(ValueClass), returns Wrapper`() { + val mock = mockk { + every { argValueClassReturnWrapper(any()) } returns dummyValueWrapperReturn + } + + assertEquals(dummyValueWrapperArg, mock.argValueClassReturnWrapper(dummyValueClassArg)) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(ValueClass), returns Wrapper`() { + val slot = slot() + + val mock = mockk { + every { argValueClassReturnWrapper(capture(slot)) } returns dummyValueWrapperReturn + } + + val result = mock.argValueClassReturnWrapper(dummyValueClassArg) + + assertEquals(dummyValueWrapperReturn, result) + + assertEquals(dummyValueClassArg, slot.captured) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is ValueClass, answers Wrapper`() { + val mock = mockk { + every { argValueClassReturnWrapper(dummyValueClassArg) } answers { dummyValueWrapperReturn } + } + + assertEquals(dummyValueWrapperReturn, mock.argValueClassReturnWrapper(dummyValueClassArg)) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(ValueClass), answers Wrapper`() { + val mock = mockk { + every { argValueClassReturnWrapper(any()) } answers { dummyValueWrapperReturn } + } + + assertEquals(dummyValueWrapperReturn, mock.argValueClassReturnWrapper(dummyValueClassArg)) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(ValueClass), answers Wrapper`() { + val slot = slot() + + val mock = mockk { + every { argValueClassReturnWrapper(capture(slot)) } answers { dummyValueWrapperReturn } + } + + val result = mock.argValueClassReturnWrapper(dummyValueClassArg) + + assertEquals(dummyValueWrapperReturn, result) + + assertEquals(dummyValueClassArg, slot.captured) + + verify { mock.argValueClassReturnWrapper(dummyValueClassArg) } + } + // + + // + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is Wrapper, returns ValueClass`() { + val mock = mockk { + every { argWrapperReturnValueClass(dummyValueWrapperArg) } returns dummyValueClassReturn + } + + assertEquals(dummyValueClassReturn, mock.argWrapperReturnValueClass(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(Wrapper), returns ValueClass`() { + val mock = mockk { + every { argWrapperReturnValueClass(any()) } returns dummyValueClassReturn + } + + assertEquals(dummyValueClassReturn, mock.argWrapperReturnValueClass(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(Wrapper), returns ValueClass`() { + val slot = slot() + val mock = mockk { + every { argWrapperReturnValueClass(capture(slot)) } returns dummyValueClassReturn + } + + val result = mock.argWrapperReturnValueClass(dummyValueWrapperArg) + + assertEquals(dummyValueClassReturn, result) + + assertEquals(dummyValueWrapperArg, slot.captured) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is Wrapper, answers ValueClass`() { + val mock = mockk { + every { argWrapperReturnValueClass(dummyValueWrapperArg) } answers { dummyValueClassReturn } + } + + assertEquals(dummyValueClassReturn, mock.argWrapperReturnValueClass(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(Wrapper), answers ValueClass`() { + val mock = mockk { + every { argWrapperReturnValueClass(any()) } answers { dummyValueClassReturn } + } + + assertEquals(dummyValueClassReturn, mock.argWrapperReturnValueClass(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(Wrapper), answers ValueClass`() { + val slot = slot() + + val mock = mockk { + every { argWrapperReturnValueClass(capture(slot)) } answers { dummyValueClassReturn } + } + + val result = mock.argWrapperReturnValueClass(dummyValueWrapperArg) + + assertEquals(dummyValueClassReturn, result) + + assertEquals(dummyValueWrapperArg, slot.captured) + + verify { mock.argWrapperReturnValueClass(dummyValueWrapperArg) } + } + // + + // + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is Wrapper, returns Wrapper`() { + val mock = mockk { + every { argWrapperReturnWrapper(dummyValueWrapperArg) } returns dummyValueWrapperReturn + } + + assertEquals(dummyValueWrapperReturn, mock.argWrapperReturnWrapper(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(Wrapper), returns Wrapper`() { + val mock = mockk { + every { argWrapperReturnWrapper(any()) } returns dummyValueWrapperReturn + } + + assertEquals(dummyValueWrapperReturn, mock.argWrapperReturnWrapper(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(Wrapper), returns Wrapper`() { + val slot = slot() + val mock = mockk { + every { argWrapperReturnWrapper(capture(slot)) } returns dummyValueWrapperReturn + } + + val result = mock.argWrapperReturnWrapper(dummyValueWrapperArg) + + assertEquals(dummyValueWrapperReturn, result) + + assertEquals(dummyValueWrapperArg, slot.captured) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is Wrapper, answers Wrapper`() { + val mock = mockk { + every { argWrapperReturnWrapper(dummyValueWrapperArg) } answers { dummyValueWrapperReturn } + } + + assertEquals(dummyValueWrapperReturn, mock.argWrapperReturnWrapper(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is any(Wrapper), answers Wrapper`() { + val mock = mockk { + every { argWrapperReturnWrapper(any()) } answers { dummyValueWrapperReturn } + } + + assertEquals(dummyValueWrapperReturn, mock.argWrapperReturnWrapper(dummyValueWrapperArg)) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + + @Test + @Ignore // TODO support nested value classes https://github.com/mockk/mockk/issues/859 + fun `arg is slot(Wrapper), answers Wrapper`() { + val slot = slot() + + val mock = mockk { + every { argWrapperReturnWrapper(capture(slot)) } answers { dummyValueWrapperReturn } + } + + val result = mock.argWrapperReturnWrapper(dummyValueWrapperArg) + + assertEquals(dummyValueWrapperReturn, result) + + assertEquals(dummyValueWrapperArg, slot.captured) + + verify { mock.argWrapperReturnWrapper(dummyValueWrapperArg) } + } + // + + // + /** https://github.com/mockk/mockk/issues/729 */ + @Test + fun `arg None, returns UInt`() { + val mock = mockk { + every { argNoneReturnsUInt() } returns 999u + } + + val result = mock.argNoneReturnsUInt() + + assertEquals(999u, result) + } + + /** https://github.com/mockk/mockk/issues/729 */ + @Test + fun `arg None, answers UInt`() { + val mock = mockk { + every { argNoneReturnsUInt() } answers { 999u } + } + + val result = mock.argNoneReturnsUInt() + + assertEquals(999u, result) + } + // + + // + // + @Test + @Ignore // TODO fix infinite loop + fun `receiver is String, return is ValueClass`() { + val fn = mockk DummyValue>() + + every { "string".fn() } returns dummyValueClassReturn + + val result = "string".fn() + + assertEquals(dummyValueClassReturn, result) + } + + @Test + @Ignore // TODO fix infinite loop + fun `receiver is String, return is Wrapper`() { + val fn = mockk DummyValueWrapper>() + + every { "string".fn() } returns dummyValueWrapperArg -private class DummyService { + val result = "string".fn() - fun requestValue() = DummyValue(0) + assertEquals(dummyValueWrapperArg, result) + } + // + + // + @Test + @Ignore // TODO fix infinite loop + fun `receiver is Wrapper, return is Wrapper`() { + val fn = mockk DummyValueWrapper>() + + every { dummyValueWrapperArg.fn() } returns dummyValueWrapperArg + + val result = dummyValueWrapperArg.fn() + + assertEquals(dummyValueWrapperArg, result) + } + + @Test + @Ignore // TODO fix infinite loop + fun `receiver is Wrapper, return is ValueClass`() { + val fn = mockk DummyValue>() + + every { dummyValueWrapperArg.fn() } returns dummyValueClassReturn + + val result = dummyValueWrapperArg.fn() + + assertEquals(dummyValueClassArg, result) + } + + @Test + @Ignore // TODO fix infinite loop + fun `receiver is Wrapper, return is String`() { + val fn = mockk String>() + + every { dummyValueWrapperArg.fn() } returns "example" + + val result = dummyValueWrapperArg.fn() + + assertEquals("example", result) + } + // + + // + @Test + @Ignore // TODO fix infinite loop + fun `receiver is ValueClass, return is Wrapper`() { + val fn = mockk DummyValueWrapper>() - fun processValue(value: DummyValue) = DummyValue(0) + every { dummyValueClassArg.fn() } returns dummyValueWrapperReturn + + val result = dummyValueClassArg.fn() + + assertEquals(dummyValueWrapperArg, result) + } + + @Test + @Ignore // TODO fix infinite loop + fun `receiver is ValueClass, return is ValueClass`() { + val fn = mockk DummyValue>() + + every { dummyValueClassArg.fn() } returns dummyValueClassReturn + + val result = dummyValueClassArg.fn() + + assertEquals(dummyValueClassReturn, result) + } + + @Test + @Ignore // TODO fix infinite loop + fun `receiver is ValueClass, return is String`() { + val fn = mockk String>() + + every { dummyValueClassArg.fn() } returns "example" + + val result = dummyValueClassArg.fn() + + assertEquals("example", result) + } + // + // + + companion object { + + @JvmInline + value class DummyValue(val value: Int) + + @JvmInline + value class DummyValueWrapper(val value: DummyValue) + + class DummyService { + + fun argWrapperReturnWrapper(wrapper: DummyValueWrapper): DummyValueWrapper = + DummyValueWrapper(DummyValue(0)) + + fun argWrapperReturnValueClass(wrapper: DummyValueWrapper): DummyValue = + DummyValue(0) + + fun argValueClassReturnWrapper(valueClass: DummyValue): DummyValueWrapper = + DummyValueWrapper(valueClass) + + fun argValueClassReturnValueClass(valueClass: DummyValue): DummyValue = + DummyValue(0) + + + fun argNoneReturnsUInt(): UInt = 123u + } + } } 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 cb586d936..4b2dc598a 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/InternalPlatform.kt @@ -63,7 +63,7 @@ actual object InternalPlatform { actual fun packRef(arg: Any?): Any? { return when { arg == null -> null - isPassedByValue(arg.boxedClass()) -> arg.boxedValue() + isPassedByValue(arg::class.boxedClass) -> arg.boxedValue else -> ref(arg) } } diff --git a/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmMockFactoryHelper.kt b/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmMockFactoryHelper.kt index 71552c367..b08b9250d 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmMockFactoryHelper.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmMockFactoryHelper.kt @@ -151,7 +151,7 @@ object JvmMockFactoryHelper { is KType -> kotlinReturnType.classifier as? KClass<*> ?: returnType.kotlin is KClass<*> -> kotlinReturnType else -> returnType.kotlin - }.boxedClass() + }.boxedClass val androidCompatibleReturnType = if (returnType.qualifiedName in androidUnsupportedTypes) { this@toDescription.returnType.kotlin diff --git a/mockk/jvm/src/main/kotlin/io/mockk/impl/recording/JvmSignatureValueGenerator.kt b/mockk/jvm/src/main/kotlin/io/mockk/impl/recording/JvmSignatureValueGenerator.kt index 07b1023ae..6428d98e7 100644 --- a/mockk/jvm/src/main/kotlin/io/mockk/impl/recording/JvmSignatureValueGenerator.kt +++ b/mockk/jvm/src/main/kotlin/io/mockk/impl/recording/JvmSignatureValueGenerator.kt @@ -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 signatureValue(cls: KClass, orInstantiateVia: () -> T): T { + override fun signatureValue( + cls: KClass, + 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() @@ -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 } ) } -} \ No newline at end of file +} diff --git a/plugins/dependencies/src/main/kotlin/io/mockk/dependencies/Deps.kt b/plugins/dependencies/src/main/kotlin/io/mockk/dependencies/Deps.kt index ebe1ea64a..bc78710ea 100644 --- a/plugins/dependencies/src/main/kotlin/io/mockk/dependencies/Deps.kt +++ b/plugins/dependencies/src/main/kotlin/io/mockk/dependencies/Deps.kt @@ -7,9 +7,9 @@ fun Project.kotlinVersion() = findProperty("kotlin.version")?.toString() ?: Deps object Deps { object Versions { const val androidTools = "7.2.1" - const val dokka = "1.7.0" + const val dokka = "1.7.10" const val kotlinDefault = "1.7.10" - const val coroutines = "1.3.3" + const val coroutines = "1.6.4" const val slfj = "1.7.32" const val logback = "1.2.11" const val junitJupiter = "5.8.2" @@ -26,8 +26,7 @@ object Deps { fun kotlinStdLib(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-stdlib:$version" fun kotlinStdLibJs(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-stdlib-js:$version" fun kotlinTestCommon(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-test-common:$version" - fun kotlinTestCommonAnnotations(version: String = Versions.kotlinDefault) = - "org.jetbrains.kotlin:kotlin-test-annotations-common:$version" + fun kotlinTestCommonAnnotations(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-test-annotations-common:$version" fun kotlinTestJunit(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-test-junit:$version" fun kotlinTestJs(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-test-js:$version" fun kotlinReflect(version: String = Versions.kotlinDefault) = "org.jetbrains.kotlin:kotlin-reflect:$version"