Hi. I'm writing unit tests for multiplatform proje...
# coroutines
r
Hi. I'm writing unit tests for multiplatform project and getting inconsistent coroutines results for Kotlin Native. Perhaps this is not the best way to write tests but it is working for Android platform. Here is simplified version of my test: Test function:
Copy code
@Test
fun testSuccess() = runBlocking {
    val repository = Repository()
    var actual: String? = null

    val job = repository.getData(TEST_URL) {      
        actual = it
    }
    job.join()

    assertEquals(TEST_MESSAGE, actual)
}
Repository getData function:
Copy code
fun getData(
    url: String,
    callback: (String) -> Unit = {}
): Job {
    return GlobalScope.launch(dispatcher) { // Dispatchers.Main (Android), NsQueueDispatcher (iOS)
        try {
            val message: String = httpClient.request(...)
            callback(message)
        } catch (e: Exception) {
            println("Exception: $e")
        }
    }
}
Test passes running for Android platform but getting infinite loop for iOS platform. Does anybody know a good way to test a callback function which was invoked within coroutine? Any help much appreciated. 🙏🏻
u
First off I am wondering why getData is not just a suspend function. Now to your question… Where do things get stuck? Does the
httpClient.request()
call start? does it return?
r
Hi @uli.
getData
is a function which is used directly in client platforms (Android & iOS). Don’t want to expose coroutines for various reasons: a) don’t want to dictate technology to be used b) although Kotlin Coroutines is supported in Objective-C but it is very experimental and
suspend
functions compiles/generates as with callback functions anyway. Seems that it is getting stuck when launching a coroutine. Cannot get anything printed inside
launch
lambda.
u
So what dispatch_queue is your NsQueueDispatcher running on?
NsQueueDispatcher(dispatch_get_main_queue())
?
r
Correct.
Copy code
actual val dispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())

internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatchQueue.freeze()) {
            block.run()
        }
    }
}
u
i.e. is your queue a serial queue and is it busy because it is blocked on
runBlocking
?
Yeah, looks like it. Your `fun testSuccess`is probably also running on the main queue, keeping it from executing whatever is scheduled with
dispatch_async
r
Limited expert in coroutines bu I think you are right - they both are running on main queue and
runningBlock
keeps blocking coroutines dispatcher for iOS.
Not sure how to fix this. I can replace dispatcher for tests but for iOS it has to run on main queue otherwise it will throw an exception. Also can remove
runBlocking
but then Android tests broken. Cannot find any good examples online which would match my case.
u
dispatch_async(background_queu) { run_blocking(NsQueueDispatcher) { } }
r
Sorry, not really following what you’re trying to suggest.
u
Me neither 😭 that's why I strike it.
You could try and use a common single threadded background dispatcher for runBlocking and for getData
r
Not sure how to do that either. The only way I can think of is
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
but this not possible in Kotlin Multiplatform.
u
NsQueueDispatcher with a serial queue
r
You mean
dispatch_queue_serial_t
instead of
dispatch_queue_t
? Sorry, I am Android/Kotlin developer. Not very familiar with Native/iOS.
u
Do you depend on the native-mt version of coroutines?
r
Yes, 1.4.2-native-mt
Otherwise you could try something like
NsQueueDispatcher(DispatchQueue())
. Not exactly sure about the syntax
In any case, make sure to use the same instance of the dispatcher in both places
r
Thank you @uli. Will give a try.
u
Copy code
fun getData(
        url: String,
        callback: (String) -> Unit = {}
    ): Job {
        return GlobalScope.launch(testContext) { // Dispatchers.Main (Android), NsQueueDispatcher (iOS)
            try {
                val message: String = url
                callback(message)
            } catch (e: Exception) {
                println("Exception: $e")
            }
        }
    }

    @Test
    fun testSuccess() = runBlocking(testContext) {
        var actual: String? = null
        val job = getData("OK") {
            actual = it
        }
        job.join()
        assertEquals("OK", actual)
    }
Works fine on my machine
r
I guess it works because of you return input value immediately (there is no suspensions points). In my case, I am using Ktor which is implemented based on Kotlin Coroutines. I would presume it is breaking not because of Ktor but rather due to coroutines.
u
i’d say it works as long as no coroutine is scheduled on the main dispatcher. It should still work if you add a delay(100)
yes, still works with delay(100)
Copy code
val message: String = url
                delay(100)
                callback(message)
r
This is the thing. Definitely, ktor schedules coroutines on main dispatcher (Kotlin Native memory is single threaded as far as I can understand). I can understand why it is not working. The problem is that none of the fixes I find, isn’t working for Kotlin Native coroutines. Typical solution could be make
getData
function suspendable but I think hiding details of implementation is a way to go in my case.