@@ -62,17 +62,20 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
6262 dispatcher : TestDispatcher ,
6363 timeDeltaMillis : Long ,
6464 marker : T ,
65+ context : CoroutineContext ,
6566 isCancelled : (T ) -> Boolean
6667 ): DisposableHandle {
6768 require(timeDeltaMillis >= 0 ) { " Attempted scheduling an event earlier in time (with the time delta $timeDeltaMillis )" }
69+ checkSchedulerInContext(this , context)
6870 val count = count.getAndIncrement()
71+ val isForeground = context[BackgroundWork ] == = null
6972 return synchronized(lock) {
7073 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) }
7275 events.addLast(event)
7376 /* * can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's
7477 * actually anything in the event queue. */
75- sendDispatchEvent()
78+ sendDispatchEvent(context )
7679 DisposableHandle {
7780 synchronized(lock) {
7881 events.remove(event)
@@ -82,10 +85,12 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
8285 }
8386
8487 /* *
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.
8690 */
87- private fun tryRunNextTask ( ): Boolean {
91+ internal fun tryRunNextTaskUnless ( condition : () -> Boolean ): Boolean {
8892 val event = synchronized(lock) {
93+ if (condition()) return false
8994 val event = events.removeFirstOrNull() ? : return false
9095 if (currentTime > event.time)
9196 currentTimeAheadOfEvents()
@@ -105,9 +110,15 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
105110 * functionality, query [currentTime] before and after the execution to achieve the same result.
106111 */
107112 @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
111122 }
112123 }
113124
@@ -169,24 +180,19 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
169180 /* *
170181 * Checks that the only tasks remaining in the scheduler are cancelled.
171182 */
172- internal fun isIdle (strict : Boolean = true): Boolean {
183+ internal fun isIdle (strict : Boolean = true): Boolean =
173184 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() }
182186 }
183- }
184187
185188 /* *
186189 * Notifies this scheduler about a dispatch event.
190+ *
191+ * [context] is the context in which the task will be dispatched.
187192 */
188- internal fun sendDispatchEvent () {
189- dispatchEvents.trySend(Unit )
193+ internal fun sendDispatchEvent (context : CoroutineContext ) {
194+ if (context[BackgroundWork ] != = BackgroundWork )
195+ dispatchEvents.trySend(Unit )
190196 }
191197
192198 /* *
@@ -216,6 +222,8 @@ private class TestDispatchEvent<T>(
216222 private val count : Long ,
217223 @JvmField val time : Long ,
218224 @JvmField val marker : T ,
225+ @JvmField val isForeground : Boolean ,
226+ // TODO: remove once the deprecated API is gone
219227 @JvmField val isCancelled : () -> Boolean
220228) : Comparable<TestDispatchEvent<*>>, ThreadSafeHeapNode {
221229 override var heap: ThreadSafeHeap <* >? = null
@@ -224,7 +232,7 @@ private class TestDispatchEvent<T>(
224232 override fun compareTo (other : TestDispatchEvent <* >) =
225233 compareValuesBy(this , other, TestDispatchEvent <* >::time, TestDispatchEvent <* >::count)
226234
227- override fun toString () = " TestDispatchEvent(time=$time , dispatcher=$dispatcher )"
235+ override fun toString () = " TestDispatchEvent(time=$time , dispatcher=$dispatcher${ if (isForeground) " " else " , background " } )"
228236}
229237
230238// works with positive `a`, `b`
@@ -238,3 +246,17 @@ internal fun checkSchedulerInContext(scheduler: TestCoroutineScheduler, context:
238246 }
239247 }
240248}
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