xref: /aosp_15_r20/external/kotlinx.coroutines/test-utils/common/src/MainDispatcherTestBase.kt (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)

<lambda>null1 package kotlinx.coroutines.testing
2 
3 import kotlinx.coroutines.*
4 import kotlin.test.*
5 
6 abstract class MainDispatcherTestBase: TestBase() {
7 
8     open fun shouldSkipTesting(): Boolean = false
9 
10     open suspend fun spinTest(testBody: Job) {
11         testBody.join()
12     }
13 
14     abstract fun isMainThread(): Boolean?
15 
16     /** Runs the given block as a test, unless [shouldSkipTesting] indicates that the environment is not suitable. */
17     fun runTestOrSkip(block: suspend CoroutineScope.() -> Unit): TestResult {
18         // written as a block body to make the need to return `TestResult` explicit
19         return runTest {
20             if (shouldSkipTesting()) return@runTest
21             val testBody = launch(Dispatchers.Default) {
22                 block()
23             }
24             spinTest(testBody)
25         }
26     }
27 
28     /** Tests the [toString] behavior of [Dispatchers.Main] and [MainCoroutineDispatcher.immediate] */
29     @Test
30     fun testMainDispatcherToString() {
31         assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
32         assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
33     }
34 
35     /** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier,
36      * even if the immediate dispatcher was entered from the main thread. */
37     @Test
38     fun testMainDispatcherOrderingInMainThread() = runTestOrSkip {
39         withContext(Dispatchers.Main) {
40             testMainDispatcherOrdering()
41         }
42     }
43 
44     /** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier
45      * if the immediate dispatcher was entered from outside the main thread. */
46     @Test
47     fun testMainDispatcherOrderingOutsideMainThread() = runTestOrSkip {
48         testMainDispatcherOrdering()
49     }
50 
51     /** Tests that [Dispatchers.Main] and its [MainCoroutineDispatcher.immediate] are treated as different values. */
52     @Test
53     fun testHandlerDispatcherNotEqualToImmediate() {
54         assertNotEquals(Dispatchers.Main, Dispatchers.Main.immediate)
55     }
56 
57     /** Tests that [Dispatchers.Main] shares its queue with [MainCoroutineDispatcher.immediate]. */
58     @Test
59     fun testImmediateDispatcherYield() = runTestOrSkip {
60         withContext(Dispatchers.Main) {
61             expect(1)
62             checkIsMainThread()
63             // launch in the immediate dispatcher
64             launch(Dispatchers.Main.immediate) {
65                 expect(2)
66                 yield()
67                 expect(4)
68             }
69             expect(3) // after yield
70             yield() // yield back
71             expect(5)
72         }
73         finish(6)
74     }
75 
76     /** Tests that entering [MainCoroutineDispatcher.immediate] from [Dispatchers.Main] happens immediately. */
77     @Test
78     fun testEnteringImmediateFromMain() = runTestOrSkip {
79         withContext(Dispatchers.Main) {
80             expect(1)
81             val job = launch { expect(3) }
82             withContext(Dispatchers.Main.immediate) {
83                 expect(2)
84             }
85             job.join()
86         }
87         finish(4)
88     }
89 
90     /** Tests that dispatching to [MainCoroutineDispatcher.immediate] is required from and only from dispatchers
91      * other than the main dispatchers and that it's always required for [Dispatchers.Main] itself. */
92     @Test
93     fun testDispatchRequirements() = runTestOrSkip {
94         checkDispatchRequirements()
95         withContext(Dispatchers.Main) {
96             checkDispatchRequirements()
97             withContext(Dispatchers.Main.immediate) {
98                 checkDispatchRequirements()
99             }
100             checkDispatchRequirements()
101         }
102         checkDispatchRequirements()
103     }
104 
105     private suspend fun checkDispatchRequirements() {
106         isMainThread()?.let {
107             assertNotEquals(
108                 it,
109                 Dispatchers.Main.immediate.isDispatchNeeded(currentCoroutineContext())
110             )
111         }
112         assertTrue(Dispatchers.Main.isDispatchNeeded(currentCoroutineContext()))
113         assertTrue(Dispatchers.Default.isDispatchNeeded(currentCoroutineContext()))
114     }
115 
116     /** Tests that launching a coroutine in [MainScope] will execute it in the main thread. */
117     @Test
118     fun testLaunchInMainScope() = runTestOrSkip {
119         var executed = false
120         withMainScope {
121             launch {
122                 checkIsMainThread()
123                 executed = true
124             }.join()
125             if (!executed) throw AssertionError("Should be executed")
126         }
127     }
128 
129     /** Tests that a failure in [MainScope] will not propagate upwards. */
130     @Test
131     fun testFailureInMainScope() = runTestOrSkip {
132         var exception: Throwable? = null
133         withMainScope {
134             launch(CoroutineExceptionHandler { ctx, e -> exception = e }) {
135                 checkIsMainThread()
136                 throw TestException()
137             }.join()
138         }
139         if (exception!! !is TestException) throw AssertionError("Expected TestException, but had $exception")
140     }
141 
142     /** Tests cancellation in [MainScope]. */
143     @Test
144     fun testCancellationInMainScope() = runTestOrSkip {
145         withMainScope {
146             cancel()
147             launch(start = CoroutineStart.ATOMIC) {
148                 checkIsMainThread()
149                 delay(Long.MAX_VALUE)
150             }.join()
151         }
152     }
153 
154     private suspend fun <R> withMainScope(block: suspend CoroutineScope.() -> R): R {
155         MainScope().apply {
156             return block().also { coroutineContext[Job]!!.cancelAndJoin() }
157         }
158     }
159 
160     private suspend fun testMainDispatcherOrdering() {
161         withContext(Dispatchers.Main.immediate) {
162             expect(1)
163             launch(Dispatchers.Main) {
164                 expect(2)
165             }
166             withContext(Dispatchers.Main) {
167                 finish(3)
168             }
169         }
170     }
171 
172     abstract class WithRealTimeDelay : MainDispatcherTestBase() {
173         abstract fun scheduleOnMainQueue(block: () -> Unit)
174 
175         /** Tests that after a delay, the execution gets back to the main thread. */
176         @Test
177         fun testDelay() = runTestOrSkip {
178             expect(1)
179             checkNotMainThread()
180             scheduleOnMainQueue { expect(2) }
181             withContext(Dispatchers.Main) {
182                 checkIsMainThread()
183                 expect(3)
184                 scheduleOnMainQueue { expect(4) }
185                 delay(100)
186                 checkIsMainThread()
187                 expect(5)
188             }
189             checkNotMainThread()
190             finish(6)
191         }
192 
193         /** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much slower. */
194         @Test
195         fun testWithTimeoutContextDelayNoTimeout() = runTestOrSkip {
196             expect(1)
197             withTimeout(1000) {
198                 withContext(Dispatchers.Main) {
199                     checkIsMainThread()
200                     expect(2)
201                     delay(100)
202                     checkIsMainThread()
203                     expect(3)
204                 }
205             }
206             checkNotMainThread()
207             finish(4)
208         }
209 
210         /** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much faster. */
211         @Test
212         fun testWithTimeoutContextDelayTimeout() = runTestOrSkip {
213             expect(1)
214             assertFailsWith<TimeoutCancellationException> {
215                 withTimeout(300) {
216                     // A substitute for withContext(Dispatcher.Main) that is started even if the 300ms
217                     // timeout happens fsater then dispatch
218                     launch(Dispatchers.Main, start = CoroutineStart.ATOMIC) {
219                         checkIsMainThread()
220                         expect(2)
221                         delay(1000)
222                         expectUnreached()
223                     }.join()
224                 }
225                 expectUnreached()
226             }
227             checkNotMainThread()
228             finish(3)
229         }
230 
231         /** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much faster. */
232         @Test
233         fun testWithContextTimeoutDelayNoTimeout() = runTestOrSkip {
234             expect(1)
235             withContext(Dispatchers.Main) {
236                 withTimeout(1000) {
237                     checkIsMainThread()
238                     expect(2)
239                     delay(100)
240                     checkIsMainThread()
241                     expect(3)
242                 }
243             }
244             checkNotMainThread()
245             finish(4)
246         }
247 
248         /** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much slower. */
249         @Test
250         fun testWithContextTimeoutDelayTimeout() = runTestOrSkip {
251             expect(1)
252             assertFailsWith<TimeoutCancellationException> {
253                 withContext(Dispatchers.Main) {
254                     withTimeout(100) {
255                         checkIsMainThread()
256                         expect(2)
257                         delay(1000)
258                         expectUnreached()
259                     }
260                 }
261                 expectUnreached()
262             }
263             checkNotMainThread()
264             finish(3)
265         }
266     }
267 
268     fun checkIsMainThread() { isMainThread()?.let { check(it) } }
269     fun checkNotMainThread() { isMainThread()?.let { check(!it) } }
270 }
271