diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 9f01535f..63fdb82d 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -9,5 +9,8 @@ object Dependencies { object Kotlinx { const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt" + const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0-native-mt" + + const val atomicfu = "org.jetbrains.kotlinx:atomicfu:0.17.1" } } \ No newline at end of file diff --git a/kmp-nativecoroutines-core/build.gradle.kts b/kmp-nativecoroutines-core/build.gradle.kts index 93a30d57..2d213faf 100644 --- a/kmp-nativecoroutines-core/build.gradle.kts +++ b/kmp-nativecoroutines-core/build.gradle.kts @@ -35,14 +35,23 @@ kotlin { val commonTest by getting { dependencies { implementation(kotlin("test")) + + implementation(Dependencies.Kotlinx.coroutinesTest) + implementation(Dependencies.Kotlinx.atomicfu) } } - val appleMain by creating { + val supportedTargetMain by creating { dependsOn(commonMain) } - val appleTest by creating { + val supportedTargetTest by creating { dependsOn(commonTest) } + val appleMain by creating { + dependsOn(supportedTargetMain) + } + val appleTest by creating { + dependsOn(supportedTargetTest) + } listOf( macosX64, macosArm64, iosArm64, iosX64, iosSimulatorArm64, @@ -56,5 +65,11 @@ kotlin { dependsOn(appleTest) } } + val jsMain by getting { + dependsOn(supportedTargetMain) + } + val jsTest by getting { + dependsOn(supportedTargetTest) + } } } diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingApple.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingApple.kt new file mode 100644 index 00000000..a14b4d5f --- /dev/null +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingApple.kt @@ -0,0 +1,5 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.native.concurrent.freeze + +actual fun T.freeze(): T = this.freeze() diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt index 2b91a447..89d26484 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt @@ -6,6 +6,13 @@ import platform.Foundation.NSError import platform.Foundation.NSLocalizedDescriptionKey import kotlin.native.concurrent.freeze +actual typealias PlatformError = NSError + +internal actual fun Throwable.asPlatformError(): PlatformError = this.asNSError() + +actual val PlatformError.kotlinCause + get() = this.userInfo["KotlinException"] as? Throwable + /** * Converts a [Throwable] to a [NSError]. * diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCallbackTests.kt similarity index 97% rename from kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt rename to kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCallbackTests.kt index e4643c50..e0b1a0dd 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCallbackTests.kt @@ -3,7 +3,7 @@ package com.rickclephas.kmp.nativecoroutines import kotlin.native.concurrent.isFrozen import kotlin.test.* -class NativeCallbackTests { +class AppleNativeCallbackTests { @Test fun `ensure frozen`() { diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCancellableTests.kt similarity index 96% rename from kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt rename to kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCancellableTests.kt index cac82616..09d717ae 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeCancellableTests.kt @@ -6,7 +6,7 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -class NativeCancellableTests { +class AppleNativeCancellableTests { @Test fun `ensure frozen`() { diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeFlowTests.kt similarity index 85% rename from kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt rename to kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeFlowTests.kt index ce5dce3f..98ff43b5 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeFlowTests.kt @@ -1,13 +1,13 @@ package com.rickclephas.kmp.nativecoroutines +import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow -import kotlin.native.concurrent.AtomicInt import kotlin.native.concurrent.isFrozen import kotlin.test.* -class NativeFlowTests { +class AppleNativeFlowTests { @Test fun `ensure frozen`() { @@ -23,10 +23,10 @@ class NativeFlowTests { val flow = flow { } val job = Job() val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) - val completionCount = AtomicInt(0) + val completionCount = atomic(0) nativeFlow({ _, _ -> }, { error, _ -> assertNull(error, "Flow should complete without an error") - completionCount.increment() + completionCount.incrementAndGet() }) job.children.forEach { it.join() } // Waits for the collection to complete assertEquals(1, completionCount.value, "Completion callback should be called once") @@ -38,12 +38,12 @@ class NativeFlowTests { val flow = flow { throw exception } val job = Job() val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) - val completionCount = AtomicInt(0) + val completionCount = atomic(0) nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") - val kotlinException = error.userInfo["KotlinException"] + val kotlinException = error.kotlinCause assertSame(exception, kotlinException, "Kotlin exception should be the same exception") - completionCount.increment() + completionCount.incrementAndGet() }) job.children.forEach { it.join() } // Waits for the collection to complete assertEquals(1, completionCount.value, "Completion callback should be called once") @@ -55,10 +55,10 @@ class NativeFlowTests { val flow = flow { values.forEach { emit(it) } } val job = Job() val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) - val receivedValueCount = AtomicInt(0) + val receivedValueCount = atomic(0) nativeFlow({ value, _ -> assertSame(values[receivedValueCount.value], value, "Received incorrect value") - receivedValueCount.increment() + receivedValueCount.incrementAndGet() }, { _, _ -> }) job.children.forEach { it.join() } // Waits for the collection to complete assertEquals(values.size, receivedValueCount.value, "Item callback should be called for every value") @@ -69,12 +69,12 @@ class NativeFlowTests { val flow = MutableSharedFlow() val job = Job() val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) - val completionCount = AtomicInt(0) + val completionCount = atomic(0) val cancel = nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") - val exception = error.userInfo["KotlinException"] + val exception = error.kotlinCause assertIs(exception, "Error should contain CancellationException") - completionCount.increment() + completionCount.incrementAndGet() }) delay(100) // Gives the collection some time to start cancel() diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeSuspendTests.kt similarity index 80% rename from kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt rename to kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeSuspendTests.kt index 40d14a7d..31e9d120 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/AppleNativeSuspendTests.kt @@ -1,15 +1,15 @@ package com.rickclephas.kmp.nativecoroutines +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlin.coroutines.cancellation.CancellationException -import kotlin.native.concurrent.AtomicInt import kotlin.native.concurrent.isFrozen import kotlin.test.* -class NativeSuspendTests { +class AppleNativeSuspendTests { private suspend fun delayAndReturn(delay: Long, value: RandomValue): RandomValue { delay(delay) @@ -35,13 +35,13 @@ class NativeSuspendTests { val value = RandomValue() val job = Job() val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(100, value) } - val receivedResultCount = AtomicInt(0) - val receivedErrorCount = AtomicInt(0) + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) nativeSuspend({ receivedValue, _ -> assertSame(value, receivedValue, "Received incorrect value") - receivedResultCount.increment() + receivedResultCount.incrementAndGet() }, { _, _ -> - receivedErrorCount.increment() + receivedErrorCount.incrementAndGet() }) job.children.forEach { it.join() } // Waits for the function to complete assertEquals(1, receivedResultCount.value, "Result callback should be called once") @@ -53,15 +53,15 @@ class NativeSuspendTests { val exception = RandomException() val job = Job() val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndThrow(100, exception) } - val receivedResultCount = AtomicInt(0) - val receivedErrorCount = AtomicInt(0) + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) nativeSuspend({ _, _ -> - receivedResultCount.increment() + receivedResultCount.incrementAndGet() }, { error, _ -> assertNotNull(error, "Function should complete with an error") - val kotlinException = error.userInfo["KotlinException"] + val kotlinException = error.kotlinCause assertSame(exception, kotlinException, "Kotlin exception should be the same exception") - receivedErrorCount.increment() + receivedErrorCount.incrementAndGet() }) job.children.forEach { it.join() } // Waits for the function to complete assertEquals(1, receivedErrorCount.value, "Error callback should be called once") @@ -72,15 +72,15 @@ class NativeSuspendTests { fun `ensure function is cancelled`() = runBlocking { val job = Job() val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(5_000, RandomValue()) } - val receivedResultCount = AtomicInt(0) - val receivedErrorCount = AtomicInt(0) + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) val cancel = nativeSuspend({ _, _ -> - receivedResultCount.increment() + receivedResultCount.incrementAndGet() }, { error, _ -> assertNotNull(error, "Function should complete with an error") - val exception = error.userInfo["KotlinException"] + val exception = error.kotlinCause assertIs(exception, "Error should contain CancellationException") - receivedErrorCount.increment() + receivedErrorCount.incrementAndGet() }) delay(100) // Gives the function some time to start cancel() diff --git a/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingJs.kt b/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingJs.kt new file mode 100644 index 00000000..7e3457c4 --- /dev/null +++ b/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/FreezingJs.kt @@ -0,0 +1,6 @@ +package com.rickclephas.kmp.nativecoroutines + +/** + * Freezing is a no-op on JS + */ +internal actual fun T.freeze() = this \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformErrorJs.kt b/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformErrorJs.kt new file mode 100644 index 00000000..5e503688 --- /dev/null +++ b/kmp-nativecoroutines-core/src/jsMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformErrorJs.kt @@ -0,0 +1,6 @@ +package com.rickclephas.kmp.nativecoroutines + +actual typealias PlatformError = Throwable +actual fun Throwable.asPlatformError() = this +actual val PlatformError.kotlinCause: Throwable? + get() = this \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/CoroutineScope.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/CoroutineScope.kt similarity index 100% rename from kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/CoroutineScope.kt rename to kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/CoroutineScope.kt diff --git a/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/Freezing.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/Freezing.kt new file mode 100644 index 00000000..34768f34 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/Freezing.kt @@ -0,0 +1,3 @@ +package com.rickclephas.kmp.nativecoroutines + +internal expect fun T.freeze(): T diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt similarity index 92% rename from kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt rename to kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt index 18390787..1aef03d0 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallback.kt @@ -1,7 +1,5 @@ package com.rickclephas.kmp.nativecoroutines -import kotlin.native.concurrent.freeze - /** * A callback with a single argument. * diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt similarity index 91% rename from kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt rename to kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt index c0786d83..d3262256 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellable.kt @@ -1,7 +1,6 @@ package com.rickclephas.kmp.nativecoroutines import kotlinx.coroutines.Job -import kotlin.native.concurrent.freeze /** * A function that cancels the coroutines [Job]. diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt similarity index 87% rename from kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt rename to kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index f1dce7e6..a682a210 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -5,8 +5,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import platform.Foundation.NSError -import kotlin.native.concurrent.freeze /** * A function that collects a [Flow] via callbacks. @@ -14,7 +12,7 @@ import kotlin.native.concurrent.freeze * The function takes an `onItem` and `onComplete` callback * and returns a cancellable that can be used to cancel the collection. */ -typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback) -> NativeCancellable +typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback) -> NativeCancellable /** * Creates a [NativeFlow] for this [Flow]. @@ -25,7 +23,7 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback */ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope - return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> + return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> val job = coroutineScope.launch { try { collect { onItem(it) } @@ -35,13 +33,13 @@ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onComplete(e.asNSError()) + onComplete(e.asPlatformError()) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onComplete(cause.asNSError()) + onComplete(cause.asPlatformError()) } return@collect job.asNativeCancellable() }).freeze() diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt similarity index 87% rename from kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt rename to kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index 676b38ca..9da1315a 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -3,8 +3,6 @@ package com.rickclephas.kmp.nativecoroutines import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import platform.Foundation.NSError -import kotlin.native.concurrent.freeze /** * A function that awaits a suspend function via callbacks. @@ -12,7 +10,7 @@ import kotlin.native.concurrent.freeze * The function takes an `onResult` and `onError` callback * and returns a cancellable that can be used to cancel the suspend function. */ -typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallback) -> NativeCancellable +typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallback) -> NativeCancellable /** * Creates a [NativeSuspend] for the provided suspend [block]. @@ -22,7 +20,7 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba */ fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope - return (collect@{ onResult: NativeCallback, onError: NativeCallback -> + return (collect@{ onResult: NativeCallback, onError: NativeCallback -> val job = coroutineScope.launch { try { onResult(block()) @@ -31,13 +29,13 @@ fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): Na // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onError(e.asNSError()) + onError(e.asPlatformError()) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onError(cause.asNSError()) + onError(cause.asPlatformError()) } return@collect job.asNativeCancellable() }).freeze() diff --git a/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformError.kt b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformError.kt new file mode 100644 index 00000000..c6a8faa6 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetMain/kotlin/com/rickclephas/kmp/nativecoroutines/PlatformError.kt @@ -0,0 +1,13 @@ +package com.rickclephas.kmp.nativecoroutines + +/** + * Represents an error in a way that the specific platform is able to handle + */ +expect class PlatformError + +/** + * Converts a [Throwable] to a [PlatformError]. + */ +internal expect fun Throwable.asPlatformError(): PlatformError + +internal expect val PlatformError.kotlinCause: Throwable? \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt new file mode 100644 index 00000000..a7803195 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCallbackTests.kt @@ -0,0 +1,21 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.test.* + +class NativeCallbackTests { + @Test + fun ensure_invoked() { + var invokeCount = 0 + var receivedValue: RandomValue? = null + val callback: NativeCallback = callback@{ value, unit -> + receivedValue = value + invokeCount++ + // This isn't required in Kotlin but it is in Swift so we'll test it anyway + return@callback unit + } + val value = RandomValue() + callback(value) + assertEquals(1, invokeCount, "NativeCallback should have been invoked once") + assertSame(value, receivedValue, "Received value should be the same as the send value") + } +} \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt new file mode 100644 index 00000000..a8f32270 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCancellableTests.kt @@ -0,0 +1,17 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlinx.coroutines.Job +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NativeCancellableTests { + @Test + fun ensure_that_the_job_gets_cancelled() { + val job = Job() + val nativeCancellable = job.asNativeCancellable() + assertFalse(job.isCancelled, "Job shouldn't be cancelled yet") + nativeCancellable() + assertTrue(job.isCancelled, "Job should be cancelled") + } +} \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt new file mode 100644 index 00000000..caaf0c01 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -0,0 +1,74 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +class NativeFlowTests { + @Test + fun ensure_completion_callback_is_invoked() = runTest { + val flow = flow { } + val job = Job() + val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) + val completionCount = atomic(0) + nativeFlow({ _, _ -> }, { error, _ -> + assertNull(error, "Flow should complete without an error") + completionCount.incrementAndGet() + }) + job.children.forEach { it.join() } // Waits for the collection to complete + assertEquals(1, completionCount.value, "Completion callback should be called once") + } + + @Test + fun ensure_exceptions_are_received_as_errors() = runTest { + val exception = RandomException() + val flow = flow { throw exception } + val job = Job() + val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) + val completionCount = atomic(0) + nativeFlow({ _, _ -> }, { error, _ -> + assertNotNull(error, "Flow should complete with an error") + val kotlinException = error.kotlinCause + assertSame(exception, kotlinException, "Kotlin exception should be the same exception") + completionCount.incrementAndGet() + }) + job.children.forEach { it.join() } // Waits for the collection to complete + assertEquals(1, completionCount.value, "Completion callback should be called once") + } + + @Test + fun ensure_values_are_received() = runTest { + val values = listOf(RandomValue(), RandomValue(), RandomValue(), RandomValue()) + val flow = flow { values.forEach { emit(it) } } + val job = Job() + val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) + val receivedValueCount = atomic(0) + nativeFlow({ value, _ -> + assertSame(values[receivedValueCount.value], value, "Received incorrect value") + receivedValueCount.incrementAndGet() + }, { _, _ -> }) + job.children.forEach { it.join() } // Waits for the collection to complete + assertEquals(values.size, receivedValueCount.value, "Item callback should be called for every value") + } + + @Test + fun ensure_collection_is_cancelled() = runTest { + val flow = MutableSharedFlow() + val job = Job() + val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) + val completionCount = atomic(0) + val cancel = nativeFlow({ _, _ -> }, { error, _ -> + assertNotNull(error, "Flow should complete with an error") + val exception = error.kotlinCause + assertIs(exception, "Error should contain CancellationException") + completionCount.incrementAndGet() + }) + delay(100) // Gives the collection some time to start + cancel() + job.children.forEach { it.join() } // Waits for the collection to complete + assertEquals(1, completionCount.value, "Completion callback should be called once") + } +} \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt new file mode 100644 index 00000000..b2a414f2 --- /dev/null +++ b/kmp-nativecoroutines-core/src/supportedTargetTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -0,0 +1,81 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.* +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.test.runTest + +class NativeSuspendTests { + + private suspend fun delayAndReturn(delay: Long, value: RandomValue): RandomValue { + delay(delay) + return value + } + + private suspend fun delayAndThrow(delay: Long, exception: RandomException): RandomValue { + delay(delay) + throw exception + } + + @Test + fun ensure_correct_result_is_received() = runTest { + val value = RandomValue() + val job = Job() + val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(100, value) } + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) + nativeSuspend({ receivedValue, _ -> + assertSame(value, receivedValue, "Received incorrect value") + receivedResultCount.incrementAndGet() + }, { _, _ -> + receivedErrorCount.incrementAndGet() + }) + job.children.forEach { it.join() } // Waits for the function to complete + assertEquals(1, receivedResultCount.value, "Result callback should be called once") + assertEquals(0, receivedErrorCount.value, "Error callback shouldn't be called") + } + + @Test + fun ensure_exceptions_are_received_as_errors() = runTest { + val exception = RandomException() + val job = Job() + val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndThrow(100, exception) } + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) + nativeSuspend({ _, _ -> + receivedResultCount.incrementAndGet() + }, { error, _ -> + assertNotNull(error, "Function should complete with an error") + val kotlinException = error.kotlinCause + assertSame(exception, kotlinException, "Kotlin exception should be the same exception") + receivedErrorCount.incrementAndGet() + }) + job.children.forEach { it.join() } // Waits for the function to complete + assertEquals(1, receivedErrorCount.value, "Error callback should be called once") + assertEquals(0, receivedResultCount.value, "Result callback shouldn't be called") + } + + @Test + fun ensure_function_is_cancelled() = runTest { + val job = Job() + val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(5_000, RandomValue()) } + val receivedResultCount = atomic(0) + val receivedErrorCount = atomic(0) + val cancel = nativeSuspend({ _, _ -> + receivedResultCount.incrementAndGet() + }, { error, _ -> + assertNotNull(error, "Function should complete with an error") + val exception = error.kotlinCause + assertIs(exception, "Error should contain CancellationException") + receivedErrorCount.incrementAndGet() + }) + delay(100) // Gives the function some time to start + cancel() + job.children.forEach { it.join() } // Waits for the function to complete + assertEquals(1, receivedErrorCount.value, "Error callback should be called once") + assertEquals(0, receivedResultCount.value, "Result callback shouldn't be called") + } +} \ No newline at end of file