Skip to content

Make PaywallViewModel dispatcher injectable to fix flaky tests#3544

Merged
vegaro merged 1 commit into
mainfrom
cesar/wfl-fix-prewarm-dispatcher-flake
Jun 5, 2026
Merged

Make PaywallViewModel dispatcher injectable to fix flaky tests#3544
vegaro merged 1 commit into
mainfrom
cesar/wfl-fix-prewarm-dispatcher-flake

Conversation

@vegaro

@vegaro vegaro commented Jun 5, 2026

Copy link
Copy Markdown
Member

PaywallViewModelWorkflowTest looks flaky on CI

I believe the reason is that preWarmWorkflowStepCache() launches on viewModelScope (the test Main dispatcher) but hops to withContext(Dispatchers.Default) for the heavy computeStateForStep work.

Making the dispatcher injectable and injecting the test dispatcher in the workflow test's VM construction so the pre-warm runs on the test scheduler should fix it.

Example failure:

https://app.circleci.com/pipelines/github/RevenueCat/purchases-android/22653/workflows/97ebb0de-5850-4621-ae49-725e31304259/jobs/223710/tests

Failure in PaywallViewModelWorkflowTest

java.lang.IllegalStateException: Dispatchers.Main is used concurrently with setting it
	at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.concurrentRW(TestMainDispatcher.kt:72)
	at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.setValue(TestMainDispatcher.kt:90)
	at kotlinx.coroutines.test.internal.TestMainDispatcher.resetDispatcher(TestMainDispatcher.kt:41)
	at kotlinx.coroutines.test.TestDispatchers.resetMain(TestDispatchers.kt:34)
	at com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelWorkflowTest.tearDown(PaywallViewModelWorkflowTest.kt:345)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.RunAfters.invokeMethod(RunAfters.java:46)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:33)
	at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)
	Suppressed: org.robolectric.android.internal.AndroidTestEnvironment$UnExecutedRunnablesException: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(Looper.getMainLooper()).idle() call.

Note

Low Risk
Production behavior is unchanged thanks to the default dispatcher; only test construction and an internal injectable hook for pre-warm offloading.

Overview
Adds an optional backgroundDispatcher constructor parameter to PaywallViewModelImpl (default Dispatchers.Default) and uses it in preWarmWorkflowStepCache instead of a hardcoded Dispatchers.Default for computeStateForStep.

PaywallViewModelWorkflowTest passes the test StandardTestDispatcher so workflow step pre-warming stays on the test coroutine scheduler. That avoids work continuing on the real default pool after Dispatchers.resetMain(), which was causing flaky CI failures. Production wiring via PaywallViewModelFactory is unchanged and still uses the default dispatcher.

Reviewed by Cursor Bugbot for commit a99fef5. Bugbot is set up for automated code reviews on this repo. Configure here.

vegaro commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.11%. Comparing base (e1de6c8) to head (a99fef5).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3544   +/-   ##
=======================================
  Coverage   80.11%   80.11%           
=======================================
  Files         371      371           
  Lines       15166    15166           
  Branches     2100     2100           
=======================================
  Hits        12150    12150           
  Misses       2166     2166           
  Partials      850      850           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro
vegaro marked this pull request as ready for review June 5, 2026 08:57
@vegaro
vegaro requested a review from a team as a code owner June 5, 2026 08:57
…ky workflow tests

preWarmWorkflowStepCache launched on viewModelScope (the test Main dispatcher
in tests) but hopped to withContext(Dispatchers.Default) for the heavy
computeStateForStep work. Dispatchers.Default is a real thread pool the
StandardTestDispatcher's scheduler doesn't control, so advanceUntilIdle() never
awaited it. The coroutine then resumed back on the test Main after the test had
finished and @after resetMain() had unset it, throwing IllegalStateException
from TestMainDispatcher and surfacing at the resetMain() call. A timing/order
race, hence the flakiness in PaywallViewModelWorkflowTest.

Make the dispatcher injectable (backgroundDispatcher: CoroutineDispatcher =
Dispatchers.Default), mirroring AppStyleExtractor, and inject the test
dispatcher in the workflow test's VM construction so the pre-warm runs on the
test scheduler and advanceUntilIdle() drains it. No production behavior change:
the default stays Dispatchers.Default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vegaro
vegaro force-pushed the cesar/wfl-fix-prewarm-dispatcher-flake branch from eb46f70 to a99fef5 Compare June 5, 2026 08:57
@vegaro vegaro changed the title test: make PaywallViewModel pre-warm dispatcher injectable to fix flaky workflow tests Make PaywallViewModel dispatcher injectable to fix flaky tests Jun 5, 2026

@facumenzella facumenzella left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't any clear way to make sure this addresses the flamines right? It would be great to link the failures to the PR description, so we can track this back

@vegaro

vegaro commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

added the failures @facumenzella

@vegaro
vegaro enabled auto-merge June 5, 2026 09:16
@vegaro
vegaro added this pull request to the merge queue Jun 5, 2026
Merged via the queue into main with commit 27fd84b Jun 5, 2026
40 checks passed
@vegaro
vegaro deleted the cesar/wfl-fix-prewarm-dispatcher-flake branch June 5, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants