https://kotlinlang.org logo
#coroutines
Title
# coroutines
b

benkuly

11/08/2023, 8:16 AM
Do I understand it right, that
advanceUntilIdle
does not "wait" when there is dispatcher switch within a coroutine? For example
Copy code
delay(100) // skipped
withContext(otherDispatcher){
    delay(100)
}
delay(200) // not called at all on advanceUntilIdle
At least that's what I observed with ktor MockEngine. If this is really the case, it should be mentioned somewhere, because it has cost us hours of debugging.
r

ross_a

11/08/2023, 9:20 AM
j

Joffrey

11/08/2023, 9:28 AM
Your sample code doesn't switch dispatchers within a coroutine, it creates another coroutine running on a different dispatcher. Could you provide a bit more details about what you're trying to do?
b

benkuly

11/08/2023, 9:29 AM
d

Dmitry Khalanskiy [JB]

11/08/2023, 9:46 AM
Do I understand it right, that
advanceUntilIdle
does not "wait" when there is dispatcher switch within a coroutine?
Yes. It is mentioned in every place I can think of.
advanceUntilIdle
,
runCurrent
, etc., all only work with the tasks that the test dispatchers know about. If some other coroutine dispatcher is responsible for the task, there's no way for the test module to know about it and ensure it's completed.
Copy code
delay(100) // skipped
val waitForResult = launch(otherDispatcher){
    delay(100)
}
delay(200) // not called at all on advanceUntilIdle
I don't know what you mean by this. If this whole block of code executes in a test dispatcher and is followed by
advanceUntilIdle
, then both the first
delay(100)
and
delay(200)
will be skipped.
delay(100)
in the
launch
won't, as it doesn't use virtual time. In this simple case, you don't even need
advanceUntilIdle
, you can just
waitForResult.join()
instead, and it will work out of the box. In general, most usages of
advanceUntilIdle
that I've seen in the wild are not needed and can be replaced with plain and simple synchronization between coroutines, the way you'd write it in production code.
b

benkuly

11/08/2023, 10:20 AM
I made a reproducer:
Copy code
class Test {
    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun test() = runTest {
        launch {
            println(1)
            delay(100) // skipped
            println(2)
            withContext(Dispatchers.Default) {
                delay(100)
            }
            println(3)
            delay(200) // not called at all on advanceUntilIdle
            println(4)
        }
        advanceUntilIdle()
        println("-------------")
    }
}
It prints the following:
Copy code
1
2
-------------
3
4
I expect:
Copy code
1
2
3
4
-------------
d

Dmitry Khalanskiy [JB]

11/08/2023, 10:23 AM
If you move
join
after
println(4)
, you'll get what you expect.
Also, this whole reproducer is very suspicious. If you just remove the outer
launch
and
advanceUntilIdle
, the test will only become more idiomatic.
b

benkuly

11/08/2023, 10:28 AM
The
launch
is hided in view model. The
launch(Dispatchers.Default)
is actually a ktor post request, I changed it above to
withContext
. This is just a simplified example to show the problem.
r

ross_a

11/08/2023, 10:29 AM
but that's what I'd expect from an advanceUntilIdle - at the point of the join there is no more work to run on the test dispatcher - it has to wait for the non-test dispatcher to finish before there is more work to run
👌 1
b

benkuly

11/08/2023, 10:33 AM
I think waiting for another coroutine is also some sort of "work". It is definitely not idle in my understanding, because there is still possible work in the future.
r

ross_a

11/08/2023, 10:37 AM
I feel that opinion goes against the grain a bit I'm afraid I am not sure why you wouldn't just rewrite it as
Copy code
class Test {
    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun test() = runTest {
            println(1)
            delay(100) // skipped
            println(2)
            withContext(Dispatchers.Default) {
                delay(100)
            }.join()
            println(3)
            delay(200) // not called at all on advanceUntilIdle
            println(4)
        println("-------------")
    }
}
d

Dmitry Khalanskiy [JB]

11/08/2023, 10:38 AM
advanceUntilIdle
and others are unintuitive when other dispatchers come into play. This is only this way for historical reasons; if we designed the framework today from scratch, they wouldn't be included. That's one of the reasons why we are looking into providing suitable replacements for them: https://github.com/Kotlin/kotlinx.coroutines/issues/3919
👍 1
b

benkuly

11/08/2023, 10:50 AM
I feel that opinion goes against the grain a bit I'm afraid
I am not sure why you wouldn't just rewrite it as
As mentioned above, the
launch
is hidden in a not suspending view model method. This is just an example to show the actual problem.
d

Dmitry Khalanskiy [JB]

11/08/2023, 10:55 AM
The official solution for such cases is to mock the dispatcher with a test dispatcher. See https://developer.android.com/kotlin/coroutines/test, "Injecting test dispatchers."
b

benkuly

11/08/2023, 11:17 AM
I know, but ktor deprecated setting it for MockEngine. I also found this behaviour of TestDispatcher very unexpected. We can never know, if a thirdparty library makes a context switch and then
advanceUntilIdle
is just useless.
3 Views