@@ -62,17 +62,20 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
62
62
dispatcher : TestDispatcher ,
63
63
timeDeltaMillis : Long ,
64
64
marker : T ,
65
+ context : CoroutineContext ,
65
66
isCancelled : (T ) -> Boolean
66
67
): DisposableHandle {
67
68
require(timeDeltaMillis >= 0 ) { " Attempted scheduling an event earlier in time (with the time delta $timeDeltaMillis )" }
69
+ checkSchedulerInContext(this , context)
68
70
val count = count.getAndIncrement()
71
+ val isForeground = context[BackgroundWork ] == = null
69
72
return synchronized(lock) {
70
73
val time = addClamping(currentTime, timeDeltaMillis)
71
- val event = TestDispatchEvent (dispatcher, count, time, marker as Any ) { isCancelled(marker) }
74
+ val event = TestDispatchEvent (dispatcher, count, time, marker as Any , isForeground ) { isCancelled(marker) }
72
75
events.addLast(event)
73
76
/* * can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's
74
77
* actually anything in the event queue. */
75
- sendDispatchEvent()
78
+ sendDispatchEvent(context )
76
79
DisposableHandle {
77
80
synchronized(lock) {
78
81
events.remove(event)
@@ -82,10 +85,12 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
82
85
}
83
86
84
87
/* *
85
- * Runs the next enqueued task, advancing the virtual time to the time of its scheduled awakening.
88
+ * Runs the next enqueued task, advancing the virtual time to the time of its scheduled awakening,
89
+ * unless [condition] holds.
86
90
*/
87
- private fun tryRunNextTask ( ): Boolean {
91
+ internal fun tryRunNextTaskUnless ( condition : () -> Boolean ): Boolean {
88
92
val event = synchronized(lock) {
93
+ if (condition()) return false
89
94
val event = events.removeFirstOrNull() ? : return false
90
95
if (currentTime > event.time)
91
96
currentTimeAheadOfEvents()
@@ -105,9 +110,15 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
105
110
* functionality, query [currentTime] before and after the execution to achieve the same result.
106
111
*/
107
112
@ExperimentalCoroutinesApi
108
- public fun advanceUntilIdle () {
109
- while (! synchronized(lock) { events.isEmpty }) {
110
- tryRunNextTask()
113
+ public fun advanceUntilIdle (): Unit = advanceUntilIdleOr { events.none(TestDispatchEvent <* >::isForeground) }
114
+
115
+ /* *
116
+ * [condition]: guaranteed to be invoked under the lock.
117
+ */
118
+ internal fun advanceUntilIdleOr (condition : () -> Boolean ) {
119
+ while (true ) {
120
+ if (! tryRunNextTaskUnless(condition))
121
+ return
111
122
}
112
123
}
113
124
@@ -169,24 +180,19 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
169
180
/* *
170
181
* Checks that the only tasks remaining in the scheduler are cancelled.
171
182
*/
172
- internal fun isIdle (strict : Boolean = true): Boolean {
183
+ internal fun isIdle (strict : Boolean = true): Boolean =
173
184
synchronized(lock) {
174
- if (strict)
175
- return events.isEmpty
176
- // TODO: also completely empties the queue, as there's no nondestructive way to iterate over [ThreadSafeHeap]
177
- val presentEvents = mutableListOf<TestDispatchEvent <* >>()
178
- while (true ) {
179
- presentEvents + = events.removeFirstOrNull() ? : break
180
- }
181
- return presentEvents.all { it.isCancelled() }
185
+ if (strict) events.isEmpty else events.none { ! it.isCancelled() }
182
186
}
183
- }
184
187
185
188
/* *
186
189
* Notifies this scheduler about a dispatch event.
190
+ *
191
+ * [context] is the context in which the task will be dispatched.
187
192
*/
188
- internal fun sendDispatchEvent () {
189
- dispatchEvents.trySend(Unit )
193
+ internal fun sendDispatchEvent (context : CoroutineContext ) {
194
+ if (context[BackgroundWork ] != = BackgroundWork )
195
+ dispatchEvents.trySend(Unit )
190
196
}
191
197
192
198
/* *
@@ -216,6 +222,8 @@ private class TestDispatchEvent<T>(
216
222
private val count : Long ,
217
223
@JvmField val time : Long ,
218
224
@JvmField val marker : T ,
225
+ @JvmField val isForeground : Boolean ,
226
+ // TODO: remove once the deprecated API is gone
219
227
@JvmField val isCancelled : () -> Boolean
220
228
) : Comparable<TestDispatchEvent<*>>, ThreadSafeHeapNode {
221
229
override var heap: ThreadSafeHeap <* >? = null
@@ -224,7 +232,7 @@ private class TestDispatchEvent<T>(
224
232
override fun compareTo (other : TestDispatchEvent <* >) =
225
233
compareValuesBy(this , other, TestDispatchEvent <* >::time, TestDispatchEvent <* >::count)
226
234
227
- override fun toString () = " TestDispatchEvent(time=$time , dispatcher=$dispatcher )"
235
+ override fun toString () = " TestDispatchEvent(time=$time , dispatcher=$dispatcher${ if (isForeground) " " else " , background " } )"
228
236
}
229
237
230
238
// works with positive `a`, `b`
@@ -238,3 +246,17 @@ internal fun checkSchedulerInContext(scheduler: TestCoroutineScheduler, context:
238
246
}
239
247
}
240
248
}
249
+
250
+ /* *
251
+ * A coroutine context key denoting that the work is to be executed in the background.
252
+ * @see [TestScope.backgroundScope]
253
+ */
254
+ internal object BackgroundWork : CoroutineContext.Key<BackgroundWork>, CoroutineContext.Element {
255
+ override val key: CoroutineContext .Key <* >
256
+ get() = this
257
+
258
+ override fun toString (): String = " BackgroundWork"
259
+ }
260
+
261
+ private fun <T > ThreadSafeHeap<T>.none (predicate : (T ) -> Boolean ) where T: ThreadSafeHeapNode, T: Comparable<T> =
262
+ find(predicate) == null
0 commit comments