Skip to content

Commit 89db4c2

Browse files
committed
Introduce coroutines-core-test module with API to inject custom Dispatchers.Main
Fixes #746
1 parent 3c4168b commit 89db4c2

File tree

12 files changed

+174
-6
lines changed

12 files changed

+174
-6
lines changed

binary-compatibility-validator/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
1414

1515
testArtifacts project(':kotlinx-coroutines-core')
16+
testArtifacts project(':kotlinx-coroutines-core-test')
1617

1718
testArtifacts project(':kotlinx-coroutines-reactive')
1819
testArtifacts project(':kotlinx-coroutines-reactor')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public final class kotlinx/coroutines/test/MainDispatcherInjector {
2+
public static final field INSTANCE Lkotlinx/coroutines/test/MainDispatcherInjector;
3+
public final fun inject (Lkotlinx/coroutines/CoroutineDispatcher;)V
4+
public final fun reset ()V
5+
}
6+

binary-compatibility-validator/resources/api.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
#
44

5-
module.roots=core integration native reactive ui
5+
module.roots=core integration native reactive ui core-test
66
module.marker=build.gradle
77
module.ignore=kotlinx-coroutines-rx-example stdlib-stubs
88

core/kotlinx-coroutines-core-test/build.gradle

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ServiceLoader support
2+
-keepnames class kotlinx.coroutines.test.internal.InjectableDispatcherFactory {}
3+
4+
# Most of volatile fields are updated with AFU and should not be mangled
5+
-keepclassmembernames class kotlinx.** {
6+
volatile <fields>;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kotlinx.coroutines.test.internal.InjectableDispatcherFactory
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package kotlinx.coroutines.test
5+
6+
import kotlinx.coroutines.*
7+
import kotlinx.coroutines.test.internal.*
8+
9+
/**
10+
* Object, responsible for injecting custom implementation of [Dispatchers.Main].
11+
* The main purpose of this class is to override default Main dispatcher in unit tests where main thread is inaccessible.
12+
* By default, [Dispatchers.Unconfined] is used.
13+
*
14+
* If this class is present in the classpath, it **overrides** default [Dispatchers.Main] implementation with its own implementation,
15+
* making usages of the original [Dispatchers.Main] impossible.
16+
*/
17+
@ExperimentalCoroutinesApi
18+
public object MainDispatcherInjector {
19+
20+
private val mainDispatcher = (Dispatchers.Main as? InjectableMainDispatcher)
21+
?: error("Injectable dispatcher is not found in class path (${Dispatchers.Main} was found instead). " +
22+
"Please check your META-INF/services and ProGuard settings")
23+
24+
/**
25+
* Injects given dispatcher as underlying dispatcher of [Dispatchers.Main].
26+
* All consecutive usages of [Dispatchers.Main] will use [dispatcher] under the hood, though it's not guaranteed
27+
* that [Dispatchers.Main] will be equal to given [dispatcher].
28+
*
29+
* Note that it is unsafe to call this method from coroutine, launched in [Dispatchers.Main].
30+
* Such usage may lead to undefined behaviour and [IllegalStateException].
31+
*/
32+
public fun inject(dispatcher: CoroutineDispatcher) {
33+
mainDispatcher.inject(dispatcher)
34+
}
35+
36+
/**
37+
* Resets state of [Dispatchers.Main] to [Dispatchers.Unconfined].
38+
* Used to cleanup all possible dependencies, should be used in tear down (`@After`) methods.
39+
*/
40+
public fun reset() = inject(Dispatchers.Unconfined)
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.test.internal
6+
7+
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.internal.*
9+
import kotlin.coroutines.*
10+
11+
internal class InjectableDispatcherFactory : MainDispatcherFactory {
12+
13+
override fun createDispatcher(): MainCoroutineDispatcher = InjectableMainDispatcher
14+
15+
override val loadPriority: Int
16+
get() = Int.MAX_VALUE
17+
}
18+
19+
internal object InjectableMainDispatcher : MainCoroutineDispatcher() {
20+
21+
private var delegate: CoroutineDispatcher = Dispatchers.Unconfined
22+
23+
public fun inject(dispatcher: CoroutineDispatcher) {
24+
delegate = dispatcher
25+
}
26+
27+
override val immediate: MainCoroutineDispatcher = (delegate as? MainCoroutineDispatcher)?.immediate ?: this
28+
29+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
30+
31+
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package kotlinx.coroutines.test
5+
6+
import kotlinx.coroutines.*
7+
import org.junit.*
8+
import org.junit.Test
9+
import kotlin.coroutines.*
10+
import kotlin.test.*
11+
12+
class MainDispatcherInjectorTest : TestBase() {
13+
14+
@Before
15+
fun setUp() {
16+
MainDispatcherInjector.reset()
17+
}
18+
19+
@Test
20+
fun testInjection() = runTest {
21+
val mainThread = Thread.currentThread()
22+
newSingleThreadContext("testInjection").use { threadPool ->
23+
withContext(Dispatchers.Main) {
24+
assertSame(mainThread, Thread.currentThread())
25+
}
26+
27+
MainDispatcherInjector.inject(threadPool)
28+
withContext(Dispatchers.Main) {
29+
assertNotSame(mainThread, Thread.currentThread())
30+
}
31+
assertSame(mainThread, Thread.currentThread())
32+
33+
withContext(Dispatchers.Main.immediate) {
34+
assertNotSame(mainThread, Thread.currentThread())
35+
}
36+
assertSame(mainThread, Thread.currentThread())
37+
38+
MainDispatcherInjector.inject(Dispatchers.Unconfined)
39+
withContext(Dispatchers.Main.immediate) {
40+
assertSame(mainThread, Thread.currentThread())
41+
}
42+
assertSame(mainThread, Thread.currentThread())
43+
}
44+
}
45+
46+
@Test
47+
fun testImmediateDispatcher() = runTest {
48+
MainDispatcherInjector.inject(ImmediateDispatcher())
49+
expect(1)
50+
withContext(Dispatchers.Main) {
51+
expect(3)
52+
}
53+
54+
MainDispatcherInjector.inject(RegularDispatcher())
55+
withContext(Dispatchers.Main) {
56+
expect(6)
57+
}
58+
59+
finish(7)
60+
}
61+
62+
private inner class ImmediateDispatcher : CoroutineDispatcher() {
63+
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
64+
expect(2)
65+
return false
66+
}
67+
68+
override fun dispatch(context: CoroutineContext, block: Runnable) = expectUnreached()
69+
}
70+
71+
private inner class RegularDispatcher : CoroutineDispatcher() {
72+
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
73+
expect(4)
74+
return true
75+
}
76+
77+
override fun dispatch(context: CoroutineContext, block: Runnable) {
78+
expect(5)
79+
block.run()
80+
}
81+
}
82+
}

core/kotlinx-coroutines-core/resources/META-INF/proguard/coroutines.pro

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ServiceLoader support
22
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
33
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
4+
-keepnames class kotlinx.coroutines.test.internal.InjectableDispatcherFactory {}
45

56
# Most of volatile fields are updated with AFU and should not be mangled
67
-keepclassmembernames class kotlinx.** {

settings.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module('binary-compatibility-validator')
2323
module('common/kotlinx-coroutines-core-common')
2424

2525
module('core/kotlinx-coroutines-core')
26+
module('core/kotlinx-coroutines-core-test')
2627
module('core/stdlib-stubs')
2728

2829
module('integration/kotlinx-coroutines-guava')

ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt

+1-5
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,11 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
3333

3434
@Keep
3535
internal class AndroidDispatcherFactory : MainDispatcherFactory {
36-
companion object {
37-
@JvmStatic // accessed reflectively from core
38-
fun getDispatcher(): MainCoroutineDispatcher = Main
39-
}
4036

4137
override fun createDispatcher(): MainCoroutineDispatcher = Main
4238

4339
override val loadPriority: Int
44-
get() = Int.MAX_VALUE
40+
get() = Int.MAX_VALUE / 2
4541
}
4642

4743
/**

0 commit comments

Comments
 (0)