Skip to content

Commit 5445796

Browse files
committed
test(scan): make the stale-scan regression test deterministic
lookup() hops through the real Dispatchers.IO, so the virtual test scheduler cannot guarantee scan A parks in runAffinity before scan B is submitted. When B landed first it cancelled A before A ever called runAffinity, and B itself became call #1 and parked on the gate — an intermittent hang/failure. Scan A now signals when it enters runAffinity and the test awaits that signal before submitting B, so B can only ever supersede A. No production change.
1 parent 1f02745 commit 5445796

1 file changed

Lines changed: 21 additions & 8 deletions

File tree

app/src/test/java/io/theficos/ereader/ui/scan/ScanViewModelTest.kt

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,31 @@ class ScanViewModelTest {
190190
/**
191191
* Regression: a slow, superseded scan must never clobber a newer scan's
192192
* terminal state. Scan A suspends inside runAffinity; scan B is submitted
193-
* and succeeds; then A's affinity fails late. With the generation guard +
194-
* CancellationException rethrow, A's stale write is dropped and B's Result
195-
* stands. Uses UnconfinedTestDispatcher (set in setUp) so each launch runs
196-
* eagerly up to its first real suspension; lookup returns synchronously and
197-
* the IO hop completes inline, so A reliably parks on gateA before B runs.
193+
* and succeeds; then A's gate is released late. The generation guard +
194+
* CancellationException rethrow drop A's stale write and B's Result stands.
195+
*
196+
* [lookup] hops through the real [Dispatchers.IO] (see [ScanViewModel]), so
197+
* we must NOT assume A parks on its gate before B is submitted — that race
198+
* used to make this test flaky (if B lands first it cancels A before A ever
199+
* calls runAffinity, and then B itself becomes call #1 and parks). Instead A
200+
* signals [aReachedAffinity] the moment it enters runAffinity, and the test
201+
* awaits that signal before submitting B — deterministic regardless of when
202+
* the IO hop lands.
198203
*/
199204
@Test fun `stale scan does not clobber a newer scan's result`() = runTest {
205+
val aReachedAffinity = CompletableDeferred<Unit>()
200206
val gateA = CompletableDeferred<AffinityResponse>()
201207
var call = 0
202208
val vm = ScanViewModel(
203209
lookup = { bundle },
204210
runAffinity = {
205211
call++
206-
if (call == 1) gateA.await() else affinity
212+
if (call == 1) {
213+
aReachedAffinity.complete(Unit)
214+
gateA.await()
215+
} else {
216+
affinity
217+
}
207218
},
208219
onReauth = { error("onReauth should not be called") },
209220
)
@@ -212,8 +223,10 @@ class ScanViewModelTest {
212223
assertThat(awaitItem()).isEqualTo(ScanUiState.Idle)
213224

214225
vm.onIsbnSubmitted(isbn13) // scan A — will park awaiting gateA
215-
// A's lookup IO hop completes, then A parks in runAffinity; the only
216-
// emission is the Working flash.
226+
// Block until A is provably the in-flight scan parked in runAffinity,
227+
// so submitting B can only ever supersede A (never the reverse).
228+
aReachedAffinity.await()
229+
217230
var s = awaitItem()
218231
while (s is ScanUiState.Idle) s = awaitItem()
219232
assertThat(s).isEqualTo(ScanUiState.Working)

0 commit comments

Comments
 (0)