https://kotlinlang.org logo
Title
r

rsetkus

02/18/2021, 12:43 PM
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:
@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:
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

uli

02/18/2021, 4:02 PM
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

rsetkus

02/18/2021, 5:31 PM
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

uli

02/18/2021, 5:43 PM
So what dispatch_queue is your NsQueueDispatcher running on?
NsQueueDispatcher(dispatch_get_main_queue())
?
r

rsetkus

02/18/2021, 5:50 PM
Correct.
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

uli

02/18/2021, 5:51 PM
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

rsetkus

02/18/2021, 5:57 PM
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

uli

02/18/2021, 6:16 PM
dispatch_async(background_queu) { run_blocking(NsQueueDispatcher) { } }
r

rsetkus

02/18/2021, 6:39 PM
Sorry, not really following what you’re trying to suggest.
u

uli

02/18/2021, 7:46 PM
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

rsetkus

02/18/2021, 9:43 PM
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

uli

02/18/2021, 9:53 PM
NsQueueDispatcher with a serial queue
r

rsetkus

02/18/2021, 10:06 PM
You mean
dispatch_queue_serial_t
instead of
dispatch_queue_t
? Sorry, I am Android/Kotlin developer. Not very familiar with Native/iOS.
u

uli

02/18/2021, 10:30 PM
Do you depend on the native-mt version of coroutines?
r

rsetkus

02/18/2021, 10:50 PM
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

rsetkus

02/18/2021, 11:06 PM
Thank you @uli. Will give a try.
u

uli

02/19/2021, 10:19 AM
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

rsetkus

02/19/2021, 11:24 AM
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

uli

02/19/2021, 11:26 AM
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)
val message: String = url
                delay(100)
                callback(message)
r

rsetkus

02/19/2021, 11:47 AM
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.