CLOVIS
11/06/2023, 10:28 AMrunTest
?
The standard way of integrating other systems with delay skipping is to inject them a Clock
that reads from the virtual time. However, sometimes there are systems that cannot be injected a clock in this way, but expose an API method like foo.setTime(…)
. It would be great if there was a way to register a callback that would be invoked each time the virtual time changes, to notify those services.Dmitry Khalanskiy [JB]
11/06/2023, 10:33 AMIs it possible to get notified when the virtual time changes inIt's not, and I don't think anyone asked for this before. If you're fully confident in your use case, please file an issue on Github; if not, we can discuss it here and maybe arrive at a solution that doesn't need such notifications.?runTest
CLOVIS
11/06/2023, 10:40 AMClock
, and has no otherwise option to "ask what the current time is".
• That system can be forced to accept a different current time than the real one.
• We could mock it, but that would mean writing non-coroutine-based tests to ensure its correctness, and the coroutines virtual time is the best tool we have to reproduce complex timing issues, so we'd much rather use that if possible.
Essentially, it's a system that can be pushed time events to, instead of the usually recommended approach of having a system that pulls time events (through the Clock
or TimeSource
).runTest {
registerOnVirtualTimeAdvancement {
// suspend callback called each time the virtual time changes
}
…
delay(100) // the callback notifies the other systems that the time has changed
}
If you see another way to integrate such a system, I'm interested.kevin.cianfarini
11/06/2023, 1:56 PMClock
class MyThirdPartyClock(val thing: ThirdParty) : Clock {
override fun now() = thing.now().toinstant()
}
Dmitry Khalanskiy [JB]
11/06/2023, 1:59 PMThirdParty
automatically via normal calls to delay
.kevin.cianfarini
11/06/2023, 1:59 PMDmitry Khalanskiy [JB]
11/06/2023, 2:06 PMbackgroundScope.launch {
while (true) {
delay(50.milliseconds)
thirdParty.advanceTime(50.milliseconds)
}
}
If not, does the third-party system provide an interface like "this callback will be run when a new task is scheduled"?
backgroundScope.launch {
val taskChannel = Channel<suspend () -> Unit>()
thirdParty.onTaskRegistration { task ->
taskChannel.trySend {
delay(task.shouldBeStartedIn)
}
}
for (delayTask in taskChannel) {
launch {
delayTask()
thirdParty.setTime(currentTime)
}
}
}
CLOVIS
11/06/2023, 2:43 PMI'm wondering if a layer of custom abstraction from the third party dep will help. In this case that would be a custom impl of ClockI know this would be better, but we can't modify the third party system.
If it's large enough, maybe something like this would do the trick:
[while true]I think it would work, but wouldn't this break
advanceUntilIdle()
, runTest
's timeout
argument, etc?
does the third-party system provide an interface like "this callback will be run when a new task is scheduled"I'll have to look into it. It's not a great workaround, but maybe it could work. Out of curiosity, are there any issues with the original proposal (other than the cost of adding something to the API)?
Dmitry Khalanskiy [JB]
11/06/2023, 2:50 PMwouldn't this breakNope, if the only tasks available are the ones inadvanceUntilIdle()
backgroundScope
, advanceUntilIdle
will stop.
`runTest`'s timeout is in terms of the real time, not virtual time.'srunTest
argument, etc?timeout
are there any issues with the original proposalYes. It's a brittle API that's easy to misuse. For example,
thirdParty.scheduleAfter(1.seconds) {
println(1)
thirdParty.scheduleAfter(1.seconds) {
println(2)
thirdParty.scheduleAfter(1.seconds) {
println(3)
}
}
}
delay(5.seconds)
What do we expect to happen? I'd expect 1
, 2
, 3
to be printed, but in fact, only 1
will.
Why? It's because thirdParty
will only be notified when 5.seconds
pass. Only then will it run the block scheduled to run after one second, and only then will the new block get scheduled.CLOVIS
11/06/2023, 3:01 PMbackgroundScope
, advanceUntilIdle
will stop.
Oh, that's great to know. I haven't seen this mentioned anywhere in the documentation.
> `runTest`'s timeout is in terms of the real time, not virtual time.
doesn't your comment here imply the opposite?Dmitry Khalanskiy [JB]
11/06/2023, 3:04 PMI haven't seen this mentioned anywhere in the documentation.https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/background-scope.html
doesn't your comment here imply the opposite?That's a comment about how
runTest
used to operate, but we changed that since then.CLOVIS
11/06/2023, 3:11 PMIt's a brittle API that's easy to misuse. For example,I think there was a misunderstanding, this example does not look like what I had in mind. My thinking process was more along the lines of:
runTest {
registerOnVirtualTimeChange { println("foo") }
// from now on, each time the virtual time changes, "foo" is printed
somethingHappens()
delay(2.minutes)
somethingElse()
}
The assumption is that the correct usage would be something like
runTest {
val system = …
registerOnVirtualTimeChange { system.setTime(currentTime) }
system.scheduleAt(currentTime + 1.minutes) {
…
system.scheduleAt(currentTime + 1.minutes) {
…
}
}
advanceUntilIdle()
}
Dmitry Khalanskiy [JB]
11/06/2023, 3:13 PMcurrentTime
inside the block will only be evaluated when currentTime
is already 5 seconds.
As I said, brittle!CLOVIS
11/06/2023, 3:14 PMDmitry Khalanskiy [JB]
11/06/2023, 3:16 PMCLOVIS
11/06/2023, 4:29 PMadvanceUntilIdle()
etc ignores tasks scheduled in backgroundScope
does in fact make this entire use-case trivial via the infinite-loop+notify solution.