Skip to content

Commit 7b55366

Browse files
refactor: event lookahead milestone one fast follow p1 (#176)
* test: no more thread.sleep * fix: bug bot removeAll and removeEntry don't clear allEntries cache Medium Severity The PR introduces allEntries for search filtering but removeAll() and removeEntry() only clear/modify entries, leaving allEntries unchanged. This causes removed events to reappear when the user clears a search query, since setEventsToDisplay() restores entries from allEntries. Both methods need to also update allEntries to maintain consistency between the cached data and displayed data. * fix: update waitForAsyncTasks for Robolectric PAUSED looper mode Uses globalAsyncTaskCallback with CountDownLatch for efficient blocking: - No Thread.sleep() or busy-wait loops - Latch created when first task starts, signaled when last completes - await() blocks efficiently until tasks finish (5s timeout) This fixes 27 failing Robolectric tests that were using the deprecated flushBackgroundThreadScheduler() method. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent f2a6efb commit 7b55366

7 files changed

Lines changed: 85 additions & 64 deletions

File tree

android/app/src/main/java/com/github/quarck/calnotify/ui/DismissedEventListAdapter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,14 @@ class DismissedEventListAdapter(
340340
fun removeEntry(entry: DismissedEventAlertRecord)
341341
= synchronized(this) {
342342
val idx = entries.indexOf(entry)
343+
allEntries = allEntries.filter { ev -> ev != entry }.toTypedArray()
343344
entries = entries.filter { ev -> ev != entry }.toTypedArray()
344345
notifyItemRemoved(idx)
345346
}
346347

347-
fun removeAll() {
348-
entries = arrayOf<DismissedEventAlertRecord>()
348+
fun removeAll() = synchronized(this) {
349+
allEntries = arrayOf()
350+
entries = arrayOf()
349351
notifyDataSetChanged()
350352
}
351353
}

android/app/src/test/java/com/github/quarck/calnotify/testutils/UITestFixtureRobolectric.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.github.quarck.calnotify.testutils
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.os.Looper
56
import android.content.pm.PackageManager
67
import android.os.Bundle
78
import androidx.fragment.app.testing.FragmentScenario
@@ -30,10 +31,15 @@ import com.github.quarck.calnotify.ui.SettingsActivityX
3031
import com.github.quarck.calnotify.ui.SnoozeAllActivity
3132
import com.github.quarck.calnotify.ui.UpcomingEventsFragment
3233
import com.github.quarck.calnotify.ui.ViewEventActivityNoRecents
34+
import com.github.quarck.calnotify.utils.AsyncTaskCallback
35+
import com.github.quarck.calnotify.utils.globalAsyncTaskCallback
3336
import io.mockk.every
3437
import io.mockk.mockkObject
3538
import io.mockk.unmockkAll
3639
import org.robolectric.Shadows.shadowOf
40+
import java.util.concurrent.CountDownLatch
41+
import java.util.concurrent.TimeUnit
42+
import java.util.concurrent.atomic.AtomicInteger
3743

3844
/**
3945
* Test fixture for Robolectric UI tests.
@@ -475,9 +481,55 @@ class UITestFixtureRobolectric {
475481
mockkObject(ApplicationController)
476482
}
477483

484+
/**
485+
* Waits for all async tasks to complete.
486+
*
487+
* Uses the globalAsyncTaskCallback mechanism to track pending tasks
488+
* with a CountDownLatch for efficient blocking (no busy-wait).
489+
* Then idles the main looper to process onPostExecute callbacks.
490+
*/
491+
fun waitForAsyncTasks() {
492+
// Wait for any pending async tasks to complete (with timeout)
493+
if (pendingTaskCount.get() > 0) {
494+
completionLatch?.await(5, TimeUnit.SECONDS)
495+
}
496+
// Idle the main looper to process onPostExecute callbacks
497+
shadowOf(Looper.getMainLooper()).idle()
498+
}
499+
478500
companion object {
479501
private const val LOG_TAG = "UITestFixtureRobolectric"
480502

503+
/** Tracks number of async tasks currently in flight */
504+
private val pendingTaskCount = AtomicInteger(0)
505+
506+
/** Latch that signals when all tasks complete */
507+
@Volatile
508+
private var completionLatch: CountDownLatch? = null
509+
510+
/** Callback installed to track async task lifecycle */
511+
private val taskTrackingCallback = object : AsyncTaskCallback {
512+
override fun onTaskStarted() {
513+
val count = pendingTaskCount.incrementAndGet()
514+
if (count == 1) {
515+
// First task started - create a fresh latch
516+
completionLatch = CountDownLatch(1)
517+
}
518+
}
519+
override fun onTaskCompleted() {
520+
val count = pendingTaskCount.decrementAndGet()
521+
if (count == 0) {
522+
// All tasks done - signal the latch
523+
completionLatch?.countDown()
524+
}
525+
}
526+
}
527+
528+
init {
529+
// Install the callback globally to track all background { } calls
530+
globalAsyncTaskCallback = taskTrackingCallback
531+
}
532+
481533
fun create(): UITestFixtureRobolectric = UITestFixtureRobolectric()
482534
}
483535
}

android/app/src/test/java/com/github/quarck/calnotify/ui/ActiveEventsFragmentRobolectricTest.kt

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
package com.github.quarck.calnotify.ui
2121

22-
import android.os.Looper
2322
import android.view.View
2423
import android.widget.TextView
2524
import androidx.recyclerview.widget.RecyclerView
@@ -32,7 +31,6 @@ import org.junit.Before
3231
import org.junit.Test
3332
import org.junit.runner.RunWith
3433
import org.robolectric.RobolectricTestRunner
35-
import org.robolectric.Shadows.shadowOf
3634
import org.robolectric.annotation.Config
3735

3836
/**
@@ -78,8 +76,7 @@ class ActiveEventsFragmentRobolectricTest {
7876
val scenario = fixture.launchActiveEventsFragment()
7977

8078
// Wait for async data loading
81-
shadowOf(Looper.getMainLooper()).idle()
82-
Thread.sleep(100)
79+
fixture.waitForAsyncTasks()
8380

8481
scenario.onFragment { fragment ->
8582
val emptyView = fragment.requireView().findViewById<View>(R.id.empty_view)
@@ -97,8 +94,7 @@ class ActiveEventsFragmentRobolectricTest {
9794
val scenario = fixture.launchActiveEventsFragment()
9895

9996
// Wait for async data loading
100-
shadowOf(Looper.getMainLooper()).idle()
101-
Thread.sleep(100)
97+
fixture.waitForAsyncTasks()
10298

10399
scenario.onFragment { fragment ->
104100
val emptyView = fragment.requireView().findViewById<View>(R.id.empty_view)
@@ -131,8 +127,7 @@ class ActiveEventsFragmentRobolectricTest {
131127
val scenario = fixture.launchActiveEventsFragment()
132128

133129
// Wait for async data loading
134-
shadowOf(Looper.getMainLooper()).idle()
135-
Thread.sleep(100)
130+
fixture.waitForAsyncTasks()
136131

137132
scenario.onFragment { fragment ->
138133
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -152,8 +147,7 @@ class ActiveEventsFragmentRobolectricTest {
152147
val scenario = fixture.launchActiveEventsFragment()
153148

154149
// Wait for async data loading
155-
shadowOf(Looper.getMainLooper()).idle()
156-
Thread.sleep(100)
150+
fixture.waitForAsyncTasks()
157151

158152
scenario.onFragment { fragment ->
159153
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -189,8 +183,7 @@ class ActiveEventsFragmentRobolectricTest {
189183
val scenario = fixture.launchActiveEventsFragment()
190184

191185
// Wait for async data loading
192-
shadowOf(Looper.getMainLooper()).idle()
193-
Thread.sleep(100)
186+
fixture.waitForAsyncTasks()
194187

195188
scenario.onFragment { fragment ->
196189
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -211,8 +204,7 @@ class ActiveEventsFragmentRobolectricTest {
211204
val scenario = fixture.launchActiveEventsFragment()
212205

213206
// Wait for async data loading
214-
shadowOf(Looper.getMainLooper()).idle()
215-
Thread.sleep(100)
207+
fixture.waitForAsyncTasks()
216208

217209
scenario.onFragment { fragment ->
218210
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -233,8 +225,7 @@ class ActiveEventsFragmentRobolectricTest {
233225
val scenario = fixture.launchActiveEventsFragment()
234226

235227
// Wait for async data loading
236-
shadowOf(Looper.getMainLooper()).idle()
237-
Thread.sleep(100)
228+
fixture.waitForAsyncTasks()
238229

239230
scenario.onFragment { fragment ->
240231
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -253,8 +244,7 @@ class ActiveEventsFragmentRobolectricTest {
253244
val scenario = fixture.launchActiveEventsFragment()
254245

255246
// Wait for async data loading
256-
shadowOf(Looper.getMainLooper()).idle()
257-
Thread.sleep(100)
247+
fixture.waitForAsyncTasks()
258248

259249
scenario.onFragment { fragment ->
260250
val emptyView = fragment.requireView().findViewById<TextView>(R.id.empty_view)

android/app/src/test/java/com/github/quarck/calnotify/ui/DismissedEventsActivityRobolectricTest.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ class DismissedEventsActivityRobolectricTest {
8787
val scenario = fixture.launchDismissedEventsActivity()
8888

8989
// Wait for async data loading (reloadData uses background{})
90-
shadowOf(Looper.getMainLooper()).idle()
91-
Thread.sleep(100)
90+
fixture.waitForAsyncTasks()
9291

9392
scenario.onActivity { activity ->
9493
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)
@@ -125,8 +124,7 @@ class DismissedEventsActivityRobolectricTest {
125124
val scenario = fixture.launchDismissedEventsActivity()
126125

127126
// Wait for async data loading
128-
shadowOf(Looper.getMainLooper()).idle()
129-
Thread.sleep(100)
127+
fixture.waitForAsyncTasks()
130128

131129
scenario.onActivity { activity ->
132130
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)

android/app/src/test/java/com/github/quarck/calnotify/ui/DismissedEventsFragmentRobolectricTest.kt

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
package com.github.quarck.calnotify.ui
2121

22-
import android.os.Looper
2322
import android.view.View
2423
import android.widget.TextView
2524
import androidx.recyclerview.widget.RecyclerView
@@ -32,7 +31,6 @@ import org.junit.Before
3231
import org.junit.Test
3332
import org.junit.runner.RunWith
3433
import org.robolectric.RobolectricTestRunner
35-
import org.robolectric.Shadows.shadowOf
3634
import org.robolectric.annotation.Config
3735

3836
/**
@@ -96,8 +94,7 @@ class DismissedEventsFragmentRobolectricTest {
9694
val scenario = fixture.launchDismissedEventsFragment()
9795

9896
// Wait for async data loading
99-
shadowOf(Looper.getMainLooper()).idle()
100-
Thread.sleep(100)
97+
fixture.waitForAsyncTasks()
10198

10299
scenario.onFragment { fragment ->
103100
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -115,8 +112,7 @@ class DismissedEventsFragmentRobolectricTest {
115112
val scenario = fixture.launchDismissedEventsFragment()
116113

117114
// Wait for async data loading
118-
shadowOf(Looper.getMainLooper()).idle()
119-
Thread.sleep(100)
115+
fixture.waitForAsyncTasks()
120116

121117
scenario.onFragment { fragment ->
122118
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -141,8 +137,7 @@ class DismissedEventsFragmentRobolectricTest {
141137
val scenario = fixture.launchDismissedEventsFragment()
142138

143139
// Wait for async data loading
144-
shadowOf(Looper.getMainLooper()).idle()
145-
Thread.sleep(100)
140+
fixture.waitForAsyncTasks()
146141

147142
scenario.onFragment { fragment ->
148143
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -176,8 +171,7 @@ class DismissedEventsFragmentRobolectricTest {
176171
val scenario = fixture.launchDismissedEventsFragment()
177172

178173
// Wait for async data loading
179-
shadowOf(Looper.getMainLooper()).idle()
180-
Thread.sleep(100)
174+
fixture.waitForAsyncTasks()
181175

182176
scenario.onFragment { fragment ->
183177
val emptyView = fragment.requireView().findViewById<TextView>(R.id.empty_view)
@@ -197,8 +191,7 @@ class DismissedEventsFragmentRobolectricTest {
197191
fixture.createDismissedEvent(title = "Meeting Notes")
198192

199193
val scenario = fixture.launchDismissedEventsFragment()
200-
shadowOf(Looper.getMainLooper()).idle()
201-
Thread.sleep(100)
194+
fixture.waitForAsyncTasks()
202195

203196
scenario.onFragment { fragment ->
204197
val recyclerView = fragment.requireView().findViewById<RecyclerView>(R.id.recycler_view)
@@ -208,12 +201,12 @@ class DismissedEventsFragmentRobolectricTest {
208201

209202
// Filter by "meeting" - should show 2
210203
(fragment as SearchableFragment).setSearchQuery("meeting")
211-
shadowOf(Looper.getMainLooper()).idle()
204+
fixture.waitForAsyncTasks()
212205
assertEquals(2, recyclerView.adapter?.itemCount)
213206

214207
// Clear filter - back to 3
215208
fragment.setSearchQuery(null)
216-
shadowOf(Looper.getMainLooper()).idle()
209+
fixture.waitForAsyncTasks()
217210
assertEquals(3, recyclerView.adapter?.itemCount)
218211
}
219212

android/app/src/test/java/com/github/quarck/calnotify/ui/MainActivityRobolectricTest.kt

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ class MainActivityRobolectricTest {
7272
val scenario = fixture.launchMainActivity()
7373

7474
// Wait for async data loading
75-
shadowOf(Looper.getMainLooper()).idle()
76-
Thread.sleep(100)
75+
fixture.waitForAsyncTasks()
7776

7877
scenario.onActivity { activity ->
7978
val emptyView = activity.findViewById<View>(R.id.empty_view)
@@ -138,8 +137,7 @@ class MainActivityRobolectricTest {
138137
val scenario = fixture.launchMainActivity()
139138

140139
// Wait for async data loading
141-
shadowOf(Looper.getMainLooper()).idle()
142-
Thread.sleep(100)
140+
fixture.waitForAsyncTasks()
143141

144142
scenario.onActivity { activity ->
145143
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)
@@ -190,8 +188,7 @@ class MainActivityRobolectricTest {
190188
val scenario = fixture.launchMainActivity()
191189

192190
// Wait for async data loading
193-
shadowOf(Looper.getMainLooper()).idle()
194-
Thread.sleep(100)
191+
fixture.waitForAsyncTasks()
195192

196193
scenario.onActivity { activity ->
197194
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)
@@ -212,8 +209,7 @@ class MainActivityRobolectricTest {
212209
val scenario = fixture.launchMainActivity()
213210

214211
// Wait for async data loading
215-
shadowOf(Looper.getMainLooper()).idle()
216-
Thread.sleep(100)
212+
fixture.waitForAsyncTasks()
217213

218214
scenario.onActivity { activity ->
219215
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)
@@ -234,8 +230,7 @@ class MainActivityRobolectricTest {
234230
val scenario = fixture.launchMainActivity()
235231

236232
// Wait for async data loading
237-
shadowOf(Looper.getMainLooper()).idle()
238-
Thread.sleep(100)
233+
fixture.waitForAsyncTasks()
239234

240235
scenario.onActivity { activity ->
241236
val recyclerView = activity.findViewById<RecyclerView>(R.id.list_events)

0 commit comments

Comments
 (0)