Skip to content

Commit

Permalink
Merge remote-tracking branch 'mockk/master' into feature/dependency-u…
Browse files Browse the repository at this point in the history
…pdates-v2

# Conflicts:
#	.github/workflows/gradle.yml
#	build.gradle
#	gradle.properties
#	mockk/common/src/main/kotlin/io/mockk/impl/recording/PermanentMocker.kt
#	mockk/common/src/main/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt
#	mockk/common/src/main/kotlin/io/mockk/impl/recording/states/VerifyingState.kt
#	mockk/common/src/test/kotlin/io/mockk/it/ValueClassTest.kt
#	plugins/dependencies/src/main/kotlin/io/mockk/dependencies/Deps.kt
  • Loading branch information
aSemy committed Jul 26, 2022
2 parents c868942 + 02a6d45 commit 5026089
Show file tree
Hide file tree
Showing 32 changed files with 1,079 additions and 427 deletions.
81 changes: 49 additions & 32 deletions .github/workflows/gradle.yml
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions 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)
Expand Down Expand Up @@ -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.

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

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

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

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

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

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

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

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

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

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


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

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

backingField
} as KProperty1<out T, *>
}

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

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

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

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

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

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

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

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

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

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

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

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


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

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

backingField
} as KProperty1<out T, *>
}

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

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

}

0 comments on commit 5026089

Please sign in to comment.