Skip to content

Commit 7f32340

Browse files
authored
Ensure Dispatchers.Main != Dispatchers.Main.immediate on Android (#3924)
* Simplify some code * Unify and generalize the tests for all Main dispatchers * Cleanup build configuration in JavaFx * Allow using kotlin.test from tests in the core module from JavaFx Fixes #3545
1 parent 3ceb35d commit 7f32340

File tree

9 files changed

+336
-274
lines changed

9 files changed

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

kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44

55
package kotlinx.coroutines
66

7+
import kotlin.coroutines.*
78
import kotlin.test.*
89

9-
class ImmediateDispatcherTest : TestBase() {
10+
class ImmediateDispatcherTest : MainDispatcherTestBase.WithRealTimeDelay() {
1011

12+
/** Tests that [MainCoroutineDispatcher.immediate] doesn't require dispatches from the test context. */
1113
@Test
1214
fun testImmediate() = runTest {
1315
expect(1)
1416
val job = launch { expect(3) }
17+
assertFalse(Dispatchers.Main.immediate.isDispatchNeeded(currentCoroutineContext()))
1518
withContext(Dispatchers.Main.immediate) {
1619
expect(2)
1720
}
@@ -29,4 +32,10 @@ class ImmediateDispatcherTest : TestBase() {
2932
job.join()
3033
finish(4)
3134
}
35+
36+
override fun isMainThread(): Boolean? = null
37+
38+
override fun scheduleOnMainQueue(block: () -> Unit) {
39+
Dispatchers.Default.dispatch(EmptyCoroutineContext, Runnable { block() })
40+
}
3241
}

kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private class DarwinMainDispatcher(
6868
}
6969

7070
override fun toString(): String =
71-
"MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }"
71+
if (invokeImmediately) "Dispatchers.Main.immediate" else "Dispatchers.Main"
7272
}
7373

7474
private typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit

0 commit comments

Comments
 (0)