Skip to content

Commit

Permalink
Merge branch 'master' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
qwwdfsad committed Apr 4, 2022
2 parents a5dd74b + 0d26d6c commit 262876b
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 52 deletions.
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Version 1.6.0

Note that this is a full changelog relative to 1.5.2 version. Changelog relative to 1.6.0-RC3 can be found in the end.
Note that this is a full changelog relative to the 1.5.2 version. Changelog relative to 1.6.0-RC3 can be found at the end.

### kotlinx-coroutines-test rework

Expand All @@ -25,6 +25,7 @@ Note that this is a full changelog relative to 1.5.2 version. Changelog relative
* `Dispatchers.Main` is now used as the default source of time for `delay` and `withTimeout` when present(#2972).
* To opt-out from this behaviour, `kotlinx.coroutines.main.delay` system property can be set to `false`.
* Java target of coroutines build is now 8 instead of 6 (#1589).
* **Source-breaking change**: extension `collect` no longer resolves when used with a non-in-place argument of a functional type. This is a candidate for a fix, uncovered after 1.6.0, see #3107 for the additional details.

### Bug fixes and improvements

Expand Down
48 changes: 17 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,55 +99,39 @@ And make sure that you use the latest Kotlin version:

Add dependencies (you can also add other modules that you need):

```groovy
```kotlin
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
}
```

And make sure that you use the latest Kotlin version:

```groovy
buildscript {
ext.kotlin_version = '1.6.0'
```kotlin
plugins {
// For build.gradle.kts (Kotlin DSL)
kotlin("jvm") version "1.6.0"

// For build.gradle (Groovy DSL)
id "org.jetbrains.kotlin.jvm" version "1.6.0"
}
```

Make sure that you have `mavenCentral()` in the list of repositories:

```
repository {
```kotlin
repositories {
mavenCentral()
}
```

### Gradle Kotlin DSL

Add dependencies (you can also add other modules that you need):

```groovy
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
}
```

And make sure that you use the latest Kotlin version:

```groovy
plugins {
kotlin("jvm") version "1.5.30"
}
```

Make sure that you have `mavenCentral()` in the list of repositories.

### Android

Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android)
module as a dependency when using `kotlinx.coroutines` on Android:

```groovy
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
```kotlin
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
```

This gives you access to the Android [Dispatchers.Main]
Expand All @@ -165,7 +149,8 @@ For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-
The `kotlinx-coroutines-core` artifact contains a resource file that is not required for the coroutines to operate
normally and is only used by the debugger. To exclude it at no loss of functionality, add the following snippet to the
`android` block in your Gradle file for the application subproject:
```groovy

```kotlin
packagingOptions {
resources.excludes += "DebugProbesKt.bin"
}
Expand All @@ -177,7 +162,8 @@ Core modules of `kotlinx.coroutines` are also available for
[Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) and [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html).

In common code that should get compiled for different platforms, you can add a dependency to `kotlinx-coroutines-core` right to the `commonMain` source set:
```groovy

```kotlin
commonMain {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
Expand Down
2 changes: 1 addition & 1 deletion docs/topics/coroutine-context-and-dispatchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ However, this parent-child relation can be explicitly overriden in one of two wa

1. When a different scope is explicitly specified when launching a coroutine (for example, `GlobalScope.launch`),
then it does not inherit a `Job` from the parent scope.
2. When a different `Job` object is passed as the context for the new coroutine (as show in the example below),
2. When a different `Job` object is passed as the context for the new coroutine (as shown in the example below),
then it overrides the `Job` of the parent scope.

In both cases, the launched coroutine is not tied to the scope it was launched from and operates independently.
Expand Down
4 changes: 2 additions & 2 deletions kotlinx-coroutines-core/common/src/Await.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public suspend fun <T> awaitAll(vararg deferreds: Deferred<T>): List<T> =
* when all deferred computations are complete or resumes with the first thrown exception if any of computations
* complete exceptionally including cancellation.
*
* This function is **not** equivalent to `this.map { it.await() }` which fails only when when it sequentially
* gets to wait the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
* This function is **not** equivalent to `this.map { it.await() }` which fails only when it sequentially
* gets to wait for the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public val CoroutineScope.isActive: Boolean
* ```
* // concurrently load configuration and data
* suspend fun loadConfigurationAndData() {
* coroutinesScope {
* coroutineScope {
* launch { loadConfiguration() }
* launch { loadData() }
* }
Expand Down
12 changes: 6 additions & 6 deletions kotlinx-coroutines-core/common/src/Dispatchers.common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public expect object Dispatchers {
*
* Access to this property may throw an [IllegalStateException] if no main dispatchers are present in the classpath.
*
* Depending on platform and classpath it can be mapped to different dispatchers:
* Depending on platform and classpath, it can be mapped to different dispatchers:
* - On JS and Native it is equivalent to the [Default] dispatcher.
* - On JVM it either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the
* - On JVM it is either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the
* [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
*
* In order to work with the `Main` dispatcher, the following artifact should be added to the project runtime dependencies:
Expand All @@ -48,7 +48,7 @@ public expect object Dispatchers {
* stack overflows.
*
* ### Event loop
* Event loop semantics is a purely internal concept and have no guarantees on the order of execution
* Event loop semantics is a purely internal concept and has no guarantees on the order of execution
* except that all queued coroutines will be executed on the current thread in the lexical scope of the outermost
* unconfined coroutine.
*
Expand All @@ -63,11 +63,11 @@ public expect object Dispatchers {
* }
* println("Done")
* ```
* Can print both "1 2 3" and "1 3 2", this is an implementation detail that can be changed.
* But it is guaranteed that "Done" will be printed only when both `withContext` calls are completed.
* Can print both "1 2 3" and "1 3 2". This is an implementation detail that can be changed.
* However, it is guaranteed that "Done" will be printed only when both `withContext` calls are completed.
*
* If you need your coroutine to be confined to a particular thread or a thread-pool after resumption,
* but still want to execute it in the current call-frame until its first suspension, then you can use
* but still want to execute it in the current call-frame until its first suspension, you can use
* an optional [CoroutineStart] parameter in coroutine builders like
* [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to
* the value of [CoroutineStart.UNDISPATCHED].
Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/src/channels/Channel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ public interface ChannelIterator<out E> {
* exception which is either rethrown from the caller method or handed off to the exception handler in the current context
* (see [CoroutineExceptionHandler]) when one is available.
*
* A typical usage for `onDeliveredElement` is to close a resource that is being transferred via the channel. The
* A typical usage for `onUndeliveredElement` is to close a resource that is being transferred via the channel. The
* following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel
* are cancelled. Resources are never lost.
*
Expand Down
138 changes: 130 additions & 8 deletions kotlinx-coroutines-test/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,140 @@ No action on your part is required, other than replacing `runBlocking` with `run

By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
variant of `TestCoroutineDispatcher` should be used.
This version of the dispatcher, which can be observed has the property of eagerly entering `launch` and `async` blocks:
This version of the dispatcher has the property of eagerly entering `launch` and `async` blocks:
code until the first suspension is executed without dispatching.

We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
blocks, but *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
There are two common ways in which this property is useful.

#### `TestCoroutineDispatcher` for the top-level coroutine

Some tests that rely on `launch` and `async` blocks being entered immediately have a form similar to this:
```kotlin
runTest(TestCoroutineDispatcher()) {
launch {
updateSomething()
}
checkThatSomethingWasUpdated()
launch {
updateSomethingElse()
}
checkThatSomethingElseWasUpdated()
}
```

If the `TestCoroutineDispatcher()` is simply removed, `StandardTestDispatcher()` will be used, which will cause
the test to fail.

In these cases, `UnconfinedTestDispatcher()` should be used.
We ensured that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
blocks.

Note though that *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
any guarantees about their dispatching order.

So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it
will need to be tweaked.
If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
#### `TestCoroutineDispatcher` for testing intermediate emissions

Some code tests `StateFlow` or channels in a manner similar to this:

```kotlin
@Test
fun testAllEmissions() = runTest(TestCoroutineDispatcher()) {
val values = mutableListOf<Int>()
val stateFlow = MutableStateFlow(0)
val job = launch {
stateFlow.collect {
values.add(it)
}
}
stateFlow.value = 1
stateFlow.value = 2
stateFlow.value = 3
job.cancel()
// each assignment will immediately resume the collecting child coroutine,
// so no values will be skipped.
assertEquals(listOf(0, 1, 2, 3), values)
}
```

Such code will fail when `TestCoroutineDispatcher()` is not used: not every emission will be listed.
In this particular case, none will be listed at all.

The reason for this is that setting `stateFlow.value` (as is sending to a channel, as are some other things) wakes up
the coroutine waiting for the new value, but *typically* does not immediately run the collecting code, instead simply
dispatching it.
The exceptions are the coroutines running in dispatchers that don't (always) go through a dispatch,
`Dispatchers.Unconfined`, `Dispatchers.Main.immediate`, `UnconfinedTestDispatcher`, or `TestCoroutineDispatcher` in
the unpaused state.

Therefore, a solution is to launch the collection in an unconfined dispatcher:

```kotlin
@Test
fun testAllEmissions() = runTest {
val values = mutableListOf<Int>()
val stateFlow = MutableStateFlow(0)
val job = launch(UnconfinedTestDispatcher(testScheduler)) { // <------
stateFlow.collect {
values.add(it)
}
}
stateFlow.value = 1
stateFlow.value = 2
stateFlow.value = 3
job.cancel()
// each assignment will immediately resume the collecting child coroutine,
// so no values will be skipped.
assertEquals(listOf(0, 1, 2, 3), values)
}
```

Note that `testScheduler` is passed so that the unconfined dispatcher is linked to `runTest`.
Also, note that `UnconfinedTestDispatcher` is not passed to `runTest`.
This is due to the fact that, *inside* the `UnconfinedTestDispatcher`, there are no execution order guarantees,
so it would not be guaranteed that setting `stateFlow.value` would immediately run the collecting code
(though in this case, it does).

#### Other considerations

Using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
did, but it's still possible that the test relies on the specific dispatching order of `TestCoroutineDispatcher`,
so it will need to be tweaked.

If some code is expected to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
at this moment of time to run.
For example, the `StateFlow` example above can also be forced to succeed by doing this:

```kotlin
@Test
fun testAllEmissions() = runTest {
val values = mutableListOf<Int>()
val stateFlow = MutableStateFlow(0)
val job = launch {
stateFlow.collect {
values.add(it)
}
}
runCurrent()
stateFlow.value = 1
runCurrent()
stateFlow.value = 2
runCurrent()
stateFlow.value = 3
runCurrent()
job.cancel()
// each assignment will immediately resume the collecting child coroutine,
// so no values will be skipped.
assertEquals(listOf(0, 1, 2, 3), values)
}
```

Be wary though of this approach: using `runCurrent`, `advanceTimeBy`, or `advanceUntilIdle` is, essentially,
simulating some particular execution order, which is not guaranteed to happen in production code.
For example, using `UnconfinedTestDispatcher` to fix this test reflects how, in production code, one could use
`Dispatchers.Unconfined` to observe all emitted values without conflation, but the `runCurrent()` approach only
states that the behavior would be observed if a dispatch were to happen at some chosen points.
It is, therefore, recommended to structure tests in a way that does not rely on a particular interleaving, unless
that is the intention.

### The job hierarchy is completely different.

Expand Down Expand Up @@ -322,4 +444,4 @@ fun testFoo() = runTest {
```

The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
the current `Dispatchers.Main`.
the current `Dispatchers.Main`.
2 changes: 1 addition & 1 deletion kotlinx-coroutines-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This package provides utilities for efficiently testing coroutines.
| [runTest] | Runs the test code, automatically skipping delays and handling uncaught exceptions. |
| [TestCoroutineScheduler] | The shared source of virtual time, used for controlling execution order and skipping delays. |
| [TestScope] | A [CoroutineScope] that integrates with [runTest], providing access to [TestCoroutineScheduler]. |
| [TestDispatcher] | A [CoroutineDispatcher] that whose delays are controlled by a [TestCoroutineScheduler]. |
| [TestDispatcher] | A [CoroutineDispatcher] whose delays are controlled by a [TestCoroutineScheduler]. |
| [Dispatchers.setMain] | Mocks the main dispatcher using the provided one. If mocked with a [TestDispatcher], its [TestCoroutineScheduler] is used everywhere by default. |

Provided [TestDispatcher] implementations:
Expand Down
5 changes: 5 additions & 0 deletions kotlinx-coroutines-test/common/src/TestDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import kotlin.jvm.*

/**
* A test dispatcher that can interface with a [TestCoroutineScheduler].
*
* The available implementations are:
* * [StandardTestDispatcher] is a dispatcher that places new tasks into a queue.
* * [UnconfinedTestDispatcher] is a dispatcher that behaves like [Dispatchers.Unconfined] while allowing to control
* the virtual time.
*/
@ExperimentalCoroutinesApi
public abstract class TestDispatcher internal constructor(): CoroutineDispatcher(), Delay {
Expand Down

0 comments on commit 262876b

Please sign in to comment.