-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
3ceb35d
commit 7f32340
Showing
9 changed files
with
336 additions
and
274 deletions.
There are no files selected for viewing
266 changes: 266 additions & 0 deletions
266
kotlinx-coroutines-core/common/test/MainDispatcherTestBase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/* | ||
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines | ||
|
||
import kotlin.test.* | ||
|
||
abstract class MainDispatcherTestBase: TestBase() { | ||
|
||
open fun shouldSkipTesting(): Boolean = false | ||
|
||
open suspend fun spinTest(testBody: Job) { | ||
testBody.join() | ||
} | ||
|
||
abstract fun isMainThread(): Boolean? | ||
|
||
/** Runs the given block as a test, unless [shouldSkipTesting] indicates that the environment is not suitable. */ | ||
fun runTestOrSkip(block: suspend CoroutineScope.() -> Unit): TestResult { | ||
// written as a block body to make the need to return `TestResult` explicit | ||
return runTest { | ||
if (shouldSkipTesting()) return@runTest | ||
val testBody = launch(Dispatchers.Default) { | ||
block() | ||
} | ||
spinTest(testBody) | ||
} | ||
} | ||
|
||
/** Tests the [toString] behavior of [Dispatchers.Main] and [MainCoroutineDispatcher.immediate] */ | ||
@Test | ||
fun testMainDispatcherToString() { | ||
assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) | ||
assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) | ||
} | ||
|
||
/** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier, | ||
* even if the immediate dispatcher was entered from the main thread. */ | ||
@Test | ||
fun testMainDispatcherOrderingInMainThread() = runTestOrSkip { | ||
withContext(Dispatchers.Main) { | ||
testMainDispatcherOrdering() | ||
} | ||
} | ||
|
||
/** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier | ||
* if the immediate dispatcher was entered from outside the main thread. */ | ||
@Test | ||
fun testMainDispatcherOrderingOutsideMainThread() = runTestOrSkip { | ||
testMainDispatcherOrdering() | ||
} | ||
|
||
/** Tests that [Dispatchers.Main] and its [MainCoroutineDispatcher.immediate] are treated as different values. */ | ||
@Test | ||
fun testHandlerDispatcherNotEqualToImmediate() { | ||
assertNotEquals(Dispatchers.Main, Dispatchers.Main.immediate) | ||
} | ||
|
||
/** Tests that [Dispatchers.Main] shares its queue with [MainCoroutineDispatcher.immediate]. */ | ||
@Test | ||
fun testImmediateDispatcherYield() = runTestOrSkip { | ||
withContext(Dispatchers.Main) { | ||
expect(1) | ||
checkIsMainThread() | ||
// launch in the immediate dispatcher | ||
launch(Dispatchers.Main.immediate) { | ||
expect(2) | ||
yield() | ||
expect(4) | ||
} | ||
expect(3) // after yield | ||
yield() // yield back | ||
expect(5) | ||
} | ||
finish(6) | ||
} | ||
|
||
/** Tests that entering [MainCoroutineDispatcher.immediate] from [Dispatchers.Main] happens immediately. */ | ||
@Test | ||
fun testEnteringImmediateFromMain() = runTestOrSkip { | ||
withContext(Dispatchers.Main) { | ||
expect(1) | ||
val job = launch { expect(3) } | ||
withContext(Dispatchers.Main.immediate) { | ||
expect(2) | ||
} | ||
job.join() | ||
} | ||
finish(4) | ||
} | ||
|
||
/** Tests that dispatching to [MainCoroutineDispatcher.immediate] is required from and only from dispatchers | ||
* other than the main dispatchers and that it's always required for [Dispatchers.Main] itself. */ | ||
@Test | ||
fun testDispatchRequirements() = runTestOrSkip { | ||
checkDispatchRequirements() | ||
withContext(Dispatchers.Main) { | ||
checkDispatchRequirements() | ||
withContext(Dispatchers.Main.immediate) { | ||
checkDispatchRequirements() | ||
} | ||
checkDispatchRequirements() | ||
} | ||
checkDispatchRequirements() | ||
} | ||
|
||
private suspend fun checkDispatchRequirements() { | ||
isMainThread()?.let { assertNotEquals(it, Dispatchers.Main.immediate.isDispatchNeeded(currentCoroutineContext())) } | ||
assertTrue(Dispatchers.Main.isDispatchNeeded(currentCoroutineContext())) | ||
assertTrue(Dispatchers.Default.isDispatchNeeded(currentCoroutineContext())) | ||
} | ||
|
||
/** Tests that launching a coroutine in [MainScope] will execute it in the main thread. */ | ||
@Test | ||
fun testLaunchInMainScope() = runTestOrSkip { | ||
var executed = false | ||
withMainScope { | ||
launch { | ||
checkIsMainThread() | ||
executed = true | ||
}.join() | ||
if (!executed) throw AssertionError("Should be executed") | ||
} | ||
} | ||
|
||
/** Tests that a failure in [MainScope] will not propagate upwards. */ | ||
@Test | ||
fun testFailureInMainScope() = runTestOrSkip { | ||
var exception: Throwable? = null | ||
withMainScope { | ||
launch(CoroutineExceptionHandler { ctx, e -> exception = e }) { | ||
checkIsMainThread() | ||
throw TestException() | ||
}.join() | ||
} | ||
if (exception!! !is TestException) throw AssertionError("Expected TestException, but had $exception") | ||
} | ||
|
||
/** Tests cancellation in [MainScope]. */ | ||
@Test | ||
fun testCancellationInMainScope() = runTestOrSkip { | ||
withMainScope { | ||
cancel() | ||
launch(start = CoroutineStart.ATOMIC) { | ||
checkIsMainThread() | ||
delay(Long.MAX_VALUE) | ||
}.join() | ||
} | ||
} | ||
|
||
private suspend fun <R> withMainScope(block: suspend CoroutineScope.() -> R): R { | ||
MainScope().apply { | ||
return block().also { coroutineContext[Job]!!.cancelAndJoin() } | ||
} | ||
} | ||
|
||
private suspend fun testMainDispatcherOrdering() { | ||
withContext(Dispatchers.Main.immediate) { | ||
expect(1) | ||
launch(Dispatchers.Main) { | ||
expect(2) | ||
} | ||
withContext(Dispatchers.Main) { | ||
finish(3) | ||
} | ||
} | ||
} | ||
|
||
abstract class WithRealTimeDelay : MainDispatcherTestBase() { | ||
abstract fun scheduleOnMainQueue(block: () -> Unit) | ||
|
||
/** Tests that after a delay, the execution gets back to the main thread. */ | ||
@Test | ||
fun testDelay() = runTestOrSkip { | ||
expect(1) | ||
checkNotMainThread() | ||
scheduleOnMainQueue { expect(2) } | ||
withContext(Dispatchers.Main) { | ||
checkIsMainThread() | ||
expect(3) | ||
scheduleOnMainQueue { expect(4) } | ||
delay(100) | ||
checkIsMainThread() | ||
expect(5) | ||
} | ||
checkNotMainThread() | ||
finish(6) | ||
} | ||
|
||
/** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much slower. */ | ||
@Test | ||
fun testWithTimeoutContextDelayNoTimeout() = runTestOrSkip { | ||
expect(1) | ||
withTimeout(1000) { | ||
withContext(Dispatchers.Main) { | ||
checkIsMainThread() | ||
expect(2) | ||
delay(100) | ||
checkIsMainThread() | ||
expect(3) | ||
} | ||
} | ||
checkNotMainThread() | ||
finish(4) | ||
} | ||
|
||
/** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much faster. */ | ||
@Test | ||
fun testWithTimeoutContextDelayTimeout() = runTestOrSkip { | ||
expect(1) | ||
assertFailsWith<TimeoutCancellationException> { | ||
withTimeout(300) { | ||
withContext(Dispatchers.Main) { | ||
checkIsMainThread() | ||
expect(2) | ||
delay(1000) | ||
expectUnreached() | ||
} | ||
} | ||
expectUnreached() | ||
} | ||
checkNotMainThread() | ||
finish(3) | ||
} | ||
|
||
/** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much faster. */ | ||
@Test | ||
fun testWithContextTimeoutDelayNoTimeout() = runTestOrSkip { | ||
expect(1) | ||
withContext(Dispatchers.Main) { | ||
withTimeout(1000) { | ||
checkIsMainThread() | ||
expect(2) | ||
delay(100) | ||
checkIsMainThread() | ||
expect(3) | ||
} | ||
} | ||
checkNotMainThread() | ||
finish(4) | ||
} | ||
|
||
/** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much slower. */ | ||
@Test | ||
fun testWithContextTimeoutDelayTimeout() = runTestOrSkip { | ||
expect(1) | ||
assertFailsWith<TimeoutCancellationException> { | ||
withContext(Dispatchers.Main) { | ||
withTimeout(100) { | ||
checkIsMainThread() | ||
expect(2) | ||
delay(1000) | ||
expectUnreached() | ||
} | ||
} | ||
expectUnreached() | ||
} | ||
checkNotMainThread() | ||
finish(3) | ||
} | ||
} | ||
|
||
fun checkIsMainThread() { isMainThread()?.let { check(it) } } | ||
fun checkNotMainThread() { isMainThread()?.let { check(!it) } } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.