-
Notifications
You must be signed in to change notification settings - Fork 214
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
Paparazzi breaks delay functionality when overriding Dispatchers.Main with a TestDispatcher #1198
Labels
bug
Something isn't working
Comments
Nice :micdrop: report. Re solution 3. see #1161; otherwise I'll let someone more knowledgeable reply. |
I believe this has been fixed by #1164. Wanna verify this by using |
prfarlow1
added a commit
to WhoopInc/paparazzi
that referenced
this issue
Dec 28, 2023
prfarlow1
added a commit
to WhoopInc/paparazzi
that referenced
this issue
Dec 28, 2023
@saket Indeed 1.3.2-SNAPSHOT has resolved this issue. Many thanks again! I'll close this ticket. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Description
The Android Developer Documentation recommends overriding
Dispatchers.Main
with aTestDispatcher
. This enables control over the execution of coroutines. Doing this after a Paparazzi snapshot has been taken breaks delay scheduling, causing tests executed using kotlinx-coroutiens-test'runTest
to instantly time out.Steps to Reproduce
The following test requires
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version = "1.7.3" }
.Please note that the error also happens when the code within
runTest { ... }
is in a separate@Test
method. Using Paparazzi therefore has an impact on tests that are unrelated to the Paparazzi tests of a given project.Expected behavior
The
runTest
code should not time out.Additional information:
Analysis:
First off, I believe this change introduced the bug. It sets the system property
kotlinx.coroutines.main.delay
totrue
. This then causes a very unfortunate interaction of different global coroutine initialization and testing mechanisms. Now onto more details:Paparazzi needs a simulated Android environment. When it runs, it initializes the main
Looper
:This has an impact on how coroutines'
Dispatchers.Main
is initialized when it's first accessed. Paparazzi also happens to access it as it uses aComposeView
under the hood, which in turn uses coroutines andDispatchers.Main
.Dispatchers.Main
is initialized inMainDispatcherLoader
by trying different factories. One of these factories is theAndroidDispatcherFactory
:We can immediately see here that this factory only succeeds if the main
Looper
is initialized, which is the case whenDispatchers.Main
is initialized from within a Paparazzi test. If this wasn't the case, theMainDispatcherLoader
would fall back to the next factory, i.e.TestMainDispatcherFactory
. This would create aTestMainDispatcher
that partially delegates to a placeholderMissingMainCoroutineDispatcher
.Even though Paparazzi tears down the main Looper in
Paparazzi.close()
when a Paparazzi test completes, it cannot undo the initialization ofDispatchers.Main
. Surprisingly, evenDispatchers.resetMain()
does not fully reset it, as it just switches back to the defaultmainDispatcher
which is already affected by Paparazzi.Now let's assume our Paparazzi snapshot got taken and we proceed to the call to
runTest
withtimeout = 10.seconds
. This sets up a timeout usingcoroutineContext.delay
(seesetupTimeout
inTimeout.kt
). It callscoroutineContext.delay.invokeOnTimeout
, which delegates toDefaultDelay.invokeOnTimeout
in the default implementation.DefaultDelay
is a global variable that gets initialized as follows:There are two ways this can go (assuming
kotlinx.coroutines.main.delay
istrue
, as introduced by the change linked to earlier):DefaultExecutor
is picked as the implementation forDefaultDelay
.DefaultDelay
.Remember that the main dispatcher is not missing (
= MissingMainCoroutineDispatcher
) as theAndroidDispatcherFactory
succeeded. So now delays are scheduled using the main dispatcher. Note that withkotlinx.coroutines.main.delay
set tofalse
theDefaultExecutor
would always used as theDefaultDelay
, ensuring delays would be scheduled correctly.But this is all not a problem yet: The
HandlerContext
returned by theAndroidDispatcherFactory
has been installed as the main dispatcher and knows how to schedule delays.However, note that the test installs an
UnconfinedTestDispatcher
asDispatchers.Main
, as is recommended.Among other features, the
TestDispatcher
skips delays! It does this to accelerate tests. Unfortunately for us, this has the side-effect that the delay of 10 seconds scheduled byinvokeOnTimeout
is skipped and the timeout hits instantly.So in short, this is what happens:
Looper
Dispatchers.Main
is theHandlerContext
returned byAndroidDispatcherFactory
instead ofTestMainDispatcher
backed by aMissingMainCoroutineDispatcher
DefaultDelay
to delegate toDispatchers.Main
as the Paparazzi Gradle plugin has setkotlinx.coroutines.main.delay
totrue
Dispatchers.Main
with a dispatcher that skips delaysrunTest
is scheduled using theDefaultDelay
implementation, which isDispatchers.Main
. Unfortunately we have overriddenDispatchers.Main
with a dispatcher that skips delays, so the timeout happens instantly.To solve this problem, I found multiple possible alternatives to consider. There may be others.
StandardTestDispatcher
and instead to use one that does not skip delays. This requires users to touch non-Paparazzi tests and potentially implement a custom dispatcher if they at least need the control features provided byTestDispatcher
, likeadvanceUntilIdle()
Dispatchers.Main
andDefaultDelay
once before Paparazzi prepares the Looper, to force them to initialize with the correct environment. This is what we do in our project now.The text was updated successfully, but these errors were encountered: