From 1e20b17e68442a1efbce53da2e4006995fef0c86 Mon Sep 17 00:00:00 2001 From: Carlo Marinangeli Date: Tue, 28 Nov 2023 22:25:18 +0400 Subject: [PATCH 1/2] Fix concurrent invocations of InvocationRecorder --- .idea/codeStyles/Project.xml | 1 - mockingbird/build.gradle.kts | 7 ++ .../com/careem/mockingbird/test/Functions.kt | 14 ++-- .../mockingbird/test/InvocationRecorder.kt | 24 +++---- .../test/InvocationRecorderTest.kt | 64 +++++++++++++++++-- versions.toml | 3 +- 6 files changed, 91 insertions(+), 22 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 8a40d3d..77c0b55 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -105,7 +105,6 @@ http://schemas.android.com/apk/res/android - ANDROID_ATTRIBUTE_ORDER
diff --git a/mockingbird/build.gradle.kts b/mockingbird/build.gradle.kts index 68d8d09..7555a69 100644 --- a/mockingbird/build.gradle.kts +++ b/mockingbird/build.gradle.kts @@ -36,9 +36,16 @@ kotlin { implementation(libs.touchlab.stately.concurrency) implementation(libs.touchlab.stately.concurrent.collections) implementation(libs.kotlinx.coroutines) + } + } + + val commonTest by getting { + dependencies { implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) } } + val iosMain by getting val iosSimulatorArm64Main by getting { dependsOn(iosMain) diff --git a/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/Functions.kt b/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/Functions.kt index d086a8a..72075ea 100644 --- a/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/Functions.kt +++ b/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/Functions.kt @@ -18,14 +18,10 @@ package com.careem.mockingbird.test import kotlinx.atomicfu.AtomicInt import kotlinx.atomicfu.atomic -import kotlin.native.concurrent.SharedImmutable -import kotlin.test.assertEquals -@SharedImmutable internal const val AWAIT_POOLING_TIME = 10L -@SharedImmutable private val uuidGenerator: AtomicInt = atomic(0) public interface Mock { @@ -151,6 +147,16 @@ internal fun T.rawVerify( } } +private fun assertEquals( + expected: Int, + actual: Int, + message: String +) { + if (expected != actual) { + throw AssertionError(message) + } +} + /** * Function to mock a function * @param methodName name of the method that you want to mock diff --git a/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/InvocationRecorder.kt b/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/InvocationRecorder.kt index 35ebec6..8c4b088 100644 --- a/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/InvocationRecorder.kt +++ b/mockingbird/src/commonMain/kotlin/com/careem/mockingbird/test/InvocationRecorder.kt @@ -16,21 +16,23 @@ */ package com.careem.mockingbird.test +import co.touchlab.stately.collections.ConcurrentMutableMap + internal class InvocationRecorder { - private val recorder = mutableMapOf>() - private val responses = mutableMapOf Any?>>() + private val recorder = ConcurrentMutableMap>() + private val responses = ConcurrentMutableMap Any?>>() /** - * This function must be called by the mock when a function call is exceuted on it + * This function must be called by the mock when a function call is executed on it * @param uuid the uuid of the mock * @param invocation the Invocation object @see [Invocation] */ fun storeInvocation(uuid: String, invocation: Invocation) { - if (!recorder.containsKey(uuid)) { - recorder[uuid] = mutableListOf() + recorder.block { map -> + val list = map.getOrPut(uuid) { mutableListOf() } + list.add(invocation) } - recorder[uuid]!!.add(invocation) } /** @@ -60,10 +62,10 @@ internal class InvocationRecorder { * @param answer the lambda that must be invoked when the invocation happen */ fun storeAnswer(uuid: String, invocation: Invocation, answer: (Invocation) -> T) { - if (!responses.containsKey(uuid)) { - responses[uuid] = LinkedHashMap() + responses.block { map -> + val invocationsMap = map.getOrPut(uuid) { LinkedHashMap() } + invocationsMap[invocation] = answer as (Invocation) -> Any? } - responses[uuid]!![invocation] = answer as (Invocation) -> Any? } /** @@ -75,8 +77,8 @@ internal class InvocationRecorder { * @return the mocked response, or null if relaxed (throws if not relaxed) */ fun getResponse(uuid: String, invocation: Invocation, relaxed: Boolean = false): Any? { - return if (uuid in responses.keys) { - responses[uuid]!!.let { + return if (uuid in responses) { + responses.getValue(uuid).let { val lambda = findResponseByInvocation(it, invocation, relaxed) return@let lambda(invocation) } diff --git a/mockingbird/src/commonTest/kotlin/com/careem/mockingbird/test/InvocationRecorderTest.kt b/mockingbird/src/commonTest/kotlin/com/careem/mockingbird/test/InvocationRecorderTest.kt index 7e6bde6..93bed5b 100644 --- a/mockingbird/src/commonTest/kotlin/com/careem/mockingbird/test/InvocationRecorderTest.kt +++ b/mockingbird/src/commonTest/kotlin/com/careem/mockingbird/test/InvocationRecorderTest.kt @@ -16,14 +16,24 @@ */ package com.careem.mockingbird.test -import kotlin.test.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue class InvocationRecorderTest { private val invocationRecorder = InvocationRecorder() @Test - fun testEmptyListWhenNoInvocationsRegisteredForAnInstance(){ + fun testEmptyListWhenNoInvocationsRegisteredForAnInstance() { val mock = newMock() val mockInvocations = invocationRecorder.getInvocations(mock.uuid) @@ -85,7 +95,7 @@ class InvocationRecorderTest { val invocation1 = Invocation(METHOD_1, ARGS_1) val uuid = mutableSetOf() - (0 until iterations).forEach { + (0 until iterations).forEach { _ -> val mock = Mocks.MyDependencyMock() uuid.add(mock.uuid) invocationRecorder.storeInvocation(mock.uuid, invocation1) @@ -178,7 +188,10 @@ class InvocationRecorderTest { invocationRecorder.getResponse(mock.uuid, invocation1) } catch (ise: IllegalStateException) { e = ise - assertEquals("Not mocked response for current object and instance, instance:${mock.uuid}, invocation: $invocation1", e.message) + assertEquals( + "Not mocked response for current object and instance, instance:${mock.uuid}, invocation: $invocation1", + e.message + ) } assertNotNull(e) } @@ -247,7 +260,48 @@ class InvocationRecorderTest { assertEquals(responseInv2, response2) } - fun newMock(): Mock{ + @Test + fun concurrentRecordInvocation() = runTest { + val jobs = mutableListOf() + + repeat(10_000) { + launch(Dispatchers.Default) { + val invocation = Invocation("abc $it", mapOf(ARG_NAME_1 to "value1")) + invocationRecorder.storeInvocation("uuid", invocation) + }.also { jobs.add(it) } + } + + jobs.joinAll() + + assertEquals(10_000, invocationRecorder.getInvocations("uuid").size) + } + + @Test + fun concurrentRecordAnswers() = runTest { + val jobs = mutableListOf() + + repeat(1000) { counter -> + launch(Dispatchers.Default) { + val invocation = Invocation("abc $counter", mapOf(ARG_NAME_1 to "value1")) + invocationRecorder.storeAnswer("uuid", invocation) { "Yo $counter" } + }.also { jobs.add(it) } + } + + jobs.joinAll() + + repeat(1000) { counter -> + assertEquals( + "Yo $counter", invocationRecorder.getResponse( + "uuid", + Invocation("abc $counter", mapOf(ARG_NAME_1 to "value1")), + relaxed = false + ) + ) + } + + } + + private fun newMock(): Mock { return object : Mock { override val uuid: String by uuid() } diff --git a/versions.toml b/versions.toml index 9c5bc26..38ba661 100644 --- a/versions.toml +++ b/versions.toml @@ -6,7 +6,7 @@ kotlin = "1.9.10" kspVersion = "1.9.10-1.0.13" junit = "4.13.1" jacoco = "0.8.10" -stately = "2.0.0-rc3" +stately = "2.0.5" atomicFu = "0.22.0" kotlinPoet = "1.14.2" kotlinxMetadata = "0.7.0" @@ -20,6 +20,7 @@ touchlab-stately-concurrency = { module = "co.touchlab:stately-concurrency", ver touchlab-stately-concurrent-collections = { module = "co.touchlab:stately-concurrent-collections", version.ref = "stately" } kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicFu" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } kotlin-test-annotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } From ddbf1a457b3614d35309bf8a58dddf8a8cb612ef Mon Sep 17 00:00:00 2001 From: Carlo Marinangeli Date: Thu, 30 Nov 2023 14:57:45 +0400 Subject: [PATCH 2/2] Fix gradle config when environment is not set --- build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 81944e9..2dd55ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ nexusPublishing { sonatype { nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - username.set((prop["ossrhUsername"] ?: System.getenv("OSSRH_USERNAME")) as String) - password.set((prop["ossrhPassword"] ?: System.getenv("OSSRH_PASSWORD")) as String) + username.set((prop["ossrhUsername"] ?: System.getenv("OSSRH_USERNAME") ?: "not-set") as String) + password.set((prop["ossrhPassword"] ?: System.getenv("OSSRH_PASSWORD") ?: "not-set") as String) stagingProfileId.set((prop["sonatypeStagingProfileId"] ?: System.getenv("SONATYPE_STAGING_PROFILE_ID")) as String?) } } @@ -122,9 +122,9 @@ subprojects { pluginManager.withPlugin("signing") { extensions.configure { useInMemoryPgpKeys( - (prop["signing.keyId"] ?: System.getenv("SIGNING_KEY_ID")).toString(), - (prop["signing.key"] ?: System.getenv("SIGNING_KEY")).toString(), - (prop["signing.password"] ?: System.getenv("SIGNING_PASSWORD")).toString() + (prop["signing.keyId"] ?: System.getenv("SIGNING_KEY_ID") ?: "not-set").toString(), + (prop["signing.key"] ?: System.getenv("SIGNING_KEY") ?: "not-set").toString(), + (prop["signing.password"] ?: System.getenv("SIGNING_PASSWORD") ?: "not-set").toString() ) sign(publishing.publications) }