Skip to content

Introduce test module with testable main dispatcher #749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 17, 2018
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ GlobalScope.launch {
* [core](core/README.md) — Kotlin/JVM implementation of common coroutines with additional features:
* `Dispatchers.IO` dispatcher for blocking coroutines;
* `Executor.asCoroutineDispatcher()` extension, custom thread pools, and more.
* [core-test](core/README.md) — test utilities for coroutines, currently with one feature:
* `MainDispatcherInjector.inject()` to override `Dispatchers.Main` in tests.
* [js](js/README.md) — Kotlin/JS implementation of common coroutines with `Promise` support.
* [native](native/README.md) — Kotlin/Native implementation of common coroutines with `runBlocking` single-threaded event loop.
* [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries:
Expand Down
2 changes: 1 addition & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ To release new `<version>` of `kotlinx-coroutines`:
`git merge origin/master`

4. Search & replace `<old-version>` with `<version>` across the project files. Should replace in:
* [`README.md`](README.md)
* [`README.md`](README.md) (native, core, core-test modules)
* [`coroutines-guide.md`](docs/coroutines-guide.md)
* [`gradle.properties`](gradle.properties)
* [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties)
Expand Down
1 change: 1 addition & 0 deletions binary-compatibility-validator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"

testArtifacts project(':kotlinx-coroutines-core')
testArtifacts project(':kotlinx-coroutines-test')

testArtifacts project(':kotlinx-coroutines-reactive')
testArtifacts project(':kotlinx-coroutines-reactor')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public final class kotlinx/coroutines/test/TestDispatchersKt {
public static final fun resetMain (Lkotlinx/coroutines/Dispatchers;)V
public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V
public static synthetic fun setMain$default (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)V
}

2 changes: 1 addition & 1 deletion binary-compatibility-validator/resources/api.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
#

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

Expand Down
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ configure(subprojects.findAll { !internal.contains(it.name) && it.name != 'kotli

// --------------- Configure sub-projects that are published ---------------

// todo: native is not published yet
def unpublished = internal + ['kotlinx-coroutines-rx-example', 'example-frontend-js']
def unpublished = internal + ['kotlinx-coroutines-rx-example', 'example-frontend-js', 'android-unit-tests']

def core_docs_url = "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/"
def core_docs_file = "$projectDir/core/kotlinx-coroutines-core/build/dokka/kotlinx-coroutines-core/package-list"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ import kotlinx.coroutines.*
public interface MainDispatcherFactory {
val loadPriority: Int // higher priority wins

fun createDispatcher(): MainCoroutineDispatcher
fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher
}
3 changes: 2 additions & 1 deletion core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ Module name below corresponds to the artifact name in Maven/Gradle.

## Modules

* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) -- core coroutine builders and synchronization primitives.
* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) &mdash; core coroutine builders and synchronization primitives.
* [kotlinx-coroutines-core-test](kotlinx-coroutines-core-test/README.md) &mdash; coroutines test utilities such as Main dispatcher injection.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.test.internal.InjectableDispatcherFactory {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
Expand Down
44 changes: 0 additions & 44 deletions core/kotlinx-coroutines-core/src/Dispatchers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,47 +87,3 @@ public actual object Dispatchers {
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

// Lazy loader for the main dispatcher
private object MainDispatcherLoader {
@JvmField
val dispatcher: MainCoroutineDispatcher =
MainDispatcherFactory::class.java.let { clz ->
ServiceLoader.load(clz, clz.classLoader).toList()
}.maxBy { it.loadPriority }?.tryCreateDispatcher() ?: MissingMainCoroutineDispatcher(null)

/**
* If anything goes wrong while trying to create main dispatcher (class not found,
* initialization failed, etc), then replace the main dispatcher with a special
* stub that throws an error message on any attempt to actually use it.
*/
private fun MainDispatcherFactory.tryCreateDispatcher(): MainCoroutineDispatcher =
try {
createDispatcher()
} catch (cause: Throwable) {
MissingMainCoroutineDispatcher(cause)
}
}

private class MissingMainCoroutineDispatcher(val cause: Throwable?) : MainCoroutineDispatcher(), Delay {
override val immediate: MainCoroutineDispatcher get() = this

override fun dispatch(context: CoroutineContext, block: Runnable) =
missing()

override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
missing()

private fun missing() {
if (cause == null) {
throw IllegalStateException(
"Module with the Main dispatcher is missing. " +
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
)
} else {
throw IllegalStateException("Module with the Main dispatcher had failed to initialize", cause)
}
}

override fun toString(): String = "Main[missing${if (cause != null) ", cause=$cause" else ""}]"
}
71 changes: 71 additions & 0 deletions core/kotlinx-coroutines-core/src/internal/MainDispatchers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package kotlinx.coroutines.internal

import kotlinx.coroutines.*
import java.util.*
import kotlin.coroutines.*

// Lazy loader for the main dispatcher
internal object MainDispatcherLoader {
@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = MainDispatcherFactory::class.java.let { clz ->
ServiceLoader.load(clz, clz.classLoader).toList()
}

factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: MissingMainCoroutineDispatcher(null)
} catch (e: Throwable) {
// Catch any initialization errors or ServiceConfigurationError
MissingMainCoroutineDispatcher(e)
}
}
}

/**
* If anything goes wrong while trying to create main dispatcher (class not found,
* initialization failed, etc), then replace the main dispatcher with a special
* stub that throws an error message on any attempt to actually use it.
*/
@InternalCoroutinesApi
public fun MainDispatcherFactory.tryCreateDispatcher(factories: List<MainDispatcherFactory>): MainCoroutineDispatcher =
try {
createDispatcher(factories)
} catch (cause: Throwable) {
MissingMainCoroutineDispatcher(cause)
}

private class MissingMainCoroutineDispatcher(val cause: Throwable?) : MainCoroutineDispatcher(), Delay {
override val immediate: MainCoroutineDispatcher get() = this

override fun dispatch(context: CoroutineContext, block: Runnable) =
missing()

override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
missing()

private fun missing() {
if (cause == null) {
throw IllegalStateException(
"Module with the Main dispatcher is missing. " +
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
)
} else {
throw IllegalStateException("Module with the Main dispatcher had failed to initialize", cause)
}
}

override fun toString(): String = "Main[missing${if (cause != null) ", cause=$cause" else ""}]"
}

@InternalCoroutinesApi
public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory {
override val loadPriority: Int
get() = -1

override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
return MissingMainCoroutineDispatcher(null)
}
}
51 changes: 51 additions & 0 deletions core/kotlinx-coroutines-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Module kotlinx-coroutines-test

Test utilities for `kotlinx.coroutines`. Provides `Dispatchers.setMain` to override `Main` dispatcher.

## Using in your project

Add `kotlinx-coroutines-test` to your project test dependencies:
```
dependencies {
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.1.0'
}
```

**Do not** depend on this project in your main sources, all utilities are intended and designed to be used only from tests.

Once you have it in runtime, [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism will
overwrite [Dispatchers.Main] will testable implementation.

You can override this implementation using [setMain][Dispatchers.setMain] method with any [CoroutineDispatcher] implementation, e.g.:

```kotlin

class SomeTest {

private val mainThreadSurrogate = newSingleThreadContext("UI thread")

@Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
}

@After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to original Main dispatcher
mainThreadSurrogate.close()
}

@Test
fun testSomeUI() = runBlocking {
launch(Dispatchers.Main) {
...
}
}
}
```

<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
<!--- END -->
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ServiceLoader support
-keepnames class kotlinx.coroutines.test.internal.InjectableDispatcherFactory {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlinx.coroutines.test.internal.TestMainDispatcherFactory
36 changes: 36 additions & 0 deletions core/kotlinx-coroutines-test/src/TestDispatchers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("unused")

package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlinx.coroutines.test.internal.*

/**
* Sets given dispatcher as an underlying dispatcher of [Dispatchers.Main].
* All consecutive usages of [Dispatchers.Main] will use given [dispatcher] under the hood, though it's not guaranteed
* that [Dispatchers.Main] will be equal to given [dispatcher].
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
public fun Dispatchers.setMain(dispatcher: CoroutineDispatcher = Dispatchers.Unconfined) {
val mainDispatcher = Dispatchers.Main
require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
mainDispatcher.setDispatcher(dispatcher)

}

/**
* Resets state of [Dispatchers.Main] to the original main dispatcher.
* For example, in Android Main thread dispatcher will be set as [Dispatchers.Main].
* Used to cleanup all possible dependencies, should be used in tear down (`@After`) methods.
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
public fun Dispatchers.resetMain() {
val mainDispatcher = Dispatchers.Main
require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
mainDispatcher.resetDispatcher()
}
71 changes: 71 additions & 0 deletions core/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.test.internal

import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*

/**
* Testable main dispatcher used by kotlinx-coroutines-test.
* It is a [MainCoroutineDispatcher] which delegates all actions to a settable delegate.
*/
// TODO implement delay
internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory) : MainCoroutineDispatcher() {

private var _delegate: CoroutineDispatcher? = null
private val delegate: CoroutineDispatcher get() {
if (_delegate != null) return _delegate!!

val newInstance = createDispatcher()
if (newInstance != null) {
_delegate = newInstance
return newInstance
}

// Return missing dispatcher, but do not set _delegate
return mainFactory.tryCreateDispatcher(emptyList())
}

@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this

override fun dispatch(context: CoroutineContext, block: Runnable) {
delegate.dispatch(context, block)
}

@ExperimentalCoroutinesApi
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)

public fun setDispatcher(dispatcher: CoroutineDispatcher) {
_delegate = dispatcher
}

public fun resetDispatcher() {
_delegate = null
}

private fun createDispatcher(): CoroutineDispatcher? {
return try {
mainFactory.createDispatcher(emptyList())
} catch (cause: Throwable) {
null
}
}
}

internal class TestMainDispatcherFactory : MainDispatcherFactory {

override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
val originalFactory = allFactories.asSequence()
.filter { it !== this }
.maxBy { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory
return TestMainDispatcher(originalFactory)
}

override val loadPriority: Int
get() = Int.MAX_VALUE
}
Loading