-
Notifications
You must be signed in to change notification settings - Fork 1.9k
TestTimeSource/currentTime does not increase after switching to a non-delay skipping Dispatcher #4045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
It's impossible to support the test Imagine we have this function: suspend fun loadPage() = withContext(Dispatchers.IO) {
withTimeout(2.seconds) {
getPageFromServer()
}
} and this test: fun testConnectingToNonresponsiveServer() = runTest {
assertEquals(0, currentTime)
assertFailsWith<TimeoutCancellationException> {
loadPage()
}
assertEquals(2000, currentTime)
} The test works. Then you change the internal implementation (that the test shouldn't know about) to something like suspend fun loadPage() = suspendCancellableCoroutine { cont ->
FancyJavaHttpClient
.getPageFromServer()
.timingOutAfter(2_000)
.onSuccess { cont.resume(it) }
.onError { cont.resumeWithException(it) }
.onTimeout { cont.resumeWithException(TimeoutCancellationException() }
} From the point of view of the test framework, these implementations should behave the same way: since you haven't mocked anything to allow the test framework to intrusively depend on your implementation, the What is less impossible is to support the This solution is also not without its downsides and strange moments, though. For example: fun test() = runTest {
launch(Dispatchers.IO) {
delay(1.seconds)
println("Coroutine 1 completed")
}
launch {
delay(100.seconds)
println("Coroutine 2 completed")
}
launch {
withContext(Dispatchers.IO) {
delay(100.milliseconds)
}
delay(5.seconds)
println("Coroutine 2 completed")
}
} This will print This could be mitigated with #3179: when we introduce the option to avoid delay-skipping, you'd be able to write the @Test
fun timeSource() = runTest {
val timeSource = testScheduler.timeSource
val now = timeSource.markNow()
assertEquals(0.seconds, now.elapsedNow())
delay(1.seconds)
assertEquals(1.seconds, now.elapsedNow())
val time = measureTime {
withContext(NoDelaySkipping) {
yourBlackBoxThatFinishesInTwoSeconds()
}
}
assertTrue(time in (2.seconds)..(2.1.seconds))
assertEquals(1.seconds + time, now.elapsedNow()) // fails: expected 3s, actual 1s
delay(1.seconds)
assertEquals(2.seconds + time, now.elapsedNow())
}
|
Thank you for the detailed response and the example, I got it. I think, the solution |
That wasn't the plan. The context element can't magically signal all test schedulers, "Hey, a coroutine is using me at the moment! Don't advance time while it's doing it!" Coroutine context elements are completely passive entities. Therefore, the test framework must itself see the context element to make it count. If there's a Also, the recommended overall approach to making code testable is to support injecting new context elements. |
Describe the bug
The time source or the underlaying
currentTime
is not updated after switching the context to a non delay skipping dispatcher.Our use-case: Ktor
We want to change Ktors public test framework
testApplication
fromrunBlocking
torunTest
, including its delay skipping behavior in tests, to align with the common test behavior of the well-knowncoroutines-test
api.But if the test contains a request to an external real server, Ktor executes the test on
Dispatchers.Default/IO
and uses a real-time clock to not mess up http connections, eg TLS handshakes.After the request, the virtual time should be used again for user tests, but the virtual time isn't increased after the switch. This is confusing because in fact the time was increased.
It also does not work with custom user plugins testing with which could be part of the user tests, resulting into different times, see timeSource test as an example.
Provide a Reproducer
The text was updated successfully, but these errors were encountered: