Two different tests in this class failed spuriously today and passed on re-run, on unrelated PRs:
Both present as icloud_device_container still being GONE after the Eventually window, which reads as a slow screen. It is not.
What the logcat says
W FetchFromICloudActivity: iCloud flow stopped: UNKNOWN - Attempt to invoke interface method
'io.reactivex.rxjava3.core.Observable dev.wander.android.opentagviewer.python.icloud.ICloudService.records(java.util.List)'
on a null object reference
Logged at 15:47:45.079, ~5ms after the activity reached RESUMED and well before the assertion gave up at 15:47:57. The screen was never going to populate — the fake was gone by the time the activity asked for it.
Why it is a race and not a slow screen
AppDependencies is process-global static state. open(fake) calls replaceICloud(() -> fake) and then launches the activity, while @After putTheRealOneBack calls AppDependencies.reset(). Activity teardown is asynchronous, so a previous test's reset can land after the next test has installed its fake, or a lingering activity can be recreated against a reset dependency. Nothing serialises the two.
Consistent with it being intermittent, with it hitting different tests in the class, and with it never reproducing in isolation.
Why it matters beyond the noise
It is indistinguishable, from the outside, from the failure mode rule 12 is about: a screen that renders empty. Today it twice looked like a real regression on a PR that could not have caused it, and the second time cost a full 20-minute re-run to rule out. A suite that cries wolf is a suite people stop reading.
Notes toward a fix
AppDependencies has no guard against being read after reset — a null factory could throw with a message naming the test seam rather than an NPE deep in an Rx chain
- the class launches activities in
open() rather than through a rule, so teardown ordering is not enforced
ActivityScenario.close() is called in @After, but so is reset(), and their order relative to the activity actually finishing is not guaranteed
Logcats are in the instrumented-logcat artifact of the runs above, per test.
Interactively co-authored by Claude Code and @parawanderer
Two different tests in this class failed spuriously today and passed on re-run, on unrelated PRs:
theattemptsAreCapped— on Restore exported location history #173's runtheresultsSayWhatIsNowOnTheAppleAccount— on Offer the log from the login screen, where failures were undebuggable #197's runBoth present as
icloud_device_containerstill beingGONEafter theEventuallywindow, which reads as a slow screen. It is not.What the logcat says
Logged at 15:47:45.079, ~5ms after the activity reached RESUMED and well before the assertion gave up at 15:47:57. The screen was never going to populate — the fake was gone by the time the activity asked for it.
Why it is a race and not a slow screen
AppDependenciesis process-global static state.open(fake)callsreplaceICloud(() -> fake)and then launches the activity, while@After putTheRealOneBackcallsAppDependencies.reset(). Activity teardown is asynchronous, so a previous test's reset can land after the next test has installed its fake, or a lingering activity can be recreated against a reset dependency. Nothing serialises the two.Consistent with it being intermittent, with it hitting different tests in the class, and with it never reproducing in isolation.
Why it matters beyond the noise
It is indistinguishable, from the outside, from the failure mode rule 12 is about: a screen that renders empty. Today it twice looked like a real regression on a PR that could not have caused it, and the second time cost a full 20-minute re-run to rule out. A suite that cries wolf is a suite people stop reading.
Notes toward a fix
AppDependencieshas no guard against being read after reset — a null factory could throw with a message naming the test seam rather than an NPE deep in an Rx chainopen()rather than through a rule, so teardown ordering is not enforcedActivityScenario.close()is called in@After, but so isreset(), and their order relative to the activity actually finishing is not guaranteedLogcats are in the
instrumented-logcatartifact of the runs above, per test.