Skip to content

Commit

Permalink
Merge branch 'master' into feature/GH-29-improve-exception-conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
rickclephas committed Feb 19, 2022
2 parents 4781c30 + 53fa62f commit e99ddef
Show file tree
Hide file tree
Showing 22 changed files with 183 additions and 115 deletions.
10 changes: 8 additions & 2 deletions kmp-nativecoroutines-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ kotlin {
implementation(Dependencies.Kotlinx.atomicfu)
}
}
val appleMain by creating {
val nativeCoroutinesMain by creating {
dependsOn(commonMain)
}
val appleTest by creating {
val nativeCoroutinesTest by creating {
dependsOn(commonTest)
}
val appleMain by creating {
dependsOn(nativeCoroutinesMain)
}
val appleTest by creating {
dependsOn(nativeCoroutinesTest)
}
listOf(
macosX64, macosArm64,
iosArm64, iosX64, iosSimulatorArm64,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.freeze

actual fun <T> T.freeze(): T = this.freeze()
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import kotlin.native.concurrent.freeze
import kotlin.native.internal.GCUnsafeCall
import kotlin.reflect.KClass

actual typealias NativeError = NSError

/**
* Uses Kotlin Native runtime functions to convert a [Throwable] to a [NSError].
*
Expand Down Expand Up @@ -43,4 +45,5 @@ private external fun rethrowExceptionAsNSError(
exception: Throwable,
error: CPointer<ObjCObjectVar<NSError>>,
types: CArrayPointer<CPointerVar<*>>
)
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.rickclephas.kmp.nativecoroutines

internal actual val NativeError.kotlinCause: Throwable?
get() = this.userInfo["KotlinException"] as? Throwable
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeCallbackAppleTests {

@Test
fun ensureFrozen() {
var receivedValue: RandomValue? = null
val callback: NativeCallback<RandomValue> = callback@{ value, unit ->
receivedValue = value
// This isn't required in Kotlin, but it is in Swift, so we'll test it anyway
return@callback unit
}
val value = RandomValue()
assertFalse(value.isFrozen, "Value shouldn't be frozen yet")
callback(value)
assertTrue(value.isFrozen, "Value should be frozen")
assertTrue(receivedValue?.isFrozen == true, "Received value should be frozen")
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,16 @@ package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.Job
import kotlin.native.concurrent.isFrozen
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.*

class NativeCancellableTests {
class NativeCancellableAppleTests {

@Test
fun `ensure frozen`() {
fun ensureFrozen() {
val job = Job()
assertFalse(job.isFrozen, "Job shouldn't be frozen yet")
val nativeCancellable = job.asNativeCancellable()
assertTrue(nativeCancellable.isFrozen, "NativeCancellable should be frozen")
assertTrue(job.isFrozen, "Job should be frozen after getting the NativeCancellable")
}

@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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import kotlin.native.internal.ObjCErrorException
import kotlin.random.Random
import kotlin.test.*

class NSErrorTests {
class NativeErrorAppleTests {

@Test
fun `ensure frozen`() {
fun ensureFrozen() {
val exception = RandomException()
assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet")
val nsError = exception.asNSError(arrayOf(RandomException::class))
Expand All @@ -20,26 +20,28 @@ class NSErrorTests {
}

@Test
fun `ensure NSError domain and code are correct`() {
@OptIn(UnsafeNumber::class)
fun ensureNSErrorDomainAndCodeAreCorrect() {
val exception = RandomException()
val nsError = exception.asNSError(arrayOf(RandomException::class))
assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain")
assertEquals(0.convert(), nsError.code, "Incorrect NSError code")
}

@Test
fun `ensure localizedDescription is set to message`() {
fun ensureLocalizedDescriptionIsSetToMessage() {
val exception = RandomException()
val nsError = exception.asNSError(arrayOf(RandomException::class))
assertEquals(exception.message, nsError.localizedDescription,
"Localized description isn't set to message")
}

@Test
fun `ensure exception is part of user info`() {
fun ensureExceptionIsPartOfUserInfo() {
val exception = RandomException()
val nsError = exception.asNSError(arrayOf(RandomException::class))
assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info")
assertSame(exception, nsError.kotlinCause, "Incorrect kotlinCause")
}

@Test
Expand All @@ -56,4 +58,4 @@ class NSErrorTests {
val nsError = exception.asNSError(arrayOf())
assertEquals(error, nsError, "NSError isn't equal")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.flow.flow
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeFlowAppleTests {

@Test
fun ensureFrozen() {
val flow = flow<RandomValue> { }
assertFalse(flow.isFrozen, "Flow shouldn't be frozen yet")
val nativeFlow = flow.asNativeFlow()
assertTrue(nativeFlow.isFrozen, "NativeFlow should be frozen")
assertTrue(flow.isFrozen, "Flow should be frozen")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.delay
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeSuspendAppleTests {

private suspend fun delayAndReturn(delay: Long, value: RandomValue): RandomValue {
delay(delay)
return value
}

@Test
fun ensureFrozen() {
val value = RandomValue()
assertFalse(value.isFrozen, "Value shouldn't be frozen yet")
val nativeSuspend = nativeSuspend { delayAndReturn(0, value) }
assertTrue(nativeSuspend.isFrozen, "NativeSuspend should be frozen")
assertTrue(value.isFrozen, "Value should be frozen")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.rickclephas.kmp.nativecoroutines

/**
* Freezes this object in Kotlin/Native.
*/
internal expect fun <T> T.freeze(): T
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.freeze

/**
* A callback with a single argument.
*
* We don't want the Swift code to known how to get the [Unit] object so we'll provide it as the second argument.
* We don't want the Swift code to known how to get the [Unit] object, so we'll provide it as the second argument.
* This way Swift can just return the value that it received without knowing what it is/how to get it.
*/
typealias NativeCallback<T> = (T, Unit) -> Unit
Expand Down
Original file line number Diff line number Diff line change
@@ -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].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rickclephas.kmp.nativecoroutines

/**
* Represents an error in a way that the specific platform is able to handle
*/
expect class NativeError

/**
* Converts a [Throwable] to a [NativeError].
*/
internal expect fun Throwable.asNativeError(): NativeError
Original file line number Diff line number Diff line change
Expand Up @@ -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
import kotlin.reflect.KClass

/**
Expand All @@ -15,7 +13,7 @@ import kotlin.reflect.KClass
* The function takes an `onItem` and `onComplete` callback
* and returns a cancellable that can be used to cancel the collection.
*/
typealias NativeFlow<T> = (onItem: NativeCallback<T>, onComplete: NativeCallback<NSError?>) -> NativeCancellable
typealias NativeFlow<T> = (onItem: NativeCallback<T>, onComplete: NativeCallback<NativeError?>) -> NativeCancellable

/**
* Creates a [NativeFlow] for this [Flow].
Expand All @@ -30,7 +28,7 @@ fun <T> Flow<T>.asNativeFlow(
propagatedExceptions: Array<KClass<out Throwable>> = arrayOf()
): NativeFlow<T> {
val coroutineScope = scope ?: defaultCoroutineScope
return (collect@{ onItem: NativeCallback<T>, onComplete: NativeCallback<NSError?> ->
return (collect@{ onItem: NativeCallback<T>, onComplete: NativeCallback<NativeError?> ->
val job = coroutineScope.launch {
try {
collect { onItem(it) }
Expand All @@ -50,4 +48,4 @@ fun <T> Flow<T>.asNativeFlow(
}
return@collect job.asNativeCancellable()
}).freeze()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
import kotlin.reflect.KClass

/**
Expand All @@ -13,7 +11,7 @@ import kotlin.reflect.KClass
* The function takes an `onResult` and `onError` callback
* and returns a cancellable that can be used to cancel the suspend function.
*/
typealias NativeSuspend<T> = (onResult: NativeCallback<T>, onError: NativeCallback<NSError>) -> NativeCancellable
typealias NativeSuspend<T> = (onResult: NativeCallback<T>, onError: NativeCallback<NativeError>) -> NativeCancellable

/**
* Creates a [NativeSuspend] for the provided suspend [block].
Expand All @@ -28,7 +26,7 @@ fun <T> nativeSuspend(
block: suspend () -> T
): NativeSuspend<T> {
val coroutineScope = scope ?: defaultCoroutineScope
return (collect@{ onResult: NativeCallback<T>, onError: NativeCallback<NSError> ->
return (collect@{ onResult: NativeCallback<T>, onError: NativeCallback<NativeError> ->
val job = coroutineScope.launch {
try {
onResult(block())
Expand All @@ -47,4 +45,4 @@ fun <T> nativeSuspend(
}
return@collect job.asNativeCancellable()
}).freeze()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.rickclephas.kmp.nativecoroutines

/**
* Gets the [Throwable] from the [NativeError].
*
* Note: this doesn't actually convert a [NativeError] to a [Throwable],
* it should only be used to test [asNativeError] logic.
*/
internal expect val NativeError.kotlinCause: Throwable?
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.test.*

class NativeCallbackTests {

@Test
fun ensureInvoked() {
var invokeCount = 0
var receivedValue: RandomValue? = null
val callback: NativeCallback<RandomValue> = 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")
}
}
Loading

0 comments on commit e99ddef

Please sign in to comment.