test(home): drive the veil tests' clock explicitly, not advanceUntilIdle
android / Build + lint + test (push) Successful in 3m54s
android / Build + lint + test (push) Successful in 3m54s
All seven new UpdateVeilController tests failed in CI run 3163, and the one test that passed is the tell: it was the only one that never called advanceUntilIdle(). advanceUntilIdle() advances only while *foreground* work remains. Every coroutine this controller owns lives in backgroundScope — it has to, because its consumer loop runs forever and would otherwise stop runTest from completing — so advanceUntilIdle() returned having run nothing at all, and the assertions landed on a session that never started. Hence "exhausts its attempts. Expected <3>, actual <0>" and, where an earlier advanceTimeBy had got a session partway, "retries until the pull succeeds. Expected <3>, actual <2>". Each wait is now an explicit advanceTimeBy sized for what that test still has pending, and the class KDoc says why so nobody folds them back. The drains stay deliberately under maxHoldMs. If a drain overshot the ceiling, "the veil lowered" would stop distinguishing "it settled" from "it gave up" — which is exactly what these tests exist to tell apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-9
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -22,6 +21,13 @@ private const val ART_IN_FLIGHT = 3
|
||||
private const val SUCCEED_ON_ATTEMPT = 3
|
||||
private const val QUIET_WINDOWS_TO_OUTLAST = 3
|
||||
|
||||
// Time to let a session finish once the screen has stopped changing: the
|
||||
// retry backoffs, the quiet window and the minimum hold all fit inside it,
|
||||
// while staying well under maxHoldMs. That gap matters — if a drain ran
|
||||
// past the ceiling, "the veil lowered" would no longer distinguish
|
||||
// "it settled" from "it gave up", which is the whole point of these tests.
|
||||
private const val DRAIN_MS = 3_000L
|
||||
|
||||
/**
|
||||
* The veil's job is to stay up until the screen has stopped moving. Each
|
||||
* test pins one of the ways the previous fixed-delay implementation
|
||||
@@ -30,6 +36,14 @@ private const val QUIET_WINDOWS_TO_OUTLAST = 3
|
||||
* The controller is built on `backgroundScope` throughout: its consumer
|
||||
* loop runs forever, so hanging it off the test's own scope would stop
|
||||
* `runTest` from ever completing.
|
||||
*
|
||||
* Consequence, and the reason every wait below is an explicit
|
||||
* `advanceTimeBy`: **`advanceUntilIdle()` is useless here.** It advances
|
||||
* only while *foreground* work remains, and everything this controller
|
||||
* does lives in `backgroundScope` — so it returns having run nothing, and
|
||||
* assertions land on a session that never started (CI run 3163 failed all
|
||||
* seven of these with "expected 3, actual 0" and friends). Drive the clock
|
||||
* deliberately instead; don't "simplify" these back to advanceUntilIdle.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class UpdateVeilControllerTest {
|
||||
@@ -101,7 +115,7 @@ class UpdateVeilControllerTest {
|
||||
assertTrue(controller.visible.value, "veil must hold across staged churn")
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertFalse(controller.visible.value, "veil lowers once the screen goes quiet")
|
||||
}
|
||||
|
||||
@@ -118,7 +132,7 @@ class UpdateVeilControllerTest {
|
||||
assertTrue(controller.visible.value, "veil must wait on in-flight art")
|
||||
|
||||
screen.artLoading(0)
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertFalse(controller.visible.value, "veil lowers once art has landed")
|
||||
}
|
||||
|
||||
@@ -138,7 +152,7 @@ class UpdateVeilControllerTest {
|
||||
advanceTimeBy(timings.retryBackoffMs + 1)
|
||||
assertTrue(controller.visible.value, "veil stays up across the backoff")
|
||||
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertEquals(SUCCEED_ON_ATTEMPT, attempts, "retries until the pull succeeds")
|
||||
assertEquals(listOf(false, true, false), seen, "raised once, lowered once")
|
||||
}
|
||||
@@ -154,7 +168,7 @@ class UpdateVeilControllerTest {
|
||||
}
|
||||
|
||||
controller.request()
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertEquals(timings.attempts, attempts, "exhausts its attempts")
|
||||
assertFalse(controller.visible.value, "gives up silently")
|
||||
|
||||
@@ -162,7 +176,7 @@ class UpdateVeilControllerTest {
|
||||
// — the reconnect-driven recovery still gets to try again later.
|
||||
succeed = true
|
||||
controller.request()
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertEquals(timings.attempts + 1, attempts, "a later request still runs")
|
||||
}
|
||||
|
||||
@@ -186,7 +200,7 @@ class UpdateVeilControllerTest {
|
||||
advanceTimeBy(WORK_MS + 1)
|
||||
assertTrue(controller.visible.value, "second trigger extends the same veil")
|
||||
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertEquals(2, started, "the mid-session trigger still did its pull")
|
||||
assertEquals(listOf(false, true, false), seen, "one veil session, not two")
|
||||
}
|
||||
@@ -215,7 +229,7 @@ class UpdateVeilControllerTest {
|
||||
}
|
||||
|
||||
controller.request()
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(DRAIN_MS)
|
||||
assertTrue(ran, "the refresh still happens")
|
||||
assertFalse(controller.visible.value, "but no veil over a skeleton")
|
||||
}
|
||||
@@ -236,7 +250,7 @@ class UpdateVeilControllerTest {
|
||||
runCurrent()
|
||||
assertTrue(controller.visible.value, "raises the moment cached content paints")
|
||||
|
||||
advanceUntilIdle()
|
||||
advanceTimeBy(SLOW_WORK_MS + DRAIN_MS)
|
||||
assertFalse(controller.visible.value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user