diff --git a/.gitignore b/.gitignore index 52843ca5b7..36de0e50da 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ build out target local.properties +/kotlin-js-store diff --git a/CHANGES.md b/CHANGES.md index 611e9c9c74..fb60da3017 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,161 @@ # Change log for kotlinx.coroutines +## Version 1.6.3 + +* Updated atomicfu version to 0.17.3 (#3321), fixing the projects using this library with JS IR failing to build (#3305). + +## Version 1.6.2 + +* Fixed a bug with `ThreadLocalElement` not being correctly updated when the most outer `suspend` function was called directly without `kotlinx.coroutines` (#2930). +* Fixed multiple data races: one that might have been affecting `runBlocking` event loop, and a benign data race in `Mutex` (#3250, #3251). +* Obsolete `TestCoroutineContext` is removed, which fixes the `kotlinx-coroutines-test` JPMS package being split between `kotlinx-coroutines-core` and `kotlinx-coroutines-test` (#3218). +* Updated the ProGuard rules to further shrink the size of the resulting DEX file with coroutines (#3111, #3263). Thanks, @agrieve! +* Atomicfu is updated to `0.17.2`, which includes a more efficient and robust JS IR transformer (#3255). +* Kotlin is updated to `1.6.21`, Gradle version is updated to `7.4.2` (#3281). Thanks, @wojtek-kalicinski! +* Various documentation improvements. + +## Version 1.6.1 + +* Rollback of time-related functions dispatching on `Dispatchers.Main`. + This behavior was introduced in 1.6.0 and then found inconvenient and erroneous (#3106, #3113). +* Reworked the newly-introduced `CopyableThreadContextElement` to solve issues uncovered after the initial release (#3227). +* Fixed a bug with `ThreadLocalElement` not being properly updated in racy scenarios (#2930). +* Reverted eager loading of default `CoroutineExceptionHandler` that triggered ANR on some devices (#3180). +* New API to convert a `CoroutineDispatcher` to a Rx scheduler (#968, #548). Thanks @recheej! +* Fixed a memory leak with the very last element emitted from `flow` builder being retained in memory (#3197). +* Fixed a bug with `limitedParallelism` on K/N with new memory model throwing `ClassCastException` (#3223). +* `CoroutineContext` is added to the exception printed to the default `CoroutineExceptionHandler` to improve debuggability (#3153). +* Static memory consumption of `Dispatchers.Default` was significantly reduced (#3137). +* Updated slf4j version in `kotlinx-coroutines-slf4j` from 1.7.25 to 1.7.32. + +## Version 1.6.0 + +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 + +* `kotlinx-coroutines-test` became a multiplatform library usable from K/JVM, K/JS, and K/N. +* Its API was completely reworked to address long-standing issues with consistency, structured concurrency and correctness (#1203, #1609, #2379, #1749, #1204, #1390, #1222, #1395, #1881, #1910, #1772, #1626, #1742, #2082, #2102, #2405, #2462 + ). +* The old API is deprecated for removal, but the new API is based on the similar concepts ([README](kotlinx-coroutines-test/README.md)), and the migration path is designed to be graceful: [migration guide](kotlinx-coroutines-test/MIGRATION.md). + +### Dispatchers + +* Introduced `CoroutineDispatcher.limitedParallelism` that allows obtaining a view of the original dispatcher with limited parallelism (#2919). +* `Dispatchers.IO.limitedParallelism` usages ignore the bound on the parallelism level of `Dispatchers.IO` itself to avoid starvation (#2943). +* Introduced new `Dispatchers.shutdown` method for containerized environments (#2558). +* `newSingleThreadContext` and `newFixedThreadPoolContext` are promoted to delicate API (#2919). + +### Breaking changes + +* When racing with cancellation, the `future` builder no longer reports unhandled exceptions into the global `CoroutineExceptionHandler`. Thanks @vadimsemenov! (#2774, #2791). +* `Mutex.onLock` is deprecated for removal (#2794). +* `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 + +* Kotlin is updated to 1.6.0. +* Kotlin/Native [new memory model](https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/) is now supported in regular builds of coroutines conditionally depending on whether `kotlin.native.binary.memoryModel` is enabled (#2914). +* Introduced `CopyableThreadContextElement` for mutable context elements shared among multiple coroutines. Thanks @yorickhenning! (#2893). +* `transformWhile`, `awaitClose`, `ProducerScope`, `merge`, `runningFold`, `runingReduce`, and `scan` are promoted to stable API (#2971). +* `SharedFlow.subscriptionCount` no longer conflates incoming updates and gives all subscribers a chance to observe a short-lived subscription (#2488, #2863, #2871). +* `Flow` exception transparency mechanism is improved to be more exception-friendly (#3017, #2860). +* Cancellation from `flat*` operators that leverage multiple coroutines is no longer propagated upstream (#2964). +* `SharedFlow.collect` now returns `Nothing` (#2789, #2502). +* `DisposableHandle` is now `fun interface`, and corresponding inline extension is removed (#2790). +* `FlowCollector` is now `fun interface`, and corresponding inline extension is removed (#3047). +* Deprecation level of all previously deprecated signatures is raised (#3024). +* The version file is shipped with each JAR as a resource (#2941). +* Unhandled exceptions on K/N are passed to the standard library function `processUnhandledException` (#2981). +* A direct executor is used for `Task` callbacks in `kotlinx-coroutines-play-services` (#2990). +* Metadata of coroutines artifacts leverages Gradle platform to have all versions of dependencies aligned (#2865). +* Default `CoroutineExceptionHandler` is loaded eagerly and does not invoke `ServiceLoader` on its exception-handling path (#2552). +* Fixed the R8 rules for `ServiceLoader` optimization (#2880). +* Fixed BlockHound integration false-positives (#2894, #2866, #2937). +* Fixed the exception handler being invoked several times on Android, thanks to @1zaman (#3056). +* `SendChannel.trySendBlocking` is now available on Kotlin/Native (#3064). +* The exception recovery mechanism now uses `ClassValue` when available (#2997). +* JNA is updated to 5.9.0 to support Apple M1 (#3001). +* Obsolete method on internal `Delay` interface is deprecated (#2979). +* Support of deprecated `CommonPool` is removed. +* `@ExperimentalTime` is no longer needed for methods that use `Duration` (#3041). +* JDK 1.6 is no longer required for building the project (#3043). +* New version of Dokka is used, fixing the memory leak when building the coroutines and providing brand new reference visuals (https://kotlin.github.io/kotlinx.coroutines/) (#3051, #3054). + +### Changelog relative to version 1.6.0-RC3 + +* Restored MPP binary compatibility on K/JS and K/N (#3104). +* Fixed Dispatchers.Main not being fully initialized on Android and Swing (#3101). + +## Version 1.6.0-RC3 + +* Fixed the error in 1.6.0-RC2 because of which `Flow.collect` couldn't be called due to the `@InternalCoroutinesApi` annotation (#3082) +* Fixed some R8 warnings introduced in 1.6.0-RC (#3090) +* `TestCoroutineScheduler` now provides a `TimeSource` with its virtual time via the `timeSource` property. Thanks @hfhbd! (#3087) + +## Version 1.6.0-RC2 + +* `@ExperimentalTime` is no longer needed for methods that use `Duration` (#3041). +* `FlowCollector` is now `fun interface`, and corresponding inline extension is removed (#3047). +* Fixed the exception handler being invoked several times on Android, thanks to @1zaman (#3056). +* The deprecated `TestCoroutineScope` is no longer sealed, to simplify migration from it (#3072). +* `runTest` gives more informative errors when it times out waiting for external completion (#3071). +* `SendChannel.trySendBlocking` is now available on Kotlin/Native (#3064). +* Fixed the bug due to which `Dispatchers.Main` was not used for `delay` and `withTimeout` (#3046). +* JDK 1.6 is no longer required for building the project (#3043). +* New version of Dokka is used, fixing the memory leak when building the coroutines and providing brand new reference visuals (https://kotlin.github.io/kotlinx.coroutines/) (#3051, #3054). + +## Version 1.6.0-RC + +### kotlinx-coroutines-test rework + +* `kotlinx-coroutines-test` became a multiplatform library usable from K/JVM, K/JS, and K/N. +* Its API was completely reworked to address long-standing issues with consistency, structured concurrency and correctness (#1203, #1609, #2379, #1749, #1204, #1390, #1222, #1395, #1881, #1910, #1772, #1626, #1742, #2082, #2102, #2405, #2462 + ). +* The old API is deprecated for removal, but the new API is based on the similar concepts ([README](kotlinx-coroutines-test/README.md)), and the migration path is designed to be graceful: [migration guide](kotlinx-coroutines-test/MIGRATION.md) + +### Dispatchers + +* Introduced `CoroutineDispatcher.limitedParallelism` that allows obtaining a view of the original dispatcher with limited parallelism (#2919). +* `Dispatchers.IO.limitedParallelism` usages ignore the bound on the parallelism level of `Dispatchers.IO` itself to avoid starvation (#2943). +* Introduced new `Dispatchers.shutdown` method for containerized environments (#2558). +* `newSingleThreadContext` and `newFixedThreadPoolContext` are promoted to delicate API (#2919). + +### Breaking changes + +* When racing with cancellation, the `future` builder no longer reports unhandled exceptions into the global `CoroutineExceptionHandler`. Thanks @vadimsemenov! (#2774, #2791). +* `Mutex.onLock` is deprecated for removal (#2794). +* `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). + +### Bug fixes and improvements + +* Kotlin is updated to 1.6.0. +* Kotlin/Native [new memory model](https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/) is now supported in regular builds of coroutines conditionally depending on whether `kotlin.native.binary.memoryModel` is enabled (#2914). +* Introduced `CopyableThreadContextElement` for mutable context elements shared among multiple coroutines. Thanks @yorickhenning! (#2893). +* `transformWhile`, `awaitClose`, `ProducerScope`, `merge`, `runningFold`, `runingReduce`, and `scan` are promoted to stable API (#2971). +* `SharedFlow.subscriptionCount` no longer conflates incoming updates and gives all subscribers a chance to observe a short-lived subscription (#2488, #2863, #2871). +* `Flow` exception transparency mechanism is improved to be more exception-friendly (#3017, #2860). +* Cancellation from `flat*` operators that leverage multiple coroutines is no longer propagated upstream (#2964). +* `SharedFlow.collect` now returns `Nothing` (#2789, #2502). +* `DisposableHandle` is now `fun interface`, and corresponding inline extension is removed (#2790). +* Deprecation level of all previously deprecated signatures is raised (#3024). +* The version file is shipped with each JAR as a resource (#2941). +* Unhandled exceptions on K/N are passed to the standard library function `processUnhandledException` (#2981). +* A direct executor is used for `Task` callbacks in `kotlinx-coroutines-play-services` (#2990). +* Metadata of coroutines artifacts leverages Gradle platform to have all versions of dependencies aligned (#2865). +* Default `CoroutineExceptionHandler` is loaded eagerly and does not invoke `ServiceLoader` on its exception-handling path (#2552). +* Fixed the R8 rules for `ServiceLoader` optimization (#2880). +* Fixed BlockHound integration false-positives (#2894, #2866, #2937). +* The exception recovery mechanism now uses `ClassValue` when available (#2997). +* JNA is updated to 5.9.0 to support Apple M1 (#3001). +* Obsolete method on internal `Delay` interface is deprecated (#2979). +* Support of deprecated `CommonPool` is removed. + ## Version 1.5.2 * Kotlin is updated to 1.5.30. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d6e32d878..77b727b4c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,15 +80,12 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad ### Environment requirements * JDK >= 11 referred to by the `JAVA_HOME` environment variable. -* JDK 1.6 referred to by the `JDK_16` environment variable. - It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions. * JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is OK to have `JDK_18` to a non 1.8 JDK (e.g. `JAVA_HOME`) for external contributions. - + For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`): ```shell # This assumes JAVA_HOME is set already to a JDK >= 11 version -export JDK_16="$JAVA_HOME" export JDK_18="$JAVA_HOME" ``` diff --git a/README.md b/README.md index 6a13f07aa3..3b24731d25 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # kotlinx.coroutines -[![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) +[![Kotlin Stable](https://kotl.in/badges/stable.svg)](https://kotlinlang.org/docs/components-stability.html) +[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2/pom) -[![Kotlin](https://img.shields.io/badge/kotlin-1.5.30-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.6.3)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.6.3/pom) +[![Kotlin](https://img.shields.io/badge/kotlin-1.6.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for the Kotlin `1.5.30` release. +This is a companion version for the Kotlin `1.6.0` release. ```kotlin suspend fun main() = coroutineScope { @@ -61,9 +62,9 @@ suspend fun main() = coroutineScope { ## Documentation * Presentations and videos: - * [Introduction to Coroutines](https://www.youtube.com/watch?v=_hfBv0a09Jc) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/introduction-to-coroutines-kotlinconf-2017)) - * [Deep dive into Coroutines](https://www.youtube.com/watch?v=YrrUCSi72E8) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/deep-dive-into-coroutines-on-jvm-kotlinconf-2017)) * [Kotlin Coroutines in Practice](https://www.youtube.com/watch?v=a3agLJQ6vt8) (Roman Elizarov at KotlinConf 2018, [slides](https://www.slideshare.net/elizarov/kotlin-coroutines-in-practice-kotlinconf-2018)) + * [Deep Dive into Coroutines](https://www.youtube.com/watch?v=YrrUCSi72E8) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/deep-dive-into-coroutines-on-jvm-kotlinconf-2017)) + * [History of Structured Concurrency in Coroutines](https://www.youtube.com/watch?v=Mj5P47F6nJg) (Roman Elizarov at Hydra 2019, [slides](https://speakerdeck.com/elizarov/structured-concurrency)) * Guides and manuals: * [Guide to kotlinx.coroutines by example](https://kotlinlang.org/docs/coroutines-guide.html) (**read it first**) * [Guide to UI programming with coroutines](ui/coroutines-guide-ui.md) @@ -83,7 +84,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.5.2 + 1.6.3 ``` @@ -91,7 +92,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.5.30 + 1.6.21 ``` @@ -99,55 +100,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.5.2' + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") } ``` And make sure that you use the latest Kotlin version: -```groovy -buildscript { - ext.kotlin_version = '1.5.30' +```kotlin +plugins { + // For build.gradle.kts (Kotlin DSL) + kotlin("jvm") version "1.6.21" + + // For build.gradle (Groovy DSL) + id "org.jetbrains.kotlin.jvm" version "1.6.21" } ``` 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.5.2") -} -``` - -And make sure that you use the latest Kotlin version: - -```groovy -plugins { - kotlin("jvm") version "1.5.20" -} -``` - -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.5.2' +```kotlin +implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3") ``` This gives you access to the Android [Dispatchers.Main] @@ -165,7 +150,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" } @@ -177,10 +163,11 @@ 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.5.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") } } ``` @@ -192,7 +179,7 @@ Platform-specific dependencies are recommended to be used only for non-multiplat #### JS Kotlin/JS version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.5.2/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.6.3/jar) (follow the link to get the dependency declaration snippet) and as [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) NPM package. #### Native diff --git a/RELEASE.md b/RELEASE.md index edc7726a0a..e297abd3c4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,15 +1,15 @@ # kotlinx.coroutines release checklist -To release new `` of `kotlinx-coroutines`: +To release a new `` of `kotlinx-coroutines`: -1. Checkout `develop` branch:
+1. Checkout the `develop` branch:
`git checkout develop` -2. Retrieve the most recent `develop`:
+2. Retrieve the most recent `develop`:
`git pull` - + 3. Make sure the `master` branch is fully merged into `develop`: - `git merge origin/master` + `git merge origin/master` 4. Search & replace `` with `` across the project files. Should replace in: * Docs @@ -17,62 +17,64 @@ To release new `` of `kotlinx-coroutines`: * [`kotlinx-coroutines-debug/README.md`](kotlinx-coroutines-debug/README.md) * [`kotlinx-coroutines-test/README.md`](kotlinx-coroutines-test/README.md) * [`coroutines-guide-ui.md`](ui/coroutines-guide-ui.md) - * Properties - * [`gradle.properties`](gradle.properties) + * Properties + * [`gradle.properties`](gradle.properties) * Make sure to **exclude** `CHANGES.md` from replacements. - - As an alternative approach you can use `./bump-version.sh old_version new_version` - + + As an alternative approach, you can use `./bump-version.sh old_version new_version` + 5. Write release notes in [`CHANGES.md`](CHANGES.md): - * Use old releases as example of style. + * Use the old releases for style guidance. * Write each change on a single line (don't wrap with CR). - * Study commit message from previous release. + * Look through the commit messages since the previous release. 6. Create the branch for this release: `git checkout -b version-` -7. Commit updated files to a new version branch:
+7. Commit the updated files to the new version branch:
`git commit -a -m "Version "` - -8. Push the new version into the branch:
+ +8. Push the new version to GitHub:
`git push -u origin version-` - -9. Create Pull-Request on GitHub from `version-` branch into `master`: + +9. Create a Pull-Request on GitHub from the `version-` branch into `master`: * Review it. - * Make sure it build on CI. + * Make sure it builds on CI. * Get approval for it. - -0. Merge new version branch into `master`:
- `git checkout master`
- `git merge version-`
- `git push` -1. On [TeamCity integration server](https://teamcity.jetbrains.com/project.html?projectId=KotlinTools_KotlinxCoroutines): +0. On [TeamCity integration server](https://teamcity.jetbrains.com/project.html?projectId=KotlinTools_KotlinxCoroutines): * Wait until "Build" configuration for committed `master` branch passes tests. - * Run "Deploy (Configure, RUN THIS ONE)" configuration with the corresponding new version. + * Run "Deploy (Configure, RUN THIS ONE)" configuration with the corresponding new version: + - Use the `version-` branch + - Set the `DeployVersion` build parameter to `` + * Wait until all four "Deploy" configurations finish. -2. In [GitHub](https://github.com/kotlin/kotlinx.coroutines) interface: - * Create a release named ``. - * Cut & paste lines from [`CHANGES.md`](CHANGES.md) into description. - -3. Build and publish documentation for web-site - (make sure you have [Docker](https://www.docker.com/) installed first):
- `site/deploy.sh push` - -4. In [Nexus](https://oss.sonatype.org/#stagingRepositories) admin interface: +1. In [Nexus](https://oss.sonatype.org/#stagingRepositories) admin interface: * Close the repository and wait for it to verify. * Release the repository. - -5. Announce new release in [Slack](https://kotlinlang.slack.com) -6. Switch into `develop` branch:
+2. Merge the new version branch into `master`:
+ `git checkout master`
+ `git merge version-`
+ `git push` + +3. In [GitHub](https://github.com/kotlin/kotlinx.coroutines) interface: + * Create a release named ``, creating the `` tag. + * Cut & paste lines from [`CHANGES.md`](CHANGES.md) into description. + +4. Build and publish the documentation for the web-site:
+ `site/deploy.sh push` + +5. Announce the new release in [Slack](https://kotlinlang.slack.com) + +6. Switch onto the `develop` branch:
`git checkout develop` - + 7. Fetch the latest `master`:
- `git fetch` - -8. Merge release from `master`:
+ `git fetch` + +8. Merge the release from `master`:
`git merge origin/master` - -9. Push updates to `develop`:
- `git push` + +9. Push the updates to GitHub:
+ `git push` diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index ce0bff1cdf..f64c4aaa21 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -8,7 +8,6 @@ import me.champeau.gradle.* import org.jetbrains.kotlin.gradle.tasks.* plugins { - id("net.ltgt.apt") id("com.github.johnrengelman.shadow") id("me.champeau.gradle.jmh") apply false } @@ -31,8 +30,6 @@ tasks.named("compileJmhKotlin") { } } - - // It is better to use the following to run benchmarks, otherwise you may get unexpected errors: // ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark" extensions.configure("jmh") { @@ -46,21 +43,34 @@ extensions.configure("jmh") { // includeTests = false } -tasks.named("jmhJar") { +val jmhJarTask = tasks.named("jmhJar") { archiveBaseName by "benchmarks" archiveClassifier by null archiveVersion by null destinationDirectory.file("$rootDir") } +tasks { + // For some reason the DuplicatesStrategy from jmh is not enough + // and errors with duplicates appear unless I force it to WARN only: + withType { + duplicatesStrategy = DuplicatesStrategy.WARN + } + + build { + dependsOn(jmhJarTask) + } +} + dependencies { - compile("org.openjdk.jmh:jmh-core:1.26") - compile("io.projectreactor:reactor-core:${version("reactor")}") - compile("io.reactivex.rxjava2:rxjava:2.1.9") - compile("com.github.akarnokd:rxjava2-extensions:0.20.8") + implementation("org.openjdk.jmh:jmh-core:1.26") + implementation("io.projectreactor:reactor-core:${version("reactor")}") + implementation("io.reactivex.rxjava2:rxjava:2.1.9") + implementation("com.github.akarnokd:rxjava2-extensions:0.20.8") - compile("com.typesafe.akka:akka-actor_2.12:2.5.0") - compile(project(":kotlinx-coroutines-core")) + implementation("com.typesafe.akka:akka-actor_2.12:2.5.0") + implementation(project(":kotlinx-coroutines-core")) + implementation(project(":kotlinx-coroutines-reactive")) // add jmh dependency on main "jmhImplementation"(sourceSets.main.get().runtimeClasspath) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt index 9948a371bc..ce64c6a49b 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt @@ -25,12 +25,11 @@ abstract class ParametrizedDispatcherBase : CoroutineScope { private var closeable: Closeable? = null @Setup - @UseExperimental(InternalCoroutinesApi::class) open fun setup() { coroutineContext = when { dispatcher == "fjp" -> ForkJoinPool.commonPool().asCoroutineDispatcher() dispatcher == "scheduler" -> { - ExperimentalCoroutineDispatcher(CORES_COUNT).also { closeable = it } + Dispatchers.Default } dispatcher.startsWith("ftp") -> { newFixedThreadPoolContext(dispatcher.substring(4).toInt(), dispatcher).also { closeable = it } diff --git a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt index 40ddc8ec36..9e1bfc43bb 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt @@ -6,13 +6,10 @@ package benchmarks import benchmarks.common.* import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.sync.* import org.openjdk.jmh.annotations.* -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.TimeUnit +import java.util.concurrent.* @Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MICROSECONDS) @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MICROSECONDS) @@ -84,7 +81,7 @@ open class SemaphoreBenchmark { enum class SemaphoreBenchDispatcherCreator(val create: (parallelism: Int) -> CoroutineDispatcher) { FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }), - EXPERIMENTAL({ parallelism -> ExperimentalCoroutineDispatcher(corePoolSize = parallelism, maxPoolSize = parallelism) }) + EXPERIMENTAL({ parallelism -> Dispatchers.Default }) // TODO doesn't take parallelism into account } private const val WORK_INSIDE = 80 diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt index e7bd1f5fb9..acfb3f3c6d 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt @@ -30,7 +30,7 @@ open class SequencePlaysScrabble : ShakespearePlaysScrabble() { val bonusForDoubleLetter: (String) -> Int = { word: String -> toBeMaxed(word) .map { letterScores[it - 'a'.toInt()] } - .max()!! + .maxOrNull()!! } val score3: (String) -> Int = { word: String -> diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt index a6f0a473c1..d874f3bbe1 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt @@ -27,10 +27,8 @@ import kotlin.coroutines.* @State(Scope.Benchmark) open class PingPongWithBlockingContext { - @UseExperimental(InternalCoroutinesApi::class) - private val experimental = ExperimentalCoroutineDispatcher(8) - @UseExperimental(InternalCoroutinesApi::class) - private val blocking = experimental.blocking(8) + private val experimental = Dispatchers.Default + private val blocking = Dispatchers.IO.limitedParallelism(8) private val threadPool = newFixedThreadPoolContext(8, "PongCtx") @TearDown diff --git a/build.gradle b/build.gradle index e4b12ff3ad..649434bba8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,21 +2,16 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ + +import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile import org.jetbrains.kotlin.konan.target.HostManager -import org.gradle.util.VersionNumber import org.jetbrains.dokka.gradle.DokkaTaskPartial -import org.jetbrains.dokka.gradle.DokkaMultiModuleTask -apply plugin: 'jdk-convention' -apply from: rootProject.file("gradle/opt-in.gradle") +import static Projects.* -def coreModule = "kotlinx-coroutines-core" -// Not applicable for Kotlin plugin -def sourceless = ['kotlinx.coroutines', 'kotlinx-coroutines-bom', 'integration-testing'] -def internal = ['kotlinx.coroutines', 'benchmarks', 'integration-testing'] -// Not published -def unpublished = internal + ['example-frontend-js', 'android-unit-tests'] +apply plugin: 'jdk-convention' buildscript { /* @@ -47,12 +42,6 @@ buildscript { } } - if (using_snapshot_version) { - repositories { - mavenLocal() - } - } - repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } @@ -65,11 +54,13 @@ buildscript { classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" classpath "org.jetbrains.kotlinx:kotlinx-knit:$knit_version" - classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" + classpath "com.github.node-gradle:gradle-node-plugin:$gradle_node_version" classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$binary_compatibility_validator_version" + classpath "ru.vyarus:gradle-animalsniffer-plugin:1.5.4" // Android API check + classpath "org.jetbrains.kotlinx:kover:$kover_version" // JMH plugins - classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" + classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" } CacheRedirector.configureBuildScript(buildscript, rootProject) @@ -102,13 +93,6 @@ allprojects { kotlin_version = rootProject.properties['kotlin_snapshot_version'] } - if (using_snapshot_version) { - repositories { - mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } - } - } - ext.unpublished = unpublished // This project property is set during nightly stress test @@ -121,11 +105,14 @@ allprojects { } apply plugin: "binary-compatibility-validator" +apply plugin: "base" +apply plugin: "kover-conventions" + apiValidation { ignoredProjects += unpublished + ["kotlinx-coroutines-bom"] if (build_snapshot_train) { ignoredProjects.remove("example-frontend-js") - ignoredProjects.add("kotlinx-coroutines-core") + ignoredProjects.add(coreModule) } ignoredPackages += "kotlinx.coroutines.internal" } @@ -139,30 +126,54 @@ allprojects { */ google() mavenCentral() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } } } +// needs to be before evaluationDependsOn due to weird Gradle ordering +apply plugin: "animalsniffer-conventions" + // Add dependency to core source sets. Core is configured in kx-core/build.gradle configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) { evaluationDependsOn(":$coreModule") - def platform = PlatformKt.platformOf(it) - apply plugin: "kotlin-${platform}-conventions" - dependencies { - // See comment below for rationale, it will be replaced with "project" dependency - api project(":$coreModule") - // the only way IDEA can resolve test classes - testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs + if (isMultiplatform(it)) { + apply plugin: "kotlin-multiplatform" + apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") + apply from: rootProject.file("gradle/compile-common.gradle") + + if (rootProject.ext["native_targets_enabled"] as Boolean) { + apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") + } + + apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") + apply from: rootProject.file("gradle/publish-npm-js.gradle") + kotlin.sourceSets.commonMain.dependencies { + api project(":$coreModule") + } + kotlin.sourceSets.jvmTest.dependencies { + implementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs + } + } else { + def platform = PlatformKt.platformOf(it) + apply plugin: "kotlin-${platform}-conventions" + dependencies { + api project(":$coreModule") + // the only way IDEA can resolve test classes + testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs + } } } +apply plugin: "bom-conventions" + // Configure subprojects with Kotlin sources configure(subprojects.findAll { !sourceless.contains(it.name) }) { // Use atomicfu plugin, it also adds all the necessary dependencies apply plugin: 'kotlinx-atomicfu' // Configure options for all Kotlin compilation tasks - tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all { - kotlinOptions.freeCompilerArgs += optInAnnotations.collect { "-Xopt-in=" + it } + tasks.withType(AbstractKotlinCompile).all { + kotlinOptions.freeCompilerArgs += OptInPreset.optInAnnotations.collect { "-Xopt-in=" + it } kotlinOptions.freeCompilerArgs += "-progressive" // Disable KT-36770 for RxJava2 integration kotlinOptions.freeCompilerArgs += "-XXLanguage:-ProhibitUsingNullableTypeParameterAgainstNotNullAnnotated" @@ -189,7 +200,7 @@ if (build_snapshot_train) { } println "Manifest of kotlin-compiler-embeddable.jar for coroutines" - configure(subprojects.findAll { it.name == "kotlinx-coroutines-core" }) { + configure(subprojects.findAll { it.name == coreModule }) { configurations.matching { it.name == "kotlinCompilerClasspath" }.all { resolvedConfiguration.getFiles().findAll { it.name.contains("kotlin-compiler-embeddable") }.each { def manifest = zipTree(it).matching { @@ -206,9 +217,8 @@ if (build_snapshot_train) { // Redefine source sets because we are not using 'kotlin/main/fqn' folder convention configure(subprojects.findAll { - !sourceless.contains(it.name) && + !sourceless.contains(it.name) && !isMultiplatform(it) && it.name != "benchmarks" && - it.name != coreModule && it.name != "example-frontend-js" }) { // Pure JS and pure MPP doesn't have this notion and are configured separately @@ -225,7 +235,7 @@ def core_docs_url = "https://kotlin.github.io/kotlinx.coroutines/$coreModule/" def core_docs_file = "$projectDir/kotlinx-coroutines-core/build/dokka/htmlPartial/package-list" apply plugin: "org.jetbrains.dokka" -configure(subprojects.findAll { !unpublished.contains(it.name) }) { +configure(subprojects.findAll { !unpublished.contains(it.name) && it.name != coreModule }) { if (it.name != 'kotlinx-coroutines-bom') { apply from: rootProject.file('gradle/dokka.gradle.kts') } @@ -245,10 +255,44 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) { } } } + + def thisProject = it + if (thisProject.name in sourceless) { + return + } + + def versionFileTask = thisProject.tasks.register("versionFileTask") { + def name = thisProject.name.replace("-", "_") + def versionFile = thisProject.layout.buildDirectory.file("${name}.version") + it.outputs.file(versionFile) + + it.doLast { + versionFile.get().asFile.text = version.toString() + } + } + + List jarTasks + if (isMultiplatform(it)) { + jarTasks = ["jvmJar", "metadataJar"] + } else if (it.name == "kotlinx-coroutines-debug") { + // We shadow debug module instead of just packaging it + jarTasks = ["shadowJar"] + } else { + jarTasks = ["jar"] + } + + for (name in jarTasks) { + thisProject.tasks.named(name, Jar) { + it.dependsOn versionFileTask + it.from(versionFileTask) { + into("META-INF") + } + } + } } // Report Kotlin compiler version when building project -println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION") +println("Using Kotlin compiler version: $KotlinCompilerVersion.VERSION") // --------------- Cache redirector --------------- @@ -262,8 +306,6 @@ def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm" task deploy(dependsOn: publishTasks) -apply plugin: 'base' - clean.dependsOn gradle.includedBuilds.collect { it.task(':clean') } // --------------- Knit configuration --------------- @@ -302,12 +344,12 @@ allprojects { subProject -> .matching { // Excluding substituted project itself because of circular dependencies, but still do it // for "*Test*" configurations - subProject.name != "kotlinx-coroutines-core" || it.name.contains("Test") + subProject.name != coreModule || it.name.contains("Test") } .configureEach { conf -> conf.resolutionStrategy.dependencySubstitution { - substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core")) - .using(project(":kotlinx-coroutines-core")) + substitute(module("org.jetbrains.kotlinx:$coreModule")) + .using(project(":$coreModule")) .because("Because Kotlin compiler embeddable leaks coroutines into the runtime classpath, " + "triggering all sort of incompatible class changes errors") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c54e226af1..eaa03f2f15 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -19,7 +19,6 @@ repositories { maven("https://plugins.gradle.org/m2") } maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") - if (buildSnapshotTrain) { mavenLocal() } @@ -44,6 +43,25 @@ fun version(target: String): String { dependencies { implementation(kotlin("gradle-plugin", version("kotlin"))) - implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") - implementation("org.jetbrains.dokka:dokka-core:${version("dokka")}") + /* + * Dokka is compiled with language level = 1.4, but depends on Kotlin 1.6.0, while + * our version of Gradle bundles Kotlin 1.4.x and can read metadata only up to 1.5.x, + * thus we're excluding stdlib compiled with 1.6.0 from dependencies. + */ + implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + } + implementation("org.jetbrains.dokka:dokka-core:${version("dokka")}") { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + } + implementation("ru.vyarus:gradle-animalsniffer-plugin:1.5.3") // Android API check + implementation("org.jetbrains.kotlinx:kover:${version("kover")}") { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + } } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index e30c3ee597..c2e859f65d 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -4,7 +4,6 @@ pluginManagement { val build_snapshot_train: String? by settings repositories { - maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/") val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true if (cacheRedirectorEnabled) { println("Redirecting repositories for buildSrc buildscript") diff --git a/buildSrc/src/main/kotlin/OptInPreset.kt b/buildSrc/src/main/kotlin/OptInPreset.kt new file mode 100644 index 0000000000..fdcdb8ecf8 --- /dev/null +++ b/buildSrc/src/main/kotlin/OptInPreset.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:JvmName("OptInPreset") + +val optInAnnotations = listOf( + "kotlin.RequiresOptIn", + "kotlin.experimental.ExperimentalTypeInference", + "kotlin.ExperimentalMultiplatform", + "kotlinx.coroutines.DelicateCoroutinesApi", + "kotlinx.coroutines.ExperimentalCoroutinesApi", + "kotlinx.coroutines.ObsoleteCoroutinesApi", + "kotlinx.coroutines.InternalCoroutinesApi", + "kotlinx.coroutines.FlowPreview") diff --git a/buildSrc/src/main/kotlin/Projects.kt b/buildSrc/src/main/kotlin/Projects.kt index dd284b6132..af7098935d 100644 --- a/buildSrc/src/main/kotlin/Projects.kt +++ b/buildSrc/src/main/kotlin/Projects.kt @@ -1,8 +1,31 @@ /* * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - -import org.gradle.api.Project +@file:JvmName("Projects") +import org.gradle.api.* fun Project.version(target: String): String = property("${target}_version") as String + +val coreModule = "kotlinx-coroutines-core" +val testModule = "kotlinx-coroutines-test" + +val multiplatform = setOf(coreModule, testModule) +// Not applicable for Kotlin plugin +val sourceless = setOf("kotlinx.coroutines", "kotlinx-coroutines-bom") +val internal = setOf("kotlinx.coroutines", "benchmarks") +// Not published +val unpublished = internal + setOf("example-frontend-js", "android-unit-tests") + +val Project.isMultiplatform: Boolean get() = name in multiplatform + +// Projects that we do not check for Android API level 14 check due to various limitations +val androidNonCompatibleProjects = setOf( + "kotlinx-coroutines-debug", + "kotlinx-coroutines-swing", + "kotlinx-coroutines-javafx", + "kotlinx-coroutines-jdk8", + "kotlinx-coroutines-jdk9", + "kotlinx-coroutines-reactor", + "kotlinx-coroutines-test" +) diff --git a/buildSrc/src/main/kotlin/Publishing.kt b/buildSrc/src/main/kotlin/Publishing.kt index 8c6dd5de3d..cb612c5077 100644 --- a/buildSrc/src/main/kotlin/Publishing.kt +++ b/buildSrc/src/main/kotlin/Publishing.kt @@ -7,6 +7,7 @@ import org.gradle.api.Project import org.gradle.api.artifacts.dsl.* import org.gradle.api.publish.maven.* +import org.gradle.kotlin.dsl.* import org.gradle.plugins.signing.* import java.net.* @@ -56,6 +57,11 @@ fun configureMavenPublication(rh: RepositoryHandler, project: Project) { password = project.getSensitiveProperty("libs.sonatype.password") } } + + // Something that's easy to "clean" for development, not mavenLocal + rh.maven("${project.rootProject.buildDir}/repo") { + name = "buildRepo" + } } fun signPublicationIfKeyPresent(project: Project, publication: MavenPublication) { diff --git a/buildSrc/src/main/kotlin/SourceSets.kt b/buildSrc/src/main/kotlin/SourceSets.kt new file mode 100644 index 0000000000..3ad1dd4dcc --- /dev/null +++ b/buildSrc/src/main/kotlin/SourceSets.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.jetbrains.kotlin.gradle.plugin.* + +fun KotlinSourceSet.configureMultiplatform() { + val srcDir = if (name.endsWith("Main")) "src" else "test" + val platform = name.dropLast(4) + kotlin.srcDir("$platform/$srcDir") + if (name == "jvmMain") { + resources.srcDir("$platform/resources") + } else if (name == "jvmTest") { + resources.srcDir("$platform/test-resources") + } + languageSettings { + optInAnnotations.forEach { optIn(it) } + progressiveMode = true + } +} diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt index b3152d7ab0..afe2627a3d 100644 --- a/buildSrc/src/main/kotlin/UnpackAar.kt +++ b/buildSrc/src/main/kotlin/UnpackAar.kt @@ -2,18 +2,49 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +import org.gradle.api.* import org.gradle.api.artifacts.transform.InputArtifact import org.gradle.api.artifacts.transform.TransformAction import org.gradle.api.artifacts.transform.TransformOutputs import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.attributes.* import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.* import java.io.File import java.nio.file.Files import java.util.zip.ZipEntry import java.util.zip.ZipFile -// TODO move back to kotlinx-coroutines-play-services when it's migrated to the kts +// Attributes used by aar dependencies +val artifactType = Attribute.of("artifactType", String::class.java) +val unpackedAar = Attribute.of("unpackedAar", Boolean::class.javaObjectType) + +fun Project.configureAar() = configurations.configureEach { + afterEvaluate { + if (isCanBeResolved && !isCanBeConsumed) { + attributes.attribute(unpackedAar, true) // request all AARs to be unpacked + } + } +} + +fun DependencyHandlerScope.configureAarUnpacking() { + attributesSchema { + attribute(unpackedAar) + } + + artifactTypes { + create("aar") { + attributes.attribute(unpackedAar, false) + } + } + + registerTransform(UnpackAar::class.java) { + from.attribute(unpackedAar, false).attribute(artifactType, "aar") + to.attribute(unpackedAar, true).attribute(artifactType, "jar") + } +} + @Suppress("UnstableApiUsage") abstract class UnpackAar : TransformAction { @get:InputArtifact diff --git a/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts b/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts new file mode 100644 index 0000000000..f00a0b315f --- /dev/null +++ b/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import ru.vyarus.gradle.plugin.animalsniffer.* + +configure(subprojects) { + // Skip JDK 8 projects or unpublished ones + if (!shouldSniff()) return@configure + apply(plugin = "ru.vyarus.animalsniffer") + project.plugins.withType(JavaPlugin::class.java) { + configure { + sourceSets = listOf((project.extensions.getByName("sourceSets") as SourceSetContainer).getByName("main")) + } + val signature: Configuration by configurations + dependencies { + signature("net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature") + signature("org.codehaus.mojo.signature:java17:1.0@signature") + } + } +} + +fun Project.shouldSniff(): Boolean { + // Skip all non-JVM projects + if (platformOf(project) != "jvm") return false + val name = project.name + if (name in unpublished || name in sourceless || name in androidNonCompatibleProjects) return false + return true +} diff --git a/buildSrc/src/main/kotlin/bom-conventions.gradle.kts b/buildSrc/src/main/kotlin/bom-conventions.gradle.kts new file mode 100644 index 0000000000..45f30edff1 --- /dev/null +++ b/buildSrc/src/main/kotlin/bom-conventions.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.gradle.kotlin.dsl.* +import org.jetbrains.kotlin.gradle.dsl.* + + +configure(subprojects.filter { it.name !in unpublished }) { + if (name == "kotlinx-coroutines-bom" || name == "kotlinx.coroutines") return@configure + if (isMultiplatform) { + kotlinExtension.sourceSets.getByName("jvmMain").dependencies { + api(project.dependencies.platform(project(":kotlinx-coroutines-bom"))) + } + } else { + dependencies { + "api"(platform(project(":kotlinx-coroutines-bom"))) + } + } +} diff --git a/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts index c7744f8702..90847f4567 100644 --- a/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts @@ -11,8 +11,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_6 - targetCompatibility = JavaVersion.VERSION_1_6 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { diff --git a/buildSrc/src/main/kotlin/kover-conventions.gradle.kts b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts new file mode 100644 index 0000000000..052e2bb684 --- /dev/null +++ b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts @@ -0,0 +1,54 @@ +import kotlinx.kover.api.* +import kotlinx.kover.tasks.* + +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +apply(plugin = "kover") + +val notCovered = sourceless + internal + unpublished + +val expectedCoverage = mutableMapOf( + // These have lower coverage in general, it can be eventually fixed + "kotlinx-coroutines-swing" to 70, // awaitFrame is not tested + "kotlinx-coroutines-javafx" to 39, // JavaFx is not tested on TC because its graphic subsystem cannot be initialized in headless mode + + // Reactor has lower coverage in general due to various fatal error handling features + "kotlinx-coroutines-reactor" to 75) + +extensions.configure { + disabledProjects = notCovered + /* + * Is explicitly enabled on TC in a separate build step. + * Examples: + * ./gradlew :p:check -- doesn't verify coverage + * ./gradlew :p:check -Pkover.enabled=true -- verifies coverage + * ./gradlew :p:koverReport -Pkover.enabled=true -- generates report + */ + isDisabled = !(properties["kover.enabled"]?.toString()?.toBoolean() ?: false) + // TODO remove when updating Kover to version 0.5.x + intellijEngineVersion.set("1.0.657") +} + +subprojects { + val projectName = name + if (projectName in notCovered) return@subprojects + tasks.withType { + rule { + bound { + /* + * 85 is our baseline that we aim to raise to 90+. + * Missing coverage is typically due to bugs in the agent + * (e.g. signatures deprecated with an error are counted), + * sometimes it's various diagnostic `toString` or `catch` for OOMs/VerificationErrors, + * but some places are definitely worth visiting. + */ + minValue = expectedCoverage[projectName] ?: 85 // COVERED_LINES_PERCENTAGE + } + } + } + + tasks.withType { + htmlReportDir.set(file(rootProject.buildDir.toString() + "/kover/" + project.name + "/html")) + } +} diff --git a/bump-version.sh b/bump-version.sh index ae0fc0b02c..e49910f428 100755 --- a/bump-version.sh +++ b/bump-version.sh @@ -21,6 +21,7 @@ update_version "kotlinx-coroutines-debug/README.md" update_version "kotlinx-coroutines-test/README.md" update_version "ui/coroutines-guide-ui.md" update_version "gradle.properties" +update_version "integration-test/gradle.properties" # Escape dots, e.g. 1.0.0 -> 1\.0\.0 escaped_old_version=$(echo $old_version | sed s/[.]/\\\\./g) diff --git a/coroutines-guide.md b/coroutines-guide.md index 3b4707cf83..3cc035ae6a 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -1,14 +1,3 @@ The main coroutines guide has moved to the [docs folder](docs/topics/coroutines-guide.md) and split up into smaller documents. -## Table of contents - - - - - - - - - - - +It is recommended to read the guide on the [kotlinlang website](https://kotlinlang.org/docs/coroutines-guide.html), with proper HTML formatting and runnable samples. diff --git a/docs/images/coroutine-breakpoint.png b/docs/images/coroutine-breakpoint.png index b547e77da6..d0e34e89c0 100644 Binary files a/docs/images/coroutine-breakpoint.png and b/docs/images/coroutine-breakpoint.png differ diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png index 0afe992515..c824307290 100644 Binary files a/docs/images/coroutine-idea-debugging-1.png and b/docs/images/coroutine-idea-debugging-1.png differ diff --git a/docs/images/flow-breakpoint.png b/docs/images/flow-breakpoint.png index aa98e18e7d..a7a38cceaa 100644 Binary files a/docs/images/flow-breakpoint.png and b/docs/images/flow-breakpoint.png differ diff --git a/docs/images/flow-build-project.png b/docs/images/flow-build-project.png index 22186213cf..12221c77a0 100644 Binary files a/docs/images/flow-build-project.png and b/docs/images/flow-build-project.png differ diff --git a/docs/images/flow-debug-project.png b/docs/images/flow-debug-project.png index 98d392e2cc..f5b20bd9f2 100644 Binary files a/docs/images/flow-debug-project.png and b/docs/images/flow-debug-project.png differ diff --git a/docs/topics/cancellation-and-timeouts.md b/docs/topics/cancellation-and-timeouts.md index 5221db922a..47f465ad60 100644 --- a/docs/topics/cancellation-and-timeouts.md +++ b/docs/topics/cancellation-and-timeouts.md @@ -103,6 +103,42 @@ job: I'm sleeping 4 ... main: Now I can quit. --> +The same problem can be observed by catching a [CancellationException] and not rethrowing it: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { +//sampleStart + val job = launch(Dispatchers.Default) { + repeat(5) { i -> + try { + // print a message twice a second + println("job: I'm sleeping $i ...") + delay(500) + } catch (e: Exception) { + // log the exception + println(e) + } + } + } + delay(1300L) // delay a bit + println("main: I'm tired of waiting!") + job.cancelAndJoin() // cancels the job and waits for its completion + println("main: Now I can quit.") +//sampleEnd +} +``` +{kotlin-runnable="true" kotlin-min-compiler-version="1.3"} + +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). +> +{type="note"} + +While catching `Exception` is an anti-pattern, this issue may surface in more subtle ways, like when using the +[`runCatching`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html) function, +which does not rethrow [CancellationException]. + ## Making computation code cancellable There are two approaches to making computation code cancellable. The first one is to periodically @@ -137,7 +173,7 @@ fun main() = runBlocking { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). > {type="note"} @@ -154,8 +190,8 @@ main: Now I can quit. ## Closing resources with `finally` -Cancellable suspending functions throw [CancellationException] on cancellation which can be handled in -the usual way. For example, `try {...} finally {...}` expression and Kotlin `use` function execute their +Cancellable suspending functions throw [CancellationException] on cancellation, which can be handled in +the usual way. For example, the `try {...} finally {...}` expression and Kotlin's `use` function execute their finalization actions normally when a coroutine is cancelled: ```kotlin @@ -182,7 +218,7 @@ fun main() = runBlocking { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). > {type="note"} @@ -237,7 +273,7 @@ fun main() = runBlocking { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt). > {type="note"} @@ -275,7 +311,7 @@ fun main() = runBlocking { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt). > {type="note"} @@ -318,7 +354,7 @@ fun main() = runBlocking { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). > {type="note"} @@ -378,7 +414,7 @@ fun main() { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). > {type="note"} @@ -387,13 +423,13 @@ fun main() { If you run the above code you'll see that it does not always print zero, though it may depend on the timings of your machine you may need to tweak timeouts in this example to actually see non-zero values. -> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, -> since it always happens from the same main thread. More on that will be explained in the next chapter +> Note that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, +> since it always happens from the same main thread. More on that will be explained in the chapter > on coroutine context. > {type="note"} -To workaround this problem you can store a reference to the resource in the variable as opposed to returning it +To work around this problem you can store a reference to the resource in the variable as opposed to returning it from the `withTimeout` block. ```kotlin @@ -431,7 +467,7 @@ fun main() { ``` {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} -> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). +> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt). > {type="note"} diff --git a/docs/topics/channels.md b/docs/topics/channels.md index 7f41eaec2b..7cf222c8a0 100644 --- a/docs/topics/channels.md +++ b/docs/topics/channels.md @@ -573,6 +573,7 @@ Now let's see how it works in practice: import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +//sampleStart fun main() = runBlocking { val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // create ticker channel var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() } @@ -596,7 +597,9 @@ fun main() = runBlocking { tickerChannel.cancel() // indicate that no more elements are needed } +//sampleEnd ``` +{kotlin-runnable="true" kotlin-min-compiler-version="1.3"} > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt). > diff --git a/docs/topics/coroutine-context-and-dispatchers.md b/docs/topics/coroutine-context-and-dispatchers.md index 9648214848..6c06b14d07 100644 --- a/docs/topics/coroutine-context-and-dispatchers.md +++ b/docs/topics/coroutine-context-and-dispatchers.md @@ -148,7 +148,7 @@ The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in I The **Debug** tool window contains the **Coroutines** tab. In this tab, you can find information about both currently running and suspended coroutines. The coroutines are grouped by the dispatcher they are running on. -![Debugging coroutines](coroutine-idea-debugging-1.png) +![Debugging coroutines](coroutine-idea-debugging-1.png){width=700} With the coroutine debugger, you can: * Check the state of each coroutine. @@ -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. @@ -334,8 +334,8 @@ fun main() = runBlocking { } delay(500) request.cancel() // cancel processing of the request - delay(1000) // delay a second to see what happens println("main: Who has survived request cancellation?") + delay(1000) // delay the main thread for a second to see what happens //sampleEnd } ``` @@ -350,8 +350,8 @@ The output of this code is: ```text job1: I run in my own Job and execute independently! job2: I am a child of the request coroutine -job1: I am not affected by cancellation of the request main: Who has survived request cancellation? +job1: I am not affected by cancellation of the request ``` diff --git a/docs/topics/coroutines-basics.md b/docs/topics/coroutines-basics.md index 5d9d0e6db1..68ae97886f 100644 --- a/docs/topics/coroutines-basics.md +++ b/docs/topics/coroutines-basics.md @@ -75,7 +75,7 @@ Coroutines follow a principle of which delimits the lifetime of the coroutine. The above example shows that [runBlocking] establishes the corresponding scope and that is why the previous example waits until `World!` is printed after a second's delay and only then exits. -In the real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not +In a real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not lost and do not leak. An outer scope cannot complete until all its children coroutines complete. Structured concurrency also ensures that any errors in the code are properly reported and are never lost. @@ -245,14 +245,17 @@ Done -## Coroutines ARE light-weight +## Coroutines are light-weight -Run the following code: +Coroutines are less resource-intensive than JVM threads. Code that exhausts the +JVM's available memory when using threads can be expressed using coroutines +without hitting resource limits. For example, the following code launches +100000 distinct coroutines that each wait 5 seconds and then print a period +('.') while consuming very little memory: ```kotlin import kotlinx.coroutines.* -//sampleStart fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { @@ -261,8 +264,9 @@ fun main() = runBlocking { } } } -//sampleEnd ``` + > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-basic-06.kt). > @@ -270,10 +274,9 @@ fun main() = runBlocking { -It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot. - -Now, try that with threads (remove `runBlocking`, replace `launch` with `thread`, and replace `delay` with `Thread.sleep`). -What would happen? (Most likely your code will produce some sort of out-of-memory error) +If you write the same program using threads (remove `runBlocking`, replace +`launch` with `thread`, and replace `delay` with `Thread.sleep`), it will +likely consume too much memory and throw an out-of-memory error. diff --git a/docs/topics/debug-coroutines-with-idea.md b/docs/topics/debug-coroutines-with-idea.md index e59075e071..8bbbb98585 100644 --- a/docs/topics/debug-coroutines-with-idea.md +++ b/docs/topics/debug-coroutines-with-idea.md @@ -16,7 +16,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui The `src` directory contains Kotlin source files and resources. The `main.kt` file contains sample code that will print `Hello World!`. -2. Change code in the `main()` function: +3. Change code in the `main()` function: * Use the [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine. * Use the [`async()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) function to create coroutines that compute deferred values `a` and `b`. @@ -61,7 +61,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui ![Debug the coroutine](coroutine-debug-1.png) -3. Resume the debugger session by clicking **Resume program** in the **Debug** tool window: +3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window: ![Debug the coroutine](coroutine-debug-2.png) @@ -70,7 +70,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui * The second coroutine is calculating the `a` value – it has the **RUNNING** status. * The third coroutine has the **CREATED** status and isn’t calculating the value of `b`. -4. Resume the debugger session by clicking **Resume program** in the **Debug** tool window: +4. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window: ![Build a console application](coroutine-debug-3.png) diff --git a/docs/topics/debug-flow-with-idea.md b/docs/topics/debug-flow-with-idea.md index 745dcb1762..edc841d3b5 100644 --- a/docs/topics/debug-flow-with-idea.md +++ b/docs/topics/debug-flow-with-idea.md @@ -18,7 +18,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou The `src` directory contains Kotlin source files and resources. The `main.kt` file contains sample code that will print `Hello World!`. -2. Create the `simple()` function that returns a flow of three numbers: +3. Create the `simple()` function that returns a flow of three numbers: * Use the [`delay()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) function to imitate CPU-consuming blocking code. It suspends the coroutine for 100 ms without blocking the thread. * Produce the values in the `for` loop using the [`emit()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html) function. @@ -36,7 +36,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou } ``` -3. Change the code in the `main()` function: +4. Change the code in the `main()` function: * Use the [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine. * Collect the emitted values using the [`collect()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html) function. @@ -53,13 +53,13 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou } ``` -4. Build the code by clicking **Build Project**. +5. Build the code by clicking **Build Project**. ![Build an application](flow-build-project.png) ## Debug the coroutine -1. Set a breakpoint at the at the line where the `emit()` function is called: +1. Set a breakpoint at the line where the `emit()` function is called: ![Build a console application](flow-breakpoint.png) @@ -74,7 +74,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou ![Debug the coroutine](flow-debug-1.png) -3. Resume the debugger session by clicking **Resume program** in the **Debug** tool window. The program stops at the same breakpoint. +3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window. The program stops at the same breakpoint. ![Debug the coroutine](flow-resume-debug.png) @@ -101,7 +101,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou } ``` -4. Build the code by clicking **Build Project**. +3. Build the code by clicking **Build Project**. ## Debug a Kotlin flow with two coroutines @@ -117,7 +117,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou The `buffer()` function buffers emitted values from the flow. The emitter coroutine has the **RUNNING** status, and the collector coroutine has the **SUSPENDED** status. -2. Resume the debugger session by clicking **Resume program** in the **Debug** tool window. +3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window. ![Debugging coroutines](flow-debug-4.png) diff --git a/docs/topics/debugging.md b/docs/topics/debugging.md index 5ff4d549e0..6faad018bd 100644 --- a/docs/topics/debugging.md +++ b/docs/topics/debugging.md @@ -7,7 +7,6 @@ * [Stacktrace recovery](#stacktrace-recovery) * [Stacktrace recovery machinery](#stacktrace-recovery-machinery) * [Debug agent](#debug-agent) - * [Debug agent and Android](#debug-agent-and-android) * [Android optimization](#android-optimization) @@ -77,12 +76,6 @@ additionally enhancing stacktraces with information where coroutine was created. The full tutorial of how to use debug agent can be found in the corresponding [readme](../../kotlinx-coroutines-debug/README.md). -### Debug agent and Android - -Unfortunately, Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`. - -Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support android-gradle 3.3 - -There are couple of observations to make out of it. +There are a couple of observations to make out of it. First of all, `select` is _biased_ to the first clause. When several clauses are selectable at the same time, the first one among them gets selected. Here, both channels are constantly producing strings, so `a` channel, @@ -228,7 +228,7 @@ channel is already closed. Select expression has [onSend][SendChannel.onSend] clause that can be used for a great good in combination with a biased nature of selection. -Let us write an example of producer of integers that sends its values to a `side` channel when +Let us write an example of a producer of integers that sends its values to a `side` channel when the consumers on its primary channel cannot keep up with it: ```kotlin diff --git a/docs/topics/shared-mutable-state-and-concurrency.md b/docs/topics/shared-mutable-state-and-concurrency.md index 40b0134a4b..1b84f136ce 100644 --- a/docs/topics/shared-mutable-state-and-concurrency.md +++ b/docs/topics/shared-mutable-state-and-concurrency.md @@ -9,7 +9,7 @@ but others are unique. ## The problem -Let us launch a hundred coroutines all doing the same action thousand times. +Let us launch a hundred coroutines all doing the same action a thousand times. We'll also measure their completion time for further comparisons: ```kotlin @@ -384,7 +384,7 @@ single reference to the actor can be carried around as its handle. The first step of using an actor is to define a class of messages that an actor is going to process. Kotlin's [sealed classes](https://kotlinlang.org/docs/reference/sealed-classes.html) are well suited for that purpose. We define `CounterMsg` sealed class with `IncCounter` message to increment a counter and `GetCounter` message -to get its value. The later needs to send a response. A [CompletableDeferred] communication +to get its value. The latter needs to send a response. A [CompletableDeferred] communication primitive, that represents a single value that will be known (communicated) in the future, is used here for that purpose. diff --git a/gradle.properties b/gradle.properties index 26e5147c51..63fdf28c62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,18 +3,18 @@ # # Kotlin -version=1.5.2-SNAPSHOT +version=1.6.3-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.5.30 +kotlin_version=1.6.21 # Dependencies junit_version=4.12 junit5_version=5.7.0 -atomicfu_version=0.16.3 -knit_version=0.3.0 +atomicfu_version=0.17.3 +knit_version=0.4.0 html_version=0.7.2 lincheck_version=2.14 -dokka_version=1.5.0 +dokka_version=1.6.21 byte_buddy_version=1.10.9 reactor_version=3.4.1 reactive_streams_version=1.0.3 @@ -22,20 +22,21 @@ rxjava2_version=2.2.8 rxjava3_version=3.0.2 javafx_version=11.0.2 javafx_plugin_version=0.0.8 -binary_compatibility_validator_version=0.7.0 +binary_compatibility_validator_version=0.10.0 +kover_version=0.5.0 blockhound_version=1.0.2.RELEASE -jna_version=5.5.0 +jna_version=5.9.0 # Android versions android_version=4.1.1.4 androidx_annotation_version=1.1.0 -robolectric_version=4.0.2 +robolectric_version=4.4 baksmali_version=2.2.7 # JS kotlin.js.compiler=both -gradle_node_version=1.2.0 -node_version=8.9.3 +gradle_node_version=3.1.1 +node_version=10.0.0 npm_version=5.7.1 mocha_version=6.2.2 mocha_headless_chrome_version=1.8.2 @@ -53,7 +54,8 @@ jekyll_version=4.0 # JS IR backend sometimes crashes with out-of-memory # TODO: Remove once KT-37187 is fixed -org.gradle.jvmargs=-Xmx4g +org.gradle.jvmargs=-Xmx3g kotlin.mpp.enableCompatibilityMetadataVariant=true kotlin.mpp.stability.nowarn=true +kotlinx.atomicfu.enableIrTransformation=true diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index d6df7e403a..c6fc757c7d 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -60,7 +60,7 @@ compileTestJsLegacy.configure { task populateNodeModules(type: Copy, dependsOn: compileTestJsLegacy) { // we must copy output that is transformed by atomicfu from(kotlin.js().compilations.main.output.allOutputs) - into "$node.nodeModulesDir/node_modules" + into node.nodeProjectDir.dir("node_modules") def configuration = configurations.hasProperty("jsLegacyTestRuntimeClasspath") ? configurations.jsLegacyTestRuntimeClasspath diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index 5e65042746..88b717976d 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -2,12 +2,16 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 kotlin { jvm {} sourceSets { + jvmMain.dependencies { + compileOnly "org.codehaus.mojo:animal-sniffer-annotations:1.20" + } + jvmTest.dependencies { api "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests diff --git a/gradle/dokka.gradle.kts b/gradle/dokka.gradle.kts index 659890a30b..2470ded3ea 100644 --- a/gradle/dokka.gradle.kts +++ b/gradle/dokka.gradle.kts @@ -37,12 +37,8 @@ tasks.withType(DokkaTaskPartial::class).configureEach { packageListUrl.set(rootProject.projectDir.toPath().resolve("site/stdlib.package.list").toUri().toURL()) } - if (project.name != "kotlinx-coroutines-core") { + if (!project.isMultiplatform) { dependsOn(project.configurations["compileClasspath"]) - doFirst { - // resolve classpath only during execution - classpath.from(project.configurations["compileClasspath"].files)// + project.sourceSets.main.output.files) - } } } } @@ -66,10 +62,6 @@ if (project.name == "kotlinx-coroutines-core") { val jvmMain by getting { makeLinkMapping(project.file("jvm")) } - - configureEach { - classpath.from(project.configurations["jvmCompileClasspath"].files) - } } } } diff --git a/gradle/node-js.gradle b/gradle/node-js.gradle index 42f101c5f4..5eddc5fa37 100644 --- a/gradle/node-js.gradle +++ b/gradle/node-js.gradle @@ -2,13 +2,13 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'com.moowork.node' +apply plugin: 'com.github.node-gradle.node' node { version = "$node_version" npmVersion = "$npm_version" download = true - nodeModulesDir = file(buildDir) + nodeProjectDir = file(buildDir) } // Configures testing for JS modules @@ -25,7 +25,7 @@ task prepareNodePackage(type: Copy) { from("npm") { exclude 'package.json' } - into "$node.nodeModulesDir" + into node.nodeProjectDir } npmInstall.dependsOn prepareNodePackage diff --git a/gradle/opt-in.gradle b/gradle/opt-in.gradle deleted file mode 100644 index 22f022dbb5..0000000000 --- a/gradle/opt-in.gradle +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -ext.optInAnnotations = [ - "kotlin.RequiresOptIn", - "kotlin.experimental.ExperimentalTypeInference", - "kotlin.ExperimentalMultiplatform", - "kotlinx.coroutines.DelicateCoroutinesApi", - "kotlinx.coroutines.ExperimentalCoroutinesApi", - "kotlinx.coroutines.ObsoleteCoroutinesApi", - "kotlinx.coroutines.InternalCoroutinesApi", - "kotlinx.coroutines.FlowPreview"] diff --git a/gradle/publish-npm-js.gradle b/gradle/publish-npm-js.gradle index 382c6749ca..9d4152770c 100644 --- a/gradle/publish-npm-js.gradle +++ b/gradle/publish-npm-js.gradle @@ -40,17 +40,15 @@ task preparePublishNpm(type: Copy) { task publishNpm(type: NpmTask, dependsOn: [preparePublishNpm]) { workingDir = npmDeployDir - - doFirst { - def npmDeployTag = distTag(version) - def deployArgs = ['publish', - "--//registry.npmjs.org/:_authToken=$authToken", - "--tag=$npmDeployTag"] - if (dryRun == "true") { - println("$npmDeployDir \$ npm arguments: $deployArgs") - args = ['pack'] - } else { - args = deployArgs - } + + def npmDeployTag = distTag(version) + def deployArgs = ['publish', + "--//registry.npmjs.org/:_authToken=$authToken", + "--tag=$npmDeployTag"] + if (dryRun == "true") { + println("$npmDeployDir \$ npm arguments: $deployArgs") + args = ['pack'] + } else { + args = deployArgs } } diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 3a0a4224ab..00034bfa87 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -6,17 +6,18 @@ import org.gradle.util.VersionNumber // Configures publishing of Maven artifacts to Maven Central -apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'signing' // ------------- tasks -def isMultiplatform = project.name == "kotlinx-coroutines-core" +def isMultiplatform = project.name == "kotlinx-coroutines-core" || project.name == "kotlinx-coroutines-test" def isBom = project.name == "kotlinx-coroutines-bom" if (!isBom) { - apply plugin: "com.github.johnrengelman.shadow" + if (project.name == "kotlinx-coroutines-debug") { + apply plugin: "com.github.johnrengelman.shadow" + } // empty xxx-javadoc.jar task javadocJar(type: Jar) { diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle index d011eeaa20..27d2e5b394 100644 --- a/gradle/test-mocha-js.gradle +++ b/gradle/test-mocha-js.gradle @@ -9,7 +9,7 @@ task installDependenciesMochaNode(type: NpmTask, dependsOn: [npmInstall]) { "mocha@$mocha_version", "source-map-support@$source_map_support_version", '--no-save'] - if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] + if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]) } def compileJsLegacy = tasks.hasProperty("compileKotlinJsLegacy") @@ -22,9 +22,9 @@ def compileTestJsLegacy = tasks.hasProperty("compileTestKotlinJsLegacy") // todo: use atomicfu-transformed test files here (not critical) task testMochaNode(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaNode]) { - script = file("$node.nodeModulesDir/node_modules/mocha/bin/mocha") - args = [compileTestJsLegacy.outputFile, '--require', 'source-map-support/register'] - if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter'] + script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha") + args = [compileTestJsLegacy.outputFile.path, '--require', 'source-map-support/register'] + if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter']) } def jsLegacyTestTask = project.tasks.findByName('jsLegacyTest') ? jsLegacyTest : jsTest @@ -40,8 +40,8 @@ task installDependenciesMochaChrome(type: NpmTask, dependsOn: [npmInstall]) { "kotlin@$kotlin_version", "kotlin-test@$kotlin_version", '--no-save'] - if (project.hasProperty("teamcity")) args += [ - "mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] + if (project.hasProperty("teamcity")) args.addAll([ + "mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]) } def mochaChromeTestPage = file("$buildDir/test-page.html") @@ -51,19 +51,20 @@ task prepareMochaChrome(dependsOn: [compileTestJsLegacy, installDependenciesMoch } prepareMochaChrome.doLast { + def nodeProjDir = node.nodeProjectDir.getAsFile().get() mochaChromeTestPage.text = """ Mocha Tests - +
- + - - + + @@ -73,9 +74,9 @@ prepareMochaChrome.doLast { } task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) { - script = file("$node.nodeModulesDir/node_modules/mocha-headless-chrome/bin/start") - args = [compileTestJsLegacy.outputFile, '--file', mochaChromeTestPage] - if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter'] + script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha-headless-chrome/bin/start") + args = [compileTestJsLegacy.outputFile.path, '--file', mochaChromeTestPage] + if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter']) } // todo: Commented out because mocha-headless-chrome does not work on TeamCity @@ -90,13 +91,13 @@ task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) { "jsdom-global@$jsdom_global_version", "source-map-support@$source_map_support_version", '--no-save'] - if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] + if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]) } task testMochaJsdom(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaJsdom]) { - script = file("$node.nodeModulesDir/node_modules/mocha/bin/mocha") - args = [compileTestJsLegacy.outputFile, '--require', 'source-map-support/register', '--require', 'jsdom-global/register'] - if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter'] + script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha") + args = [compileTestJsLegacy.outputFile.path, '--require', 'source-map-support/register', '--require', 'jsdom-global/register'] + if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter']) } jsLegacyTestTask.dependsOn testMochaJsdom diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d7ae858e5a..f57489cf8a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,6 +4,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/integration-testing/README.md b/integration-testing/README.md index 4754081a45..0ede9b254e 100644 --- a/integration-testing/README.md +++ b/integration-testing/README.md @@ -1,14 +1,13 @@ # Integration tests -This is a supplementary subproject of kotlinx.coroutines that provides -integration tests. +This is a supplementary project that provides integration tests. The tests are the following: -* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu - In order for the test to work, one needs to run gradle with `-PdryRun=true`. - `-PdryRun` affects `npmPublish` so that it only provides a packed publication - and does not in fact attempt to send the build for publication. -* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath +* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath. +* `CoreAgentTest` checks that `kotlinx-coroutines-core` can be run as a Java agent. * `DebugAgentTest` checks that the coroutine debugger can be run as a Java agent. +* `smokeTest` builds the test project that depends on coroutines. -All the available tests can be run with `integration-testing:test`. +The `integration-testing` project is expected to be in a subdirectory of the main `kotlinx.coroutines` project. + +To run all the available tests: `cd integration-testing` + `./gradlew check`. diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index 6efa3a14e6..60b6cdcd07 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType plugins { - id("kotlin-jvm-conventions") + id "org.jetbrains.kotlin.jvm" } repositories { @@ -13,27 +13,44 @@ repositories { mavenCentral() } +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" +} + sourceSets { - npmTest { - kotlin - compileClasspath += sourceSets.test.runtimeClasspath - runtimeClasspath += sourceSets.test.runtimeClasspath - } mavenTest { kotlin compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath + + dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + } } debugAgentTest { kotlin compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath - } + dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutines_version" + } + } coreAgentTest { kotlin compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath + + dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + } } } @@ -43,68 +60,34 @@ compileDebugAgentTestKotlin { } } -task npmTest(type: Test) { - def sourceSet = sourceSets.npmTest - environment "projectRoot", project.rootDir - environment "deployVersion", version - def dryRunNpm = project.properties['dryRun'] - def doRun = dryRunNpm == "true" // so that we don't accidentally publish anything, especially before the test - onlyIf { doRun } - if (doRun) { // `onlyIf` only affects execution of the task, not the dependency subtree - dependsOn(project(':').getTasksByName("publishNpm", true)) - } - testClassesDirs = sourceSet.output.classesDirs - classpath = sourceSet.runtimeClasspath -} - task mavenTest(type: Test) { + environment "version", coroutines_version def sourceSet = sourceSets.mavenTest - dependsOn(project(':').getTasksByName("publishToMavenLocal", true)) testClassesDirs = sourceSet.output.classesDirs classpath = sourceSet.runtimeClasspath - // we can't depend on the subprojects because we need to test the classfiles that are published in the end. - // also, we can't put this in the `dependencies` block because the resolution would happen before publication. - def mavenTestClasspathConfiguration = project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"), - project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$version")) - - mavenTestClasspathConfiguration.attributes { - attribute(KotlinPlatformType.attribute, KotlinPlatformType.jvm) - } - - classpath += mavenTestClasspathConfiguration } task debugAgentTest(type: Test) { def sourceSet = sourceSets.debugAgentTest - dependsOn(project(':kotlinx-coroutines-debug').shadowJar) - jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0]) + def coroutinesDebugJar = sourceSet.runtimeClasspath.filter {it.name == "kotlinx-coroutines-debug-${coroutines_version}.jar" }.singleFile + jvmArgs ('-javaagent:' + coroutinesDebugJar) testClassesDirs = sourceSet.output.classesDirs classpath = sourceSet.runtimeClasspath + systemProperties project.properties.subMap(["overwrite.probes"]) } task coreAgentTest(type: Test) { def sourceSet = sourceSets.coreAgentTest - dependsOn(project(':kotlinx-coroutines-core').jvmJar) - jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-core').jvmJar.outputs.files.getFiles()[0]) + def coroutinesCoreJar = sourceSet.runtimeClasspath.filter {it.name == "kotlinx-coroutines-core-jvm-${coroutines_version}.jar" }.singleFile + jvmArgs ('-javaagent:' + coroutinesCoreJar) testClassesDirs = sourceSet.output.classesDirs classpath = sourceSet.runtimeClasspath } -dependencies { - testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - testImplementation 'junit:junit:4.12' - npmTestImplementation 'org.apache.commons:commons-compress:1.18' - npmTestImplementation 'com.google.code.gson:gson:2.8.5' - debugAgentTestCompile project(':kotlinx-coroutines-core') - debugAgentTestCompile project(':kotlinx-coroutines-debug') - coreAgentTestCompile project(':kotlinx-coroutines-core') -} - compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } check { - dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest]) + dependsOn([mavenTest, debugAgentTest, coreAgentTest, 'smokeTest:build']) } diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties new file mode 100644 index 0000000000..1f334cafea --- /dev/null +++ b/integration-testing/gradle.properties @@ -0,0 +1,4 @@ +kotlin_version=1.6.21 +coroutines_version=1.6.3-SNAPSHOT + +kotlin.code.style=official diff --git a/integration-testing/gradle/wrapper/gradle-wrapper.jar b/integration-testing/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7454180f2a Binary files /dev/null and b/integration-testing/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-testing/gradle/wrapper/gradle-wrapper.properties b/integration-testing/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..92f06b50fd --- /dev/null +++ b/integration-testing/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-testing/gradlew b/integration-testing/gradlew new file mode 100755 index 0000000000..1b6c787337 --- /dev/null +++ b/integration-testing/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-testing/gradlew.bat b/integration-testing/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/integration-testing/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-testing/settings.gradle b/integration-testing/settings.gradle new file mode 100644 index 0000000000..67336c9880 --- /dev/null +++ b/integration-testing/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.jetbrains.kotlin.multiplatform" || requested.id.id == "org.jetbrains.kotlin.jvm") { + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } + } + } + + repositories { + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + mavenLocal() + } +} + +include 'smokeTest' + +rootProject.name = "kotlinx-coroutines-integration-testing" diff --git a/integration-testing/smokeTest/build.gradle b/integration-testing/smokeTest/build.gradle new file mode 100644 index 0000000000..b200bb2fe8 --- /dev/null +++ b/integration-testing/smokeTest/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'org.jetbrains.kotlin.multiplatform' +} + +repositories { + // Coroutines from the outer project are published by previous CI buils step + mavenLocal() + mavenCentral() +} + +kotlin { + jvm() + js(IR) { + nodejs() + } + + sourceSets { + commonMain { + dependencies { + implementation kotlin('stdlib-common') + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + } + } + commonTest { + dependencies { + implementation kotlin('test-common') + implementation kotlin('test-annotations-common') + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + } + } + jsTest { + dependencies { + implementation kotlin('test-js') + } + } + jvmTest { + dependencies { + implementation kotlin('test') + implementation kotlin('test-junit') + } + } + } +} diff --git a/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt b/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt new file mode 100644 index 0000000000..c5da677bb1 --- /dev/null +++ b/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt @@ -0,0 +1,9 @@ +import kotlinx.coroutines.* + +suspend fun doWorld() = coroutineScope { + launch { + delay(1000L) + println("World!") + } + println("Hello") +} diff --git a/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt b/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt new file mode 100644 index 0000000000..a8c6598e88 --- /dev/null +++ b/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.test.* +import kotlin.test.* + +class SampleTest { + @Test + fun test() = runTest { + doWorld() + } +} diff --git a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt index ce82e577ca..84886a18ab 100644 --- a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt +++ b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt @@ -20,20 +20,19 @@ class PrecompiledDebugProbesTest { val classFileResourcePath = className.replace(".", "/") + ".class" val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!! val array = stream.readBytes() - val binFile = clz.classLoader.getResourceAsStream("DebugProbesKt.bin")!! - val binContent = binFile.readBytes() + // we expect the integration testing project to be in a subdirectory of the main kotlinx.coroutines project + val base = File("").absoluteFile.parentFile + val probes = File(base, "kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin") + val binContent = probes.readBytes() if (overwrite) { - val url = clz.classLoader.getResource("DebugProbesKt.bin")!! - val base = url.toExternalForm().toString().removePrefix("jar:file:").substringBefore("/build") - val probes = File(base, "jvm/resources/DebugProbesKt.bin") FileOutputStream(probes).use { it.write(array) } println("Content was successfully overwritten!") } else { assertTrue( array.contentEquals(binContent), "Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " + - "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true and " + - "ensure that classfile has major version equal to 50 (Java 6 compliance)") + "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true." + ) } } } diff --git a/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt similarity index 97% rename from integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt rename to integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt index 39d6598b55..dbb1921d80 100644 --- a/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt +++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt @@ -8,7 +8,7 @@ import org.junit.* import org.junit.Assert.assertTrue import java.util.jar.* -class MavenPublicationValidator { +class MavenPublicationAtomicfuValidator { private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray() @Test diff --git a/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt new file mode 100644 index 0000000000..da87d4cc59 --- /dev/null +++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.validator + +import org.junit.* +import org.junit.Test +import java.util.jar.* +import kotlin.test.* + +class MavenPublicationVersionValidator { + + @Test + fun testMppJar() { + val clazz = Class.forName("kotlinx.coroutines.Job") + JarFile(clazz.protectionDomain.codeSource.location.file).checkForVersion("kotlinx_coroutines_core.version") + } + + @Test + fun testAndroidJar() { + val clazz = Class.forName("kotlinx.coroutines.android.HandlerDispatcher") + JarFile(clazz.protectionDomain.codeSource.location.file).checkForVersion("kotlinx_coroutines_android.version") + } + + private fun JarFile.checkForVersion(file: String) { + val actualFile = "META-INF/$file" + val version = System.getenv("version") + use { + for (e in entries()) { + if (e.name == actualFile) { + val string = getInputStream(e).readAllBytes().decodeToString() + assertEquals(version, string) + return + } + } + error("File $file not found") + } + } +} diff --git a/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt b/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt deleted file mode 100644 index 8e1b9f99bf..0000000000 --- a/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.validator - -import com.google.gson.* -import org.apache.commons.compress.archivers.tar.* -import org.junit.* -import java.io.* -import java.util.zip.* -import org.junit.Assert.* - -class NpmPublicationValidator { - private val VERSION = System.getenv("deployVersion")!! - private val BUILD_DIR = System.getenv("projectRoot")!! - private val NPM_ARTIFACT = "$BUILD_DIR/kotlinx-coroutines-core/build/npm/kotlinx-coroutines-core-$VERSION.tgz" - - @Test - fun testPackageJson() { - println("Checking dependencies of $NPM_ARTIFACT") - val visited = visit("package.json") { - val json = JsonParser().parse(content()).asJsonObject - assertEquals(VERSION, json["version"].asString) - assertNull(json["dependencies"]) - val peerDependencies = json["peerDependencies"].asJsonObject - assertEquals(1, peerDependencies.size()) - assertNotNull(peerDependencies["kotlin"]) - } - assertEquals(1, visited) - } - - @Test - fun testAtomicfuDependencies() { - println("Checking contents of $NPM_ARTIFACT") - val visited = visit(".js") { - val content = content() - assertFalse(content, content.contains("atomicfu", true)) - assertFalse(content, content.contains("atomicint", true)) - assertFalse(content, content.contains("atomicboolean", true)) - } - assertEquals(2, visited) - } - - private fun InputStream.content(): String { - val bais = ByteArrayOutputStream() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read = read(buffer, 0, DEFAULT_BUFFER_SIZE) - while (read >= 0) { - bais.write(buffer, 0, read) - read = read(buffer, 0, DEFAULT_BUFFER_SIZE) - } - return bais.toString() - } - - private inline fun visit(fileSuffix: String, block: InputStream.(entry: TarArchiveEntry) -> Unit): Int { - var visited = 0 - TarArchiveInputStream(GZIPInputStream(FileInputStream(NPM_ARTIFACT))).use { tais -> - var entry: TarArchiveEntry? = tais.nextTarEntry ?: return 0 - do { - if (entry!!.name.endsWith(fileSuffix)) { - ++visited - tais.block(entry) - } - entry = tais.nextTarEntry - } while (entry != null) - - return visited - } - } -} diff --git a/integration/kotlinx-coroutines-guava/README.md b/integration/kotlinx-coroutines-guava/README.md index 34b8e5818f..b930a6194c 100644 --- a/integration/kotlinx-coroutines-guava/README.md +++ b/integration/kotlinx-coroutines-guava/README.md @@ -62,6 +62,6 @@ Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/L -[com.google.common.util.concurrent.ListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/https://google.github.io/guava/releases/28.0-jre/api/docs/com/google/common/util/concurrent/ListenableFuture.html +[com.google.common.util.concurrent.ListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/https://google.github.io/guava/releases/31.0.1-jre/api/docs/com/google/common/util/concurrent/ListenableFuture.html diff --git a/integration/kotlinx-coroutines-guava/build.gradle.kts b/integration/kotlinx-coroutines-guava/build.gradle.kts index 12a6ca70b7..2a84ca937e 100644 --- a/integration/kotlinx-coroutines-guava/build.gradle.kts +++ b/integration/kotlinx-coroutines-guava/build.gradle.kts @@ -2,10 +2,15 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -val guavaVersion = "28.0-jre" +val guavaVersion = "31.0.1-jre" dependencies { - compile("com.google.guava:guava:$guavaVersion") + api("com.google.guava:guava:$guavaVersion") +} + +java { + targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 } externalDocumentationLink( diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index 8f11e0a916..0820f1f101 100644 --- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt +++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* /** * Starts [block] in a new coroutine and returns a [ListenableFuture] pointing to its result. * - * The coroutine is immediately started. Passing [CoroutineStart.LAZY] to [start] throws + * The coroutine is started immediately. Passing [CoroutineStart.LAZY] to [start] throws * [IllegalArgumentException], because Futures don't have a way to start lazily. * * When the created coroutine [isCompleted][Job.isCompleted], it will try to @@ -35,10 +35,12 @@ import kotlin.coroutines.* * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging * facilities. * - * Note that the error and cancellation semantics of [future] are _subtly different_ than [asListenableFuture]'s. - * In particular, any exception that happens in the coroutine after returned future is - * successfully cancelled will be passed to the [CoroutineExceptionHandler] from the [context]. - * See [ListenableFutureCoroutine] for details. + * Note that the error and cancellation semantics of [future] are _different_ than [async]'s. + * In contrast to [Deferred], [Future] doesn't have an intermediate `Cancelling` state. If + * the returned `Future` is successfully cancelled, and `block` throws afterward, the thrown + * error is dropped, and getting the `Future`'s value will throw a `CancellationException` with + * no cause. This is to match the specification and behavior of + * `java.util.concurrent.FutureTask`. * * @param context added overlaying [CoroutineScope.coroutineContext] to form the new context. * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. @@ -133,10 +135,8 @@ public fun ListenableFuture.asDeferred(): Deferred { // Finally, if this isn't done yet, attach a Listener that will complete the Deferred. val deferred = CompletableDeferred() Futures.addCallback(this, object : FutureCallback { - override fun onSuccess(result: T?) { - // Here we work with flexible types, so we unchecked cast to trick the type system - @Suppress("UNCHECKED_CAST") - runCatching { deferred.complete(result as T) } + override fun onSuccess(result: T) { + runCatching { deferred.complete(result) } .onFailure { handleCoroutineException(EmptyCoroutineContext, it) } } @@ -241,8 +241,8 @@ public suspend fun ListenableFuture.await(): T { return suspendCancellableCoroutine { cont: CancellableContinuation -> addListener( - ToContinuation(this, cont), - MoreExecutors.directExecutor()) + ToContinuation(this, cont), + MoreExecutors.directExecutor()) cont.invokeOnCancellation { cancel(false) } @@ -284,16 +284,13 @@ private class ToContinuation( * By documented contract, a [Future] has been cancelled if * and only if its `isCancelled()` method returns true. * - * Any error that occurs after successfully cancelling a [ListenableFuture] will be passed - * to the [CoroutineExceptionHandler] from the context. The contract of [Future] does not permit - * it to return an error after it is successfully cancelled. - * - * By calling [asListenableFuture] on a [Deferred], any error that occurs after successfully - * cancelling the [ListenableFuture] representation of the [Deferred] will _not_ be passed to - * the [CoroutineExceptionHandler]. Cancelling a [Deferred] places that [Deferred] in the - * cancelling/cancelled states defined by [Job], which _can_ show the error. It's assumed that - * the [Deferred] pointing to the task will be used to observe any error outcome occurring after - * cancellation. + * Any error that occurs after successfully cancelling a [ListenableFuture] is lost. + * The contract of [Future] does not permit it to return an error after it is successfully cancelled. + * On the other hand, we can't report an unhandled exception to [CoroutineExceptionHandler], + * otherwise [Future.cancel] can lead to an app crash which arguably is a contract violation. + * In contrast to [Future] which can't change its outcome after a successful cancellation, + * cancelling a [Deferred] places that [Deferred] in the cancelling/cancelled states defined by [Job], + * which _can_ show the error. * * This may be counterintuitive, but it maintains the error and cancellation contracts of both * the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point @@ -312,10 +309,14 @@ private class ListenableFutureCoroutine( } override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!future.completeExceptionallyOrCancel(cause) && !handled) { - // prevents loss of exception that was not handled by parent & could not be set to JobListenableFuture - handleCoroutineException(context, cause) - } + // Note: if future was cancelled in a race with a cancellation of this + // coroutine, and the future was successfully cancelled first, the cause of coroutine + // cancellation is dropped in this promise. A Future can only be completed once. + // + // This is consistent with FutureTask behaviour. A race between a Future.cancel() and + // a FutureTask.setException() for the same Future will similarly drop the + // cause of a failure-after-cancellation. + future.completeExceptionallyOrCancel(cause) } } @@ -348,7 +349,7 @@ private class JobListenableFuture(private val jobToCancel: Job): ListenableFu * * To preserve Coroutine's [CancellationException], this future points to either `T` or [Cancelled]. */ - private val auxFuture = SettableFuture.create() + private val auxFuture = SettableFuture.create() /** * `true` if [auxFuture.get][ListenableFuture.get] throws [ExecutionException]. @@ -433,7 +434,7 @@ private class JobListenableFuture(private val jobToCancel: Job): ListenableFu } /** See [get()]. */ - private fun getInternal(result: Any): T = if (result is Cancelled) { + private fun getInternal(result: Any?): T = if (result is Cancelled) { throw CancellationException().initCause(result.exception) } else { // We know that `auxFuture` can contain either `T` or `Cancelled`. diff --git a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt index 69ba193071..511b1b0322 100644 --- a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt +++ b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt @@ -555,11 +555,7 @@ class ListenableFutureTest : TestBase() { } @Test - fun testUnhandledExceptionOnExternalCancellation() = runTest( - unhandled = listOf( - { it -> it is TestException } // exception is unhandled because there is no parent - ) - ) { + fun testUnhandledExceptionOnExternalCancellation() = runTest { expect(1) // No parent here (NonCancellable), so nowhere to propagate exception val result = future(NonCancellable + Dispatchers.Unconfined) { @@ -567,7 +563,7 @@ class ListenableFutureTest : TestBase() { delay(Long.MAX_VALUE) } finally { expect(2) - throw TestException() // this exception cannot be handled + throw TestException() // this exception cannot be handled and is set to be lost. } } result.cancel(true) @@ -708,23 +704,6 @@ class ListenableFutureTest : TestBase() { assertEquals(testException, thrown.cause) } - @Test - fun stressTestJobListenableFutureIsCancelledDoesNotThrow() = runTest { - repeat(1000) { - val deferred = CompletableDeferred() - val asListenableFuture = deferred.asListenableFuture() - // We heed two threads to test a race condition. - withContext(Dispatchers.Default) { - val cancellationJob = launch { - asListenableFuture.cancel(false) - } - while (!cancellationJob.isCompleted) { - asListenableFuture.isCancelled // Shouldn't throw. - } - } - } - } - private inline fun ListenableFuture<*>.checkFutureException() { val e = assertFailsWith { get() } val cause = e.cause!! @@ -775,4 +754,61 @@ class ListenableFutureTest : TestBase() { assertEquals(count, completed.get()) } } + + @Test + fun testFuturePropagatesExceptionToParentAfterCancellation() = runTest { + val throwLatch = CompletableDeferred() + val cancelLatch = CompletableDeferred() + val parent = Job() + val scope = CoroutineScope(parent) + val exception = TestException("propagated to parent") + val future = scope.future { + cancelLatch.complete(true) + withContext(NonCancellable) { + throwLatch.await() + throw exception + } + } + cancelLatch.await() + future.cancel(true) + throwLatch.complete(true) + parent.join() + assertTrue(parent.isCancelled) + assertEquals(exception, parent.getCancellationException().cause) + } + + // Stress tests. + + @Test + fun testFutureDoesNotReportToCoroutineExceptionHandler() = runTest { + repeat(1000) { + supervisorScope { // Don't propagate failures in children to parent and other children. + val innerFuture = SettableFuture.create() + val outerFuture = async { innerFuture.await() } + + withContext(Dispatchers.Default) { + launch { innerFuture.setException(TestException("can be lost")) } + launch { outerFuture.cancel() } + // nothing should be reported to CoroutineExceptionHandler, otherwise `Future.cancel` contract violation. + } + } + } + } + + @Test + fun testJobListenableFutureIsCancelledDoesNotThrow() = runTest { + repeat(1000) { + val deferred = CompletableDeferred() + val asListenableFuture = deferred.asListenableFuture() + // We heed two threads to test a race condition. + withContext(Dispatchers.Default) { + val cancellationJob = launch { + asListenableFuture.cancel(false) + } + while (!cancellationJob.isCompleted) { + asListenableFuture.isCancelled // Shouldn't throw. + } + } + } + } } diff --git a/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt index 1d804e5950..b0d72de893 100644 --- a/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt +++ b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt @@ -19,7 +19,6 @@ public fun Stream.consumeAsFlow(): Flow = StreamFlow(this) private class StreamFlow(private val stream: Stream) : Flow { private val consumed = atomic(false) - @InternalCoroutinesApi override suspend fun collect(collector: FlowCollector) { if (!consumed.compareAndSet(false, true)) error("Stream.consumeAsFlow can be collected only once") try { diff --git a/integration/kotlinx-coroutines-play-services/README.md b/integration/kotlinx-coroutines-play-services/README.md index e5e0e613b3..647dafd2c1 100644 --- a/integration/kotlinx-coroutines-play-services/README.md +++ b/integration/kotlinx-coroutines-play-services/README.md @@ -34,6 +34,12 @@ val currentLocationTask = fusedLocationProviderClient.getCurrentLocation(PRIORIT val currentLocation = currentLocationTask.await(cancellationTokenSource) // cancelling `await` also cancels `currentLocationTask`, and vice versa ``` -[asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/as-deferred.html -[await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/await.html -[asTask]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/kotlinx.coroutines.-deferred/as-task.html + + + + +[asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/as-deferred.html +[await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/await.html +[asTask]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/as-task.html + + diff --git a/integration/kotlinx-coroutines-play-services/build.gradle.kts b/integration/kotlinx-coroutines-play-services/build.gradle.kts index 59f3b0bd5a..9f8a128703 100644 --- a/integration/kotlinx-coroutines-play-services/build.gradle.kts +++ b/integration/kotlinx-coroutines-play-services/build.gradle.kts @@ -4,36 +4,17 @@ val tasksVersion = "16.0.1" -val artifactType = Attribute.of("artifactType", String::class.java) -val unpackedAar = Attribute.of("unpackedAar", Boolean::class.javaObjectType) - -configurations.configureEach { - afterEvaluate { - if (isCanBeResolved) { - attributes.attribute(unpackedAar, true) // request all AARs to be unpacked - } - } -} +project.configureAar() dependencies { - attributesSchema { - attribute(unpackedAar) - } - - artifactTypes { - create("aar") { - attributes.attribute(unpackedAar, false) - } - } - - registerTransform(UnpackAar::class.java) { - from.attribute(unpackedAar, false).attribute(artifactType, "aar") - to.attribute(unpackedAar, true).attribute(artifactType, "jar") - } - + configureAarUnpacking() api("com.google.android.gms:play-services-tasks:$tasksVersion") { exclude(group="com.android.support") } + + // Required by robolectric + testImplementation("androidx.test:core:1.2.0") + testImplementation("androidx.test:monitor:1.2.0") } externalDocumentationLink( diff --git a/integration/kotlinx-coroutines-play-services/src/Tasks.kt b/integration/kotlinx-coroutines-play-services/src/Tasks.kt index c37ac7a02d..0451d7beb8 100644 --- a/integration/kotlinx-coroutines-play-services/src/Tasks.kt +++ b/integration/kotlinx-coroutines-play-services/src/Tasks.kt @@ -8,6 +8,8 @@ package kotlinx.coroutines.tasks import com.google.android.gms.tasks.* import kotlinx.coroutines.* +import java.lang.Runnable +import java.util.concurrent.Executor import kotlin.coroutines.* /** @@ -71,7 +73,8 @@ private fun Task.asDeferredImpl(cancellationTokenSource: CancellationToke deferred.completeExceptionally(e) } } else { - addOnCompleteListener { + // Run the callback directly to avoid unnecessarily scheduling on the main thread. + addOnCompleteListener(DirectExecutor) { val e = it.exception if (e == null) { @Suppress("UNCHECKED_CAST") @@ -114,7 +117,8 @@ public suspend fun Task.await(): T = awaitImpl(null) * leads to an unspecified behaviour. */ @ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0 -public suspend fun Task.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource) +public suspend fun Task.await(cancellationTokenSource: CancellationTokenSource): T = + awaitImpl(cancellationTokenSource) private suspend fun Task.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T { // fast path @@ -133,7 +137,8 @@ private suspend fun Task.awaitImpl(cancellationTokenSource: CancellationT } return suspendCancellableCoroutine { cont -> - addOnCompleteListener { + // Run the callback directly to avoid unnecessarily scheduling on the main thread. + addOnCompleteListener(DirectExecutor) { val e = it.exception if (e == null) { @Suppress("UNCHECKED_CAST") @@ -150,3 +155,12 @@ private suspend fun Task.awaitImpl(cancellationTokenSource: CancellationT } } } + +/** + * An [Executor] that just directly executes the [Runnable]. + */ +private object DirectExecutor : Executor { + override fun execute(r: Runnable) { + r.run() + } +} diff --git a/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt b/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt index 6026ffd75d..e286ee197b 100644 --- a/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt +++ b/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt @@ -2,10 +2,17 @@ package android.os import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.util.concurrent.* class Handler(val looper: Looper) { fun post(r: Runnable): Boolean { - GlobalScope.launch { r.run() } + try { + GlobalScope.launch { r.run() } + } catch (e: RejectedExecutionException) { + // Execute leftover callbacks in place for tests + r.run() + } + return true } } diff --git a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt index b125192e93..34fbe23b55 100644 --- a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt +++ b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt @@ -45,8 +45,8 @@ class TaskTest : TestBase() { } @Test - fun testCancelledAsTask() { - val deferred = GlobalScope.async { + fun testCancelledAsTask() = runTest { + val deferred = async(Dispatchers.Default) { delay(100) }.apply { cancel() } @@ -60,8 +60,8 @@ class TaskTest : TestBase() { } @Test - fun testThrowingAsTask() { - val deferred = GlobalScope.async { + fun testThrowingAsTask() = runTest({ e -> e is TestException }) { + val deferred = async(Dispatchers.Default) { throw TestException("Fail") } diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle.kts b/integration/kotlinx-coroutines-slf4j/build.gradle.kts index a341eefe13..3552333311 100644 --- a/integration/kotlinx-coroutines-slf4j/build.gradle.kts +++ b/integration/kotlinx-coroutines-slf4j/build.gradle.kts @@ -3,10 +3,10 @@ */ dependencies { - compile("org.slf4j:slf4j-api:1.7.25") - testCompile("io.github.microutils:kotlin-logging:1.5.4") - testRuntime("ch.qos.logback:logback-classic:1.2.3") - testRuntime("ch.qos.logback:logback-core:1.2.3") + implementation("org.slf4j:slf4j-api:1.7.32") + testImplementation("io.github.microutils:kotlin-logging:2.1.0") + testRuntimeOnly("ch.qos.logback:logback-classic:1.2.7") + testRuntimeOnly("ch.qos.logback:logback-core:1.2.7") } externalDocumentationLink( diff --git a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt index 9528f2b22d..0fbfece600 100644 --- a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt +++ b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt @@ -28,7 +28,7 @@ public typealias MDCContextMap = Map? * } * ``` * - * Note that you cannot update MDC context from inside of the coroutine simply + * Note that you cannot update MDC context from inside the coroutine simply * using [MDC.put]. These updates are going to be lost on the next suspension and * reinstalled to the MDC context that was captured or explicitly specified in * [contextMap] when this object was created on the next resumption. @@ -43,6 +43,7 @@ public class MDCContext( /** * The value of [MDC] context map. */ + @Suppress("MemberVisibilityCanBePrivate") public val contextMap: MDCContextMap = MDC.getCopyOfContextMap() ) : ThreadContextElement, AbstractCoroutineContextElement(Key) { /** diff --git a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt index 7d18359c5d..532c47e9ed 100644 --- a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt +++ b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt @@ -102,9 +102,10 @@ class MDCContextTest : TestBase() { val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!! withContext(Dispatchers.Default + MDCContext()) { assertEquals("myValue", MDC.get("myKey")) + assertEquals("myValue", coroutineContext[MDCContext]?.contextMap?.get("myKey")) withContext(mainDispatcher) { assertEquals("myValue", MDC.get("myKey")) } } } -} \ No newline at end of file +} diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index d4e530b04a..67c6ef04e7 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -8,7 +8,7 @@ import kotlinx.html.div import kotlinx.html.dom.* import kotlinx.html.js.onClickFunction import org.w3c.dom.* -import kotlin.browser.* +import kotlinx.browser.* import kotlin.coroutines.* import kotlin.math.* import kotlin.random.Random diff --git a/kotlinx-coroutines-core/README.md b/kotlinx-coroutines-core/README.md index c21e5048f6..38a112e89d 100644 --- a/kotlinx-coroutines-core/README.md +++ b/kotlinx-coroutines-core/README.md @@ -57,7 +57,6 @@ helper function. [NonCancellable] job object is provided to suppress cancellatio | [SendChannel][kotlinx.coroutines.channels.SendChannel] | [send][kotlinx.coroutines.channels.SendChannel.send] | [onSend][kotlinx.coroutines.channels.SendChannel.onSend] | [trySend][kotlinx.coroutines.channels.SendChannel.trySend] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.channels.ReceiveChannel.onReceive] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receiveCatching][kotlinx.coroutines.channels.receiveCatching] | [onReceiveCatching][kotlinx.coroutines.channels.onReceiveCatching] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive] -| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | [onLock][kotlinx.coroutines.sync.Mutex.onLock] | [tryLock][kotlinx.coroutines.sync.Mutex.tryLock] | none | [delay][kotlinx.coroutines.delay] | [onTimeout][kotlinx.coroutines.selects.SelectBuilder.onTimeout] | none # Package kotlinx.coroutines @@ -84,10 +83,6 @@ Select expression to perform multiple suspending operations simultaneously until Low-level primitives for finer-grained control of coroutines. -# Package kotlinx.coroutines.test - -Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-coroutines-test` module. - @@ -121,8 +116,6 @@ Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-corout [kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html -[kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html -[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 50bfb60d62..d227eb879b 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -140,11 +140,24 @@ public final class kotlinx/coroutines/CompletionHandlerException : java/lang/Run public fun (Ljava/lang/String;Ljava/lang/Throwable;)V } +public abstract interface class kotlinx/coroutines/CopyableThreadContextElement : kotlinx/coroutines/ThreadContextElement { + public abstract fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement; + public abstract fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext; +} + +public final class kotlinx/coroutines/CopyableThreadContextElement$DefaultImpls { + public static fun fold (Lkotlinx/coroutines/CopyableThreadContextElement;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun get (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; + public static fun minusKey (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; + public static fun plus (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; +} + public abstract interface class kotlinx/coroutines/CopyableThrowable { public abstract fun createCopy ()Ljava/lang/Throwable; } public final class kotlinx/coroutines/CoroutineContextKt { + public static final fun newCoroutineContext (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; public static final fun newCoroutineContext (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; } @@ -156,6 +169,7 @@ public abstract class kotlinx/coroutines/CoroutineDispatcher : kotlin/coroutines public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public final fun interceptContinuation (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; public fun isDispatchNeeded (Lkotlin/coroutines/CoroutineContext;)Z + public fun limitedParallelism (I)Lkotlinx/coroutines/CoroutineDispatcher; public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; public final fun plus (Lkotlinx/coroutines/CoroutineDispatcher;)Lkotlinx/coroutines/CoroutineDispatcher; public final fun releaseInterceptedContinuation (Lkotlin/coroutines/Continuation;)V @@ -279,6 +293,7 @@ public final class kotlinx/coroutines/Dispatchers { public static final fun getIO ()Lkotlinx/coroutines/CoroutineDispatcher; public static final fun getMain ()Lkotlinx/coroutines/MainCoroutineDispatcher; public static final fun getUnconfined ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun shutdown ()V } public final class kotlinx/coroutines/DispatchersKt { @@ -367,7 +382,6 @@ public final class kotlinx/coroutines/Job$Key : kotlin/coroutines/CoroutineConte } public final class kotlinx/coroutines/JobKt { - public static final fun DisposableHandle (Lkotlin/jvm/functions/Function0;)Lkotlinx/coroutines/DisposableHandle; public static final fun Job (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/CompletableJob; public static final synthetic fun Job (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job; public static synthetic fun Job$default (Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableJob; @@ -447,6 +461,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public abstract class kotlinx/coroutines/MainCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher { public fun ()V public abstract fun getImmediate ()Lkotlinx/coroutines/MainCoroutineDispatcher; + public fun limitedParallelism (I)Lkotlinx/coroutines/CoroutineDispatcher; public fun toString ()Ljava/lang/String; protected final fun toStringInternalImpl ()Ljava/lang/String; } @@ -543,6 +558,15 @@ public final class kotlinx/coroutines/TimeoutKt { public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement { + public static final field Key Lkotlinx/coroutines/YieldContext$Key; + public field dispatcherWasUnconfined Z + public fun ()V +} + +public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key { +} + public final class kotlinx/coroutines/YieldKt { public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -887,8 +911,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun asSharedFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun asStateFlow (Lkotlinx/coroutines/flow/MutableStateFlow;)Lkotlinx/coroutines/flow/StateFlow; - public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; public static final synthetic fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun buffer (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -899,7 +921,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun catch (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun channelFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final synthetic fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun collectIndexed (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun collectLatest (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun combine (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; @@ -958,10 +980,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun flowOf (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOf ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowViaChannel (ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flowViaChannel$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowWith (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flowWith$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun fold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun forEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun getDEFAULT_CONCURRENCY ()I @@ -978,8 +996,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onEmpty (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static final fun onErrorCollect (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun onErrorCollect$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorResume (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorResumeNext (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -995,9 +1011,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun replay (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun replay (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; - public static final synthetic fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun retry (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun retryWhen (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow; public static final fun runningFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; @@ -1061,6 +1075,7 @@ public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotli } public abstract interface class kotlinx/coroutines/flow/SharedFlow : kotlinx/coroutines/flow/Flow { + public abstract fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getReplayCache ()Ljava/util/List; } @@ -1284,36 +1299,3 @@ public final class kotlinx/coroutines/sync/SemaphoreKt { public static final fun withPermit (Lkotlinx/coroutines/sync/Semaphore;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/test/TestCoroutineContext : kotlin/coroutines/CoroutineContext { - public fun ()V - public fun (Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun advanceTimeBy (JLjava/util/concurrent/TimeUnit;)J - public static synthetic fun advanceTimeBy$default (Lkotlinx/coroutines/test/TestCoroutineContext;JLjava/util/concurrent/TimeUnit;ILjava/lang/Object;)J - public final fun advanceTimeTo (JLjava/util/concurrent/TimeUnit;)V - public static synthetic fun advanceTimeTo$default (Lkotlinx/coroutines/test/TestCoroutineContext;JLjava/util/concurrent/TimeUnit;ILjava/lang/Object;)V - public final fun assertAllUnhandledExceptions (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun assertAllUnhandledExceptions$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun assertAnyUnhandledException (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun assertAnyUnhandledException$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun assertExceptions (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun assertExceptions$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun assertUnhandledException (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun assertUnhandledException$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun cancelAllActions ()V - public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; - public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; - public final fun getExceptions ()Ljava/util/List; - public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; - public final fun now (Ljava/util/concurrent/TimeUnit;)J - public static synthetic fun now$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/util/concurrent/TimeUnit;ILjava/lang/Object;)J - public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; - public fun toString ()Ljava/lang/String; - public final fun triggerActions ()V -} - -public final class kotlinx/coroutines/test/TestCoroutineContextKt { - public static final fun withTestContext (Lkotlinx/coroutines/test/TestCoroutineContext;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun withTestContext$default (Lkotlinx/coroutines/test/TestCoroutineContext;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V -} - diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index c45ca08cef..9791b445bf 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -14,6 +14,8 @@ if (rootProject.ext.native_targets_enabled) { apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") apply from: rootProject.file('gradle/publish-npm-js.gradle') +apply from: rootProject.file('gradle/dokka.gradle.kts') +apply from: rootProject.file('gradle/publish.gradle') /* ========================================================================== Configure source sets structure for kotlinx-coroutines-core: @@ -70,39 +72,65 @@ if (rootProject.ext.native_targets_enabled) { * because JMV-only projects depend on core, thus core should always be initialized before configuration. */ kotlin { - configure(sourceSets) { - def srcDir = name.endsWith('Main') ? 'src' : 'test' - def platform = name[0..-5] - kotlin.srcDirs = ["$platform/$srcDir"] - if (name == "jvmMain") { - resources.srcDirs = ["$platform/resources"] - } else if (name == "jvmTest") { - resources.srcDirs = ["$platform/test-resources"] + sourceSets.forEach { + SourceSetsKt.configureMultiplatform(it) + } + + /* + * Configure four test runs: + * 1) Old memory model, Main thread + * 2) New memory model, Main thread + * 3) Old memory model, BG thread + * 4) New memory model, BG thread (required for Dispatchers.Main tests on Darwin) + * + * All new MM targets are build with optimize = true to have stress tests properly run. + */ + targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithTests.class).configureEach { + binaries { + // Test for memory leaks using a special entry point that does not exit but returns from main + binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] } - languageSettings { - progressiveMode = true - optInAnnotations.each { useExperimentalAnnotation(it) } + + binaries.test("newMM", [DEBUG]) { + def thisTest = it + freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] + optimized = true + binaryOptions["memoryModel"] = "experimental" + testRuns.create("newMM") { + setExecutionSourceFrom(thisTest) + // A hack to get different suffixes in the aggregated report. + executionTask.configure { targetName = "$targetName new MM" } + } } - } - configure(targets) { - // Configure additional binaries and test runs -- one for each OS - if (["macos", "linux", "mingw"].any { name.startsWith(it) }) { - binaries { - // Test for memory leaks using a special entry point that does not exit but returns from main - binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] - // Configure a separate test where code runs in background - test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) { - freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] - } + binaries.test("worker", [DEBUG]) { + def thisTest = it + freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] + testRuns.create("worker") { + setExecutionSourceFrom(thisTest) + executionTask.configure { targetName = "$targetName worker" } } - testRuns { - background { setExecutionSourceFrom(binaries.backgroundDebugTest) } + } + + binaries.test("workerWithNewMM", [DEBUG]) { + def thisTest = it + optimized = true + freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] + binaryOptions["memoryModel"] = "experimental" + testRuns.create("workerWithNewMM") { + setExecutionSourceFrom(thisTest) + executionTask.configure { targetName = "$targetName worker with new MM" } } } } + + jvm { + // For animal sniffer + withJava() + } } + configurations { configureKotlinJvmPlatform(kotlinCompilerPluginClasspath) } @@ -159,28 +187,10 @@ kotlin.sourceSets { jvmTest.dependencies { api "org.jetbrains.kotlinx:lincheck:$lincheck_version" api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version" - api "com.esotericsoftware:kryo:4.0.0" implementation project(":android-unit-tests") } } -task checkJdk16() { - // only fail w/o JDK_16 when actually trying to compile, not during project setup phase - doLast { - if (!System.env.JDK_16) { - throw new GradleException("JDK_16 environment variable is not defined. " + - "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + - "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") - } - } -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { - kotlinOptions.jdkHome = System.env.JDK_16 - // only fail when actually trying to compile, not during project setup phase - dependsOn(checkJdk16) -} - jvmTest { minHeapSize = '1g' maxHeapSize = '1g' @@ -246,30 +256,37 @@ task jvmLincheckTest(type: Test, dependsOn: compileTestKotlinJvm) { static void configureJvmForLincheck(task) { task.minHeapSize = '1g' - task.maxHeapSize = '6g' // we may need more space for building an interleaving tree in the model checking mode + task.maxHeapSize = '4g' // we may need more space for building an interleaving tree in the model checking mode task.jvmArgs = ['--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED', // required for transformation - '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode + '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode task.systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2' task.systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '1' // better for the model checking mode } -task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { - classpath = files { jvmTest.classpath } - testClassesDirs = files { jvmTest.testClassesDirs } - executable = "$System.env.JDK_16/bin/java" - exclude '**/*LFStressTest.*' // lock-freedom tests use LockFreedomTestEnvironment which needs JDK8 - exclude '**/*LincheckTest.*' // Lincheck tests use LinChecker which needs JDK8 - exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8 - exclude '**/ExceptionsGuideTest.*' - exclude '**/RunInterruptibleStressTest.*' // fails on JDK 1.6 due to JDK bug +// Always check additional test sets +task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest]) +check.dependsOn moreTest + +tasks.jvmLincheckTest { + kover { + enabled = false // Always disabled, lincheck doesn't really support coverage + } } -// Run jdk16Test test only during nightly stress test -jdk16Test.onlyIf { project.properties['stressTest'] != null } +def commonKoverExcludes = + ["kotlinx.coroutines.debug.*", // Tested by debug module + "kotlinx.coroutines.channels.ChannelsKt__DeprecatedKt.*", // Deprecated + "kotlinx.coroutines.scheduling.LimitingDispatcher", // Deprecated + "kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher" // Deprecated + ] -// Always check additional test sets -task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest, jdk16Test]) -check.dependsOn moreTest +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} task testsJar(type: Jar, dependsOn: jvmTestClasses) { classifier = 'tests' diff --git a/kotlinx-coroutines-core/common/README.md b/kotlinx-coroutines-core/common/README.md index fcfe334c62..b09c44c75e 100644 --- a/kotlinx-coroutines-core/common/README.md +++ b/kotlinx-coroutines-core/common/README.md @@ -60,17 +60,12 @@ helper function. [NonCancellable] job object is provided to suppress cancellatio | [SendChannel][kotlinx.coroutines.channels.SendChannel] | [send][kotlinx.coroutines.channels.SendChannel.send] | [onSend][kotlinx.coroutines.channels.SendChannel.onSend] | [trySend][kotlinx.coroutines.channels.SendChannel.trySend] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.channels.ReceiveChannel.onReceive] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receiveCatching][kotlinx.coroutines.channels.ReceiveChannel.receiveCatching] | [onReceiveCatching][kotlinx.coroutines.channels.ReceiveChannel.onReceiveCatching] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive] -| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | [onLock][kotlinx.coroutines.sync.Mutex.onLock] | [tryLock][kotlinx.coroutines.sync.Mutex.tryLock] | none | [delay] | [onTimeout][kotlinx.coroutines.selects.SelectBuilder.onTimeout] | none This module provides debugging facilities for coroutines (run JVM with `-ea` or `-Dkotlinx.coroutines.debug` options) and [newCoroutineContext] function to write user-defined coroutine builders that work with these debugging facilities. See [DEBUG_PROPERTY_NAME] for more details. -This module provides a special CoroutineContext type [TestCoroutineCoroutineContext][kotlinx.coroutines.test.TestCoroutineContext] that -allows the writer of code that contains Coroutines with delays and timeouts to write non-flaky unit-tests for that code allowing these tests to -terminate in near zero time. See the documentation for this class for more information. - # Package kotlinx.coroutines General-purpose coroutine builders, contexts, and helper functions. @@ -131,8 +126,6 @@ Low-level primitives for finer-grained control of coroutines. [kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html -[kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html -[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html @@ -157,8 +150,4 @@ Low-level primitives for finer-grained control of coroutines. [kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html [kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/on-timeout.html - - -[kotlinx.coroutines.test.TestCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.test/-test-coroutine-context/index.html - diff --git a/kotlinx-coroutines-core/common/src/Annotations.kt b/kotlinx-coroutines-core/common/src/Annotations.kt index 724cc8cb87..bacce39408 100644 --- a/kotlinx-coroutines-core/common/src/Annotations.kt +++ b/kotlinx-coroutines-core/common/src/Annotations.kt @@ -30,6 +30,19 @@ public annotation class DelicateCoroutinesApi */ @MustBeDocumented @Retention(value = AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.TYPEALIAS +) @RequiresOptIn(level = RequiresOptIn.Level.WARNING) public annotation class ExperimentalCoroutinesApi diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt index e06ed33025..c1669e2554 100644 --- a/kotlinx-coroutines-core/common/src/Await.kt +++ b/kotlinx-coroutines-core/common/src/Await.kt @@ -29,8 +29,8 @@ public suspend fun awaitAll(vararg deferreds: Deferred): List = * 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, diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index a11ffe9eb4..3dea68cfde 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -126,12 +126,15 @@ private class LazyDeferredCoroutine( * This suspending function is cancellable. It immediately checks for cancellation of * the resulting context and throws [CancellationException] if it is not [active][CoroutineContext.isActive]. * - * This function uses dispatcher from the new context, shifting execution of the [block] into the - * different thread if a new dispatcher is specified, and back to the original dispatcher - * when it completes. Note that the result of `withContext` invocation is - * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**, - * which means that if the original [coroutineContext], in which `withContext` was invoked, - * is cancelled by the time its dispatcher starts to execute the code, + * Calls to [withContext] whose [context] argument provides a [CoroutineDispatcher] that is + * different from the current one, by necessity, perform additional dispatches: the [block] + * can not be executed immediately and needs to be dispatched for execution on + * the passed [CoroutineDispatcher], and then when the [block] completes, the execution + * has to shift back to the original dispatcher. + * + * Note that the result of `withContext` invocation is dispatched into the original context in a cancellable way + * with a **prompt cancellation guarantee**, which means that if the original [coroutineContext] + * in which `withContext` was invoked is cancelled by the time its dispatcher starts to execute the code, * it discards the result of `withContext` and throws [CancellationException]. * * The cancellation behaviour described above is enabled if and only if the dispatcher is being changed. @@ -148,7 +151,8 @@ public suspend fun withContext( return suspendCoroutineUninterceptedOrReturn sc@ { uCont -> // compute new context val oldContext = uCont.context - val newContext = oldContext + context + // Copy CopyableThreadContextElement if necessary + val newContext = oldContext.newCoroutineContext(context) // always check for cancellation of new context newContext.ensureActive() // FAST PATH #1 -- new context is the same as the old one diff --git a/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt new file mode 100644 index 0000000000..9c6703291a --- /dev/null +++ b/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +/** + * [CoroutineDispatcher] that provides a method to close it, + * causing the rejection of any new tasks and cleanup of all underlying resources + * associated with the current dispatcher. + * Examples of closeable dispatchers are dispatchers backed by `java.lang.Executor` and + * by `kotlin.native.Worker`. + * + * **The `CloseableCoroutineDispatcher` class is not stable for inheritance in 3rd party libraries**, as new methods + * might be added to this interface in the future, but is stable for use. + */ +@ExperimentalCoroutinesApi +public expect abstract class CloseableCoroutineDispatcher() : CoroutineDispatcher { + + /** + * Initiate the closing sequence of the coroutine dispatcher. + * After a successful call to [close], no new tasks will + * be accepted to be [dispatched][dispatch], but the previously dispatched tasks will be run. + * + * Invocations of `close` are idempotent and thread-safe. + */ + public abstract fun close() +} diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt index 68b4b1a393..9153f39821 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -7,13 +7,20 @@ package kotlinx.coroutines import kotlin.coroutines.* /** - * Creates a context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher or - * [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on). + * Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or + * [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on) + * and copyable-thread-local facilities on JVM. */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext -internal expect fun createDefaultDispatcher(): CoroutineDispatcher +/** + * Creates a context for coroutine builder functions that do not launch a new coroutine, e.g. [withContext]. + * @suppress + */ +@InternalCoroutinesApi +public expect fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext +@PublishedApi @Suppress("PropertyName") internal expect val DefaultDelay: Delay diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index d5613d4110..71b7ec726f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -61,6 +61,46 @@ public abstract class CoroutineDispatcher : */ public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true + /** + * Creates a view of the current dispatcher that limits the parallelism to the given [value][parallelism]. + * The resulting view uses the original dispatcher for execution, but with the guarantee that + * no more than [parallelism] coroutines are executed at the same time. + * + * This method does not impose restrictions on the number of views or the total sum of parallelism values, + * each view controls its own parallelism independently with the guarantee that the effective parallelism + * of all views cannot exceed the actual parallelism of the original dispatcher. + * + * ### Limitations + * + * The default implementation of `limitedParallelism` does not support direct dispatchers, + * such as executing the given runnable in place during [dispatch] calls. + * Any dispatcher that may return `false` from [isDispatchNeeded] is considered direct. + * For direct dispatchers, it is recommended to override this method + * and provide a domain-specific implementation or to throw an [UnsupportedOperationException]. + * + * ### Example of usage + * ``` + * private val backgroundDispatcher = newFixedThreadPoolContext(4, "App Background") + * // At most 2 threads will be processing images as it is really slow and CPU-intensive + * private val imageProcessingDispatcher = backgroundDispatcher.limitedParallelism(2) + * // At most 3 threads will be processing JSON to avoid image processing starvation + * private val jsonProcessingDispatcher = backgroundDispatcher.limitedParallelism(3) + * // At most 1 thread will be doing IO + * private val fileWriterDispatcher = backgroundDispatcher.limitedParallelism(1) + * ``` + * Note how in this example the application has an executor with 4 threads, but the total sum of all limits + * is 6. Still, at most 4 coroutines can be executed simultaneously as each view limits only its own parallelism. + * + * Note that this example was structured in such a way that it illustrates the parallelism guarantees. + * In practice, it is usually better to use [Dispatchers.IO] or [Dispatchers.Default] instead of creating a + * `backgroundDispatcher`. It is both possible and advised to call `limitedParallelism` on them. + */ + @ExperimentalCoroutinesApi + public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + return LimitedDispatcher(this, parallelism) + } + /** * Dispatches execution of a runnable [block] onto another thread in the given [context]. * This method should guarantee that the given [block] will be eventually invoked, diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 3ed233bfb9..b0928d5c58 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -12,7 +12,7 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* /** - * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc) + * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc.) * is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext] * to automatically propagate all its elements and cancellation. * @@ -28,8 +28,8 @@ import kotlin.coroutines.intrinsics.* * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a * [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation. * - * Every coroutine builder (like [launch], [async], etc) - * and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope + * Every coroutine builder (like [launch], [async], and others) + * and every scoping function (like [coroutineScope] and [withContext]) provides _its own_ scope * with its own [Job] instance into the inner block of code it runs. * By convention, they all wait for all the coroutines inside their block to complete before completing themselves, * thus enforcing the structured concurrency. See [Job] documentation for more details. @@ -42,14 +42,14 @@ import kotlin.coroutines.intrinsics.* * ### Custom usage * * `CoroutineScope` should be declared as a property on entities with a well-defined lifecycle that are - * responsible for launching children coroutines. The corresponding instance of `CoroutineScope` shall be created - * with either `CoroutineScope()` or `MainScope()` functions. The difference between them is only in the - * [CoroutineDispatcher]: + * responsible for launching child coroutines. The corresponding instance of `CoroutineScope` shall be created + * with either `CoroutineScope()` or `MainScope()`: * - * * `CoroutineScope()` uses [Dispatchers.Default] for its coroutines. - * * `MainScope()` uses [Dispatchers.Main] for its coroutines. + * * `CoroutineScope()` uses the [context][CoroutineContext] provided to it as a parameter for its coroutines + * and adds a [Job] if one is not provided as part of the context. + * * `MainScope()` uses [Dispatchers.Main] for its coroutines and has a [SupervisorJob]. * - * **The key part of custom usage of `CustomScope` is cancelling it at the end of the lifecycle.** + * **The key part of custom usage of `CoroutineScope` is cancelling it at the end of the lifecycle.** * The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines * is no longer needed. It cancels all the coroutines that might still be running on behalf of it. * @@ -178,7 +178,7 @@ public val CoroutineScope.isActive: Boolean * ``` * // concurrently load configuration and data * suspend fun loadConfigurationAndData() { - * coroutinesScope { + * coroutineScope { * launch { loadConfiguration() } * launch { loadData() } * } @@ -269,8 +269,8 @@ public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R * Creates a [CoroutineScope] that wraps the given coroutine [context]. * * If the given [context] does not contain a [Job] element, then a default `Job()` is created. - * This way, cancellation or failure of any child coroutine in this scope cancels all the other children, - * just like inside [coroutineScope] block. + * This way, failure of any child coroutine in this scope or [cancellation][CoroutineScope.cancel] of the scope itself + * cancels all the scope's children, just like inside [coroutineScope] block. */ @Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index 4543c5dda1..301ed2d322 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -19,15 +19,12 @@ import kotlin.time.* */ @InternalCoroutinesApi public interface Delay { - /** - * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. - * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function - * immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. - */ + + /** @suppress **/ + @Deprecated( + message = "Deprecated without replacement as an internal method never intended for public use", + level = DeprecationLevel.ERROR + ) // Error since 1.6.0 public suspend fun delay(time: Long) { if (time <= 0) return // don't delay return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) } @@ -54,8 +51,6 @@ public interface Delay { * Schedules invocation of a specified [block] after a specified delay [timeMillis]. * The resulting [DisposableHandle] can be used to [dispose][DisposableHandle.dispose] of this invocation * request if it is not needed anymore. - * - * This implementation uses a built-in single-threaded scheduled executor service. */ public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = DefaultDelay.invokeOnTimeout(timeMillis, block, context) @@ -138,7 +133,6 @@ public suspend fun delay(timeMillis: Long) { * * Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context. */ -@ExperimentalTime public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMillis()) /** Returns [Delay] implementation of the given context */ @@ -148,6 +142,5 @@ internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) * Convert this duration to its millisecond value. * Positive durations are coerced at least `1`. */ -@ExperimentalTime internal fun Duration.toDelayMillis(): Long = if (this > Duration.ZERO) inWholeMilliseconds.coerceAtLeast(1) else 0 diff --git a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt index 8681b182d8..28e67a423d 100644 --- a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt +++ b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt @@ -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: @@ -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. * @@ -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]. diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt index e6a57c927a..12940c54e2 100644 --- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt +++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt @@ -115,7 +115,12 @@ internal abstract class EventLoop : CoroutineDispatcher() { } } - protected open fun shutdown() {} + final override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + return this + } + + open fun shutdown() {} } @ThreadLocal @@ -231,8 +236,13 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { if (timeNanos < MAX_DELAY_NS) { val now = nanoTime() DelayedResumeTask(now + timeNanos, continuation).also { task -> - continuation.disposeOnCancellation(task) + /* + * Order is important here: first we schedule the heap and only then + * publish it to continuation. Otherwise, `DelayedResumeTask` would + * have to know how to be disposed of even when it wasn't scheduled yet. + */ schedule(now, task) + continuation.disposeOnCancellation(task) } } } @@ -271,7 +281,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { // then process one event from queue val task = dequeue() if (task != null) { - task.run() + platformAutoreleasePool { task.run() } return 0 } return nextTime @@ -279,7 +289,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block) - public fun enqueue(task: Runnable) { + open fun enqueue(task: Runnable) { if (enqueueImpl(task)) { // todo: we should unpark only when this delayed task became first in the queue unpark() @@ -405,6 +415,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { */ @JvmField var nanoTime: Long ) : Runnable, Comparable, DisposableHandle, ThreadSafeHeapNode { + @Volatile private var _heap: Any? = null // null | ThreadSafeHeap | DISPOSED_TASK override var heap: ThreadSafeHeap<*>? @@ -526,3 +537,13 @@ internal expect object DefaultExecutor { public fun enqueue(task: Runnable) } +/** + * Used by Darwin targets to wrap a [Runnable.run] call in an Objective-C Autorelease Pool. It is a no-op on JVM, JS and + * non-Darwin native targets. + * + * Coroutines on Darwin targets can call into the Objective-C world, where a callee may push a to-be-returned object to + * the Autorelease Pool, so as to avoid a premature ARC release before it reaches the caller. This means the pool must + * be eventually drained to avoid leaks. Since Kotlin Coroutines does not use [NSRunLoop], which provides automatic + * pool management, it must manage the pool creation and pool drainage manually. + */ +internal expect inline fun platformAutoreleasePool(crossinline block: () -> Unit) diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 9552153aa9..31d90eeef0 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -387,7 +387,7 @@ public fun Job0(parent: Job? = null): Job = Job(parent) /** * A handle to an allocated object that can be disposed to make it eligible for garbage collection. */ -public interface DisposableHandle { +public fun interface DisposableHandle { /** * Disposes the corresponding object, making it eligible for garbage collection. * Repeated invocation of this function has no effect. @@ -395,18 +395,6 @@ public interface DisposableHandle { public fun dispose() } -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@Suppress("FunctionName") -@InternalCoroutinesApi -public inline fun DisposableHandle(crossinline block: () -> Unit): DisposableHandle = - object : DisposableHandle { - override fun dispose() { - block() - } - } - // -------------------- Parent-child communication -------------------- /** diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt index 602da6e0b5..a7065ccd15 100644 --- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* + /** * Base class for special [CoroutineDispatcher] which is confined to application "Main" or "UI" thread * and used for any UI-based activities. Instance of `MainDispatcher` can be obtained by [Dispatchers.Main]. @@ -51,6 +53,12 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { */ override fun toString(): String = toStringInternalImpl() ?: "$classSimpleName@$hexAddress" + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + // MainCoroutineDispatcher is single-threaded -- short-circuit any attempts to limit it + return this + } + /** * Internal method for more specific [toString] implementations. It returns non-null * string if this dispatcher is set in the platform as the main one. diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 264a2b9d1b..46ab4ae8c8 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -64,7 +64,6 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ -@ExperimentalTime public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) @@ -131,7 +130,6 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout * * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ -@ExperimentalTime public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? = withTimeoutOrNull(timeout.toDelayMillis(), block) diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index 4f48645895..5837ae83f3 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -11,10 +11,16 @@ import kotlin.jvm.* * A coroutine dispatcher that is not confined to any specific thread. */ internal object Unconfined : CoroutineDispatcher() { + + @ExperimentalCoroutinesApi + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + throw UnsupportedOperationException("limitedParallelism is not supported for Dispatchers.Unconfined") + } + override fun isDispatchNeeded(context: CoroutineContext): Boolean = false override fun dispatch(context: CoroutineContext, block: Runnable) { - // It can only be called by the "yield" function. See also code of "yield" function. + /** It can only be called by the [yield] function. See also code of [yield] function. */ val yieldContext = context[YieldContext] if (yieldContext != null) { // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" @@ -32,6 +38,7 @@ internal object Unconfined : CoroutineDispatcher() { /** * Used to detect calls to [Unconfined.dispatch] from [yield] function. */ +@PublishedApi internal class YieldContext : AbstractCoroutineContextElement(Key) { companion object Key : CoroutineContext.Key diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 4751296c87..b92ced6ab7 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -136,7 +136,7 @@ internal abstract class AbstractSendChannel( return sendSuspend(element) } - @Suppress("DEPRECATION") + @Suppress("DEPRECATION", "DEPRECATION_ERROR") override fun offer(element: E): Boolean { // Temporary migration for offer users who rely on onUndeliveredElement try { diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 600eb6a951..0a96f75380 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -33,6 +33,11 @@ internal class ArrayBroadcastChannel( require(capacity >= 1) { "ArrayBroadcastChannel capacity must be at least 1, but $capacity was specified" } } + /** + * NB: prior to changing any logic of ArrayBroadcastChannel internals, please ensure that + * you do not break internal invariants of the SubscriberList implementation on K/N and KJS + */ + /* * Writes to buffer are guarded by bufferLock, but reads from buffer are concurrent with writes * - Write element to buffer then write "tail" (volatile) @@ -60,6 +65,7 @@ internal class ArrayBroadcastChannel( get() = _size.value set(value) { _size.value = value } + @Suppress("DEPRECATION") private val subscribers = subscriberList>() override val isBufferAlwaysFull: Boolean get() = false diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index b15c4262ef..5ad79fdcff 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -64,7 +64,6 @@ public interface SendChannel { */ public val onSend: SelectClause2> - /** * Immediately adds the specified [element] to this channel, if this doesn't violate its capacity restrictions, * and returns the successful result. Otherwise, returns failed or closed result. @@ -158,10 +157,10 @@ public interface SendChannel { * @suppress **Deprecated**. */ @Deprecated( - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, message = "Deprecated in the favour of 'trySend' method", replaceWith = ReplaceWith("trySend(element).isSuccess") - ) // Warning since 1.5.0 + ) // Warning since 1.5.0, error since 1.6.0 public fun offer(element: E): Boolean { val result = trySend(element) if (result.isSuccess) return true @@ -314,12 +313,12 @@ public interface ReceiveChannel { * @suppress **Deprecated**. */ @Deprecated( - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, message = "Deprecated in the favour of 'tryReceive'. " + "Please note that the provided replacement does not rethrow channel's close cause as 'poll' did, " + "for the precise replacement please refer to the 'poll' documentation", replaceWith = ReplaceWith("tryReceive().getOrNull()") - ) // Warning since 1.5.0 + ) // Warning since 1.5.0, error since 1.6.0 public fun poll(): E? { val result = tryReceive() if (result.isSuccess) return result.getOrThrow() @@ -365,7 +364,7 @@ public interface ReceiveChannel { message = "Deprecated in favor of onReceiveCatching extension", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("onReceiveCatching") - ) // Warning since 1.3.0, error in 1.5.0, will be hidden or removed in 1.6.0 + ) // Warning since 1.3.0, error in 1.5.0, will be hidden or removed in 1.7.0 public val onReceiveOrNull: SelectClause1 get() { return object : SelectClause1 { @@ -685,7 +684,7 @@ public interface ChannelIterator { * 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. * diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index e0b4f9d2a5..a78e2f186d 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -50,7 +50,7 @@ public inline fun BroadcastChannel.consume(block: ReceiveChannel.() @Deprecated( "Deprecated in the favour of 'receiveCatching'", ReplaceWith("receiveCatching().getOrNull()"), - DeprecationLevel.WARNING + DeprecationLevel.ERROR ) // Warning since 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0 @Suppress("EXTENSION_SHADOWED_BY_MEMBER") public suspend fun ReceiveChannel.receiveOrNull(): E? { @@ -63,7 +63,7 @@ public suspend fun ReceiveChannel.receiveOrNull(): E? { */ @Deprecated( "Deprecated in the favour of 'onReceiveCatching'", - level = DeprecationLevel.WARNING + level = DeprecationLevel.ERROR ) // Warning since 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0 public fun ReceiveChannel.onReceiveOrNull(): SelectClause1 { @Suppress("DEPRECATION", "UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index f7f60cf97d..177e80cb49 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.selects.* */ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = false - protected final override val isBufferEmpty: Boolean get() = value === EMPTY + protected final override val isBufferEmpty: Boolean get() = lock.withLock { value === EMPTY } protected final override val isBufferAlwaysFull: Boolean get() = false protected final override val isBufferFull: Boolean get() = false @@ -139,5 +139,5 @@ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredEleme // ------ debug ------ override val bufferDebugString: String - get() = "(value=$value)" + get() = lock.withLock { "(value=$value)" } } diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 3342fb6ec9..da8f884be1 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -6,14 +6,11 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlin.coroutines.* +import kotlinx.coroutines.flow.* /** - * Scope for the [produce][CoroutineScope.produce] coroutine builder. - * - * **Note: This is an experimental api.** Behavior of producers that work as children in a parent scope with respect - * to cancellation and error handling may change in the future. + * Scope for the [produce][CoroutineScope.produce], [callbackFlow] and [channelFlow] builders. */ -@ExperimentalCoroutinesApi public interface ProducerScope : CoroutineScope, SendChannel { /** * A reference to the channel this coroutine [sends][send] elements to. @@ -45,7 +42,6 @@ public interface ProducerScope : CoroutineScope, SendChannel { * } * ``` */ -@ExperimentalCoroutinesApi public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) { check(kotlin.coroutines.coroutineContext[Job] === this) { "awaitClose() can only be invoked from the producer context" } try { @@ -137,7 +133,7 @@ internal fun CoroutineScope.produce( return coroutine } -internal open class ProducerCoroutine( +private class ProducerCoroutine( parentContext: CoroutineContext, channel: Channel ) : ChannelCoroutine(parentContext, channel, true, active = true), ProducerScope { override val isActive: Boolean diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 66b55a90c0..c4b55e104b 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -198,25 +198,6 @@ public fun LongRange.asFlow(): Flow = flow { } } -/** - * @suppress - */ -@FlowPreview -@Deprecated( - message = "Use channelFlow with awaitClose { } instead of flowViaChannel and invokeOnClose { }.", - level = DeprecationLevel.ERROR -) // To be removed in 1.4.x -@Suppress("DeprecatedCallableAddReplaceWith") -public fun flowViaChannel( - bufferSize: Int = BUFFERED, - @BuilderInference block: CoroutineScope.(channel: SendChannel) -> Unit -): Flow { - return channelFlow { - block(channel) - awaitClose() - }.buffer(bufferSize) -} - /** * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 382953efcb..51ed4270c0 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -178,46 +178,15 @@ public fun BroadcastChannel.asFlow(): Flow = flow { emitAll(openSubscription()) } -/** - * ### Deprecated - * - * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows. - * [SharedFlow] is an easier-to-use and more flow-centric API for the same purposes, so using - * [shareIn] operator is preferred. It is not a direct replacement, so please - * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb: - * - * * Replace `broadcastIn(scope)` and `broadcastIn(scope, CoroutineStart.LAZY)` with `shareIn(scope, 0, SharingStarted.Lazily)`. - * * Replace `broadcastIn(scope, CoroutineStart.DEFAULT)` with `shareIn(scope, 0, SharingStarted.Eagerly)`. - */ -@Deprecated( - message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel", - replaceWith = ReplaceWith("this.shareIn(scope, SharingStarted.Lazily, 0)"), - level = DeprecationLevel.ERROR -) // WARNING in 1.4.0, error in 1.5.0, removed in 1.6.0 (was @FlowPreview) -public fun Flow.broadcastIn( - scope: CoroutineScope, - start: CoroutineStart = CoroutineStart.LAZY -): BroadcastChannel { - // Backwards compatibility with operator fusing - val channelFlow = asChannelFlow() - val capacity = when (channelFlow.onBufferOverflow) { - BufferOverflow.SUSPEND -> channelFlow.produceCapacity - BufferOverflow.DROP_OLDEST -> Channel.CONFLATED - BufferOverflow.DROP_LATEST -> - throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST") - } - return scope.broadcast(channelFlow.context, capacity = capacity, start = start) { - collect { value -> - send(value) - } - } -} - /** * Creates a [produce] coroutine that collects the given flow. * * This transformation is **stateful**, it launches a [produce] coroutine - * that collects the given flow and thus resulting channel should be properly closed or cancelled. + * that collects the given flow, and has the same behavior: + * + * * if collecting the flow throws, the channel will be closed with that exception + * * if the [ReceiveChannel] is cancelled, the collection of the flow will be cancelled + * * if collecting the flow completes normally, the [ReceiveChannel] will be closed normally * * A channel with [default][Channel.Factory.BUFFERED] buffer size is created. * Use [buffer] operator on the flow before calling `produceIn` to specify a value other than diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 0ccd343ead..3520c48b42 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -108,7 +108,7 @@ import kotlin.coroutines.* * val myFlow = flow { * // GlobalScope.launch { // is prohibited * // launch(Dispatchers.IO) { // is prohibited - * // withContext(CoroutineName("myFlow")) // is prohibited + * // withContext(CoroutineName("myFlow")) { // is prohibited * emit(1) // OK * coroutineScope { * emit(2) // OK -- still the same coroutine @@ -131,10 +131,12 @@ import kotlin.coroutines.* * * ### Exception transparency * - * Flow implementations never catch or handle exceptions that occur in downstream flows. From the implementation standpoint - * it means that calls to [emit][FlowCollector.emit] and [emitAll] shall never be wrapped into - * `try { ... } catch { ... }` blocks. Exception handling in flows shall be performed with - * [catch][Flow.catch] operator and it is designed to only catch exceptions coming from upstream flows while passing + * When `emit` or `emitAll` throws, the Flow implementations must immediately stop emitting new values and finish with an exception. + * For diagnostics or application-specific purposes, the exception may be different from the one thrown by the emit operation, + * suppressing the original exception as discussed below. + * If there is a need to emit values after the downstream failed, please use the [catch][Flow.catch] operator. + * + * The [catch][Flow.catch] operator only catches upstream exceptions, but passes * all downstream exceptions. Similarly, terminal operators like [collect][Flow.collect] * throw any unhandled exceptions that occur in their code or in upstream flows, for example: * @@ -147,6 +149,13 @@ import kotlin.coroutines.* * ``` * The same reasoning can be applied to the [onCompletion] operator that is a declarative replacement for the `finally` block. * + * All exception-handling Flow operators follow the principle of exception suppression: + * + * If the upstream flow throws an exception during its completion when the downstream exception has been thrown, + * the downstream exception becomes superseded and suppressed by the upstream exception, being a semantic + * equivalent of throwing from `finally` block. However, this doesn't affect the operation of the exception-handling operators, + * which consider the downstream exception to be the root cause and behave as if the upstream didn't throw anything. + * * Failure to adhere to the exception transparency requirement can lead to strange behaviors which make * it hard to reason about the code because an exception in the `collect { ... }` could be somehow "caught" * by an upstream flow, limiting the ability of local reasoning about the code. @@ -163,19 +172,29 @@ import kotlin.coroutines.* * * **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use the `flow { ... }` builder function to create an implementation. + * + * Use the `flow { ... }` builder function to create an implementation, or extend [AbstractFlow]. + * These implementations ensure that the context preservation property is not violated, and prevent most + * of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation. */ public interface Flow { + /** * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. - * This method should never be implemented or used directly. * - * The only way to implement the `Flow` interface directly is to extend [AbstractFlow]. - * To collect it into a specific collector, either `collector.emitAll(flow)` or `collect { ... }` extension - * should be used. Such limitation ensures that the context preservation property is not violated and prevents most - * of the developer mistakes related to concurrency, inconsistent flow dispatchers and cancellation. + * This method can be used along with SAM-conversion of [FlowCollector]: + * ``` + * myFlow.collect { value -> println("Collected $value") } + * ``` + * + * ### Method inheritance + * + * To ensure the context preservation property, it is not recommended implementing this method directly. + * Instead, [AbstractFlow] can be used as the base type to properly ensure flow's properties. + * + * All default flow implementations ensure context preservation and exception transparency properties on a best-effort basis + * and throw [IllegalStateException] if a violation was detected. */ - @InternalCoroutinesApi public suspend fun collect(collector: FlowCollector) } @@ -205,7 +224,6 @@ public interface Flow { @FlowPreview public abstract class AbstractFlow : Flow, CancellableFlow { - @InternalCoroutinesApi public final override suspend fun collect(collector: FlowCollector) { val safeCollector = SafeCollector(collector, coroutineContext) try { diff --git a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt index d1c1565cb0..2877fe55e7 100644 --- a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt +++ b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt @@ -8,10 +8,25 @@ package kotlinx.coroutines.flow * [FlowCollector] is used as an intermediate or a terminal collector of the flow and represents * an entity that accepts values emitted by the [Flow]. * - * This interface should usually not be implemented directly, but rather used as a receiver in a [flow] builder when implementing a custom operator. + * This interface should usually not be implemented directly, but rather used as a receiver in a [flow] builder when implementing a custom operator, + * or with SAM-conversion. * Implementations of this interface are not thread-safe. + * + * Example of usage: + * + * ``` + * val flow = getMyEvents() + * try { + * flow.collect { value -> + * println("Received $value") + * } + * println("My events are consumed successfully") + * } catch (e: Throwable) { + * println("Exception from the flow: $e") + * } + * ``` */ -public interface FlowCollector { +public fun interface FlowCollector { /** * Collects the value emitted by the upstream. diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 6278081a5d..e398740bbb 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -260,7 +260,7 @@ public fun Flow.skip(count: Int): Flow = noImpl() @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'forEach' is 'collect'", - replaceWith = ReplaceWith("collect(block)") + replaceWith = ReplaceWith("collect(action)") ) public fun Flow.forEach(action: suspend (value: T) -> Unit): Unit = noImpl() @@ -354,6 +354,7 @@ public fun Flow.concatWith(value: T): Flow = noImpl() ) public fun Flow.concatWith(other: Flow): Flow = noImpl() +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'combineLatest' is 'combine'", @@ -362,6 +363,7 @@ public fun Flow.concatWith(other: Flow): Flow = noImpl() public fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = combine(this, other, transform) +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'combineLatest' is 'combine'", @@ -373,6 +375,7 @@ public fun Flow.combineLatest( transform: suspend (T1, T2, T3) -> R ) = combine(this, other, other2, transform) +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'combineLatest' is 'combine'", @@ -385,6 +388,7 @@ public fun Flow.combineLatest( transform: suspend (T1, T2, T3, T4) -> R ) = combine(this, other, other2, other3, transform) +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'combineLatest' is 'combine'", @@ -422,6 +426,7 @@ public fun Flow.delayFlow(timeMillis: Long): Flow = onStart { delay(ti ) public fun Flow.delayEach(timeMillis: Long): Flow = onEach { delay(timeMillis) } +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogues of 'switchMap' are 'transformLatest', 'flatMapLatest' and 'mapLatest'", @@ -429,6 +434,7 @@ public fun Flow.delayEach(timeMillis: Long): Flow = onEach { delay(tim ) public fun Flow.switchMap(transform: suspend (value: T) -> Flow): Flow = flatMapLatest(transform) +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, // Warning since 1.3.8, was experimental when deprecated, ERROR since 1.5.0 message = "'scanReduce' was renamed to 'runningReduce' to be consistent with Kotlin standard library", @@ -436,6 +442,7 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F ) public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'publish()' is 'shareIn'. \n" + @@ -446,6 +453,7 @@ public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) ) public fun Flow.publish(): Flow = noImpl() +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'publish(bufferSize)' is 'buffer' followed by 'shareIn'. \n" + @@ -456,6 +464,7 @@ public fun Flow.publish(): Flow = noImpl() ) public fun Flow.publish(bufferSize: Int): Flow = noImpl() +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'replay()' is 'shareIn' with unlimited replay. \n" + @@ -466,6 +475,7 @@ public fun Flow.publish(bufferSize: Int): Flow = noImpl() ) public fun Flow.replay(): Flow = noImpl() +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'replay(bufferSize)' is 'shareIn' with the specified replay parameter. \n" + @@ -476,6 +486,7 @@ public fun Flow.replay(): Flow = noImpl() ) public fun Flow.replay(bufferSize: Int): Flow = noImpl() +/** @suppress */ @Deprecated( level = DeprecationLevel.ERROR, message = "Flow analogue of 'cache()' is 'shareIn' with unlimited replay and 'started = SharingStared.Lazily' argument'", diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index d79e203464..0a291f258f 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -115,7 +115,7 @@ import kotlin.native.concurrent.* * ### Implementation notes * * Shared flow implementation uses a lock to ensure thread-safety, but suspending collector and emitter coroutines are - * resumed outside of this lock to avoid dead-locks when using unconfined coroutines. Adding new subscribers + * resumed outside of this lock to avoid deadlocks when using unconfined coroutines. Adding new subscribers * has `O(1)` amortized cost, but emitting has `O(N)` cost, where `N` is the number of subscribers. * * ### Not stable for inheritance @@ -129,6 +129,18 @@ public interface SharedFlow : Flow { * A snapshot of the replay cache. */ public val replayCache: List + + /** + * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. + * To emit values from a shared flow into a specific collector, either `collector.emitAll(flow)` or `collect { ... }` + * SAM-conversion can be used. + * + * **A shared flow never completes**. A call to [Flow.collect] or any other terminal operator + * on a shared flow never completes normally. + * + * @see [Flow.collect] for implementation and inheritance details. + */ + override suspend fun collect(collector: FlowCollector): Nothing } /** @@ -155,8 +167,15 @@ public interface SharedFlow : Flow { */ public interface MutableSharedFlow : SharedFlow, FlowCollector { /** - * Emits a [value] to this shared flow, suspending on buffer overflow if the shared flow was created - * with the default [BufferOverflow.SUSPEND] strategy. + * Emits a [value] to this shared flow, suspending on buffer overflow. + * + * This call can suspend only when the [BufferOverflow] strategy is + * [SUSPEND][BufferOverflow.SUSPEND] **and** there are subscribers collecting this shared flow. + * + * If there are no subscribers, the buffer is not used. + * Instead, the most recently emitted value is simply stored into + * the replay cache if one was configured, displacing the older elements there, + * or dropped if no replay cache was configured. * * See [tryEmit] for a non-suspending variant of this function. * @@ -167,12 +186,16 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { /** * Tries to emit a [value] to this shared flow without suspending. It returns `true` if the value was - * emitted successfully. When this function returns `false`, it means that the call to a plain [emit] - * function will suspend until there is a buffer space available. + * emitted successfully (see below). When this function returns `false`, it means that a call to a plain [emit] + * function would suspend until there is buffer space available. * - * A shared flow configured with a [BufferOverflow] strategy other than [SUSPEND][BufferOverflow.SUSPEND] - * (either [DROP_OLDEST][BufferOverflow.DROP_OLDEST] or [DROP_LATEST][BufferOverflow.DROP_LATEST]) never - * suspends on [emit], and thus `tryEmit` to such a shared flow always returns `true`. + * This call can return `false` only when the [BufferOverflow] strategy is + * [SUSPEND][BufferOverflow.SUSPEND] **and** there are subscribers collecting this shared flow. + * + * If there are no subscribers, the buffer is not used. + * Instead, the most recently emitted value is simply stored into + * the replay cache if one was configured, displacing the older elements there, + * or dropped if no replay cache was configured. In any case, `tryEmit` returns `true`. * * This method is **thread-safe** and can be safely invoked from concurrent coroutines without * external synchronization. @@ -198,6 +221,8 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * } * .launchIn(scope) // launch it * ``` + * + * Implementation note: the resulting flow **does not** conflate subscription count. */ public val subscriptionCount: StateFlow @@ -253,7 +278,7 @@ public fun MutableSharedFlow( // ------------------------------------ Implementation ------------------------------------ -private class SharedFlowSlot : AbstractSharedFlowSlot>() { +internal class SharedFlowSlot : AbstractSharedFlowSlot>() { @JvmField var index = -1L // current "to-be-emitted" index, -1 means the slot is free now @@ -275,7 +300,7 @@ private class SharedFlowSlot : AbstractSharedFlowSlot>() { } } -private class SharedFlowImpl( +internal open class SharedFlowImpl( private val replay: Int, private val bufferCapacity: Int, private val onBufferOverflow: BufferOverflow @@ -334,8 +359,15 @@ private class SharedFlowImpl( result } + /* + * A tweak for SubscriptionCountStateFlow to get the latest value. + */ + @Suppress("UNCHECKED_CAST") + protected val lastReplayedLocked: T + get() = buffer!!.getBufferAt(replayIndex + replaySize - 1) as T + @Suppress("UNCHECKED_CAST") - override suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector): Nothing { val slot = allocateSlot() try { if (collector is SubscribedFlowCollector) collector.onSubscription() diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index f4c6f2ee8d..0554142408 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -5,7 +5,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.IgnoreJreRequirement import kotlin.time.* /** @@ -135,7 +135,6 @@ public fun interface SharingStarted { * are negative. */ @Suppress("FunctionName") -@ExperimentalTime public fun SharingStarted.Companion.WhileSubscribed( stopTimeout: Duration = Duration.ZERO, replayExpiration: Duration = Duration.INFINITE @@ -204,5 +203,6 @@ private class StartedWhileSubscribed( stopTimeout == other.stopTimeout && replayExpiration == other.replayExpiration + @IgnoreJreRequirement // desugared hashcode implementation override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode() } diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 9e82e78771..be6cbd6bbd 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -380,7 +380,7 @@ private class StateFlowImpl( throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported") } - override suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector): Nothing { val slot = allocateSlot() try { if (collector is SubscribedFlowCollector) collector.onSubscription() @@ -415,10 +415,6 @@ private class StateFlowImpl( fuseStateFlow(context, capacity, onBufferOverflow) } -internal fun MutableStateFlow.increment(delta: Int) { - update { it + delta } -} - internal fun StateFlow.fuseStateFlow( context: CoroutineContext, capacity: Int, diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt index 7114cc08d3..39ca98391f 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.flow.internal +import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.internal.* import kotlin.coroutines.* @@ -26,12 +27,12 @@ internal abstract class AbstractSharedFlow> : Sync protected var nCollectors = 0 // number of allocated (!free) slots private set private var nextIndex = 0 // oracle for the next free slot index - private var _subscriptionCount: MutableStateFlow? = null // init on first need + private var _subscriptionCount: SubscriptionCountStateFlow? = null // init on first need val subscriptionCount: StateFlow get() = synchronized(this) { // allocate under lock in sync with nCollectors variable - _subscriptionCount ?: MutableStateFlow(nCollectors).also { + _subscriptionCount ?: SubscriptionCountStateFlow(nCollectors).also { _subscriptionCount = it } } @@ -43,7 +44,7 @@ internal abstract class AbstractSharedFlow> : Sync @Suppress("UNCHECKED_CAST") protected fun allocateSlot(): S { // Actually create slot under lock - var subscriptionCount: MutableStateFlow? = null + var subscriptionCount: SubscriptionCountStateFlow? = null val slot = synchronized(this) { val slots = when (val curSlots = slots) { null -> createSlotArray(2).also { slots = it } @@ -74,7 +75,7 @@ internal abstract class AbstractSharedFlow> : Sync @Suppress("UNCHECKED_CAST") protected fun freeSlot(slot: S) { // Release slot under lock - var subscriptionCount: MutableStateFlow? = null + var subscriptionCount: SubscriptionCountStateFlow? = null val resumes = synchronized(this) { nCollectors-- subscriptionCount = _subscriptionCount // retrieve under lock if initialized @@ -83,10 +84,10 @@ internal abstract class AbstractSharedFlow> : Sync (slot as AbstractSharedFlowSlot).freeLocked(this) } /* - Resume suspended coroutines. - This can happens when the subscriber that was freed was a slow one and was holding up buffer. - When this subscriber was freed, previously queued emitted can now wake up and are resumed here. - */ + * Resume suspended coroutines. + * This can happen when the subscriber that was freed was a slow one and was holding up buffer. + * When this subscriber was freed, previously queued emitted can now wake up and are resumed here. + */ for (cont in resumes) cont?.resume(Unit) // decrement subscription count subscriptionCount?.increment(-1) @@ -99,3 +100,35 @@ internal abstract class AbstractSharedFlow> : Sync } } } + +/** + * [StateFlow] that represents the number of subscriptions. + * + * It is exposed as a regular [StateFlow] in our public API, but it is implemented as [SharedFlow] undercover to + * avoid conflations of consecutive updates because the subscription count is very sensitive to it. + * + * The importance of non-conflating can be demonstrated with the following example: + * ``` + * val shared = flowOf(239).stateIn(this, SharingStarted.Lazily, 42) // stateIn for the sake of the initial value + * println(shared.first()) + * yield() + * println(shared.first()) + * ``` + * If the flow is shared within the same dispatcher (e.g. Main) or with a slow/throttled one, + * the `SharingStarted.Lazily` will never be able to start the source: `first` sees the initial value and immediately + * unsubscribes, leaving the asynchronous `SharingStarted` with conflated zero. + * + * To avoid that (especially in a more complex scenarios), we do not conflate subscription updates. + */ +private class SubscriptionCountStateFlow(initialValue: Int) : StateFlow, + SharedFlowImpl(1, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST) +{ + init { tryEmit(initialValue) } + + override val value: Int + get() = synchronized(this) { lastReplayedLocked } + + fun increment(delta: Int) = synchronized(this) { + tryEmit(lastReplayedLocked + delta) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt index b395525620..9a81eefa2d 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt @@ -51,33 +51,11 @@ internal fun scopedFlow(@BuilderInference block: suspend CoroutineScope.(Flo flowScope { block(this@flow) } } -internal fun CoroutineScope.flowProduce( - context: CoroutineContext, - capacity: Int = 0, - @BuilderInference block: suspend ProducerScope.() -> Unit -): ReceiveChannel { - val channel = Channel(capacity) - val newContext = newCoroutineContext(context) - val coroutine = FlowProduceCoroutine(newContext, channel) - coroutine.start(CoroutineStart.ATOMIC, coroutine, block) - return coroutine -} - private class FlowCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { - public override fun childCancelled(cause: Throwable): Boolean { - if (cause is ChildCancelledException) return true - return cancelImpl(cause) - } -} - -private class FlowProduceCoroutine( - parentContext: CoroutineContext, - channel: Channel -) : ProducerCoroutine(parentContext, channel) { - public override fun childCancelled(cause: Throwable): Boolean { + override fun childCancelled(cause: Throwable): Boolean { if (cause is ChildCancelledException) return true return cancelImpl(cause) } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt index 9eca8aa0c2..c18adba3b7 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt @@ -22,7 +22,7 @@ internal class ChannelFlowTransformLatest( override suspend fun flowCollect(collector: FlowCollector) { assert { collector is SendingCollector } // So cancellation behaviour is not leaking into the downstream - flowScope { + coroutineScope { var previousFlow: Job? = null flow.collect { value -> previousFlow?.apply { @@ -49,7 +49,7 @@ internal class ChannelFlowMerge( ChannelFlowMerge(flow, concurrency, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { - return scope.flowProduce(context, capacity, block = collectToFun) + return scope.produce(context, capacity, block = collectToFun) } override suspend fun collectTo(scope: ProducerScope) { @@ -87,7 +87,7 @@ internal class ChannelLimitedFlowMerge( ChannelLimitedFlowMerge(flows, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { - return scope.flowProduce(context, capacity, block = collectToFun) + return scope.produce(context, capacity, block = collectToFun) } override suspend fun collectTo(scope: ProducerScope) { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 04342ed074..8ed5606b83 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -171,7 +171,7 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capaci * ``` * * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED], - * with is, in turn, a shortcut to a buffer that only keeps the latest element as + * which is, in turn, a shortcut to a buffer that only keeps the latest element as * created by `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`. * * ### Operator fusion @@ -277,64 +277,6 @@ private class CancellableFlowImpl(private val flow: Flow) : CancellableFlo } } -/** - * The operator that changes the context where all transformations applied to the given flow within a [builder] are executed. - * This operator is context preserving and does not affect the context of the preceding and subsequent operations. - * - * Example: - * - * ``` - * flow // not affected - * .map { ... } // Not affected - * .flowWith(Dispatchers.IO) { - * map { ... } // in IO - * .filter { ... } // in IO - * } - * .map { ... } // Not affected - * ``` - * - * For more explanation of context preservation please refer to [Flow] documentation. - * - * This operator is deprecated without replacement because it was discovered that it doesn't play well with coroutines - * and flow semantics: - * - * 1) It doesn't prevent context elements from the downstream to leak into its body - * ``` - * flowOf(1).flowWith(EmptyCoroutineContext) { - * onEach { println(kotlin.coroutines.coroutineContext[CoroutineName]) } // Will print 42 - * }.flowOn(CoroutineName(42)) - * ``` - * 2) To avoid such leaks, new primitive should be introduced to `kotlinx.coroutines` -- the subtraction of contexts. - * And this will become a new concept to learn, maintain and explain. - * 3) It defers the execution of declarative [builder] until the moment of [collection][Flow.collect] similarly - * to `Observable.defer`. But it is unexpected because nothing in the name `flowWith` reflects this fact. - * 4) It can be confused with [flowOn] operator, though [flowWith] is much rarer. - * - * @suppress - */ -@FlowPreview -@Deprecated(message = "flowWith is deprecated without replacement, please refer to its KDoc for an explanation", level = DeprecationLevel.ERROR) // Error in beta release, removal in 1.4 -public fun Flow.flowWith( - flowContext: CoroutineContext, - bufferSize: Int = BUFFERED, - builder: Flow.() -> Flow -): Flow { - checkFlowContext(flowContext) - val source = this - return unsafeFlow { - /** - * Here we should remove a Job instance from the context. - * All builders are written using scoping and no global coroutines are launched, so it is safe not to provide explicit Job. - * It is also necessary not to mess with cancellation if multiple flowWith are used. - */ - val originalContext = currentCoroutineContext().minusKey(Job) - val prepared = source.flowOn(originalContext).buffer(bufferSize) - builder(prepared).flowOn(flowContext).buffer(bufferSize).collect { value -> - return@collect emit(value) - } - } -} - private fun checkFlowContext(context: CoroutineContext) { require(context[Job] == null) { "Flow context cannot contain job in it. Had $context" diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index fed5962bd5..258dc3eeb1 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -17,12 +17,12 @@ import kotlin.time.* /* Scaffolding for Knit code examples -[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html +[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html -[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html -[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html -[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html +[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html +[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html [yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html -[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html -[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html +[runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html +[TestCoroutineScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/index.html +[TestScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/index.html +[TestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-dispatcher/index.html +[Dispatchers.setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html +[StandardTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-standard-test-dispatcher.html +[UnconfinedTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-unconfined-test-dispatcher.html [setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html -[runBlockingTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html -[UncompletedCoroutinesError]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-uncompleted-coroutines-error/index.html -[DelayController]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/index.html -[DelayController.advanceUntilIdle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/advance-until-idle.html -[DelayController.pauseDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/pause-dispatcher.html -[TestCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html -[DelayController.resumeDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/resume-dispatcher.html -[TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html -[TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html -[TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html -[DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html +[TestScope.testScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/test-scheduler.html +[TestScope.runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html +[runCurrent]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-current.html diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index c99ec5cbf1..4786b81bf9 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -13,27 +13,45 @@ public final class kotlinx/coroutines/test/TestBuildersKt { public static final fun runBlockingTest (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineDispatcher;Lkotlin/jvm/functions/Function2;)V public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V + public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestScope;Lkotlin/jvm/functions/Function2;)V public static synthetic fun runBlockingTest$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final fun runBlockingTestOnTestScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun runBlockingTestOnTestScope$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final fun runTest (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V + public static final fun runTest (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;)V + public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V + public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final fun runTestWithLegacyScope (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V + public static synthetic fun runTestWithLegacyScope$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } -public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/DelayController { +public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/SchedulerAsDelayController { public fun ()V + public fun (Lkotlinx/coroutines/test/TestCoroutineScheduler;)V + public synthetic fun (Lkotlinx/coroutines/test/TestCoroutineScheduler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun advanceTimeBy (J)J public fun advanceUntilIdle ()J public fun cleanupTestCoroutines ()V - public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun getCurrentTime ()J - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler; public fun pauseDispatcher ()V public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resumeDispatcher ()V public fun runCurrent ()V - public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } +public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt { + public static final fun StandardTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher; + public static synthetic fun StandardTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher; + public static final fun UnconfinedTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher; + public static synthetic fun UnconfinedTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher; +} + public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor { public fun ()V public fun cleanupTestCoroutines ()V @@ -41,13 +59,44 @@ public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotli public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V } -public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope, kotlinx/coroutines/test/DelayController, kotlinx/coroutines/test/UncaughtExceptionCaptor { +public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/coroutines/AbstractCoroutineContextElement, kotlin/coroutines/CoroutineContext$Element { + public static final field Key Lkotlinx/coroutines/test/TestCoroutineScheduler$Key; + public fun ()V + public final fun advanceTimeBy (J)V + public final fun advanceUntilIdle ()V + public final fun getCurrentTime ()J + public final fun getTimeSource ()Lkotlin/time/TimeSource; + public final fun runCurrent ()V +} + +public final class kotlinx/coroutines/test/TestCoroutineScheduler$Key : kotlin/coroutines/CoroutineContext$Key { +} + +public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope { public abstract fun cleanupTestCoroutines ()V + public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler; } public final class kotlinx/coroutines/test/TestCoroutineScopeKt { public static final fun TestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope; public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope; + public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V + public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V + public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope; + public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope; + public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J + public static final fun getUncaughtExceptions (Lkotlinx/coroutines/test/TestCoroutineScope;)Ljava/util/List; + public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V + public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun resumeDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V + public static final fun runCurrent (Lkotlinx/coroutines/test/TestCoroutineScope;)V +} + +public abstract class kotlinx/coroutines/test/TestDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { + public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } public final class kotlinx/coroutines/test/TestDispatchers { @@ -55,13 +104,22 @@ public final class kotlinx/coroutines/test/TestDispatchers { public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V } +public abstract interface class kotlinx/coroutines/test/TestScope : kotlinx/coroutines/CoroutineScope { + public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler; +} + +public final class kotlinx/coroutines/test/TestScopeKt { + public static final fun TestScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestScope; + public static synthetic fun TestScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestScope; + public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestScope;J)V + public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestScope;)V + public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestScope;)J + public static final fun getTestTimeSource (Lkotlinx/coroutines/test/TestScope;)Lkotlin/time/TimeSource; + public static final fun runCurrent (Lkotlinx/coroutines/test/TestScope;)V +} + public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor { public abstract fun cleanupTestCoroutines ()V public abstract fun getUncaughtExceptions ()Ljava/util/List; } -public final class kotlinx/coroutines/test/UncompletedCoroutinesError : java/lang/AssertionError { - public fun (Ljava/lang/String;Ljava/lang/Throwable;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - diff --git a/kotlinx-coroutines-test/build.gradle.kts b/kotlinx-coroutines-test/build.gradle.kts index fef0a146f7..7b244bb091 100644 --- a/kotlinx-coroutines-test/build.gradle.kts +++ b/kotlinx-coroutines-test/build.gradle.kts @@ -2,6 +2,12 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -dependencies { - implementation(project(":kotlinx-coroutines-debug")) +val experimentalAnnotations = listOf( + "kotlin.Experimental", + "kotlinx.coroutines.ExperimentalCoroutinesApi", + "kotlinx.coroutines.InternalCoroutinesApi" +) + +kotlin { + sourceSets.all { configureMultiplatform() } } diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt new file mode 100644 index 0000000000..d8e9357611 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt @@ -0,0 +1,283 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:JvmName("TestBuildersKt") +@file:JvmMultifileClass + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import kotlin.coroutines.* +import kotlin.jvm.* + +/** + * A test result. + * + * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these + * platforms: a call to a function returning a [TestResult] will simply execute the test inside it. + * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to + * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it. + * + * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors: + * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the + * test finishes. + * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do + * with a [TestResult] is to immediately `return` it from a test. + * * Don't nest functions returning a [TestResult]. + */ +@Suppress("NO_ACTUAL_FOR_EXPECT") +@ExperimentalCoroutinesApi +public expect class TestResult + +/** + * Executes [testBody] as a test in a new coroutine, returning [TestResult]. + * + * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs + * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary. + * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior. + * + * ``` + * @Test + * fun exampleTest() = runTest { + * val deferred = async { + * delay(1_000) + * async { + * delay(1_000) + * }.await() + * } + * + * deferred.await() // result available immediately + * } + * ``` + * + * The platform difference entails that, in order to use this function correctly in common code, one must always + * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See + * [TestResult] for details on this. + * + * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines. + * Because of this, child coroutines are not executed in parallel to the test body. + * In order to for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the + * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]). + * + * ``` + * @Test + * fun exampleWaitingForAsyncTasks1() = runTest { + * // 1 + * val job = launch { + * // 3 + * } + * // 2 + * job.join() // the main test coroutine suspends here, so the child is executed + * // 4 + * } + * + * @Test + * fun exampleWaitingForAsyncTasks2() = runTest { + * // 1 + * launch { + * // 3 + * } + * // 2 + * advanceUntilIdle() // runs the tasks until their queue is empty + * // 4 + * } + * ``` + * + * ### Task scheduling + * + * Delay-skipping is achieved by using virtual time. + * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test, + * then its [TestCoroutineScheduler] is used; + * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control + * the virtual time, advancing it, running the tasks scheduled at a specific time etc. + * Some convenience methods are available on [TestScope] to control the scheduler. + * + * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped: + * ``` + * @Test + * fun exampleTest() = runTest { + * val elapsed = TimeSource.Monotonic.measureTime { + * val deferred = async { + * delay(1_000) // will be skipped + * withContext(Dispatchers.Default) { + * delay(5_000) // Dispatchers.Default doesn't know about TestCoroutineScheduler + * } + * } + * deferred.await() + * } + * println(elapsed) // about five seconds + * } + * ``` + * + * ### Failures + * + * #### Test body failures + * + * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test. + * + * #### Reported exceptions + * + * Unhandled exceptions will be thrown at the end of the test. + * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner. + * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it. + * + * #### Uncompleted coroutines + * + * This method requires that, after the test coroutine has completed, all the other coroutines launched inside + * [testBody] also complete, or are cancelled. + * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw + * [AssertionError], whereas on JS, the `Promise` will fail with it). + * + * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due + * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait + * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes + * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a + * task during that time, the timer gets reset. + * + * ### Configuration + * + * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine + * scope created for the test, [context] also can be used to change how the test is executed. + * See the [TestScope] constructor function documentation for details. + * + * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details. + */ +@ExperimentalCoroutinesApi +public fun runTest( + context: CoroutineContext = EmptyCoroutineContext, + dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, + testBody: suspend TestScope.() -> Unit +): TestResult { + if (context[RunningInRunTest] != null) + throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.") + return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs, testBody) +} + +/** + * Performs [runTest] on an existing [TestScope]. + */ +@ExperimentalCoroutinesApi +public fun TestScope.runTest( + dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, + testBody: suspend TestScope.() -> Unit +): TestResult = asSpecificImplementation().let { + it.enter() + createTestResult { + runTestCoroutine(it, dispatchTimeoutMs, TestScopeImpl::tryGetCompletionCause, testBody) { it.leave() } + } +} + +/** + * Runs [testProcedure], creating a [TestResult]. + */ +@Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult` +internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestResult + +/** A coroutine context element indicating that the coroutine is running inside `runTest`. */ +internal object RunningInRunTest : CoroutineContext.Key, CoroutineContext.Element { + override val key: CoroutineContext.Key<*> + get() = this + + override fun toString(): String = "RunningInRunTest" +} + +/** The default timeout to use when waiting for asynchronous completions of the coroutines managed by + * a [TestCoroutineScheduler]. */ +internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L + +/** + * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most + * [dispatchTimeoutMs] milliseconds, and performing the [cleanup] procedure at the end. + * + * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected. + * + * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or + * return a list of uncaught exceptions that should be reported at the end of the test. + */ +internal suspend fun > runTestCoroutine( + coroutine: T, + dispatchTimeoutMs: Long, + tryGetCompletionCause: T.() -> Throwable?, + testBody: suspend T.() -> Unit, + cleanup: () -> List, +) { + val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!! + /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */ + coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) { + testBody() + } + var completed = false + while (!completed) { + scheduler.advanceUntilIdle() + if (coroutine.isCompleted) { + /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no + non-trivial dispatches. */ + completed = true + continue + } + select { + coroutine.onJoin { + completed = true + } + scheduler.onDispatchEvent { + // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout + } + onTimeout(dispatchTimeoutMs) { + handleTimeout(coroutine, dispatchTimeoutMs, tryGetCompletionCause, cleanup) + } + } + } + coroutine.getCompletionExceptionOrNull()?.let { exception -> + val exceptions = try { + cleanup() + } catch (e: UncompletedCoroutinesError) { + // it's normal that some jobs are not completed if the test body has failed, won't clutter the output + emptyList() + } + (listOf(exception) + exceptions).throwAll() + } + cleanup().throwAll() +} + +/** + * Invoked on timeout in [runTest]. Almost always just builds a nice [UncompletedCoroutinesError] and throws it. + * However, sometimes it detects that the coroutine completed, in which case it returns normally. + */ +private inline fun> handleTimeout( + coroutine: T, + dispatchTimeoutMs: Long, + tryGetCompletionCause: T.() -> Throwable?, + cleanup: () -> List, +) { + val uncaughtExceptions = try { + cleanup() + } catch (e: UncompletedCoroutinesError) { + // we expect these and will instead throw a more informative exception. + emptyList() + } + val activeChildren = coroutine.children.filter { it.isActive }.toList() + val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null + var message = "After waiting for $dispatchTimeoutMs ms" + if (completionCause == null) + message += ", the test coroutine is not completing" + if (activeChildren.isNotEmpty()) + message += ", there were active child jobs: $activeChildren" + if (completionCause != null && activeChildren.isEmpty()) { + if (coroutine.isCompleted) + return + // TODO: can this really ever happen? + message += ", the test coroutine was not completed" + } + val error = UncompletedCoroutinesError(message) + completionCause?.let { cause -> error.addSuppressed(cause) } + uncaughtExceptions.forEach { error.addSuppressed(it) } + throw error +} + +internal fun List.throwAll() { + firstOrNull()?.apply { + drop(1).forEach { addSuppressed(it) } + throw this + } +} diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt new file mode 100644 index 0000000000..4cc48f47d0 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.internal.TestMainDispatcher +import kotlin.coroutines.* + +/** + * Creates an instance of an unconfined [TestDispatcher]. + * + * This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular + * thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do. + * + * Like [Dispatchers.Unconfined], this one does not provide guarantees about the execution order when several coroutines + * are queued in this dispatcher. However, we ensure that the [launch] and [async] blocks at the top level of [runTest] + * are entered eagerly. This allows launching child coroutines and not calling [runCurrent] for them to start executing. + * + * ``` + * @Test + * fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) { + * var entered = false + * val deferred = CompletableDeferred() + * var completed = false + * launch { + * entered = true + * deferred.await() + * completed = true + * } + * assertTrue(entered) // `entered = true` already executed. + * assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued. + * deferred.complete(Unit) // resume the coroutine. + * assertTrue(completed) // now the child coroutine is immediately completed. + * } + * ``` + * + * Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and + * in which order the queued coroutines are executed. + * Another typical use case for this dispatcher is launching child coroutines that are resumed immediately, without + * going through a dispatch; this can be helpful for testing [Channel] and [StateFlow] usages. + * + * ``` + * @Test + * fun testUnconfinedDispatcher() = runTest { + * val values = mutableListOf() + * 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) + * } + * ``` + * + * Please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order + * guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing + * functionality, not the specific order of actions. + * See [Dispatchers.Unconfined] for a discussion of the execution order guarantees. + * + * In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control + * the virtual time and can be shared among many test dispatchers. + * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a + * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if + * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created. + * + * Additionally, [name] can be set to distinguish each dispatcher instance when debugging. + * + * @see StandardTestDispatcher for a more predictable [TestDispatcher]. + */ +@ExperimentalCoroutinesApi +@Suppress("FunctionName") +public fun UnconfinedTestDispatcher( + scheduler: TestCoroutineScheduler? = null, + name: String? = null +): TestDispatcher = UnconfinedTestDispatcherImpl( + scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name) + +private class UnconfinedTestDispatcherImpl( + override val scheduler: TestCoroutineScheduler, + private val name: String? = null +) : TestDispatcher() { + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = false + + @Suppress("INVISIBLE_MEMBER") + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + scheduler.sendDispatchEvent() + + /** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */ + /** It can only be called by the [yield] function. See also code of [yield] function. */ + val yieldContext = context[YieldContext] + if (yieldContext !== null) { + // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" + yieldContext.dispatcherWasUnconfined = true + return + } + throw UnsupportedOperationException( + "Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " + + "the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " + + "isDispatchNeeded and dispatch calls." + ) + } + + override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]" +} + +/** + * Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler]. + * + * This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its + * [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent], + * [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these + * tasks in a blocking manner. + * + * In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are + * parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to + * run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when + * inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines. + * + * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a + * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if + * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created. + * + * One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging. + * + * @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread. + */ +@ExperimentalCoroutinesApi +@Suppress("FunctionName") +public fun StandardTestDispatcher( + scheduler: TestCoroutineScheduler? = null, + name: String? = null +): TestDispatcher = StandardTestDispatcherImpl( + scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name) + +private class StandardTestDispatcherImpl( + override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), + private val name: String? = null +) : TestDispatcher() { + + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + scheduler.registerEvent(this, 0, block) { false } + } + + override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]" +} diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt new file mode 100644 index 0000000000..9aa90fac1d --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.selects.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.time.* + +/** + * This is a scheduler for coroutines used in tests, providing the delay-skipping behavior. + * + * [Test dispatchers][TestDispatcher] are parameterized with a scheduler. Several dispatchers can share the + * same scheduler, in which case their knowledge about the virtual time will be synchronized. When the dispatchers + * require scheduling an event at a later point in time, they notify the scheduler, which will establish the order of + * the tasks. + * + * The scheduler can be queried to advance the time (via [advanceTimeBy]), run all the scheduled tasks advancing the + * virtual time as needed (via [advanceUntilIdle]), or run the tasks that are scheduled to run as soon as possible but + * haven't yet been dispatched (via [runCurrent]). + */ +@ExperimentalCoroutinesApi +public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCoroutineScheduler), + CoroutineContext.Element { + + /** @suppress */ + public companion object Key : CoroutineContext.Key + + /** This heap stores the knowledge about which dispatchers are interested in which moments of virtual time. */ + // TODO: all the synchronization is done via a separate lock, so a non-thread-safe priority queue can be used. + private val events = ThreadSafeHeap>() + + /** Establishes that [currentTime] can't exceed the time of the earliest event in [events]. */ + private val lock = SynchronizedObject() + + /** This counter establishes some order on the events that happen at the same virtual time. */ + private val count = atomic(0L) + + /** The current virtual time in milliseconds. */ + @ExperimentalCoroutinesApi + public var currentTime: Long = 0 + get() = synchronized(lock) { field } + private set + + /** A channel for notifying about the fact that a dispatch recently happened. */ + private val dispatchEvents: Channel = Channel(CONFLATED) + + /** + * Registers a request for the scheduler to notify [dispatcher] at a virtual moment [timeDeltaMillis] milliseconds + * later via [TestDispatcher.processEvent], which will be called with the provided [marker] object. + * + * Returns the handler which can be used to cancel the registration. + */ + internal fun registerEvent( + dispatcher: TestDispatcher, + timeDeltaMillis: Long, + marker: T, + isCancelled: (T) -> Boolean + ): DisposableHandle { + require(timeDeltaMillis >= 0) { "Attempted scheduling an event earlier in time (with the time delta $timeDeltaMillis)" } + val count = count.getAndIncrement() + return synchronized(lock) { + val time = addClamping(currentTime, timeDeltaMillis) + val event = TestDispatchEvent(dispatcher, count, time, marker as Any) { isCancelled(marker) } + events.addLast(event) + /** can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's + * actually anything in the event queue. */ + sendDispatchEvent() + DisposableHandle { + synchronized(lock) { + events.remove(event) + } + } + } + } + + /** + * Runs the next enqueued task, advancing the virtual time to the time of its scheduled awakening. + */ + private fun tryRunNextTask(): Boolean { + val event = synchronized(lock) { + val event = events.removeFirstOrNull() ?: return false + if (currentTime > event.time) + currentTimeAheadOfEvents() + currentTime = event.time + event + } + event.dispatcher.processEvent(event.time, event.marker) + return true + } + + /** + * Runs the enqueued tasks in the specified order, advancing the virtual time as needed until there are no more + * tasks associated with the dispatchers linked to this scheduler. + * + * A breaking change from [TestCoroutineDispatcher.advanceTimeBy] is that it no longer returns the total number of + * milliseconds by which the execution of this method has advanced the virtual time. If you want to recreate that + * functionality, query [currentTime] before and after the execution to achieve the same result. + */ + @ExperimentalCoroutinesApi + public fun advanceUntilIdle() { + while (!synchronized(lock) { events.isEmpty }) { + tryRunNextTask() + } + } + + /** + * Runs the tasks that are scheduled to execute at this moment of virtual time. + */ + @ExperimentalCoroutinesApi + public fun runCurrent() { + val timeMark = synchronized(lock) { currentTime } + while (true) { + val event = synchronized(lock) { + events.removeFirstIf { it.time <= timeMark } ?: return + } + event.dispatcher.processEvent(event.time, event.marker) + } + } + + /** + * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the + * scheduled tasks in the meantime. + * + * Breaking changes from [TestCoroutineDispatcher.advanceTimeBy]: + * * Intentionally doesn't return a `Long` value, as its use cases are unclear. We may restore it in the future; + * please describe your use cases at [the issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues/). + * For now, it's possible to query [currentTime] before and after execution of this method, to the same effect. + * * It doesn't run the tasks that are scheduled at exactly [currentTime] + [delayTimeMillis]. For example, + * advancing the time by one millisecond used to run the tasks at the current millisecond *and* the next + * millisecond, but now will stop just before executing any task starting at the next millisecond. + * * Overflowing the target time used to lead to nothing being done, but will now run the tasks scheduled at up to + * (but not including) [Long.MAX_VALUE]. + * + * @throws IllegalStateException if passed a negative [delay][delayTimeMillis]. + */ + @ExperimentalCoroutinesApi + public fun advanceTimeBy(delayTimeMillis: Long) { + require(delayTimeMillis >= 0) { "Can not advance time by a negative delay: $delayTimeMillis" } + val startingTime = currentTime + val targetTime = addClamping(startingTime, delayTimeMillis) + while (true) { + val event = synchronized(lock) { + val timeMark = currentTime + val event = events.removeFirstIf { targetTime > it.time } + when { + event == null -> { + currentTime = targetTime + return + } + timeMark > event.time -> currentTimeAheadOfEvents() + else -> { + currentTime = event.time + event + } + } + } + event.dispatcher.processEvent(event.time, event.marker) + } + } + + /** + * Checks that the only tasks remaining in the scheduler are cancelled. + */ + internal fun isIdle(strict: Boolean = true): Boolean { + synchronized(lock) { + if (strict) + return events.isEmpty + // TODO: also completely empties the queue, as there's no nondestructive way to iterate over [ThreadSafeHeap] + val presentEvents = mutableListOf>() + while (true) { + presentEvents += events.removeFirstOrNull() ?: break + } + return presentEvents.all { it.isCancelled() } + } + } + + /** + * Notifies this scheduler about a dispatch event. + */ + internal fun sendDispatchEvent() { + dispatchEvents.trySend(Unit) + } + + /** + * Consumes the knowledge that a dispatch event happened recently. + */ + internal val onDispatchEvent: SelectClause1 get() = dispatchEvents.onReceive + + /** + * Returns the [TimeSource] representation of the virtual time of this scheduler. + */ + @ExperimentalCoroutinesApi + @ExperimentalTime + public val timeSource: TimeSource = object : AbstractLongTimeSource(DurationUnit.MILLISECONDS) { + override fun read(): Long = currentTime + } +} + +// Some error-throwing functions for pretty stack traces +private fun currentTimeAheadOfEvents(): Nothing = invalidSchedulerState() + +private fun invalidSchedulerState(): Nothing = + throw IllegalStateException("The test scheduler entered an invalid state. Please report this at https://github.com/Kotlin/kotlinx.coroutines/issues.") + +/** [ThreadSafeHeap] node representing a scheduled task, ordered by the planned execution time. */ +private class TestDispatchEvent( + @JvmField val dispatcher: TestDispatcher, + private val count: Long, + @JvmField val time: Long, + @JvmField val marker: T, + @JvmField val isCancelled: () -> Boolean +) : Comparable>, ThreadSafeHeapNode { + override var heap: ThreadSafeHeap<*>? = null + override var index: Int = 0 + + override fun compareTo(other: TestDispatchEvent<*>) = + compareValuesBy(this, other, TestDispatchEvent<*>::time, TestDispatchEvent<*>::count) + + override fun toString() = "TestDispatchEvent(time=$time, dispatcher=$dispatcher)" +} + +// works with positive `a`, `b` +private fun addClamping(a: Long, b: Long): Long = (a + b).let { if (it >= 0) it else Long.MAX_VALUE } + +internal fun checkSchedulerInContext(scheduler: TestCoroutineScheduler, context: CoroutineContext) { + context[TestCoroutineScheduler]?.let { + check(it === scheduler) { + "Detected use of different schedulers. If you need to use several test coroutine dispatchers, " + + "create one `TestCoroutineScheduler` and pass it to each of them." + } + } +} diff --git a/kotlinx-coroutines-test/common/src/TestDispatcher.kt b/kotlinx-coroutines-test/common/src/TestDispatcher.kt new file mode 100644 index 0000000000..f434572663 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.coroutines.* +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 { + /** The scheduler that this dispatcher is linked to. */ + @ExperimentalCoroutinesApi + public abstract val scheduler: TestCoroutineScheduler + + /** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */ + internal fun processEvent(time: Long, marker: Any) { + check(marker is Runnable) + marker.run() + } + + /** @suppress */ + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + checkSchedulerInContext(scheduler, continuation.context) + val timedRunnable = CancellableContinuationRunnable(continuation, this) + scheduler.registerEvent(this, timeMillis, timedRunnable, ::cancellableRunnableIsCancelled) + } + + /** @suppress */ + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + checkSchedulerInContext(scheduler, context) + return scheduler.registerEvent(this, timeMillis, block) { false } + } +} + +/** + * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled + * in the future. + */ +private class CancellableContinuationRunnable( + @JvmField val continuation: CancellableContinuation, + private val dispatcher: CoroutineDispatcher +) : Runnable { + override fun run() = with(dispatcher) { with(continuation) { resumeUndispatched(Unit) } } +} + +private fun cancellableRunnableIsCancelled(runnable: CancellableContinuationRunnable): Boolean = + !runnable.continuation.isActive diff --git a/kotlinx-coroutines-test/src/TestDispatchers.kt b/kotlinx-coroutines-test/common/src/TestDispatchers.kt similarity index 56% rename from kotlinx-coroutines-test/src/TestDispatchers.kt rename to kotlinx-coroutines-test/common/src/TestDispatchers.kt index bf068f9d7b..4454597ed7 100644 --- a/kotlinx-coroutines-test/src/TestDispatchers.kt +++ b/kotlinx-coroutines-test/common/src/TestDispatchers.kt @@ -1,38 +1,38 @@ /* * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") @file:JvmName("TestDispatchers") package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlinx.coroutines.test.internal.* +import kotlin.jvm.* /** * Sets the given [dispatcher] as an underlying dispatcher of [Dispatchers.Main]. - * All consecutive usages of [Dispatchers.Main] will use given [dispatcher] under the hood. + * All subsequent usages of [Dispatchers.Main] will use the given [dispatcher] under the hood. + * + * Using [TestDispatcher] as an argument has special behavior: subsequently-called [runTest], as well as + * [TestScope] and test dispatcher constructors, will use the [TestCoroutineScheduler] of the provided dispatcher. * * It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist. */ @ExperimentalCoroutinesApi public fun Dispatchers.setMain(dispatcher: CoroutineDispatcher) { require(dispatcher !is TestMainDispatcher) { "Dispatchers.setMain(Dispatchers.Main) is prohibited, probably Dispatchers.resetMain() should be used instead" } - val mainDispatcher = Dispatchers.Main - require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." } - mainDispatcher.setDispatcher(dispatcher) + getTestMainDispatcher().setDispatcher(dispatcher) } /** * Resets state of the [Dispatchers.Main] to the original main dispatcher. - * For example, in Android Main thread dispatcher will be set as [Dispatchers.Main]. - * Used to clean up all possible dependencies, should be used in tear down (`@After`) methods. + * + * For example, in Android, the Main thread dispatcher will be set as [Dispatchers.Main]. + * This method undoes a dependency injection performed for tests, and so should be used in tear down (`@After`) methods. * * It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist. */ @ExperimentalCoroutinesApi public fun Dispatchers.resetMain() { - val mainDispatcher = Dispatchers.Main - require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." } - mainDispatcher.resetDispatcher() + getTestMainDispatcher().resetDispatcher() } diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt new file mode 100644 index 0000000000..60585a1d50 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestScope.kt @@ -0,0 +1,249 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.time.* + +/** + * A coroutine scope that for launching test coroutines. + * + * The scope provides the following functionality: + * * The [coroutineContext] includes a [coroutine dispatcher][TestDispatcher] that supports delay-skipping, using + * a [TestCoroutineScheduler] for orchestrating the virtual time. + * This scheduler is also available via the [testScheduler] property, and some helper extension + * methods are defined to more conveniently interact with it: see [TestScope.currentTime], [TestScope.runCurrent], + * [TestScope.advanceTimeBy], and [TestScope.advanceUntilIdle]. + * * When inside [runTest], uncaught exceptions from the child coroutines of this scope will be reported at the end of + * the test. + * It is invalid for child coroutines to throw uncaught exceptions when outside the call to [TestScope.runTest]: + * the only guarantee in this case is the best effort to deliver the exception. + * + * The usual way to access a [TestScope] is to call [runTest], but it can also be constructed manually, in order to + * use it to initialize the components that participate in the test. + * + * #### Differences from the deprecated [TestCoroutineScope] + * + * * This doesn't provide an equivalent of [TestCoroutineScope.cleanupTestCoroutines], and so can't be used as a + * standalone mechanism for writing tests: it does require that [runTest] is eventually called. + * The reason for this is that a proper cleanup procedure that supports using non-test dispatchers and arbitrary + * coroutine suspensions would be equivalent to [runTest], but would also be more error-prone, due to the potential + * for forgetting to perform the cleanup. + * * [TestCoroutineScope.advanceTimeBy] also calls [TestCoroutineScheduler.runCurrent] after advancing the virtual time. + * * No support for dispatcher pausing, like [DelayController] allows. [TestCoroutineDispatcher], which supported + * pausing, is deprecated; now, instead of pausing a dispatcher, one can use [withContext] to run a dispatcher that's + * paused by default, like [StandardTestDispatcher]. + * * No access to the list of unhandled exceptions. + */ +@ExperimentalCoroutinesApi +public sealed interface TestScope : CoroutineScope { + /** + * The delay-skipping scheduler used by the test dispatchers running the code in this scope. + */ + @ExperimentalCoroutinesApi + public val testScheduler: TestCoroutineScheduler +} + +/** + * The current virtual time on [testScheduler][TestScope.testScheduler]. + * @see TestCoroutineScheduler.currentTime + */ +@ExperimentalCoroutinesApi +public val TestScope.currentTime: Long + get() = testScheduler.currentTime + +/** + * Advances the [testScheduler][TestScope.testScheduler] to the point where there are no tasks remaining. + * @see TestCoroutineScheduler.advanceUntilIdle + */ +@ExperimentalCoroutinesApi +public fun TestScope.advanceUntilIdle(): Unit = testScheduler.advanceUntilIdle() + +/** + * Run any tasks that are pending at the current virtual time, according to + * the [testScheduler][TestScope.testScheduler]. + * + * @see TestCoroutineScheduler.runCurrent + */ +@ExperimentalCoroutinesApi +public fun TestScope.runCurrent(): Unit = testScheduler.runCurrent() + +/** + * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the + * scheduled tasks in the meantime. + * + * In contrast with `TestCoroutineScope.advanceTimeBy`, this function does not run the tasks scheduled at the moment + * [currentTime] + [delayTimeMillis]. + * + * @throws IllegalStateException if passed a negative [delay][delayTimeMillis]. + * @see TestCoroutineScheduler.advanceTimeBy + */ +@ExperimentalCoroutinesApi +public fun TestScope.advanceTimeBy(delayTimeMillis: Long): Unit = testScheduler.advanceTimeBy(delayTimeMillis) + +/** + * The [test scheduler][TestScope.testScheduler] as a [TimeSource]. + * @see TestCoroutineScheduler.timeSource + */ +@ExperimentalCoroutinesApi +@ExperimentalTime +public val TestScope.testTimeSource: TimeSource get() = testScheduler.timeSource + +/** + * Creates a [TestScope]. + * + * It ensures that all the test module machinery is properly initialized. + * * If [context] doesn't provide a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping, + * a new one is created, unless either + * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used; + * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case + * its [TestCoroutineScheduler] is used. + * * If [context] doesn't have a [TestDispatcher], a [StandardTestDispatcher] is created. + * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were + * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was + * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an + * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility. + * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created + * [TestCoroutineScope] and share your use case at + * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues). + * * If [context] provides a [Job], that job is used as a parent for the new scope. + * + * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a + * different scheduler. + * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher]. + * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an + * [UncaughtExceptionCaptor]. + */ +@ExperimentalCoroutinesApi +@Suppress("FunctionName") +public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope { + val ctxWithDispatcher = context.withDelaySkipping() + var scope: TestScopeImpl? = null + val exceptionHandler = when (ctxWithDispatcher[CoroutineExceptionHandler]) { + null -> CoroutineExceptionHandler { _, exception -> + scope!!.reportException(exception) + } + else -> throw IllegalArgumentException( + "A CoroutineExceptionHandler was passed to TestScope. " + + "Please pass it as an argument to a `launch` or `async` block on an already-created scope " + + "if uncaught exceptions require special treatment." + ) + } + return TestScopeImpl(ctxWithDispatcher + exceptionHandler).also { scope = it } +} + +/** + * Adds a [TestDispatcher] and a [TestCoroutineScheduler] to the context if there aren't any already. + * + * @throws IllegalArgumentException if both a [TestCoroutineScheduler] and a [TestDispatcher] are passed. + * @throws IllegalArgumentException if a [ContinuationInterceptor] is passed that is not a [TestDispatcher]. + */ +internal fun CoroutineContext.withDelaySkipping(): CoroutineContext { + val dispatcher: TestDispatcher = when (val dispatcher = get(ContinuationInterceptor)) { + is TestDispatcher -> { + val ctxScheduler = get(TestCoroutineScheduler) + if (ctxScheduler != null) { + require(dispatcher.scheduler === ctxScheduler) { + "Both a TestCoroutineScheduler $ctxScheduler and TestDispatcher $dispatcher linked to " + + "another scheduler were passed." + } + } + dispatcher + } + null -> StandardTestDispatcher(get(TestCoroutineScheduler)) + else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher") + } + return this + dispatcher + dispatcher.scheduler +} + +internal class TestScopeImpl(context: CoroutineContext) : + AbstractCoroutine(context, initParentJob = true, active = true), TestScope { + + override val testScheduler get() = context[TestCoroutineScheduler]!! + + private var entered = false + private var finished = false + private val uncaughtExceptions = mutableListOf() + private val lock = SynchronizedObject() + + /** Called upon entry to [runTest]. Will throw if called more than once. */ + fun enter() { + val exceptions = synchronized(lock) { + if (entered) + throw IllegalStateException("Only a single call to `runTest` can be performed during one test.") + entered = true + check(!finished) + uncaughtExceptions + } + if (exceptions.isNotEmpty()) { + throw UncaughtExceptionsBeforeTest().apply { + for (e in exceptions) + addSuppressed(e) + } + } + } + + /** Called at the end of the test. May only be called once. */ + fun leave(): List { + val exceptions = synchronized(lock) { + if(!entered || finished) + throw IllegalStateException("An internal error. Please report to the Kotlinx Coroutines issue tracker") + finished = true + uncaughtExceptions + } + val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest` + if (exceptions.isEmpty()) { + if (activeJobs.isNotEmpty()) + throw UncompletedCoroutinesError( + "Active jobs found during the tear-down. " + + "Ensure that all coroutines are completed or cancelled by your test. " + + "The active jobs: $activeJobs" + ) + if (!testScheduler.isIdle()) + throw UncompletedCoroutinesError( + "Unfinished coroutines found during the tear-down. " + + "Ensure that all coroutines are completed or cancelled by your test." + ) + } + return exceptions + } + + /** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */ + fun reportException(throwable: Throwable) { + synchronized(lock) { + if (finished) { + throw throwable + } else { + uncaughtExceptions.add(throwable) + if (!entered) + throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) } + } + } + } + + /** Throws an exception if the coroutine is not completing. */ + fun tryGetCompletionCause(): Throwable? = completionCause + + override fun toString(): String = + "TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]" +} + +/** Use the knowledge that any [TestScope] that we receive is necessarily a [TestScopeImpl]. */ +internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) { + is TestScopeImpl -> this +} + +internal class UncaughtExceptionsBeforeTest : IllegalStateException( + "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," + + " as such exceptions are also reported in a platform-dependent manner so that they are not lost." +) + +/** + * Thrown when a test has completed and there are tasks that are not completed or cancelled. + */ +@ExperimentalCoroutinesApi +internal class UncompletedCoroutinesError(message: String) : AssertionError(message) diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt new file mode 100644 index 0000000000..24e093be21 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* +import kotlin.coroutines.* + +/** + * The testable main dispatcher used by kotlinx-coroutines-test. + * It is a [MainCoroutineDispatcher] that delegates all actions to a settable delegate. + */ +internal class TestMainDispatcher(delegate: CoroutineDispatcher): + MainCoroutineDispatcher(), + Delay +{ + private val mainDispatcher = delegate + private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") + + private val delay + get() = delegate.value as? Delay ?: defaultDelay + + override val immediate: MainCoroutineDispatcher + get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this + + override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block) + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context) + + override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block) + + fun setDispatcher(dispatcher: CoroutineDispatcher) { + delegate.value = dispatcher + } + + fun resetDispatcher() { + delegate.value = mainDispatcher + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = + delay.scheduleResumeAfterDelay(timeMillis, continuation) + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + delay.invokeOnTimeout(timeMillis, block, context) + + companion object { + internal val currentTestDispatcher + get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher + + internal val currentTestScheduler + get() = currentTestDispatcher?.scheduler + } + + /** + * A wrapper around a value that attempts to throw when writing happens concurrently with reading. + * + * The read operations never throw. Instead, the failures detected inside them will be remembered and thrown on the + * next modification. + */ + private class NonConcurrentlyModifiable(initialValue: T, private val name: String) { + private val readers = atomic(0) // number of concurrent readers + private val isWriting = atomic(false) // a modification is happening currently + private val exceptionWhenReading: AtomicRef = atomic(null) // exception from reading + private val _value = atomic(initialValue) // the backing field for the value + + private fun concurrentWW() = IllegalStateException("$name is modified concurrently") + private fun concurrentRW() = IllegalStateException("$name is used concurrently with setting it") + + var value: T + get() { + readers.incrementAndGet() + if (isWriting.value) exceptionWhenReading.value = concurrentRW() + val result = _value.value + readers.decrementAndGet() + return result + } + set(value) { + exceptionWhenReading.getAndSet(null)?.let { throw it } + if (readers.value != 0) throw concurrentRW() + if (!isWriting.compareAndSet(expect = false, update = true)) throw concurrentWW() + _value.value = value + isWriting.value = false + if (readers.value != 0) throw concurrentRW() + } + } +} + +@Suppress("INVISIBLE_MEMBER") +private val defaultDelay + inline get() = DefaultDelay + +@Suppress("INVISIBLE_MEMBER") +internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher diff --git a/kotlinx-coroutines-test/common/test/Helpers.kt b/kotlinx-coroutines-test/common/test/Helpers.kt new file mode 100644 index 0000000000..98375b0905 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/Helpers.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.atomicfu.* +import kotlin.test.* +import kotlin.time.* +import kotlin.time.Duration.Companion.seconds + +/** + * The number of milliseconds that is sure not to pass [assertRunsFast]. + */ +const val SLOW = 100_000L + +/** + * Asserts that a block completed within [timeout]. + */ +@OptIn(ExperimentalTime::class) +inline fun assertRunsFast(timeout: Duration, block: () -> T): T { + val result: T + val elapsed = TimeSource.Monotonic.measureTime { result = block() } + assertTrue("Should complete in $timeout, but took $elapsed") { elapsed < timeout } + return result +} + +/** + * Asserts that a block completed within two seconds. + */ +inline fun assertRunsFast(block: () -> T): T = assertRunsFast(2.seconds, block) + +/** + * Passes [test] as an argument to [block], but as a function returning not a [TestResult] but [Unit]. +*/ +expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult + +class TestException(message: String? = null): Exception(message) + +/** + * A class inheriting from which allows to check the execution order inside tests. + * + * @see TestBase + */ +open class OrderedExecutionTestBase { + private val actionIndex = atomic(0) + private val finished = atomic(false) + + /** Expect the next action to be [index] in order. */ + protected fun expect(index: Int) { + val wasIndex = actionIndex.incrementAndGet() + check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } + } + + /** Expect this action to be final, with the given [index]. */ + protected fun finish(index: Int) { + expect(index) + check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" } + } + + @AfterTest + fun ensureFinishCalls() { + assertTrue(finished.value || actionIndex.value == 0, "Expected `finish` to be called") + } +} + +internal fun T.void() { } + +@OptionalExpectation +expect annotation class NoJs() + +@OptionalExpectation +expect annotation class NoNative() diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt new file mode 100644 index 0000000000..3b6272c062 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt @@ -0,0 +1,360 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.flow.* +import kotlin.coroutines.* +import kotlin.test.* + +class RunTestTest { + + /** Tests that [withContext] that sends work to other threads works in [runTest]. */ + @Test + fun testWithContextDispatching() = runTest { + var counter = 0 + withContext(Dispatchers.Default) { + counter += 1 + } + assertEquals(counter, 1) + } + + /** Tests that joining [GlobalScope.launch] works in [runTest]. */ + @Test + fun testJoiningForkedJob() = runTest { + var counter = 0 + val job = GlobalScope.launch { + counter += 1 + } + job.join() + assertEquals(counter, 1) + } + + /** Tests [suspendCoroutine] not failing [runTest]. */ + @Test + fun testSuspendCoroutine() = runTest { + val answer = suspendCoroutine { + it.resume(42) + } + assertEquals(42, answer) + } + + /** Tests that [runTest] attempts to detect it being run inside another [runTest] and failing in such scenarios. */ + @Test + fun testNestedRunTestForbidden() = runTest { + assertFailsWith { + runTest { } + } + } + + /** Tests that even the dispatch timeout of `0` is fine if all the dispatches go through the same scheduler. */ + @Test + fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTest(dispatchTimeoutMs = 0) { + // below is some arbitrary concurrent code where all dispatches go through the same scheduler. + launch { + delay(2000) + } + val deferred = async { + val job = launch(StandardTestDispatcher(testScheduler)) { + launch { + delay(500) + } + delay(1000) + } + job.join() + } + deferred.await() + } + + /** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */ + @Test + fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTest(dispatchTimeoutMs = 0) { + withContext(Dispatchers.Default) { + delay(10) + 3 + } + fail("shouldn't be reached") + } + } + + /** Tests that too low of a dispatch timeout causes crashes. */ + @Test + @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native + fun testRunTestWithSmallTimeout() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTest(dispatchTimeoutMs = 100) { + withContext(Dispatchers.Default) { + delay(10000) + 3 + } + fail("shouldn't be reached") + } + } + + /** Tests that, on timeout, the names of the active coroutines are listed, + * whereas the names of the completed ones are not. */ + @Test + @NoJs + @NoNative + fun testListingActiveCoroutinesOnTimeout(): TestResult { + val name1 = "GoodUniqueName" + val name2 = "BadUniqueName" + return testResultMap({ + try { + it() + fail("unreached") + } catch (e: UncompletedCoroutinesError) { + assertTrue((e.message ?: "").contains(name1)) + assertFalse((e.message ?: "").contains(name2)) + } + }) { + runTest(dispatchTimeoutMs = 10) { + launch(CoroutineName(name1)) { + CompletableDeferred().await() + } + launch(CoroutineName(name2)) { + } + } + } + } + + /** Tests that the [UncompletedCoroutinesError] suppresses an exception with which the coroutine is completing. */ + @Test + fun testFailureWithPendingCoroutine() = testResultMap({ + try { + it() + fail("unreached") + } catch (e: UncompletedCoroutinesError) { + @Suppress("INVISIBLE_MEMBER") + val suppressed = unwrap(e).suppressedExceptions + assertEquals(1, suppressed.size) + assertIs(suppressed[0]).also { + assertEquals("A", it.message) + } + } + }) { + runTest(dispatchTimeoutMs = 10) { + launch { + withContext(NonCancellable) { + awaitCancellation() + } + } + yield() + throw TestException("A") + } + } + + /** Tests that real delays can be accounted for with a large enough dispatch timeout. */ + @Test + fun testRunTestWithLargeTimeout() = runTest(dispatchTimeoutMs = 5000) { + withContext(Dispatchers.Default) { + delay(50) + } + } + + /** Tests uncaught exceptions being suppressed by the dispatch timeout error. */ + @Test + @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native + fun testRunTestTimingOutAndThrowing() = testResultMap({ fn -> + try { + fn() + fail("unreached") + } catch (e: UncompletedCoroutinesError) { + @Suppress("INVISIBLE_MEMBER") + val suppressed = unwrap(e).suppressedExceptions + assertEquals(1, suppressed.size) + assertIs(suppressed[0]).also { + assertEquals("A", it.message) + } + } + }) { + runTest(dispatchTimeoutMs = 1) { + coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A")) + withContext(Dispatchers.Default) { + delay(10000) + 3 + } + fail("shouldn't be reached") + } + } + + /** Tests that passing invalid contexts to [runTest] causes it to fail (on JS, without forking). */ + @Test + fun testRunTestWithIllegalContext() { + for (ctx in TestScopeTest.invalidContexts) { + assertFailsWith { + runTest(ctx) { } + } + } + } + + /** Tests that throwing exceptions in [runTest] fails the test with them. */ + @Test + fun testThrowingInRunTestBody() = testResultMap({ + assertFailsWith { it() } + }) { + runTest { + throw RuntimeException() + } + } + + /** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */ + @Test + fun testThrowingInRunTestPendingTask() = testResultMap({ + assertFailsWith { it() } + }) { + runTest { + launch { + delay(SLOW) + throw RuntimeException() + } + } + } + + @Test + fun reproducer2405() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + var collectedError = false + withContext(dispatcher) { + flow { emit(1) } + .combine( + flow { throw IllegalArgumentException() } + ) { int, string -> int.toString() + string } + .catch { emit("error") } + .collect { + assertEquals("error", it) + collectedError = true + } + } + assertTrue(collectedError) + } + + /** Tests that, once the test body has thrown, the child coroutines are cancelled. */ + @Test + fun testChildrenCancellationOnTestBodyFailure(): TestResult { + var job: Job? = null + return testResultMap({ + assertFailsWith { it() } + assertTrue(job!!.isCancelled) + }) { + runTest { + job = launch { + while (true) { + delay(1000) + } + } + throw AssertionError() + } + } + } + + /** Tests that [runTest] reports [TimeoutCancellationException]. */ + @Test + fun testTimeout() = testResultMap({ + assertFailsWith { it() } + }) { + runTest { + withTimeout(50) { + launch { + delay(1000) + } + } + } + } + + /** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */ + @Test + fun testRunTestThrowsRootCause() = testResultMap({ + assertFailsWith { it() } + }) { + runTest { + launch { + throw TestException() + } + } + } + + /** Tests that [runTest] completes its job. */ + @Test + fun testCompletesOwnJob(): TestResult { + var handlerCalled = false + return testResultMap({ + it() + assertTrue(handlerCalled) + }) { + runTest { + coroutineContext.job.invokeOnCompletion { + handlerCalled = true + } + } + } + } + + /** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */ + @Test + fun testDoesNotCompleteGivenJob(): TestResult { + var handlerCalled = false + val job = Job() + job.invokeOnCompletion { + handlerCalled = true + } + return testResultMap({ + it() + assertFalse(handlerCalled) + assertEquals(0, job.children.filter { it.isActive }.count()) + }) { + runTest(job) { + assertTrue(coroutineContext.job in job.children) + } + } + } + + /** Tests that, when the test body fails, the reported exceptions are suppressed. */ + @Test + fun testSuppressedExceptions() = testResultMap({ + try { + it() + fail("should not be reached") + } catch (e: TestException) { + assertEquals("w", e.message) + val suppressed = e.suppressedExceptions + + (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList()) + assertEquals(3, suppressed.size) + assertEquals("x", suppressed[0].message) + assertEquals("y", suppressed[1].message) + assertEquals("z", suppressed[2].message) + } + }) { + runTest { + launch(SupervisorJob()) { throw TestException("x") } + launch(SupervisorJob()) { throw TestException("y") } + launch(SupervisorJob()) { throw TestException("z") } + throw TestException("w") + } + } + + /** Tests that [TestCoroutineScope.runTest] does not inherit the exception handler and works. */ + @Test + fun testScopeRunTestExceptionHandler(): TestResult { + val scope = TestScope() + return testResultMap({ + try { + it() + fail("should not be reached") + } catch (e: TestException) { + // expected + } + }) { + scope.runTest { + launch(SupervisorJob()) { throw TestException("x") } + } + } + } +} diff --git a/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt new file mode 100644 index 0000000000..d66be9bdb6 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class StandardTestDispatcherTest: OrderedExecutionTestBase() { + + private val scope = TestScope(StandardTestDispatcher()) + + @BeforeTest + fun init() { + scope.asSpecificImplementation().enter() + } + + @AfterTest + fun cleanup() { + scope.runCurrent() + assertEquals(listOf(), scope.asSpecificImplementation().leave()) + } + + /** Tests that the [StandardTestDispatcher] follows an execution order similar to `runBlocking`. */ + @Test + fun testFlowsNotSkippingValues() = scope.launch { + // https://github.com/Kotlin/kotlinx.coroutines/issues/1626#issuecomment-554632852 + val list = flowOf(1).onStart { emit(0) } + .combine(flowOf("A")) { int, str -> "$str$int" } + .toList() + assertEquals(list, listOf("A0", "A1")) + }.void() + + /** Tests that each [launch] gets dispatched. */ + @Test + fun testLaunchDispatched() = scope.launch { + expect(1) + launch { + expect(3) + } + finish(2) + }.void() + + /** Tests that dispatching is done in a predictable order and [yield] puts this task at the end of the queue. */ + @Test + fun testYield() = scope.launch { + expect(1) + scope.launch { + expect(3) + yield() + expect(6) + } + scope.launch { + expect(4) + yield() + finish(7) + } + expect(2) + yield() + expect(5) + }.void() + + /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */ + @Test + @NoNative + fun testSchedulerReuse() { + val dispatcher1 = StandardTestDispatcher() + Dispatchers.setMain(dispatcher1) + try { + val dispatcher2 = StandardTestDispatcher() + assertSame(dispatcher1.scheduler, dispatcher2.scheduler) + } finally { + Dispatchers.resetMain() + } + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt new file mode 100644 index 0000000000..d050e9c8c0 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt @@ -0,0 +1,335 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.test.* +import kotlin.time.* +import kotlin.time.Duration.Companion.seconds + +class TestCoroutineSchedulerTest { + /** Tests that `TestCoroutineScheduler` attempts to detect if there are several instances of it. */ + @Test + fun testContextElement() = runTest { + assertFailsWith { + withContext(StandardTestDispatcher()) { + } + } + } + + /** Tests that, as opposed to [DelayController.advanceTimeBy] or [TestCoroutineScope.advanceTimeBy], + * [TestCoroutineScheduler.advanceTimeBy] doesn't run the tasks scheduled at the target moment. */ + @Test + fun testAdvanceTimeByDoesNotRunCurrent() = runTest { + var entered = false + launch { + delay(15) + entered = true + } + testScheduler.advanceTimeBy(15) + assertFalse(entered) + testScheduler.runCurrent() + assertTrue(entered) + } + + /** Tests that [TestCoroutineScheduler.advanceTimeBy] doesn't accept negative delays. */ + @Test + fun testAdvanceTimeByWithNegativeDelay() { + val scheduler = TestCoroutineScheduler() + assertFailsWith { + scheduler.advanceTimeBy(-1) + } + } + + /** Tests that if [TestCoroutineScheduler.advanceTimeBy] encounters an arithmetic overflow, all the tasks scheduled + * until the moment [Long.MAX_VALUE] get run. */ + @Test + fun testAdvanceTimeByEnormousDelays() = forTestDispatchers { + assertRunsFast { + with (TestScope(it)) { + launch { + val initialDelay = 10L + delay(initialDelay) + assertEquals(initialDelay, currentTime) + var enteredInfinity = false + launch { + delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing + assertEquals(Long.MAX_VALUE, currentTime) + enteredInfinity = true + } + var enteredNearInfinity = false + launch { + delay(Long.MAX_VALUE - initialDelay - 1) + assertEquals(Long.MAX_VALUE - 1, currentTime) + enteredNearInfinity = true + } + testScheduler.advanceTimeBy(Long.MAX_VALUE) + assertFalse(enteredInfinity) + assertTrue(enteredNearInfinity) + assertEquals(Long.MAX_VALUE, currentTime) + testScheduler.runCurrent() + assertTrue(enteredInfinity) + } + testScheduler.advanceUntilIdle() + } + } + } + + /** Tests the basic functionality of [TestCoroutineScheduler.advanceTimeBy]. */ + @Test + fun testAdvanceTimeBy() = runTest { + assertRunsFast { + var stage = 1 + launch { + delay(1_000) + assertEquals(1_000, currentTime) + stage = 2 + delay(500) + assertEquals(1_500, currentTime) + stage = 3 + delay(501) + assertEquals(2_001, currentTime) + stage = 4 + } + assertEquals(1, stage) + assertEquals(0, currentTime) + advanceTimeBy(2_000) + assertEquals(3, stage) + assertEquals(2_000, currentTime) + advanceTimeBy(2) + assertEquals(4, stage) + assertEquals(2_002, currentTime) + } + } + + /** Tests the basic functionality of [TestCoroutineScheduler.runCurrent]. */ + @Test + fun testRunCurrent() = runTest { + var stage = 0 + launch { + delay(1) + ++stage + delay(1) + stage += 10 + } + launch { + delay(1) + ++stage + delay(1) + stage += 10 + } + testScheduler.advanceTimeBy(1) + assertEquals(0, stage) + runCurrent() + assertEquals(2, stage) + testScheduler.advanceTimeBy(1) + assertEquals(2, stage) + runCurrent() + assertEquals(22, stage) + } + + /** Tests that [TestCoroutineScheduler.runCurrent] will not run new tasks after the current time has advanced. */ + @Test + fun testRunCurrentNotDrainingQueue() = forTestDispatchers { + assertRunsFast { + val scheduler = it.scheduler + val scope = TestScope(it) + var stage = 1 + scope.launch { + delay(SLOW) + launch { + delay(SLOW) + stage = 3 + } + scheduler.advanceTimeBy(SLOW) + stage = 2 + } + scheduler.advanceTimeBy(SLOW) + assertEquals(1, stage) + scheduler.runCurrent() + assertEquals(2, stage) + scheduler.runCurrent() + assertEquals(3, stage) + } + } + + /** Tests that [TestCoroutineScheduler.advanceUntilIdle] doesn't hang when itself running in a scheduler task. */ + @Test + fun testNestedAdvanceUntilIdle() = forTestDispatchers { + assertRunsFast { + val scheduler = it.scheduler + val scope = TestScope(it) + var executed = false + scope.launch { + launch { + delay(SLOW) + executed = true + } + scheduler.advanceUntilIdle() + } + scheduler.advanceUntilIdle() + assertTrue(executed) + } + } + + /** Tests [yield] scheduling tasks for future execution and not executing immediately. */ + @Test + fun testYield() = forTestDispatchers { + val scope = TestScope(it) + var stage = 0 + scope.launch { + yield() + assertEquals(1, stage) + stage = 2 + } + scope.launch { + yield() + assertEquals(2, stage) + stage = 3 + } + assertEquals(0, stage) + stage = 1 + scope.runCurrent() + } + + /** Tests that dispatching the delayed tasks is ordered by their waking times. */ + @Test + fun testDelaysPriority() = forTestDispatchers { + val scope = TestScope(it) + var lastMeasurement = 0L + fun checkTime(time: Long) { + assertTrue(lastMeasurement < time) + assertEquals(time, scope.currentTime) + lastMeasurement = scope.currentTime + } + scope.launch { + launch { + delay(100) + checkTime(100) + val deferred = async { + delay(70) + checkTime(170) + } + delay(1) + checkTime(101) + deferred.await() + delay(1) + checkTime(171) + } + launch { + delay(200) + checkTime(200) + } + launch { + delay(150) + checkTime(150) + delay(22) + checkTime(172) + } + delay(201) + } + scope.advanceUntilIdle() + checkTime(201) + } + + private fun TestScope.checkTimeout( + timesOut: Boolean, timeoutMillis: Long = SLOW, block: suspend () -> Unit + ) = assertRunsFast { + var caughtException = false + asSpecificImplementation().enter() + launch { + try { + withTimeout(timeoutMillis) { + block() + } + } catch (e: TimeoutCancellationException) { + caughtException = true + } + } + advanceUntilIdle() + asSpecificImplementation().leave().throwAll() + if (timesOut) + assertTrue(caughtException) + else + assertFalse(caughtException) + } + + /** Tests that timeouts get triggered. */ + @Test + fun testSmallTimeouts() = forTestDispatchers { + val scope = TestScope(it) + scope.checkTimeout(true) { + val half = SLOW / 2 + delay(half) + delay(SLOW - half) + } + } + + /** Tests that timeouts don't get triggered if the code finishes in time. */ + @Test + fun testLargeTimeouts() = forTestDispatchers { + val scope = TestScope(it) + scope.checkTimeout(false) { + val half = SLOW / 2 + delay(half) + delay(SLOW - half - 1) + } + } + + /** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */ + @Test + fun testSmallAsynchronousTimeouts() = forTestDispatchers { + val scope = TestScope(it) + val deferred = CompletableDeferred() + scope.launch { + val half = SLOW / 2 + delay(half) + delay(SLOW - half) + deferred.complete(Unit) + } + scope.checkTimeout(true) { + deferred.await() + } + } + + /** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */ + @Test + fun testLargeAsynchronousTimeouts() = forTestDispatchers { + val scope = TestScope(it) + val deferred = CompletableDeferred() + scope.launch { + val half = SLOW / 2 + delay(half) + delay(SLOW - half - 1) + deferred.complete(Unit) + } + scope.checkTimeout(false) { + deferred.await() + } + } + + @Test + @ExperimentalTime + fun testAdvanceTimeSource() = runTest { + val expected = 1.seconds + val actual = testTimeSource.measureTime { + delay(expected) + } + assertEquals(expected, actual) + } + + private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit = + @Suppress("DEPRECATION") + listOf( + StandardTestDispatcher(), + UnconfinedTestDispatcher() + ).forEach { + try { + block(it) + } catch (e: Throwable) { + throw RuntimeException("Test failed for dispatcher $it", e) + } + } +} diff --git a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt new file mode 100644 index 0000000000..bcf016b3d3 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.internal.* +import kotlin.coroutines.* +import kotlin.test.* + +@NoNative +class TestDispatchersTest: OrderedExecutionTestBase() { + + @BeforeTest + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + /** Tests that asynchronous execution of tests does not happen concurrently with [AfterTest]. */ + @Test + fun testMainMocking() = runTest { + val mainAtStart = TestMainDispatcher.currentTestDispatcher + assertNotNull(mainAtStart) + withContext(Dispatchers.Main) { + delay(10) + } + withContext(Dispatchers.Default) { + delay(10) + } + withContext(Dispatchers.Main) { + delay(10) + } + assertSame(mainAtStart, TestMainDispatcher.currentTestDispatcher) + } + + /** Tests that the mocked [Dispatchers.Main] correctly forwards [Delay] methods. */ + @Test + fun testMockedMainImplementsDelay() = runTest { + val main = Dispatchers.Main + withContext(main) { + delay(10) + } + withContext(Dispatchers.Default) { + delay(10) + } + withContext(main) { + delay(10) + } + } + + /** Tests that [Distpachers.setMain] fails when called with [Dispatchers.Main]. */ + @Test + fun testSelfSet() { + assertFailsWith { Dispatchers.setMain(Dispatchers.Main) } + } + + @Test + fun testImmediateDispatcher() = runTest { + Dispatchers.setMain(ImmediateDispatcher()) + expect(1) + withContext(Dispatchers.Main) { + expect(3) + } + + Dispatchers.setMain(RegularDispatcher()) + withContext(Dispatchers.Main) { + expect(6) + } + + finish(7) + } + + private inner class ImmediateDispatcher : CoroutineDispatcher() { + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + expect(2) + return false + } + + override fun dispatch(context: CoroutineContext, block: Runnable) = throw RuntimeException("Shouldn't be reached") + } + + private inner class RegularDispatcher : CoroutineDispatcher() { + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + expect(4) + return true + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + expect(5) + block.run() + } + } +} diff --git a/kotlinx-coroutines-test/common/test/TestScopeTest.kt b/kotlinx-coroutines-test/common/test/TestScopeTest.kt new file mode 100644 index 0000000000..7031056f11 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/TestScopeTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.test.* + +class TestScopeTest { + /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */ + @Test + fun testCreateThrowsOnInvalidArguments() { + for (ctx in invalidContexts) { + assertFailsWith { + TestScope(ctx) + } + } + } + + /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */ + @Test + fun testCreateProvidesScheduler() { + // Creates a new scheduler. + run { + val scope = TestScope() + assertNotNull(scope.coroutineContext[TestCoroutineScheduler]) + } + // Reuses the scheduler that the dispatcher is linked to. + run { + val dispatcher = StandardTestDispatcher() + val scope = TestScope(dispatcher) + assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) + } + // Uses the scheduler passed to it. + run { + val scheduler = TestCoroutineScheduler() + val scope = TestScope(scheduler) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler) + } + // Doesn't touch the passed dispatcher and the scheduler if they match. + run { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val scope = TestScope(scheduler + dispatcher) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor]) + } + } + + /** Part of [testCreateProvidesScheduler], disabled for Native */ + @Test + @NoNative + fun testCreateReusesScheduler() { + // Reuses the scheduler of `Dispatchers.Main` + run { + val scheduler = TestCoroutineScheduler() + val mainDispatcher = StandardTestDispatcher(scheduler) + Dispatchers.setMain(mainDispatcher) + try { + val scope = TestScope() + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) + } finally { + Dispatchers.resetMain() + } + } + // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed + run { + val mainDispatcher = StandardTestDispatcher() + Dispatchers.setMain(mainDispatcher) + try { + val scheduler = TestCoroutineScheduler() + val scope = TestScope(scheduler) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) + } finally { + Dispatchers.resetMain() + } + } + } + + /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */ + @Test + fun testPresentDelaysThrowing() { + val scope = TestScope() + var result = false + scope.launch { + delay(5) + result = true + } + assertFalse(result) + scope.asSpecificImplementation().enter() + assertFailsWith { scope.asSpecificImplementation().leave() } + assertFalse(result) + } + + /** Tests that the cleanup procedure throws if there were active jobs by the end. */ + @Test + fun testActiveJobsThrowing() { + val scope = TestScope() + var result = false + val deferred = CompletableDeferred() + scope.launch { + deferred.await() + result = true + } + assertFalse(result) + scope.asSpecificImplementation().enter() + assertFailsWith { scope.asSpecificImplementation().leave() } + assertFalse(result) + } + + /** Tests that the cleanup procedure throws even if it detects that the job is already cancelled. */ + @Test + fun testCancelledDelaysThrowing() { + val scope = TestScope() + var result = false + val deferred = CompletableDeferred() + val job = scope.launch { + deferred.await() + result = true + } + job.cancel() + assertFalse(result) + scope.asSpecificImplementation().enter() + assertFailsWith { scope.asSpecificImplementation().leave() } + assertFalse(result) + } + + /** Tests that uncaught exceptions are thrown at the cleanup. */ + @Test + fun testGetsCancelledOnChildFailure(): TestResult { + val scope = TestScope() + val exception = TestException("test") + scope.launch { + throw exception + } + return testResultMap({ + try { + it() + fail("should not reach") + } catch (e: TestException) { + // expected + } + }) { + scope.runTest { + } + } + } + + /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */ + @Test + fun testSuppressedExceptions() { + TestScope().apply { + asSpecificImplementation().enter() + launch(SupervisorJob()) { throw TestException("x") } + launch(SupervisorJob()) { throw TestException("y") } + launch(SupervisorJob()) { throw TestException("z") } + runCurrent() + val e = asSpecificImplementation().leave() + assertEquals(3, e.size) + assertEquals("x", e[0].message) + assertEquals("y", e[1].message) + assertEquals("z", e[2].message) + } + } + + companion object { + internal val invalidContexts = listOf( + Dispatchers.Default, // not a [TestDispatcher] + CoroutineExceptionHandler { _, _ -> }, // exception handlers can't be overridden + StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler + ) + } +} diff --git a/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt new file mode 100644 index 0000000000..ee63e6d118 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class UnconfinedTestDispatcherTest { + + @Test + fun reproducer1742() { + class ObservableValue(initial: T) { + var value: T = initial + private set + + private val listeners = mutableListOf<(T) -> Unit>() + + fun set(value: T) { + this.value = value + listeners.forEach { it(value) } + } + + fun addListener(listener: (T) -> Unit) { + listeners.add(listener) + } + + fun removeListener(listener: (T) -> Unit) { + listeners.remove(listener) + } + } + + fun ObservableValue.observe(): Flow = + callbackFlow { + val listener = { value: T -> + if (!isClosedForSend) { + trySend(value) + } + } + addListener(listener) + listener(value) + awaitClose { removeListener(listener) } + } + + val intProvider = ObservableValue(0) + val stringProvider = ObservableValue("") + var data = Pair(0, "") + val scope = CoroutineScope(UnconfinedTestDispatcher()) + scope.launch { + combine( + intProvider.observe(), + stringProvider.observe() + ) { intValue, stringValue -> Pair(intValue, stringValue) } + .collect { pair -> + data = pair + } + } + + intProvider.set(1) + stringProvider.set("3") + intProvider.set(2) + intProvider.set(3) + + scope.cancel() + assertEquals(Pair(3, "3"), data) + } + + @Test + fun reproducer2082() = runTest { + val subject1 = MutableStateFlow(1) + val subject2 = MutableStateFlow("a") + val values = mutableListOf>() + + val job = launch(UnconfinedTestDispatcher(testScheduler)) { + combine(subject1, subject2) { intVal, strVal -> intVal to strVal } + .collect { + delay(10000) + values += it + } + } + + subject1.value = 2 + delay(10000) + subject2.value = "b" + delay(10000) + + subject1.value = 3 + delay(10000) + subject2.value = "c" + delay(10000) + delay(10000) + delay(1) + + job.cancel() + + assertEquals(listOf(Pair(1, "a"), Pair(2, "a"), Pair(2, "b"), Pair(3, "b"), Pair(3, "c")), values) + } + + @Test + fun reproducer2405() = createTestResult { + val dispatcher = UnconfinedTestDispatcher() + var collectedError = false + withContext(dispatcher) { + flow { emit(1) } + .combine( + flow { throw IllegalArgumentException() } + ) { int, string -> int.toString() + string } + .catch { emit("error") } + .collect { + assertEquals("error", it) + collectedError = true + } + } + assertTrue(collectedError) + } + + /** An example from the [UnconfinedTestDispatcher] documentation. */ + @Test + fun testUnconfinedDispatcher() = runTest { + val values = mutableListOf() + 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() + assertEquals(listOf(0, 1, 2, 3), values) + } + + /** Tests that child coroutines are eagerly entered. */ + @Test + fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) { + var entered = false + val deferred = CompletableDeferred() + var completed = false + launch { + entered = true + deferred.await() + completed = true + } + assertTrue(entered) // `entered = true` already executed. + assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued. + deferred.complete(Unit) // resume the coroutine. + assertTrue(completed) // now the child coroutine is immediately completed. + } + + /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */ + @Test + @NoNative + fun testSchedulerReuse() { + val dispatcher1 = StandardTestDispatcher() + Dispatchers.setMain(dispatcher1) + try { + val dispatcher2 = UnconfinedTestDispatcher() + assertSame(dispatcher1.scheduler, dispatcher2.scheduler) + } finally { + Dispatchers.resetMain() + } + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/js/src/TestBuilders.kt b/kotlinx-coroutines-test/js/src/TestBuilders.kt new file mode 100644 index 0000000000..3976885991 --- /dev/null +++ b/kotlinx-coroutines-test/js/src/TestBuilders.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test +import kotlinx.coroutines.* +import kotlin.js.* + +@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") +public actual typealias TestResult = Promise + +internal actual fun createTestResult(testProcedure: suspend () -> Unit): TestResult = + GlobalScope.promise { + testProcedure() + } \ No newline at end of file diff --git a/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt new file mode 100644 index 0000000000..4d865f83c0 --- /dev/null +++ b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2021 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.* + +@Suppress("INVISIBLE_MEMBER") +internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = + when (val mainDispatcher = Main) { + is TestMainDispatcher -> mainDispatcher + else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) } + } diff --git a/kotlinx-coroutines-test/js/test/Helpers.kt b/kotlinx-coroutines-test/js/test/Helpers.kt new file mode 100644 index 0000000000..5f19d1ac58 --- /dev/null +++ b/kotlinx-coroutines-test/js/test/Helpers.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlin.test.* + +actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult = + test().then( + { + block { + } + }, { + block { + throw it + } + }) + +actual typealias NoJs = Ignore diff --git a/kotlinx-coroutines-test/js/test/PromiseTest.kt b/kotlinx-coroutines-test/js/test/PromiseTest.kt new file mode 100644 index 0000000000..ff09d9ab86 --- /dev/null +++ b/kotlinx-coroutines-test/js/test/PromiseTest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.test.* + +class PromiseTest { + @Test + fun testCompletionFromPromise() = runTest { + var promiseEntered = false + val p = promise { + delay(1) + promiseEntered = true + } + delay(2) + p.await() + assertTrue(promiseEntered) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/resources/META-INF/proguard/coroutines.pro b/kotlinx-coroutines-test/jvm/resources/META-INF/proguard/coroutines.pro similarity index 100% rename from kotlinx-coroutines-test/resources/META-INF/proguard/coroutines.pro rename to kotlinx-coroutines-test/jvm/resources/META-INF/proguard/coroutines.pro diff --git a/kotlinx-coroutines-test/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/kotlinx-coroutines-test/jvm/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory similarity index 100% rename from kotlinx-coroutines-test/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory rename to kotlinx-coroutines-test/jvm/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory diff --git a/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt new file mode 100644 index 0000000000..7cafb54753 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* + +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias TestResult = Unit + +internal actual fun createTestResult(testProcedure: suspend () -> Unit) { + runBlocking { + testProcedure() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt new file mode 100644 index 0000000000..f86b08ea14 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2021 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.* + +internal class TestMainDispatcherFactory : MainDispatcherFactory { + + override fun createDispatcher(allFactories: List): MainCoroutineDispatcher { + val otherFactories = allFactories.filter { it !== this } + val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory + val dispatcher = secondBestFactory.tryCreateDispatcher(otherFactories) + return TestMainDispatcher(dispatcher) + } + + /** + * [Int.MAX_VALUE] -- test dispatcher always wins no matter what factories are present in the classpath. + * By default, all actions are delegated to the second-priority dispatcher, so that it won't be the issue. + */ + override val loadPriority: Int + get() = Int.MAX_VALUE +} + +internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher { + val mainDispatcher = Main + require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." } + return mainDispatcher +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt new file mode 100644 index 0000000000..3ccf2cadd7 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("DEPRECATION") + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* + +/** + * Control the virtual clock time of a [CoroutineDispatcher]. + * + * Testing libraries may expose this interface to the tests instead of [TestCoroutineDispatcher]. + * + * This interface is deprecated without replacement. + * Instead, [TestCoroutineScheduler] is supposed to be used to control the virtual time. + * Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + */ +@ExperimentalCoroutinesApi +@Deprecated( + "Use `TestCoroutineScheduler` to control virtual time.", + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public interface DelayController { + /** + * Returns the current virtual clock-time as it is known to this Dispatcher. + * + * @return The virtual clock-time + */ + @ExperimentalCoroutinesApi + public val currentTime: Long + + /** + * Moves the Dispatcher's virtual clock forward by a specified amount of time. + * + * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses + * blocking coroutines. + * + * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested + * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different + * calls to delay. + * + * ``` + * @Test + * fun advanceTimeTest() = runBlockingTest { + * foo() + * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays + * // virtual time is 2_000, next resume is at 2_001 + * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced) + * // virtual time is 2_0002 + * } + * + * fun CoroutineScope.foo() { + * launch { + * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000) + * // virtual time is 1_000 + * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500) + * // virtual time is 1_500 + * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001) + * // virtual time is 2_001 + * } + * } + * ``` + * + * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward. + * @return The amount of delay-time that this Dispatcher's clock has been forwarded. + */ + @ExperimentalCoroutinesApi + public fun advanceTimeBy(delayTimeMillis: Long): Long + + /** + * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. + * + * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle` + * returns. + * + * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. + */ + @ExperimentalCoroutinesApi + public fun advanceUntilIdle(): Long + + /** + * Run any tasks that are pending at or before the current virtual clock-time. + * + * Calling this function will never advance the clock. + */ + @ExperimentalCoroutinesApi + public fun runCurrent() + + /** + * Call after test code completes to ensure that the dispatcher is properly cleaned up. + * + * @throws AssertionError if any pending tasks are active, however it will not throw for suspended + * coroutines. + */ + @ExperimentalCoroutinesApi + @Throws(AssertionError::class) + public fun cleanupTestCoroutines() + + /** + * Run a block of code in a paused dispatcher. + * + * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher + * will resume auto-advancing. + * + * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or + * setup may be done between the time the coroutine is created and started. + */ + @Deprecated( + "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + public suspend fun pauseDispatcher(block: suspend () -> Unit) + + /** + * Pause the dispatcher. + * + * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or + * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines. + */ + @Deprecated( + "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + public fun pauseDispatcher() + + /** + * Resume the dispatcher from a paused state. + * + * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance + * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], + * or [advanceUntilIdle]. + */ + @Deprecated( + "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + public fun resumeDispatcher() +} + +internal interface SchedulerAsDelayController : DelayController { + val scheduler: TestCoroutineScheduler + + /** @suppress */ + @Deprecated( + "This property delegates to the test scheduler, which may cause confusing behavior unless made explicit.", + ReplaceWith("this.scheduler.currentTime"), + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + override val currentTime: Long + get() = scheduler.currentTime + + + /** @suppress */ + @Deprecated( + "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.", + ReplaceWith("this.scheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"), + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + override fun advanceTimeBy(delayTimeMillis: Long): Long { + val oldTime = scheduler.currentTime + scheduler.advanceTimeBy(delayTimeMillis) + scheduler.runCurrent() + return scheduler.currentTime - oldTime + } + + /** @suppress */ + @Deprecated( + "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.", + ReplaceWith("this.scheduler.advanceUntilIdle()"), + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + override fun advanceUntilIdle(): Long { + val oldTime = scheduler.currentTime + scheduler.advanceUntilIdle() + return scheduler.currentTime - oldTime + } + + /** @suppress */ + @Deprecated( + "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.", + ReplaceWith("this.scheduler.runCurrent()"), + level = DeprecationLevel.WARNING + ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + override fun runCurrent(): Unit = scheduler.runCurrent() + + /** @suppress */ + @ExperimentalCoroutinesApi + override fun cleanupTestCoroutines() { + // process any pending cancellations or completions, but don't advance time + scheduler.runCurrent() + if (!scheduler.isIdle(strict = false)) { + throw UncompletedCoroutinesError( + "Unfinished coroutines during tear-down. Ensure all coroutines are" + + " completed or cancelled by your test." + ) + } + } +} diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt new file mode 100644 index 0000000000..a60d65c1b8 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("DEPRECATION") +@file:JvmName("TestBuildersKt") +@file:JvmMultifileClass + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.jvm.* + +/** + * Executes a [testBody] inside an immediate execution dispatcher. + * + * This method is deprecated in favor of [runTest]. Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + * + * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks. + * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take + * extra time. + * + * ``` + * @Test + * fun exampleTest() = runBlockingTest { + * val deferred = async { + * delay(1_000) + * async { + * delay(1_000) + * }.await() + * } + * + * deferred.await() // result available immediately + * } + * + * ``` + * + * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test + * conditions. + * + * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test. + * + * @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches + * (including coroutines suspended on join/await). + * + * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler], + * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively. + * @param testBody The code of the unit-test. + */ +@Deprecated("Use `runTest` instead to support completing from other dispatchers. " + + "Please see the migration guide for details: " + + "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md", + level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun runBlockingTest( + context: CoroutineContext = EmptyCoroutineContext, + testBody: suspend TestCoroutineScope.() -> Unit +) { + val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context) + val scheduler = scope.testScheduler + val deferred = scope.async { + scope.testBody() + } + scheduler.advanceUntilIdle() + deferred.getCompletionExceptionOrNull()?.let { + throw it + } + scope.cleanupTestCoroutines() +} + +/** + * A version of [runBlockingTest] that works with [TestScope]. + */ +@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun runBlockingTestOnTestScope( + context: CoroutineContext = EmptyCoroutineContext, + testBody: suspend TestScope.() -> Unit +) { + val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context + val startJobs = completeContext.activeJobs() + val scope = TestScope(completeContext).asSpecificImplementation() + scope.enter() + scope.start(CoroutineStart.UNDISPATCHED, scope) { + scope.testBody() + } + scope.testScheduler.advanceUntilIdle() + try { + scope.getCompletionExceptionOrNull() + } catch (e: IllegalStateException) { + null // the deferred was not completed yet; `scope.leave()` should complain then about unfinished jobs + }?.let { + val exceptions = try { + scope.leave() + } catch (e: UncompletedCoroutinesError) { + listOf() + } + (listOf(it) + exceptions).throwAll() + return + } + scope.leave().throwAll() + val jobs = completeContext.activeJobs() - startJobs + if (jobs.isNotEmpty()) + throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs") +} + +/** + * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope]. + * + * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope]. + * Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + */ +@Deprecated("Use `runTest` instead to support completing from other dispatchers. " + + "Please see the migration guide for details: " + + "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md", + level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = + runBlockingTest(coroutineContext, block) + +/** + * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope]. + */ +@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit = + runBlockingTestOnTestScope(coroutineContext, block) + +/** + * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. + * + * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope]. + * Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + */ +@Deprecated("Use `runTest` instead to support completing from other dispatchers. " + + "Please see the migration guide for details: " + + "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md", + level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = + runBlockingTest(this, block) + +/** + * This is an overload of [runTest] that works with [TestCoroutineScope]. + */ +@ExperimentalCoroutinesApi +@Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun runTestWithLegacyScope( + context: CoroutineContext = EmptyCoroutineContext, + dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, + testBody: suspend TestCoroutineScope.() -> Unit +): TestResult { + if (context[RunningInRunTest] != null) + throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.") + val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest)) + return createTestResult { + runTestCoroutine(testScope, dispatchTimeoutMs, TestBodyCoroutine::tryGetCompletionCause, testBody) { + try { + testScope.cleanup() + emptyList() + } catch (e: UncompletedCoroutinesError) { + throw e + } catch (e: Throwable) { + listOf(e) + } + } + } +} + +/** + * Runs a test in a [TestCoroutineScope] based on this one. + * + * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the + * [block] will be different from this one, but will use its [Job] as a parent. + * + * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned + * immediately from the test body. See the docs for [TestResult] for details. + */ +@ExperimentalCoroutinesApi +@Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope.runTest( + dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, + block: suspend TestCoroutineScope.() -> Unit +): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block) + +private class TestBodyCoroutine( + private val testScope: TestCoroutineScope, +) : AbstractCoroutine(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope { + + override val testScheduler get() = testScope.testScheduler + + @Deprecated( + "This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.", + ReplaceWith("this.cleanup()"), + DeprecationLevel.ERROR + ) + override fun cleanupTestCoroutines() = + throw UnsupportedOperationException( + "Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " + + "it will be called at the end of the test in any case." + ) + + fun cleanup() = testScope.cleanupTestCoroutines() + + /** Throws an exception if the coroutine is not completing. */ + fun tryGetCompletionCause(): Throwable? = completionCause +} diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt new file mode 100644 index 0000000000..ec2a3046ee --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +/** + * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests + * and uses a [TestCoroutineScheduler] to control its virtual clock. + * + * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are + * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the + * methods on the dispatcher's [scheduler]. + * + * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will + * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the + * methods on [DelayController]. + * + * @see DelayController + */ +@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " + + "pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.", + level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()): + TestDispatcher(), Delay, SchedulerAsDelayController +{ + private var dispatchImmediately = true + set(value) { + field = value + if (value) { + // there may already be tasks from setup code we need to run + scheduler.advanceUntilIdle() + } + } + + /** @suppress */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + if (dispatchImmediately) { + scheduler.sendDispatchEvent() + block.run() + } else { + post(block) + } + } + + /** @suppress */ + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + post(block) + } + + /** @suppress */ + override fun toString(): String = "TestCoroutineDispatcher[scheduler=$scheduler]" + + private fun post(block: Runnable) = + scheduler.registerEvent(this, 0, block) { false } + + /** @suppress */ + override suspend fun pauseDispatcher(block: suspend () -> Unit) { + val previous = dispatchImmediately + dispatchImmediately = false + try { + block() + } finally { + dispatchImmediately = previous + } + } + + /** @suppress */ + override fun pauseDispatcher() { + dispatchImmediately = false + } + + /** @suppress */ + override fun resumeDispatcher() { + dispatchImmediately = true + } +} diff --git a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt similarity index 53% rename from kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt rename to kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt index 66eb235906..9da521f05c 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt @@ -5,12 +5,20 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* /** * Access uncaught coroutine exceptions captured during test execution. */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +@Deprecated( + "Deprecated for removal without a replacement. " + + "Consider whether the default mechanism of handling uncaught exceptions is sufficient. " + + "If not, try writing your own `CoroutineExceptionHandler` and " + + "please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.", + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public interface UncaughtExceptionCaptor { /** * List of uncaught coroutine exceptions. @@ -34,26 +42,34 @@ public interface UncaughtExceptionCaptor { /** * An exception handler that captures uncaught exceptions in tests. */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +@Deprecated( + "Deprecated for removal without a replacement. " + + "It may be to define one's own `CoroutineExceptionHandler` if you just need to handle '" + + "uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public class TestCoroutineExceptionHandler : - AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler -{ + AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor { private val _exceptions = mutableListOf() + private val _lock = SynchronizedObject() + private var _coroutinesCleanedUp = false - /** @suppress **/ + @Suppress("INVISIBLE_MEMBER") override fun handleException(context: CoroutineContext, exception: Throwable) { - synchronized(_exceptions) { + synchronized(_lock) { + if (_coroutinesCleanedUp) { + handleCoroutineExceptionImpl(context, exception) + } _exceptions += exception } } - /** @suppress **/ - override val uncaughtExceptions: List - get() = synchronized(_exceptions) { _exceptions.toList() } + public override val uncaughtExceptions: List + get() = synchronized(_lock) { _exceptions.toList() } - /** @suppress **/ - override fun cleanupTestCoroutines() { - synchronized(_exceptions) { + public override fun cleanupTestCoroutines() { + synchronized(_lock) { + _coroutinesCleanedUp = true val exception = _exceptions.firstOrNull() ?: return // log the rest _exceptions.drop(1).forEach { it.printStackTrace() } diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt new file mode 100644 index 0000000000..4a2cbc5c2c --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("DEPRECATION") + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +/** + * A scope which provides detailed control over the execution of coroutines for tests. + * + * This scope is deprecated in favor of [TestScope]. + * Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + */ +@ExperimentalCoroutinesApi +@Deprecated("Use `TestScope` in combination with `runTest` instead." + + "Please see the migration guide for details: " + + "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md", + level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public interface TestCoroutineScope : CoroutineScope { + /** + * Called after the test completes. + * + * * It checks that there were no uncaught exceptions caught by its [CoroutineExceptionHandler]. + * If there were any, then the first one is thrown, whereas the rest are suppressed by it. + * * It runs the tasks pending in the scheduler at the current time. If there are any uncompleted tasks afterwards, + * it fails with [UncompletedCoroutinesError]. + * * It checks whether some new child [Job]s were created but not completed since this [TestCoroutineScope] was + * created. If so, it fails with [UncompletedCoroutinesError]. + * + * For backward compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its + * [TestCoroutineExceptionHandler.cleanupTestCoroutines] behavior is performed. + * Likewise, if the [ContinuationInterceptor] is a [DelayController], its [DelayController.cleanupTestCoroutines] + * is called. + * + * @throws Throwable the first uncaught exception, if there are any uncaught exceptions. + * @throws AssertionError if any pending tasks are active. + * @throws IllegalStateException if called more than once. + */ + @ExperimentalCoroutinesApi + @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.") + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 + public fun cleanupTestCoroutines() + + /** + * The delay-skipping scheduler used by the test dispatchers running the code in this scope. + */ + @ExperimentalCoroutinesApi + public val testScheduler: TestCoroutineScheduler +} + +private class TestCoroutineScopeImpl( + override val coroutineContext: CoroutineContext +) : TestCoroutineScope { + private val lock = SynchronizedObject() + private var exceptions = mutableListOf() + private var cleanedUp = false + + /** + * Reports an exception so that it is thrown on [cleanupTestCoroutines]. + * + * If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by + * it. + * + * Returns `false` if [cleanupTestCoroutines] was already called. + */ + fun reportException(throwable: Throwable): Boolean = + synchronized(lock) { + if (cleanedUp) { + false + } else { + exceptions.add(throwable) + true + } + } + + override val testScheduler: TestCoroutineScheduler + get() = coroutineContext[TestCoroutineScheduler]!! + + /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */ + private val initialJobs = coroutineContext.activeJobs() + + override fun cleanupTestCoroutines() { + val delayController = coroutineContext.delayController + val hasUnfinishedJobs = if (delayController != null) { + try { + delayController.cleanupTestCoroutines() + false + } catch (e: UncompletedCoroutinesError) { + true + } + } else { + testScheduler.runCurrent() + !testScheduler.isIdle(strict = false) + } + (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.cleanupTestCoroutines() + synchronized(lock) { + if (cleanedUp) + throw IllegalStateException("Attempting to clean up a test coroutine scope more than once.") + cleanedUp = true + } + exceptions.firstOrNull()?.let { toThrow -> + exceptions.drop(1).forEach { toThrow.addSuppressed(it) } + throw toThrow + } + if (hasUnfinishedJobs) + throw UncompletedCoroutinesError( + "Unfinished coroutines during teardown. Ensure all coroutines are" + + " completed or cancelled by your test." + ) + val jobs = coroutineContext.activeJobs() + if ((jobs - initialJobs).isNotEmpty()) + throw UncompletedCoroutinesError("Test finished with active jobs: $jobs") + } +} + +internal fun CoroutineContext.activeJobs(): Set { + return checkNotNull(this[Job]).children.filter { it.isActive }.toSet() +} + +/** + * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher]. + * + * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher]. + */ +@Deprecated( + "This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " + + "Please use `createTestCoroutineScope` instead.", + ReplaceWith( + "createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)", + "kotlin.coroutines.EmptyCoroutineContext" + ), + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { + val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler() + return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context) +} + +/** + * A coroutine scope for launching test coroutines. + * + * This is a function for aiding in migration from [TestCoroutineScope] to [TestScope]. + * Please see the + * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) + * for an instruction on how to update the code for the new API. + * + * It ensures that all the test module machinery is properly initialized. + * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping, + * a new one is created, unless either + * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used; + * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case + * its [TestCoroutineScheduler] is used. + * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created. + * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were + * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was + * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an + * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility. + * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created + * [TestCoroutineScope] and share your use case at + * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues). + * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created. + * + * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a + * different scheduler. + * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher]. + * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an + * [UncaughtExceptionCaptor]. + */ +@ExperimentalCoroutinesApi +@Deprecated( + "This function was introduced in order to help migrate from TestCoroutineScope to TestScope. " + + "Please use TestScope() construction instead, or just runTest(), without creating a scope.", + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { + val ctxWithDispatcher = context.withDelaySkipping() + var scope: TestCoroutineScopeImpl? = null + val ownExceptionHandler = + object : AbstractCoroutineContextElement(CoroutineExceptionHandler), TestCoroutineScopeExceptionHandler { + override fun handleException(context: CoroutineContext, exception: Throwable) { + if (!scope!!.reportException(exception)) + throw exception // let this exception crash everything + } + } + val exceptionHandler = when (val exceptionHandler = ctxWithDispatcher[CoroutineExceptionHandler]) { + is UncaughtExceptionCaptor -> exceptionHandler + null -> ownExceptionHandler + is TestCoroutineScopeExceptionHandler -> ownExceptionHandler + else -> throw IllegalArgumentException( + "A CoroutineExceptionHandler was passed to TestCoroutineScope. " + + "Please pass it as an argument to a `launch` or `async` block on an already-created scope " + + "if uncaught exceptions require special treatment." + ) + } + val job: Job = ctxWithDispatcher[Job] ?: Job() + return TestCoroutineScopeImpl(ctxWithDispatcher + exceptionHandler + job).also { + scope = it + } +} + +/** A marker that shows that this [CoroutineExceptionHandler] was created for [TestCoroutineScope]. With this, + * constructing a new [TestCoroutineScope] with the [CoroutineScope.coroutineContext] of an existing one will override + * the exception handler, instead of failing. */ +private interface TestCoroutineScopeExceptionHandler : CoroutineExceptionHandler + +private inline val CoroutineContext.delayController: DelayController? + get() { + val handler = this[ContinuationInterceptor] + return handler as? DelayController + } + + +/** + * The current virtual time on [testScheduler][TestCoroutineScope.testScheduler]. + * @see TestCoroutineScheduler.currentTime + */ +@ExperimentalCoroutinesApi +public val TestCoroutineScope.currentTime: Long + get() = coroutineContext.delayController?.currentTime ?: testScheduler.currentTime + +/** + * Advances the [testScheduler][TestCoroutineScope.testScheduler] by [delayTimeMillis] and runs the tasks up to that + * moment (inclusive). + * + * @see TestCoroutineScheduler.advanceTimeBy + */ +@ExperimentalCoroutinesApi +@Deprecated( + "The name of this function is misleading: it not only advances the time, but also runs the tasks " + + "scheduled *at* the ending moment.", + ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"), + DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit = + when (val controller = coroutineContext.delayController) { + null -> { + testScheduler.advanceTimeBy(delayTimeMillis) + testScheduler.runCurrent() + } + else -> { + controller.advanceTimeBy(delayTimeMillis) + Unit + } + } + +/** + * Advances the [testScheduler][TestCoroutineScope.testScheduler] to the point where there are no tasks remaining. + * @see TestCoroutineScheduler.advanceUntilIdle + */ +@ExperimentalCoroutinesApi +public fun TestCoroutineScope.advanceUntilIdle() { + coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle() +} + +/** + * Run any tasks that are pending at the current virtual time, according to + * the [testScheduler][TestCoroutineScope.testScheduler]. + * + * @see TestCoroutineScheduler.runCurrent + */ +@ExperimentalCoroutinesApi +public fun TestCoroutineScope.runCurrent() { + coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent() +} + +@ExperimentalCoroutinesApi +@Deprecated( + "The test coroutine scope isn't able to pause its dispatchers in the general case. " + + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", + ReplaceWith( + "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)", + "kotlin.coroutines.ContinuationInterceptor" + ), + DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) { + delayControllerForPausing.pauseDispatcher(block) +} + +@ExperimentalCoroutinesApi +@Deprecated( + "The test coroutine scope isn't able to pause its dispatchers in the general case. " + + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", + ReplaceWith( + "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()", + "kotlin.coroutines.ContinuationInterceptor" + ), + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope.pauseDispatcher() { + delayControllerForPausing.pauseDispatcher() +} + +@ExperimentalCoroutinesApi +@Deprecated( + "The test coroutine scope isn't able to pause its dispatchers in the general case. " + + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", + ReplaceWith( + "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()", + "kotlin.coroutines.ContinuationInterceptor" + ), + level = DeprecationLevel.WARNING +) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public fun TestCoroutineScope.resumeDispatcher() { + delayControllerForPausing.resumeDispatcher() +} + +/** + * List of uncaught coroutine exceptions, for backward compatibility. + * + * The returned list is a copy of the exceptions caught during execution. + * During [TestCoroutineScope.cleanupTestCoroutines] the first element of this list is rethrown if it is not empty. + * + * Exceptions are only collected in this list if the [UncaughtExceptionCaptor] is in the test context. + */ +@Deprecated( + "This list is only populated if `UncaughtExceptionCaptor` is in the test context, and so can be " + + "easily misused. It is only present for backward compatibility and will be removed in the subsequent " + + "releases. If you need to check the list of exceptions, please consider creating your own " + + "`CoroutineExceptionHandler`.", + level = DeprecationLevel.WARNING +) +public val TestCoroutineScope.uncaughtExceptions: List + get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions + ?: emptyList() + +private val TestCoroutineScope.delayControllerForPausing: DelayController + get() = coroutineContext.delayController + ?: throw IllegalStateException("This scope isn't able to pause its dispatchers") diff --git a/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt new file mode 100644 index 0000000000..e9aa3ff747 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) { + block { + test() + } +} diff --git a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt new file mode 100644 index 0000000000..90a16d0622 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* +import kotlin.concurrent.* +import kotlin.coroutines.* +import kotlin.test.* + +class MultithreadingTest { + + @Test + fun incorrectlyCalledRunBlocking_doesNotHaveSameInterceptor() = runBlockingTest { + // this code is an error as a production test, please do not use this as an example + + // this test exists to document this error condition, if it's possible to make this code work please update + val outerInterceptor = coroutineContext[ContinuationInterceptor] + // runBlocking always requires an argument to pass the context in tests + runBlocking { + assertNotSame(coroutineContext[ContinuationInterceptor], outerInterceptor) + } + } + + @Test + fun testSingleThreadExecutor() = runBlocking { + val mainThread = Thread.currentThread() + Dispatchers.setMain(Dispatchers.Unconfined) + newSingleThreadContext("testSingleThread").use { threadPool -> + withContext(Dispatchers.Main) { + assertSame(mainThread, Thread.currentThread()) + } + + Dispatchers.setMain(threadPool) + withContext(Dispatchers.Main) { + assertNotSame(mainThread, Thread.currentThread()) + } + assertSame(mainThread, Thread.currentThread()) + + withContext(Dispatchers.Main.immediate) { + assertNotSame(mainThread, Thread.currentThread()) + } + assertSame(mainThread, Thread.currentThread()) + + Dispatchers.setMain(Dispatchers.Unconfined) + withContext(Dispatchers.Main.immediate) { + assertSame(mainThread, Thread.currentThread()) + } + assertSame(mainThread, Thread.currentThread()) + } + } + + @Test + fun whenDispatchCalled_runsOnCurrentThread() { + val currentThread = Thread.currentThread() + val subject = TestCoroutineDispatcher() + val scope = TestCoroutineScope(subject) + + val deferred = scope.async(Dispatchers.Default) { + withContext(subject) { + assertNotSame(currentThread, Thread.currentThread()) + 3 + } + } + + runBlocking { + // just to ensure the above code terminates + assertEquals(3, deferred.await()) + } + } + + @Test + fun whenAllDispatchersMocked_runsOnSameThread() { + val currentThread = Thread.currentThread() + val subject = TestCoroutineDispatcher() + val scope = TestCoroutineScope(subject) + + val deferred = scope.async(subject) { + withContext(subject) { + assertSame(currentThread, Thread.currentThread()) + 3 + } + } + + runBlocking { + // just to ensure the above code terminates + assertEquals(3, deferred.await()) + } + } + + /** Tests that resuming the coroutine of [runTest] asynchronously in reasonable time succeeds. */ + @Test + fun testResumingFromAnotherThread() = runTest { + suspendCancellableCoroutine { cont -> + thread { + Thread.sleep(10) + cont.resume(Unit) + } + } + } + + /** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */ + @Test + fun testStandardTestDispatcherIsConfined() = runTest { + val initialThread = Thread.currentThread() + withContext(Dispatchers.IO) { + val ioThread = Thread.currentThread() + assertNotSame(initialThread, ioThread) + } + assertEquals(initialThread, Thread.currentThread()) + } +} diff --git a/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt new file mode 100644 index 0000000000..3edaa48fbd --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.concurrent.* +import kotlin.coroutines.* +import kotlin.test.* + +class RunTestStressTest { + /** Tests that notifications about asynchronous resumptions aren't lost. */ + @Test + fun testRunTestActivityNotificationsRace() { + val n = 1_000 * stressTestMultiplier + for (i in 0 until n) { + runTest { + suspendCancellableCoroutine { cont -> + thread { + cont.resume(Unit) + } + } + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt new file mode 100644 index 0000000000..174baa0819 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +/** Copy of [RunTestTest], but for [runBlockingTestOnTestScope], where applicable. */ +@Suppress("DEPRECATION") +class RunBlockingTestOnTestScopeTest { + + @Test + fun testRunTestWithIllegalContext() { + for (ctx in TestScopeTest.invalidContexts) { + assertFailsWith { + runBlockingTestOnTestScope(ctx) { } + } + } + } + + @Test + fun testThrowingInRunTestBody() { + assertFailsWith { + runBlockingTestOnTestScope { + throw RuntimeException() + } + } + } + + @Test + fun testThrowingInRunTestPendingTask() { + assertFailsWith { + runBlockingTestOnTestScope { + launch { + delay(SLOW) + throw RuntimeException() + } + } + } + } + + @Test + fun reproducer2405() = runBlockingTestOnTestScope { + val dispatcher = StandardTestDispatcher(testScheduler) + var collectedError = false + withContext(dispatcher) { + flow { emit(1) } + .combine( + flow { throw IllegalArgumentException() } + ) { int, string -> int.toString() + string } + .catch { emit("error") } + .collect { + assertEquals("error", it) + collectedError = true + } + } + assertTrue(collectedError) + } + + @Test + fun testChildrenCancellationOnTestBodyFailure() { + var job: Job? = null + assertFailsWith { + runBlockingTestOnTestScope { + job = launch { + while (true) { + delay(1000) + } + } + throw AssertionError() + } + } + assertTrue(job!!.isCancelled) + } + + @Test + fun testTimeout() { + assertFailsWith { + runBlockingTestOnTestScope { + withTimeout(50) { + launch { + delay(1000) + } + } + } + } + } + + @Test + fun testRunTestThrowsRootCause() { + assertFailsWith { + runBlockingTestOnTestScope { + launch { + throw TestException() + } + } + } + } + + @Test + fun testCompletesOwnJob() { + var handlerCalled = false + runBlockingTestOnTestScope { + coroutineContext.job.invokeOnCompletion { + handlerCalled = true + } + } + assertTrue(handlerCalled) + } + + @Test + fun testDoesNotCompleteGivenJob() { + var handlerCalled = false + val job = Job() + job.invokeOnCompletion { + handlerCalled = true + } + runBlockingTestOnTestScope(job) { + assertTrue(coroutineContext.job in job.children) + } + assertFalse(handlerCalled) + assertEquals(0, job.children.filter { it.isActive }.count()) + } + + @Test + fun testSuppressedExceptions() { + try { + runBlockingTestOnTestScope { + launch(SupervisorJob()) { throw TestException("x") } + launch(SupervisorJob()) { throw TestException("y") } + launch(SupervisorJob()) { throw TestException("z") } + throw TestException("w") + } + fail("should not be reached") + } catch (e: TestException) { + assertEquals("w", e.message) + val suppressed = e.suppressedExceptions + + (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList()) + assertEquals(3, suppressed.size) + assertEquals("x", suppressed[0].message) + assertEquals("y", suppressed[1].message) + assertEquals("z", suppressed[2].message) + } + } + + @Test + fun testScopeRunTestExceptionHandler(): TestResult { + val scope = TestCoroutineScope() + return testResultMap({ + try { + it() + fail("should not be reached") + } catch (e: TestException) { + // expected + } + }) { + scope.runTest { + launch(SupervisorJob()) { throw TestException("x") } + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt new file mode 100644 index 0000000000..7f1dd00963 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt @@ -0,0 +1,288 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.test.* + +/** Copy of [RunTestTest], but for [TestCoroutineScope] */ +@Suppress("DEPRECATION") +class RunTestLegacyScopeTest { + + @Test + fun testWithContextDispatching() = runTestWithLegacyScope { + var counter = 0 + withContext(Dispatchers.Default) { + counter += 1 + } + assertEquals(counter, 1) + } + + @Test + fun testJoiningForkedJob() = runTestWithLegacyScope { + var counter = 0 + val job = GlobalScope.launch { + counter += 1 + } + job.join() + assertEquals(counter, 1) + } + + @Test + fun testSuspendCoroutine() = runTestWithLegacyScope { + val answer = suspendCoroutine { + it.resume(42) + } + assertEquals(42, answer) + } + + @Test + fun testNestedRunTestForbidden() = runTestWithLegacyScope { + assertFailsWith { + runTest { } + } + } + + @Test + fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTestWithLegacyScope(dispatchTimeoutMs = 0) { + // below is some arbitrary concurrent code where all dispatches go through the same scheduler. + launch { + delay(2000) + } + val deferred = async { + val job = launch(StandardTestDispatcher(testScheduler)) { + launch { + delay(500) + } + delay(1000) + } + job.join() + } + deferred.await() + } + + @Test + fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTestWithLegacyScope(dispatchTimeoutMs = 0) { + withContext(Dispatchers.Default) { + delay(10) + 3 + } + fail("shouldn't be reached") + } + } + + @Test + fun testRunTestWithSmallTimeout() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTestWithLegacyScope(dispatchTimeoutMs = 100) { + withContext(Dispatchers.Default) { + delay(10000) + 3 + } + fail("shouldn't be reached") + } + } + + @Test + fun testRunTestWithLargeTimeout() = runTestWithLegacyScope(dispatchTimeoutMs = 5000) { + withContext(Dispatchers.Default) { + delay(50) + } + } + + @Test + fun testRunTestTimingOutAndThrowing() = testResultMap({ fn -> + try { + fn() + fail("unreached") + } catch (e: UncompletedCoroutinesError) { + @Suppress("INVISIBLE_MEMBER") + val suppressed = unwrap(e).suppressedExceptions + assertEquals(1, suppressed.size) + assertIs(suppressed[0]).also { + assertEquals("A", it.message) + } + } + }) { + runTestWithLegacyScope(dispatchTimeoutMs = 1) { + coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A")) + withContext(Dispatchers.Default) { + delay(10000) + 3 + } + fail("shouldn't be reached") + } + } + + @Test + fun testRunTestWithIllegalContext() { + for (ctx in TestScopeTest.invalidContexts) { + assertFailsWith { + runTestWithLegacyScope(ctx) { } + } + } + } + + @Test + fun testThrowingInRunTestBody() = testResultMap({ + assertFailsWith { it() } + }) { + runTestWithLegacyScope { + throw RuntimeException() + } + } + + @Test + fun testThrowingInRunTestPendingTask() = testResultMap({ + assertFailsWith { it() } + }) { + runTestWithLegacyScope { + launch { + delay(SLOW) + throw RuntimeException() + } + } + } + + @Test + fun reproducer2405() = runTestWithLegacyScope { + val dispatcher = StandardTestDispatcher(testScheduler) + var collectedError = false + withContext(dispatcher) { + flow { emit(1) } + .combine( + flow { throw IllegalArgumentException() } + ) { int, string -> int.toString() + string } + .catch { emit("error") } + .collect { + assertEquals("error", it) + collectedError = true + } + } + assertTrue(collectedError) + } + + @Test + fun testChildrenCancellationOnTestBodyFailure(): TestResult { + var job: Job? = null + return testResultMap({ + assertFailsWith { it() } + assertTrue(job!!.isCancelled) + }) { + runTestWithLegacyScope { + job = launch { + while (true) { + delay(1000) + } + } + throw AssertionError() + } + } + } + + @Test + fun testTimeout() = testResultMap({ + assertFailsWith { it() } + }) { + runTestWithLegacyScope { + withTimeout(50) { + launch { + delay(1000) + } + } + } + } + + @Test + fun testRunTestThrowsRootCause() = testResultMap({ + assertFailsWith { it() } + }) { + runTestWithLegacyScope { + launch { + throw TestException() + } + } + } + + @Test + fun testCompletesOwnJob(): TestResult { + var handlerCalled = false + return testResultMap({ + it() + assertTrue(handlerCalled) + }) { + runTestWithLegacyScope { + coroutineContext.job.invokeOnCompletion { + handlerCalled = true + } + } + } + } + + @Test + fun testDoesNotCompleteGivenJob(): TestResult { + var handlerCalled = false + val job = Job() + job.invokeOnCompletion { + handlerCalled = true + } + return testResultMap({ + it() + assertFalse(handlerCalled) + assertEquals(0, job.children.filter { it.isActive }.count()) + }) { + runTestWithLegacyScope(job) { + assertTrue(coroutineContext.job in job.children) + } + } + } + + @Test + fun testSuppressedExceptions() = testResultMap({ + try { + it() + fail("should not be reached") + } catch (e: TestException) { + assertEquals("w", e.message) + val suppressed = e.suppressedExceptions + + (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList()) + assertEquals(3, suppressed.size) + assertEquals("x", suppressed[0].message) + assertEquals("y", suppressed[1].message) + assertEquals("z", suppressed[2].message) + } + }) { + runTestWithLegacyScope { + launch(SupervisorJob()) { throw TestException("x") } + launch(SupervisorJob()) { throw TestException("y") } + launch(SupervisorJob()) { throw TestException("z") } + throw TestException("w") + } + } + + @Test + fun testScopeRunTestExceptionHandler(): TestResult { + val scope = TestCoroutineScope() + return testResultMap({ + try { + it() + fail("should not be reached") + } catch (e: TestException) { + // expected + } + }) { + scope.runTest { + launch(SupervisorJob()) { throw TestException("x") } + } + } + } +} diff --git a/kotlinx-coroutines-test/test/TestBuildersTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt similarity index 95% rename from kotlinx-coroutines-test/test/TestBuildersTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt index 27c8f5fb19..6d49a01fa4 100644 --- a/kotlinx-coroutines-test/test/TestBuildersTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt @@ -5,10 +5,10 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* -import org.junit.Test import kotlin.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class TestBuildersTest { @Test @@ -59,7 +59,7 @@ class TestBuildersTest { } @Test - fun scopeRunBlocking_disablesImmedateOnExit() { + fun scopeRunBlocking_disablesImmediatelyOnExit() { val scope = TestCoroutineScope() scope.runBlockingTest { assertRunsFast { @@ -105,7 +105,7 @@ class TestBuildersTest { } @Test - fun whenInrunBlocking_runBlockingTest_nestsProperly() { + fun whenInRunBlocking_runBlockingTest_nestsProperly() { // this is not a supported use case, but it is possible so ensure it works val scope = TestCoroutineScope() diff --git a/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt similarity index 79% rename from kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt index 116aadcf8d..93fcd909cc 100644 --- a/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt @@ -1,11 +1,15 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.coroutines.test +import kotlinx.atomicfu.* import kotlinx.coroutines.* -import org.junit.* -import kotlin.coroutines.* -import kotlin.test.assertEquals +import kotlin.test.* -class TestCoroutineDispatcherOrderTest : TestBase() { +@Suppress("DEPRECATION") +class TestCoroutineDispatcherOrderTest: OrderedExecutionTestBase() { @Test fun testAdvanceTimeBy_progressesOnEachDelay() { diff --git a/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt new file mode 100644 index 0000000000..a78d923d34 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.test.* + +@Suppress("DEPRECATION") +class TestCoroutineDispatcherTest { + @Test + fun whenDispatcherPaused_doesNotAutoProgressCurrent() { + val subject = TestCoroutineDispatcher() + subject.pauseDispatcher() + val scope = CoroutineScope(subject) + var executed = 0 + scope.launch { + executed++ + } + assertEquals(0, executed) + } + + @Test + fun whenDispatcherResumed_doesAutoProgressCurrent() { + val subject = TestCoroutineDispatcher() + val scope = CoroutineScope(subject) + var executed = 0 + scope.launch { + executed++ + } + + assertEquals(1, executed) + } + + @Test + fun whenDispatcherResumed_doesNotAutoProgressTime() { + val subject = TestCoroutineDispatcher() + val scope = CoroutineScope(subject) + var executed = 0 + scope.launch { + delay(1_000) + executed++ + } + + assertEquals(0, executed) + subject.advanceUntilIdle() + assertEquals(1, executed) + } + + @Test + fun whenDispatcherPaused_thenResume_itDoesDispatchCurrent() { + val subject = TestCoroutineDispatcher() + subject.pauseDispatcher() + val scope = CoroutineScope(subject) + var executed = 0 + scope.launch { + executed++ + } + + assertEquals(0, executed) + subject.resumeDispatcher() + assertEquals(1, executed) + } + + @Test + fun whenDispatcherHasUncompletedCoroutines_itThrowsErrorInCleanup() { + val subject = TestCoroutineDispatcher() + subject.pauseDispatcher() + val scope = CoroutineScope(subject) + scope.launch { + delay(1_000) + } + assertFailsWith { subject.cleanupTestCoroutines() } + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt similarity index 72% rename from kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt index 1a0833af50..20da130725 100644 --- a/kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt @@ -1,15 +1,15 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.test -import org.junit.Test import kotlin.test.* +@Suppress("DEPRECATION") class TestCoroutineExceptionHandlerTest { @Test - fun whenExceptionsCaught_avaliableViaProperty() { + fun whenExceptionsCaught_availableViaProperty() { val subject = TestCoroutineExceptionHandler() val expected = IllegalArgumentException() subject.handleException(subject, expected) diff --git a/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt new file mode 100644 index 0000000000..1a62613790 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("DEPRECATION") + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.test.* + +class TestCoroutineScopeTest { + /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */ + @Test + fun testCreateThrowsOnInvalidArguments() { + for (ctx in invalidContexts) { + assertFailsWith { + createTestCoroutineScope(ctx) + } + } + } + + /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */ + @Test + fun testCreateProvidesScheduler() { + // Creates a new scheduler. + run { + val scope = createTestCoroutineScope() + assertNotNull(scope.coroutineContext[TestCoroutineScheduler]) + } + // Reuses the scheduler that the dispatcher is linked to. + run { + val dispatcher = StandardTestDispatcher() + val scope = createTestCoroutineScope(dispatcher) + assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) + } + // Uses the scheduler passed to it. + run { + val scheduler = TestCoroutineScheduler() + val scope = createTestCoroutineScope(scheduler) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler) + } + // Doesn't touch the passed dispatcher and the scheduler if they match. + run { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val scope = createTestCoroutineScope(scheduler + dispatcher) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor]) + } + // Reuses the scheduler of `Dispatchers.Main` + run { + val scheduler = TestCoroutineScheduler() + val mainDispatcher = StandardTestDispatcher(scheduler) + Dispatchers.setMain(mainDispatcher) + try { + val scope = createTestCoroutineScope() + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) + } finally { + Dispatchers.resetMain() + } + } + // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed + run { + val mainDispatcher = StandardTestDispatcher() + Dispatchers.setMain(mainDispatcher) + try { + val scheduler = TestCoroutineScheduler() + val scope = createTestCoroutineScope(scheduler) + assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) + assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) + } finally { + Dispatchers.resetMain() + } + } + } + + /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */ + @Test + fun testPresentDelaysThrowing() { + val scope = createTestCoroutineScope() + var result = false + scope.launch { + delay(5) + result = true + } + assertFalse(result) + assertFailsWith { scope.cleanupTestCoroutines() } + assertFalse(result) + } + + /** Tests that the cleanup procedure throws if there were active jobs by the end. */ + @Test + fun testActiveJobsThrowing() { + val scope = createTestCoroutineScope() + var result = false + val deferred = CompletableDeferred() + scope.launch { + deferred.await() + result = true + } + assertFalse(result) + assertFailsWith { scope.cleanupTestCoroutines() } + assertFalse(result) + } + + /** Tests that the cleanup procedure doesn't throw if it detects that the job is already cancelled. */ + @Test + fun testCancelledDelaysNotThrowing() { + val scope = createTestCoroutineScope() + var result = false + val deferred = CompletableDeferred() + val job = scope.launch { + deferred.await() + result = true + } + job.cancel() + assertFalse(result) + scope.cleanupTestCoroutines() + assertFalse(result) + } + + /** Tests that uncaught exceptions are thrown at the cleanup. */ + @Test + fun testThrowsUncaughtExceptionsOnCleanup() { + val scope = createTestCoroutineScope() + val exception = TestException("test") + scope.launch { + throw exception + } + assertFailsWith { + scope.cleanupTestCoroutines() + } + } + + /** Tests that uncaught exceptions take priority over uncompleted jobs when throwing on cleanup. */ + @Test + fun testUncaughtExceptionsPrioritizedOnCleanup() { + val scope = createTestCoroutineScope() + val exception = TestException("test") + scope.launch { + throw exception + } + scope.launch { + delay(1000) + } + assertFailsWith { + scope.cleanupTestCoroutines() + } + } + + /** Tests that cleaning up twice is forbidden. */ + @Test + fun testClosingTwice() { + val scope = createTestCoroutineScope() + scope.cleanupTestCoroutines() + assertFailsWith { + scope.cleanupTestCoroutines() + } + } + + /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */ + @Test + fun testSuppressedExceptions() { + createTestCoroutineScope().apply { + launch(SupervisorJob()) { throw TestException("x") } + launch(SupervisorJob()) { throw TestException("y") } + launch(SupervisorJob()) { throw TestException("z") } + try { + cleanupTestCoroutines() + fail("should not be reached") + } catch (e: TestException) { + assertEquals("x", e.message) + assertEquals(2, e.suppressedExceptions.size) + assertEquals("y", e.suppressedExceptions[0].message) + assertEquals("z", e.suppressedExceptions[1].message) + } + } + } + + /** Tests that constructing a new [TestCoroutineScope] using another one's scope works and overrides the exception + * handler. */ + @Test + fun testCopyingContexts() { + val deferred = CompletableDeferred() + val scope1 = createTestCoroutineScope() + scope1.launch { deferred.await() } // a pending job in the outer scope + val scope2 = createTestCoroutineScope(scope1.coroutineContext) + val scope3 = createTestCoroutineScope(scope1.coroutineContext) + assertEquals( + scope1.coroutineContext.minusKey(CoroutineExceptionHandler), + scope2.coroutineContext.minusKey(CoroutineExceptionHandler)) + scope2.launch(SupervisorJob()) { throw TestException("x") } // will fail the cleanup of scope2 + try { + scope2.cleanupTestCoroutines() + fail("should not be reached") + } catch (e: TestException) { } + scope3.cleanupTestCoroutines() // the pending job in the outer scope will not cause this to fail + try { + scope1.cleanupTestCoroutines() + fail("should not be reached") + } catch (e: UncompletedCoroutinesError) { + // the pending job in the outer scope + } + } + + companion object { + internal val invalidContexts = listOf( + Dispatchers.Default, // not a [TestDispatcher] + CoroutineExceptionHandler { _, _ -> }, // not an [UncaughtExceptionCaptor] + StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler + ) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt similarity index 89% rename from kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt index e21c82b95c..32514d90e8 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt @@ -1,14 +1,15 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.test import kotlinx.coroutines.* -import org.junit.* -import kotlin.coroutines.* +import kotlin.test.* + +@Suppress("DEPRECATION") +class TestRunBlockingOrderTest: OrderedExecutionTestBase() { -class TestRunBlockingOrderTest : TestBase() { @Test fun testLaunchImmediate() = runBlockingTest { expect(1) @@ -76,4 +77,4 @@ class TestRunBlockingOrderTest : TestBase() { } finish(2) } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/TestRunBlockingTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt similarity index 56% rename from kotlinx-coroutines-test/test/TestRunBlockingTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt index e0c7091505..af3b24892a 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt @@ -5,9 +5,9 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* -import kotlin.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class TestRunBlockingTest { @Test @@ -53,22 +53,14 @@ class TestRunBlockingTest { } @Test - fun incorrectlyCalledRunblocking_doesNotHaveSameInterceptor() = runBlockingTest { - // this code is an error as a production test, please do not use this as an example - - // this test exists to document this error condition, if it's possible to make this code work please update - val outerInterceptor = coroutineContext[ContinuationInterceptor] - // runBlocking always requires an argument to pass the context in tests - runBlocking { - assertNotSame(coroutineContext[ContinuationInterceptor], outerInterceptor) - } - } - - @Test(expected = TimeoutCancellationException::class) - fun whenUsingTimeout_triggersWhenDelayed() = runBlockingTest { - assertRunsFast { - withTimeout(SLOW) { - delay(SLOW) + fun whenUsingTimeout_triggersWhenDelayed() { + assertFailsWith { + runBlockingTest { + assertRunsFast { + withTimeout(SLOW) { + delay(SLOW) + } + } } } } @@ -82,12 +74,16 @@ class TestRunBlockingTest { } } - @Test(expected = TimeoutCancellationException::class) - fun whenUsingTimeout_triggersWhenWaiting() = runBlockingTest { - val uncompleted = CompletableDeferred() - assertRunsFast { - withTimeout(SLOW) { - uncompleted.await() + @Test + fun whenUsingTimeout_triggersWhenWaiting() { + assertFailsWith { + runBlockingTest { + val uncompleted = CompletableDeferred() + assertRunsFast { + withTimeout(SLOW) { + uncompleted.await() + } + } } } } @@ -114,22 +110,25 @@ class TestRunBlockingTest { } } - @Test(expected = TimeoutCancellationException::class) - fun whenUsingTimeout_inAsync_triggersWhenDelayed() = runBlockingTest { - val deferred = async { - withTimeout(SLOW) { - delay(SLOW) - } - } + @Test + fun whenUsingTimeout_inAsync_triggersWhenDelayed() { + assertFailsWith { + runBlockingTest { + val deferred = async { + withTimeout(SLOW) { + delay(SLOW) + } + } - assertRunsFast { - deferred.await() + assertRunsFast { + deferred.await() + } + } } } @Test fun whenUsingTimeout_inAsync_doesNotTriggerWhenNotDelayed() = runBlockingTest { - val testScope = this val deferred = async { withTimeout(SLOW) { delay(0) @@ -141,18 +140,21 @@ class TestRunBlockingTest { } } - @Test(expected = TimeoutCancellationException::class) - fun whenUsingTimeout_inLaunch_triggersWhenDelayed() = runBlockingTest { - val job= launch { - withTimeout(1) { - delay(SLOW + 1) - 3 - } - } + @Test + fun whenUsingTimeout_inLaunch_triggersWhenDelayed() { + assertFailsWith { + runBlockingTest { + val job = launch { + withTimeout(1) { + delay(SLOW + 1) + } + } - assertRunsFast { - job.join() - throw job.getCancellationException() + assertRunsFast { + job.join() + throw job.getCancellationException() + } + } } } @@ -170,36 +172,48 @@ class TestRunBlockingTest { } } - @Test(expected = IllegalArgumentException::class) - fun throwingException_throws() = runBlockingTest { - assertRunsFast { - delay(SLOW) - throw IllegalArgumentException("Test") + @Test + fun throwingException_throws() { + assertFailsWith { + runBlockingTest { + assertRunsFast { + delay(SLOW) + throw IllegalArgumentException("Test") + } + } } } - @Test(expected = IllegalArgumentException::class) - fun throwingException_inLaunch_throws() = runBlockingTest { - val job = launch { - delay(SLOW) - throw IllegalArgumentException("Test") - } + @Test + fun throwingException_inLaunch_throws() { + assertFailsWith { + runBlockingTest { + val job = launch { + delay(SLOW) + throw IllegalArgumentException("Test") + } - assertRunsFast { - job.join() - throw job.getCancellationException().cause ?: assertFails { "expected exception" } + assertRunsFast { + job.join() + throw job.getCancellationException().cause ?: AssertionError("expected exception") + } + } } } - @Test(expected = IllegalArgumentException::class) - fun throwingException__inAsync_throws() = runBlockingTest { - val deferred = async { - delay(SLOW) - throw IllegalArgumentException("Test") - } + @Test + fun throwingException__inAsync_throws() { + assertFailsWith { + runBlockingTest { + val deferred: Deferred = async { + delay(SLOW) + throw IllegalArgumentException("Test") + } - assertRunsFast { - deferred.await() + assertRunsFast { + deferred.await() + } + } } } @@ -221,12 +235,13 @@ class TestRunBlockingTest { fun callingAsyncFunction_executesAsyncBlockImmediately() = runBlockingTest { assertRunsFast { var executed = false - async { + val deferred = async { delay(SLOW) executed = true } advanceTimeBy(SLOW) + assertTrue(deferred.isCompleted) assertTrue(executed) } } @@ -273,25 +288,33 @@ class TestRunBlockingTest { job.join() } - @Test(expected = UncompletedCoroutinesError::class) - fun whenACoroutineLeaks_errorIsThrown() = runBlockingTest { - val uncompleted = CompletableDeferred() - launch { - uncompleted.await() + @Test + fun whenACoroutineLeaks_errorIsThrown() { + assertFailsWith { + runBlockingTest { + val uncompleted = CompletableDeferred() + launch { + uncompleted.await() + } + } } } - @Test(expected = java.lang.IllegalArgumentException::class) + @Test fun runBlockingTestBuilder_throwsOnBadDispatcher() { - runBlockingTest(newSingleThreadContext("name")) { + assertFailsWith { + runBlockingTest(Dispatchers.Default) { + } } } - @Test(expected = java.lang.IllegalArgumentException::class) + @Test fun runBlockingTestBuilder_throwsOnBadHandler() { - runBlockingTest(CoroutineExceptionHandler { _, _ -> Unit} ) { + assertFailsWith { + runBlockingTest(CoroutineExceptionHandler { _, _ -> }) { + } } } @@ -338,36 +361,48 @@ class TestRunBlockingTest { } - @Test(expected = IllegalAccessError::class) - fun testWithTestContextThrowingAnAssertionError() = runBlockingTest { - val expectedError = IllegalAccessError("hello") + @Test + fun testWithTestContextThrowingAnAssertionError() { + assertFailsWith { + runBlockingTest { + val expectedError = TestException("hello") - val job = launch { - throw expectedError - } + launch { + throw expectedError + } - // don't rethrow or handle the exception + // don't rethrow or handle the exception + } + } } - @Test(expected = IllegalAccessError::class) - fun testExceptionHandlingWithLaunch() = runBlockingTest { - val expectedError = IllegalAccessError("hello") + @Test + fun testExceptionHandlingWithLaunch() { + assertFailsWith { + runBlockingTest { + val expectedError = TestException("hello") - launch { - throw expectedError + launch { + throw expectedError + } + } } } - @Test(expected = IllegalAccessError::class) - fun testExceptions_notThrownImmediately() = runBlockingTest { - val expectedException = IllegalAccessError("hello") - val result = runCatching { - launch { - throw expectedException + @Test + fun testExceptions_notThrownImmediately() { + assertFailsWith { + runBlockingTest { + val expectedException = TestException("hello") + val result = runCatching { + launch { + throw expectedException + } + } + runCurrent() + assertEquals(true, result.isSuccess) } } - runCurrent() - assertEquals(true, result.isSuccess) } @@ -380,9 +415,13 @@ class TestRunBlockingTest { assertNotSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler) } - @Test(expected = IllegalArgumentException::class) - fun testPartialDispatcherOverride() = runBlockingTest(Dispatchers.Unconfined) { - fail("Unreached") + @Test + fun testPartialDispatcherOverride() { + assertFailsWith { + runBlockingTest(Dispatchers.Unconfined) { + fail("Unreached") + } + } } @Test @@ -390,8 +429,12 @@ class TestRunBlockingTest { assertSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler) } - @Test(expected = IllegalArgumentException::class) - fun testOverrideExceptionHandlerError() = runBlockingTest(CoroutineExceptionHandler { _, _ -> }) { - fail("Unreached") + @Test + fun testOverrideExceptionHandlerError() { + assertFailsWith { + runBlockingTest(CoroutineExceptionHandler { _, _ -> }) { + fail("Unreached") + } + } } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/native/src/TestBuilders.kt b/kotlinx-coroutines-test/native/src/TestBuilders.kt new file mode 100644 index 0000000000..c3176a03de --- /dev/null +++ b/kotlinx-coroutines-test/native/src/TestBuilders.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test +import kotlinx.coroutines.* + +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias TestResult = Unit + +internal actual fun createTestResult(testProcedure: suspend () -> Unit) { + runBlocking { + testProcedure() + } +} diff --git a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt new file mode 100644 index 0000000000..4d865f83c0 --- /dev/null +++ b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2021 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.* + +@Suppress("INVISIBLE_MEMBER") +internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = + when (val mainDispatcher = Main) { + is TestMainDispatcher -> mainDispatcher + else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) } + } diff --git a/kotlinx-coroutines-test/native/test/FailingTests.kt b/kotlinx-coroutines-test/native/test/FailingTests.kt new file mode 100644 index 0000000000..9fb77ce7c8 --- /dev/null +++ b/kotlinx-coroutines-test/native/test/FailingTests.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.test.* + +/** These are tests that we want to fail. They are here so that, when the issue is fixed, their failure indicates that + * everything is better now. */ +class FailingTests { + @Test + fun testRunTestLoopShutdownOnTimeout() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTest(dispatchTimeoutMs = 1) { + withContext(Dispatchers.Default) { + delay(10000) + } + fail("shouldn't be reached") + } + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/native/test/Helpers.kt b/kotlinx-coroutines-test/native/test/Helpers.kt new file mode 100644 index 0000000000..ef478b7eb1 --- /dev/null +++ b/kotlinx-coroutines-test/native/test/Helpers.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlin.test.* + +actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) { + block { + test() + } +} + +actual typealias NoNative = Ignore diff --git a/kotlinx-coroutines-test/npm/README.md b/kotlinx-coroutines-test/npm/README.md new file mode 100644 index 0000000000..4df4825da9 --- /dev/null +++ b/kotlinx-coroutines-test/npm/README.md @@ -0,0 +1,4 @@ +# kotlinx-coroutines-test + +Testing support for `kotlinx-coroutines` in +[Kotlin/JS](https://kotlinlang.org/docs/js-overview.html). diff --git a/kotlinx-coroutines-test/npm/package.json b/kotlinx-coroutines-test/npm/package.json new file mode 100644 index 0000000000..b59d92fe03 --- /dev/null +++ b/kotlinx-coroutines-test/npm/package.json @@ -0,0 +1,23 @@ +{ + "name": "kotlinx-coroutines-test", + "version" : "$version", + "description" : "Test utilities for kotlinx-coroutines", + "main" : "kotlinx-coroutines-test.js", + "author": "JetBrains", + "license": "Apache-2.0", + "homepage": "https://github.com/Kotlin/kotlinx.coroutines", + "bugs": { + "url": "https://github.com/Kotlin/kotlinx.coroutines/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Kotlin/kotlinx.coroutines.git" + }, + "keywords": [ + "Kotlin", + "async", + "coroutines", + "JetBrains", + "test" + ] +} diff --git a/kotlinx-coroutines-test/src/DelayController.kt b/kotlinx-coroutines-test/src/DelayController.kt deleted file mode 100644 index 6e72222718..0000000000 --- a/kotlinx-coroutines-test/src/DelayController.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi - -/** - * Control the virtual clock time of a [CoroutineDispatcher]. - * - * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher]. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public interface DelayController { - /** - * Returns the current virtual clock-time as it is known to this Dispatcher. - * - * @return The virtual clock-time - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public val currentTime: Long - - /** - * Moves the Dispatcher's virtual clock forward by a specified amount of time. - * - * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses - * blocking coroutines. - * - * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested - * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different - * calls to delay. - * - * ``` - * @Test - * fun advanceTimeTest() = runBlockingTest { - * foo() - * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays - * // virtual time is 2_000, next resume is at 2_001 - * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced) - * // virtual time is 2_0002 - * } - * - * fun CoroutineScope.foo() { - * launch { - * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000) - * // virtual time is 1_000 - * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500) - * // virtual time is 1_500 - * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001) - * // virtual time is 2_001 - * } - * } - * ``` - * - * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward. - * @return The amount of delay-time that this Dispatcher's clock has been forwarded. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun advanceTimeBy(delayTimeMillis: Long): Long - - /** - * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. - * - * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle` - * returns. - * - * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun advanceUntilIdle(): Long - - /** - * Run any tasks that are pending at or before the current virtual clock-time. - * - * Calling this function will never advance the clock. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun runCurrent() - - /** - * Call after test code completes to ensure that the dispatcher is properly cleaned up. - * - * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended - * coroutines. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - @Throws(UncompletedCoroutinesError::class) - public fun cleanupTestCoroutines() - - /** - * Run a block of code in a paused dispatcher. - * - * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher - * will resume auto-advancing. - * - * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or - * setup may be done between the time the coroutine is created and started. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public suspend fun pauseDispatcher(block: suspend () -> Unit) - - /** - * Pause the dispatcher. - * - * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or - * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun pauseDispatcher() - - /** - * Resume the dispatcher from a paused state. - * - * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance - * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], - * or [advanceUntilIdle]. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun resumeDispatcher() -} - -/** - * Thrown when a test has completed and there are tasks that are not completed or cancelled. - */ -// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type) -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause) diff --git a/kotlinx-coroutines-test/src/TestBuilders.kt b/kotlinx-coroutines-test/src/TestBuilders.kt deleted file mode 100644 index b40769ee97..0000000000 --- a/kotlinx-coroutines-test/src/TestBuilders.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import kotlin.coroutines.* - -/** - * Executes a [testBody] inside an immediate execution dispatcher. - * - * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks. - * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take - * extra time. - * - * ``` - * @Test - * fun exampleTest() = runBlockingTest { - * val deferred = async { - * delay(1_000) - * async { - * delay(1_000) - * }.await() - * } - * - * deferred.await() // result available immediately - * } - * - * ``` - * - * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test - * conditions. - * - * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test. - * - * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches - * (including coroutines suspended on join/await). - * - * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler], - * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively. - * @param testBody The code of the unit-test. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) { - val (safeContext, dispatcher) = context.checkArguments() - val startingJobs = safeContext.activeJobs() - val scope = TestCoroutineScope(safeContext) - val deferred = scope.async { - scope.testBody() - } - dispatcher.advanceUntilIdle() - deferred.getCompletionExceptionOrNull()?.let { - throw it - } - scope.cleanupTestCoroutines() - val endingJobs = safeContext.activeJobs() - if ((endingJobs - startingJobs).isNotEmpty()) { - throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") - } -} - -private fun CoroutineContext.activeJobs(): Set { - return checkNotNull(this[Job]).children.filter { it.isActive }.toSet() -} - -/** - * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope]. - */ -// todo: need documentation on how this extension is supposed to be used -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = - runBlockingTest(coroutineContext, block) - -/** - * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = - runBlockingTest(this, block) - -private fun CoroutineContext.checkArguments(): Pair { - // TODO optimize it - val dispatcher = get(ContinuationInterceptor).run { - this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } } - this ?: TestCoroutineDispatcher() - } - - val exceptionHandler = get(CoroutineExceptionHandler).run { - this?.let { - require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" } - } - this ?: TestCoroutineExceptionHandler() - } - - val job = get(Job) ?: SupervisorJob() - return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController) -} diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt deleted file mode 100644 index f6464789fc..0000000000 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import kotlinx.coroutines.internal.* -import kotlin.coroutines.* -import kotlin.math.* - -/** - * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests - * and implements [DelayController] to control its virtual clock. - * - * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are - * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the - * methods on [DelayController]. - * - * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will - * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the - * methods on [DelayController]. - * - * @see DelayController - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayController { - private var dispatchImmediately = true - set(value) { - field = value - if (value) { - // there may already be tasks from setup code we need to run - advanceUntilIdle() - } - } - - // The ordered queue for the runnable tasks. - private val queue = ThreadSafeHeap() - - // The per-scheduler global order counter. - private val _counter = atomic(0L) - - // Storing time in nanoseconds internally. - private val _time = atomic(0L) - - /** @suppress */ - override fun dispatch(context: CoroutineContext, block: Runnable) { - if (dispatchImmediately) { - block.run() - } else { - post(block) - } - } - - /** @suppress */ - @InternalCoroutinesApi - override fun dispatchYield(context: CoroutineContext, block: Runnable) { - post(block) - } - - /** @suppress */ - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - postDelayed(CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }, timeMillis) - } - - /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val node = postDelayed(block, timeMillis) - return object : DisposableHandle { - override fun dispose() { - queue.remove(node) - } - } - } - - /** @suppress */ - override fun toString(): String { - return "TestCoroutineDispatcher[currentTime=${currentTime}ms, queued=${queue.size}]" - } - - private fun post(block: Runnable) = - queue.addLast(TimedRunnable(block, _counter.getAndIncrement())) - - private fun postDelayed(block: Runnable, delayTime: Long) = - TimedRunnable(block, _counter.getAndIncrement(), safePlus(currentTime, delayTime)) - .also { - queue.addLast(it) - } - - private fun safePlus(currentTime: Long, delayTime: Long): Long { - check(delayTime >= 0) - val result = currentTime + delayTime - if (result < currentTime) return Long.MAX_VALUE // clam on overflow - return result - } - - private fun doActionsUntil(targetTime: Long) { - while (true) { - val current = queue.removeFirstIf { it.time <= targetTime } ?: break - // If the scheduled time is 0 (immediate) use current virtual time - if (current.time != 0L) _time.value = current.time - current.run() - } - } - - /** @suppress */ - override val currentTime: Long get() = _time.value - - /** @suppress */ - override fun advanceTimeBy(delayTimeMillis: Long): Long { - val oldTime = currentTime - advanceUntilTime(oldTime + delayTimeMillis) - return currentTime - oldTime - } - - /** - * Moves the CoroutineContext's clock-time to a particular moment in time. - * - * @param targetTime The point in time to which to move the CoroutineContext's clock (milliseconds). - */ - private fun advanceUntilTime(targetTime: Long) { - doActionsUntil(targetTime) - _time.update { currentValue -> max(currentValue, targetTime) } - } - - /** @suppress */ - override fun advanceUntilIdle(): Long { - val oldTime = currentTime - while(!queue.isEmpty) { - runCurrent() - val next = queue.peek() ?: break - advanceUntilTime(next.time) - } - return currentTime - oldTime - } - - /** @suppress */ - override fun runCurrent(): Unit = doActionsUntil(currentTime) - - /** @suppress */ - override suspend fun pauseDispatcher(block: suspend () -> Unit) { - val previous = dispatchImmediately - dispatchImmediately = false - try { - block() - } finally { - dispatchImmediately = previous - } - } - - /** @suppress */ - override fun pauseDispatcher() { - dispatchImmediately = false - } - - /** @suppress */ - override fun resumeDispatcher() { - dispatchImmediately = true - } - - /** @suppress */ - override fun cleanupTestCoroutines() { - // process any pending cancellations or completions, but don't advance time - doActionsUntil(currentTime) - - // run through all pending tasks, ignore any submitted coroutines that are not active - val pendingTasks = mutableListOf() - while (true) { - pendingTasks += queue.removeFirstOrNull() ?: break - } - val activeDelays = pendingTasks - .mapNotNull { it.runnable as? CancellableContinuationRunnable<*> } - .filter { it.continuation.isActive } - - val activeTimeouts = pendingTasks.filter { it.runnable !is CancellableContinuationRunnable<*> } - if (activeDelays.isNotEmpty() || activeTimeouts.isNotEmpty()) { - throw UncompletedCoroutinesError( - "Unfinished coroutines during teardown. Ensure all coroutines are" + - " completed or cancelled by your test." - ) - } - } -} - -/** - * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled - * in the future. - */ -private class CancellableContinuationRunnable( - @JvmField val continuation: CancellableContinuation, - private val block: CancellableContinuation.() -> Unit -) : Runnable { - override fun run() = continuation.block() -} - -/** - * A Runnable for our event loop that represents a task to perform at a time. - */ -private class TimedRunnable( - @JvmField val runnable: Runnable, - private val count: Long = 0, - @JvmField val time: Long = 0 -) : Comparable, Runnable by runnable, ThreadSafeHeapNode { - override var heap: ThreadSafeHeap<*>? = null - override var index: Int = 0 - - override fun compareTo(other: TimedRunnable) = if (time == other.time) { - count.compareTo(other.count) - } else { - time.compareTo(other.time) - } - - override fun toString() = "TimedRunnable(time=$time, run=$runnable)" -} diff --git a/kotlinx-coroutines-test/src/TestCoroutineScope.kt b/kotlinx-coroutines-test/src/TestCoroutineScope.kt deleted file mode 100644 index 7c1ff872ec..0000000000 --- a/kotlinx-coroutines-test/src/TestCoroutineScope.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import kotlin.coroutines.* - -/** - * A scope which provides detailed control over the execution of coroutines for tests. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public interface TestCoroutineScope: CoroutineScope, UncaughtExceptionCaptor, DelayController { - /** - * Call after the test completes. - * Calls [UncaughtExceptionCaptor.cleanupTestCoroutines] and [DelayController.cleanupTestCoroutines]. - * - * @throws Throwable the first uncaught exception, if there are any uncaught exceptions. - * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended - * coroutines. - */ - public override fun cleanupTestCoroutines() -} - -private class TestCoroutineScopeImpl ( - override val coroutineContext: CoroutineContext -): - TestCoroutineScope, - UncaughtExceptionCaptor by coroutineContext.uncaughtExceptionCaptor, - DelayController by coroutineContext.delayController -{ - override fun cleanupTestCoroutines() { - coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutines() - coroutineContext.delayController.cleanupTestCoroutines() - } -} - -/** - * A scope which provides detailed control over the execution of coroutines for tests. - * - * If the provided context does not provide a [ContinuationInterceptor] (Dispatcher) or [CoroutineExceptionHandler], the - * scope adds [TestCoroutineDispatcher] and [TestCoroutineExceptionHandler] automatically. - * - * @param context an optional context that MAY provide [UncaughtExceptionCaptor] and/or [DelayController] - */ -@Suppress("FunctionName") -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { - var safeContext = context - if (context[ContinuationInterceptor] == null) safeContext += TestCoroutineDispatcher() - if (context[CoroutineExceptionHandler] == null) safeContext += TestCoroutineExceptionHandler() - return TestCoroutineScopeImpl(safeContext) -} - -private inline val CoroutineContext.uncaughtExceptionCaptor: UncaughtExceptionCaptor - get() { - val handler = this[CoroutineExceptionHandler] - return handler as? UncaughtExceptionCaptor ?: throw IllegalArgumentException( - "TestCoroutineScope requires a UncaughtExceptionCaptor such as " + - "TestCoroutineExceptionHandler as the CoroutineExceptionHandler" - ) - } - -private inline val CoroutineContext.delayController: DelayController - get() { - val handler = this[ContinuationInterceptor] - return handler as? DelayController ?: throw IllegalArgumentException( - "TestCoroutineScope requires a DelayController such as TestCoroutineDispatcher as " + - "the ContinuationInterceptor (Dispatcher)" - ) - } diff --git a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt deleted file mode 100644 index c85d27ea87..0000000000 --- a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016-2021 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.* - -/** - * The testable main dispatcher used by kotlinx-coroutines-test. - * It is a [MainCoroutineDispatcher] which delegates all actions to a settable delegate. - */ -internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory) : MainCoroutineDispatcher(), Delay { - private var _delegate: CoroutineDispatcher? = null - private val delegate: CoroutineDispatcher get() { - _delegate?.let { return it } - mainFactory.tryCreateDispatcher(emptyList()).let { - // If we've failed to create a dispatcher, do no set _delegate - if (!isMissing()) { - _delegate = it - } - return it - } - } - - @Suppress("INVISIBLE_MEMBER") - private val delay: Delay get() = delegate as? Delay ?: DefaultDelay - - override val immediate: MainCoroutineDispatcher - get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this - - override fun dispatch(context: CoroutineContext, block: Runnable) { - delegate.dispatch(context, block) - } - - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - delay.scheduleResumeAfterDelay(timeMillis, continuation) - } - - override suspend fun delay(time: Long) { - delay.delay(time) - } - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - return delay.invokeOnTimeout(timeMillis, block, context) - } - - fun setDispatcher(dispatcher: CoroutineDispatcher) { - _delegate = dispatcher - } - - fun resetDispatcher() { - _delegate = null - } -} - -internal class TestMainDispatcherFactory : MainDispatcherFactory { - - override fun createDispatcher(allFactories: List): MainCoroutineDispatcher { - val originalFactory = allFactories.asSequence() - .filter { it !== this } - .maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory - return TestMainDispatcher(originalFactory) - } - - /** - * [Int.MAX_VALUE] -- test dispatcher always wins no matter what factories are present in the classpath. - * By default all actions are delegated to the second-priority dispatcher, so that it won't be the issue. - */ - override val loadPriority: Int - get() = Int.MAX_VALUE -} diff --git a/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt deleted file mode 100644 index 260edf9dc8..0000000000 --- a/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import org.junit.Test -import kotlin.test.* - -class TestCoroutineDispatcherTest { - @Test - fun whenStringCalled_itReturnsString() { - val subject = TestCoroutineDispatcher() - assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=0]", subject.toString()) - } - - @Test - fun whenStringCalled_itReturnsCurrentTime() { - val subject = TestCoroutineDispatcher() - subject.advanceTimeBy(1000) - assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString()) - } - - @Test - fun whenStringCalled_itShowsQueuedJobs() { - val subject = TestCoroutineDispatcher() - val scope = TestCoroutineScope(subject) - scope.pauseDispatcher() - scope.launch { - delay(1_000) - } - assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=1]", subject.toString()) - scope.advanceTimeBy(50) - assertEquals("TestCoroutineDispatcher[currentTime=50ms, queued=1]", subject.toString()) - scope.advanceUntilIdle() - assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString()) - } - - @Test - fun whenDispatcherPaused_doesntAutoProgressCurrent() { - val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() - val scope = CoroutineScope(subject) - var executed = 0 - scope.launch { - executed++ - } - assertEquals(0, executed) - } - - @Test - fun whenDispatcherResumed_doesAutoProgressCurrent() { - val subject = TestCoroutineDispatcher() - val scope = CoroutineScope(subject) - var executed = 0 - scope.launch { - executed++ - } - - assertEquals(1, executed) - } - - @Test - fun whenDispatcherResumed_doesNotAutoProgressTime() { - val subject = TestCoroutineDispatcher() - val scope = CoroutineScope(subject) - var executed = 0 - scope.launch { - delay(1_000) - executed++ - } - - assertEquals(0, executed) - subject.advanceUntilIdle() - assertEquals(1, executed) - } - - @Test - fun whenDispatcherPaused_thenResume_itDoesDispatchCurrent() { - val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() - val scope = CoroutineScope(subject) - var executed = 0 - scope.launch { - executed++ - } - - assertEquals(0, executed) - subject.resumeDispatcher() - assertEquals(1, executed) - } - - @Test(expected = UncompletedCoroutinesError::class) - fun whenDispatcherHasUncompletedCoroutines_itThrowsErrorInCleanup() { - val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() - val scope = CoroutineScope(subject) - scope.launch { - delay(1_000) - } - subject.cleanupTestCoroutines() - } - - @Test - fun whenDispatchCalled_runsOnCurrentThread() { - val currentThread = Thread.currentThread() - val subject = TestCoroutineDispatcher() - val scope = TestCoroutineScope(subject) - - val deferred = scope.async(Dispatchers.Default) { - withContext(subject) { - assertNotSame(currentThread, Thread.currentThread()) - 3 - } - } - - runBlocking { - // just to ensure the above code terminates - assertEquals(3, deferred.await()) - } - } - - @Test - fun whenAllDispatchersMocked_runsOnSameThread() { - val currentThread = Thread.currentThread() - val subject = TestCoroutineDispatcher() - val scope = TestCoroutineScope(subject) - - val deferred = scope.async(subject) { - withContext(subject) { - assertSame(currentThread, Thread.currentThread()) - 3 - } - } - - runBlocking { - // just to ensure the above code terminates - assertEquals(3, deferred.await()) - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt deleted file mode 100644 index fa14c38409..0000000000 --- a/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import org.junit.Test -import kotlin.test.* - -class TestCoroutineScopeTest { - @Test - fun whenGivenInvalidExceptionHandler_throwsException() { - val handler = CoroutineExceptionHandler { _, _ -> Unit } - assertFails { - TestCoroutineScope(handler) - } - } - - @Test - fun whenGivenInvalidDispatcher_throwsException() { - assertFails { - TestCoroutineScope(newSingleThreadContext("incorrect call")) - } - } -} diff --git a/kotlinx-coroutines-test/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/test/TestDispatchersTest.kt deleted file mode 100644 index 98d9705311..0000000000 --- a/kotlinx-coroutines-test/test/TestDispatchersTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import org.junit.* -import org.junit.Test -import kotlin.coroutines.* -import kotlin.test.* - -class TestDispatchersTest : TestBase() { - - @Before - fun setUp() { - Dispatchers.resetMain() - } - - @Test(expected = IllegalArgumentException::class) - fun testSelfSet() = runTest { - Dispatchers.setMain(Dispatchers.Main) - } - - @Test - fun testSingleThreadExecutor() = runTest { - val mainThread = Thread.currentThread() - Dispatchers.setMain(Dispatchers.Unconfined) - newSingleThreadContext("testSingleThread").use { threadPool -> - withContext(Dispatchers.Main) { - assertSame(mainThread, Thread.currentThread()) - } - - Dispatchers.setMain(threadPool) - withContext(Dispatchers.Main) { - assertNotSame(mainThread, Thread.currentThread()) - } - assertSame(mainThread, Thread.currentThread()) - - withContext(Dispatchers.Main.immediate) { - assertNotSame(mainThread, Thread.currentThread()) - } - assertSame(mainThread, Thread.currentThread()) - - Dispatchers.setMain(Dispatchers.Unconfined) - withContext(Dispatchers.Main.immediate) { - assertSame(mainThread, Thread.currentThread()) - } - assertSame(mainThread, Thread.currentThread()) - } - } - - @Test - fun testImmediateDispatcher() = runTest { - Dispatchers.setMain(ImmediateDispatcher()) - expect(1) - withContext(Dispatchers.Main) { - expect(3) - } - - Dispatchers.setMain(RegularDispatcher()) - withContext(Dispatchers.Main) { - expect(6) - } - - finish(7) - } - - private inner class ImmediateDispatcher : CoroutineDispatcher() { - override fun isDispatchNeeded(context: CoroutineContext): Boolean { - expect(2) - return false - } - - override fun dispatch(context: CoroutineContext, block: Runnable) = expectUnreached() - } - - private inner class RegularDispatcher : CoroutineDispatcher() { - override fun isDispatchNeeded(context: CoroutineContext): Boolean { - expect(4) - return true - } - - override fun dispatch(context: CoroutineContext, block: Runnable) { - expect(5) - block.run() - } - } -} diff --git a/kotlinx-coroutines-test/test/TestModuleHelpers.kt b/kotlinx-coroutines-test/test/TestModuleHelpers.kt deleted file mode 100644 index 12541bd90f..0000000000 --- a/kotlinx-coroutines-test/test/TestModuleHelpers.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.test - -import kotlinx.coroutines.* -import org.junit.* -import java.time.* - -const val SLOW = 10_000L - -/** - * Assert a block completes within a second or fail the suite - */ -suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) { - val start = Instant.now().toEpochMilli() - // don't need to be fancy with timeouts here since anything longer than a few ms is an error - block() - val duration = Instant.now().minusMillis(start).toEpochMilli() - Assert.assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)", duration < 2_000) -} diff --git a/license/NOTICE.txt b/license/NOTICE.txt index d1d00c1a87..8d1100a3a5 100644 --- a/license/NOTICE.txt +++ b/license/NOTICE.txt @@ -5,4 +5,4 @@ ========================================================================= kotlinx.coroutines library. -Copyright 2016-2019 JetBrains s.r.o and respective authors and developers \ No newline at end of file +Copyright 2016-2021 JetBrains s.r.o and respective authors and developers diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt index 3682d5e318..0479028d80 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt @@ -278,10 +278,8 @@ class PublishTest : TestBase() { val publisher = flowPublish { assertFailsWith { send(null) } assertFailsWith { trySend(null) } - @Suppress("DEPRECATION") - assertFailsWith { offer(null) } send("OK") } assertEquals("OK", publisher.awaitFirstOrNull()) } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api index 75f1b306d3..c82880233a 100644 --- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api +++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api @@ -12,7 +12,7 @@ public final class kotlinx/coroutines/reactive/AwaitKt { public final class kotlinx/coroutines/reactive/ChannelKt { public static final fun collect (Lorg/reactivestreams/Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun openSubscription (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final synthetic fun openSubscription (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel; public static synthetic fun openSubscription$default (Lorg/reactivestreams/Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel; public static final fun toChannel (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel; } @@ -22,7 +22,7 @@ public abstract interface class kotlinx/coroutines/reactive/ContextInjector { } public final class kotlinx/coroutines/reactive/ConvertKt { - public static final fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher; + public static final synthetic fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher; public static synthetic fun asPublisher$default (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; } diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index 128d4d86ab..c2e4b5c9f0 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -5,8 +5,8 @@ val reactiveStreamsVersion = property("reactive_streams_version") dependencies { - compile("org.reactivestreams:reactive-streams:$reactiveStreamsVersion") - testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") + api("org.reactivestreams:reactive-streams:$reactiveStreamsVersion") + testImplementation("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") } val testNG by tasks.registering(Test::class) { @@ -34,3 +34,17 @@ tasks.check { externalDocumentationLink( url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/" ) + +val commonKoverExcludes = listOf( + "kotlinx.coroutines.reactive.FlowKt", // Deprecated + "kotlinx.coroutines.reactive.FlowKt__MigrationKt", // Deprecated + "kotlinx.coroutines.reactive.ConvertKt" // Deprecated +) + +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} diff --git a/reactive/kotlinx-coroutines-reactive/src/Await.kt b/reactive/kotlinx-coroutines-reactive/src/Await.kt index fef1205a8a..da8632bffc 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Await.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Await.kt @@ -106,7 +106,7 @@ public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) @Deprecated( message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " + "Please consider using awaitFirstOrDefault().", - level = DeprecationLevel.WARNING + level = DeprecationLevel.ERROR ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun Publisher.awaitSingleOrDefault(default: T): T = awaitOne(Mode.SINGLE_OR_DEFAULT, default) @@ -135,7 +135,7 @@ public suspend fun Publisher.awaitSingleOrDefault(default: T): T = awaitO message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " + "There is a specialized version for Reactor's Mono, please use that where applicable. " + "Alternatively, please consider using awaitFirstOrNull().", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull()", "kotlinx.coroutines.reactor") ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun Publisher.awaitSingleOrNull(): T? = awaitOne(Mode.SINGLE_OR_DEFAULT) @@ -164,7 +164,7 @@ public suspend fun Publisher.awaitSingleOrNull(): T? = awaitOne(Mode.SING @Deprecated( message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " + "Please consider using awaitFirstOrElse().", - level = DeprecationLevel.WARNING + level = DeprecationLevel.ERROR ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun Publisher.awaitSingleOrElse(defaultValue: () -> T): T = awaitOne(Mode.SINGLE_OR_DEFAULT) ?: defaultValue() diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt index b7fbf134c5..a8db21711d 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt @@ -10,29 +10,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.internal.* import org.reactivestreams.* -/** - * Subscribes to this [Publisher] and returns a channel to receive the elements emitted by it. - * The resulting channel needs to be [cancelled][ReceiveChannel.cancel] in order to unsubscribe from this publisher. - - * @param request how many items to request from the publisher in advance (optional, a single element by default). - * - * This method is deprecated in the favor of [Flow]. - * Instead of iterating over the resulting channel please use [collect][Flow.collect]: - * ``` - * asFlow().collect { value -> - * // process value - * } - * ``` - */ -@Deprecated( - message = "Transforming publisher to channel is deprecated, use asFlow() instead", - level = DeprecationLevel.ERROR) // Will be error in 1.4 -public fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { - val channel = SubscriptionChannel(request) - subscribe(channel) - return channel -} - /** * Subscribes to this [Publisher] and performs the specified action for each received element. * @@ -123,3 +100,12 @@ private class SubscriptionChannel( } } +/** @suppress */ +@Deprecated( + message = "Transforming publisher to channel is deprecated, use asFlow() instead", + level = DeprecationLevel.HIDDEN) // ERROR in 1.4, HIDDEN in 1.6.0 +public fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { + val channel = SubscriptionChannel(request) + subscribe(channel) + return channel +} diff --git a/reactive/kotlinx-coroutines-reactive/src/Convert.kt b/reactive/kotlinx-coroutines-reactive/src/Convert.kt index 3cb05b60fd..9492b49871 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Convert.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Convert.kt @@ -8,15 +8,9 @@ import kotlinx.coroutines.channels.* import org.reactivestreams.* import kotlin.coroutines.* -/** - * Converts a stream of elements received from the channel to the hot reactive publisher. - * - * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, - * they'll receive values in round-robin way. - * @param context -- the coroutine context from which the resulting observable is going to be signalled - */ +/** @suppress */ @Deprecated(message = "Deprecated in the favour of consumeAsFlow()", - level = DeprecationLevel.ERROR, // Error in 1.4 + level = DeprecationLevel.HIDDEN, // Error in 1.4, HIDDEN in 1.6.0 replaceWith = ReplaceWith("this.consumeAsFlow().asPublisher(context)", imports = ["kotlinx.coroutines.flow.consumeAsFlow"])) public fun ReceiveChannel.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = publish(context) { for (t in this@asPublisher) diff --git a/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt index 4928a7439e..1b8683ce64 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.reactive import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlinx.coroutines.sync.* import org.reactivestreams.* @@ -104,10 +105,21 @@ public class PublisherCoroutine( // registerSelectSend @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") override fun registerSelectClause2(select: SelectInstance, element: T, block: suspend (SendChannel) -> R) { - mutex.onLock.registerSelectClause2(select, null) { + val clause = suspend { doLockedNext(element)?.let { throw it } block(this) } + + launch(start = CoroutineStart.UNDISPATCHED) { + mutex.lock() + // Already selected -- bail out + if (!select.trySelect()) { + mutex.unlock() + return@launch + } + + clause.startCoroutineCancellable(select.completion) + } } /* diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt index efe7ec7e45..fa039897d7 100644 --- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt @@ -52,9 +52,6 @@ class IntegrationTest( assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) assertFailsWith { pub.awaitLast() } assertFailsWith { pub.awaitSingle() } - assertEquals("OK", pub.awaitSingleOrDefault("OK")) - assertNull(pub.awaitSingleOrNull()) - assertEquals("ELSE", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -72,9 +69,6 @@ class IntegrationTest( assertEquals("OK", pub.awaitFirstOrElse { "ELSE" }) assertEquals("OK", pub.awaitLast()) assertEquals("OK", pub.awaitSingle()) - assertEquals("OK", pub.awaitSingleOrDefault("!")) - assertEquals("OK", pub.awaitSingleOrNull()) - assertEquals("OK", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { assertEquals("OK", it) @@ -189,10 +183,6 @@ class IntegrationTest( onError(dummyThrowable) onComplete() } - assertDetectsBadPublisher({ awaitSingleOrDefault(2) }, "terminal state") { - onComplete() - onError(dummyThrowable) - } assertDetectsBadPublisher({ awaitFirst() }, "terminal state") { onNext(0) onComplete() @@ -251,4 +241,4 @@ internal suspend inline fun assertCallsExceptionHandlerWi it } } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt index 095b724d40..d92a8883be 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt @@ -5,9 +5,13 @@ package kotlinx.coroutines.reactive import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.* import org.junit.Test import org.reactivestreams.* +import java.util.concurrent.* import kotlin.test.* class PublishTest : TestBase() { @@ -278,10 +282,40 @@ class PublishTest : TestBase() { val publisher = publish { assertFailsWith { send(null) } assertFailsWith { trySend(null) } - @Suppress("DEPRECATION") - assertFailsWith { offer(null) } send("OK") } assertEquals("OK", publisher.awaitFirstOrNull()) } -} \ No newline at end of file + + @Test + fun testOnSendCancelled() = runTest { + val latch = CountDownLatch(1) + val published = publish(Dispatchers.Default) { + expect(2) + // Collector is ready + send(1) + try { + send(2) + expectUnreached() + } catch (e: CancellationException) { + // publisher cancellation is async + latch.countDown() + throw e + } + } + + expect(1) + val collectorLatch = Mutex(true) + val job = launch { + published.asFlow().buffer(0).collect { + collectorLatch.unlock() + hang { expect(4) } + } + } + collectorLatch.lock() + expect(3) + job.cancelAndJoin() + latch.await() + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt index e3b1d3b384..4a552b5f8d 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.reactive import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* import org.junit.Test import kotlin.test.* @@ -16,7 +17,7 @@ class PublisherMultiTest : TestBase() { // concurrent emitters (many coroutines) val jobs = List(n) { // launch - launch { + launch(Dispatchers.Default) { send(it) } } @@ -28,4 +29,26 @@ class PublisherMultiTest : TestBase() { } assertEquals(n, resultSet.size) } + + @Test + fun testConcurrentStressOnSend() = runBlocking { + val n = 10_000 * stressTestMultiplier + val observable = publish { + // concurrent emitters (many coroutines) + val jobs = List(n) { + // launch + launch(Dispatchers.Default) { + select { + onSend(it) {} + } + } + } + jobs.forEach { it.join() } + } + val resultSet = mutableSetOf() + observable.collect { + assertTrue(resultSet.add(it)) + } + assertEquals(n, resultSet.size) + } } diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts index 03af7f4fda..d4bb135f73 100644 --- a/reactive/kotlinx-coroutines-reactor/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactor/build.gradle.kts @@ -5,8 +5,8 @@ val reactorVersion = version("reactor") dependencies { - compile("io.projectreactor:reactor-core:$reactorVersion") - compile(project(":kotlinx-coroutines-reactive")) + api("io.projectreactor:reactor-core:$reactorVersion") + api(project(":kotlinx-coroutines-reactive")) } java { @@ -27,3 +27,16 @@ tasks { externalDocumentationLink( url = "https://projectreactor.io/docs/core/$reactorVersion/api/" ) + +val commonKoverExcludes = listOf( + "kotlinx.coroutines.reactor.FlowKt", // Deprecated + "kotlinx.coroutines.reactor.ConvertKt\$asFlux$1" // Deprecated +) + +tasks.koverHtmlReport { + excludes = commonKoverExcludes +} + +tasks.koverVerify { + excludes = commonKoverExcludes +} diff --git a/reactive/kotlinx-coroutines-reactor/src/Mono.kt b/reactive/kotlinx-coroutines-reactor/src/Mono.kt index e86d51c614..f31004b665 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Mono.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Mono.kt @@ -157,7 +157,7 @@ public fun CoroutineScope.mono( @Deprecated( message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " + "Please use awaitSingle() instead.", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingle()") ) // Warning since 1.5, error in 1.6 public suspend fun Mono.awaitFirst(): T = awaitSingle() @@ -181,7 +181,7 @@ public suspend fun Mono.awaitFirst(): T = awaitSingle() @Deprecated( message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " + "Please use awaitSingleOrNull() instead.", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default") ) // Warning since 1.5, error in 1.6 public suspend fun Mono.awaitFirstOrDefault(default: T): T = awaitSingleOrNull() ?: default @@ -205,7 +205,7 @@ public suspend fun Mono.awaitFirstOrDefault(default: T): T = awaitSingleO @Deprecated( message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " + "Please use awaitSingleOrNull() instead.", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull()") ) // Warning since 1.5, error in 1.6 public suspend fun Mono.awaitFirstOrNull(): T? = awaitSingleOrNull() @@ -229,7 +229,7 @@ public suspend fun Mono.awaitFirstOrNull(): T? = awaitSingleOrNull() @Deprecated( message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " + "Please use awaitSingleOrNull() instead.", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: defaultValue()") ) // Warning since 1.5, error in 1.6 public suspend fun Mono.awaitFirstOrElse(defaultValue: () -> T): T = awaitSingleOrNull() ?: defaultValue() @@ -253,7 +253,7 @@ public suspend fun Mono.awaitFirstOrElse(defaultValue: () -> T): T = awai @Deprecated( message = "Mono produces at most one value, so the last element is the same as the first. " + "Please use awaitSingle() instead.", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingle()") ) // Warning since 1.5, error in 1.6 public suspend fun Mono.awaitLast(): T = awaitSingle() diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt index d9228409db..912fb6e577 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines.reactor -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlin.coroutines.* import kotlinx.coroutines.reactive.* import reactor.util.context.* @@ -65,11 +64,7 @@ public class ReactorContext(public val context: Context) : AbstractCoroutineCont */ public fun ContextView.asCoroutineContext(): ReactorContext = ReactorContext(this) -/** - * Wraps the given [Context] into [ReactorContext], so it can be added to the coroutine's context - * and later used via `coroutineContext[ReactorContext]`. - * @suppress - */ +/** @suppress */ @Deprecated("The more general version for ContextView should be used instead", level = DeprecationLevel.HIDDEN) public fun Context.asCoroutineContext(): ReactorContext = readOnly().asCoroutineContext() // `readOnly()` is zero-cost. diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index cc336ba6b5..3879c62c71 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -68,72 +68,6 @@ class FluxSingleTest : TestBase() { } } - @Test - fun testAwaitSingleOrDefault() { - val flux = flux { - send(Flux.empty().awaitSingleOrDefault("O") + "K") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrDefaultException() { - val flux = flux { - send(Flux.just("O", "#").awaitSingleOrDefault("!") + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - - @Test - fun testAwaitSingleOrNull() { - val flux = flux { - send(Flux.empty().awaitSingleOrNull() ?: "OK") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrNullException() { - val flux = flux { - send((Flux.just("O", "#").awaitSingleOrNull() ?: "!") + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - - @Test - fun testAwaitSingleOrElse() { - val flux = flux { - send(Flux.empty().awaitSingleOrElse { "O" } + "K") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrElseException() { - val flux = flux { - send(Flux.just("O", "#").awaitSingleOrElse { "!" } + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - @Test fun testAwaitFirst() { val flux = flux { diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt index d059eb6622..f575af4101 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt @@ -170,10 +170,8 @@ class FluxTest : TestBase() { val flux = flux { assertFailsWith { send(null) } assertFailsWith { trySend(null) } - @Suppress("DEPRECATION") - assertFailsWith { offer(null) } send("OK") } assertEquals("OK", flux.awaitFirstOrNull()) } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt index 421295d115..2a5e5dc107 100644 --- a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt @@ -119,29 +119,6 @@ class MonoTest : TestBase() { assertNull(Mono.empty().awaitSingleOrNull()) } - /** Tests that the versions of the await methods specialized for Mono for deprecation behave correctly and we don't - * break any code by introducing them. */ - @Test - @Suppress("DEPRECATION") - fun testDeprecatedAwaitMethods() = runBlocking { - val filledMono = mono { "OK" } - assertEquals("OK", filledMono.awaitFirst()) - assertEquals("OK", filledMono.awaitFirstOrDefault("!")) - assertEquals("OK", filledMono.awaitFirstOrNull()) - assertEquals("OK", filledMono.awaitFirstOrElse { "ELSE" }) - assertEquals("OK", filledMono.awaitLast()) - assertEquals("OK", filledMono.awaitSingleOrDefault("!")) - assertEquals("OK", filledMono.awaitSingleOrElse { "ELSE" }) - val emptyMono = mono { null } - assertFailsWith { emptyMono.awaitFirst() } - assertEquals("OK", emptyMono.awaitFirstOrDefault("OK")) - assertNull(emptyMono.awaitFirstOrNull()) - assertEquals("ELSE", emptyMono.awaitFirstOrElse { "ELSE" }) - assertFailsWith { emptyMono.awaitLast() } - assertEquals("OK", emptyMono.awaitSingleOrDefault("OK")) - assertEquals("ELSE", emptyMono.awaitSingleOrElse { "ELSE" }) - } - /** Tests that calls to [awaitSingleOrNull] (and, thus, to the rest of such functions) throw [CancellationException] * and unsubscribe from the publisher when their [Job] is cancelled. */ @Test diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index c27ef4d796..c2d1c4bf1d 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -16,8 +16,8 @@ public final class kotlinx/coroutines/rx2/RxAwaitKt { public final class kotlinx/coroutines/rx2/RxChannelKt { public static final fun collect (Lio/reactivex/MaybeSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun collect (Lio/reactivex/ObservableSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun openSubscription (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel; - public static final fun openSubscription (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final synthetic fun openSubscription (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final synthetic fun openSubscription (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel; public static final fun toChannel (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel; public static final fun toChannel (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel; } @@ -69,7 +69,9 @@ public final class kotlinx/coroutines/rx2/RxObservableKt { } public final class kotlinx/coroutines/rx2/RxSchedulerKt { - public static final fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/rx2/SchedulerCoroutineDispatcher; + public static final fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/CoroutineDispatcher; + public static final synthetic fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/rx2/SchedulerCoroutineDispatcher; + public static final fun asScheduler (Lkotlinx/coroutines/CoroutineDispatcher;)Lio/reactivex/Scheduler; } public final class kotlinx/coroutines/rx2/RxSingleKt { diff --git a/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt b/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt index 0e0b47ebe8..da9809c9f8 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt @@ -80,7 +80,7 @@ public suspend fun MaybeSource.awaitSingle(): T = awaitSingleOrNull() ?: */ @Deprecated( message = "Deprecated in favor of awaitSingleOrNull()", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull()") ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun MaybeSource.await(): T? = awaitSingleOrNull() @@ -102,7 +102,7 @@ public suspend fun MaybeSource.await(): T? = awaitSingleOrNull() */ @Deprecated( message = "Deprecated in favor of awaitSingleOrNull()", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default") ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun MaybeSource.awaitOrDefault(default: T): T = awaitSingleOrNull() ?: default diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt index bb093b0793..fc09bf9ee3 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt @@ -12,36 +12,6 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* -/** - * Subscribes to this [MaybeSource] and returns a channel to receive elements emitted by it. - * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source. - * - * This API is deprecated in the favour of [Flow]. - * [MaybeSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first. - * @suppress - */ -@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.ERROR) // Will be hidden in 1.5 -public fun MaybeSource.openSubscription(): ReceiveChannel { - val channel = SubscriptionChannel() - subscribe(channel) - return channel -} - -/** - * Subscribes to this [ObservableSource] and returns a channel to receive elements emitted by it. - * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source. - * - * This API is deprecated in the favour of [Flow]. - * [ObservableSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first. - * @suppress - */ -@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.ERROR) // Will be hidden in 1.5 -public fun ObservableSource.openSubscription(): ReceiveChannel { - val channel = SubscriptionChannel() - subscribe(channel) - return channel -} - /** * Subscribes to this [MaybeSource] and performs the specified action for each received element. * @@ -107,3 +77,19 @@ private class SubscriptionChannel : close(cause = e) } } + +/** @suppress */ +@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.HIDDEN) // ERROR in 1.4.0, HIDDEN in 1.6.0 +public fun ObservableSource.openSubscription(): ReceiveChannel { + val channel = SubscriptionChannel() + subscribe(channel) + return channel +} + +/** @suppress */ +@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.HIDDEN) // ERROR in 1.4.0, HIDDEN in 1.6.0 +public fun MaybeSource.openSubscription(): ReceiveChannel { + val channel = SubscriptionChannel() + subscribe(channel) + return channel +} diff --git a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt index 3f9153822b..e4670f3579 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt @@ -20,7 +20,7 @@ public fun rxCompletable( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit ): Completable { - require(context[Job] === null) { "Completable context cannot contain job in it." + + require(context[Job] === null) { "Completable context cannot contain job in it. " + "Its lifecycle should be managed via Disposable handle. Had $context" } return rxCompletableInternal(GlobalScope, context, block) } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt index 5f409815af..90e770bb4f 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt @@ -10,6 +10,7 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.internal.* +import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlinx.coroutines.sync.* import kotlin.coroutines.* @@ -95,10 +96,22 @@ private class RxObservableCoroutine( element: T, block: suspend (SendChannel) -> R ) { - mutex.onLock.registerSelectClause2(select, null) { + val clause = suspend { doLockedNext(element)?.let { throw it } block(this) } + + // This is the default replacement proposed in onLock replacement + launch(start = CoroutineStart.UNDISPATCHED) { + mutex.lock() + // Already selected -- bail out + if (!select.trySelect()) { + mutex.unlock() + return@launch + } + + clause.startCoroutineCancellable(select.completion) + } } // assert: mutex.isLocked() diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt index 0262fc12f7..d7d5f6cfbf 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt @@ -4,16 +4,143 @@ package kotlinx.coroutines.rx2 -import io.reactivex.Scheduler +import io.reactivex.* +import io.reactivex.disposables.* +import io.reactivex.plugins.* +import kotlinx.atomicfu.* import kotlinx.coroutines.* -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.channels.* +import java.util.concurrent.* +import kotlin.coroutines.* /** * Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher] * and provides native support of [delay] and [withTimeout]. */ -public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this) +public fun Scheduler.asCoroutineDispatcher(): CoroutineDispatcher = + if (this is DispatcherScheduler) { + dispatcher + } else { + SchedulerCoroutineDispatcher(this) + } + +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.2, binary compatibility with earlier versions") +@JvmName("asCoroutineDispatcher") +public fun Scheduler.asCoroutineDispatcher0(): SchedulerCoroutineDispatcher = + SchedulerCoroutineDispatcher(this) + +/** + * Converts an instance of [CoroutineDispatcher] to an implementation of [Scheduler]. + */ +public fun CoroutineDispatcher.asScheduler(): Scheduler = + if (this is SchedulerCoroutineDispatcher) { + scheduler + } else { + DispatcherScheduler(this) + } + +private class DispatcherScheduler(@JvmField val dispatcher: CoroutineDispatcher) : Scheduler() { + + private val schedulerJob = SupervisorJob() + + /** + * The scope for everything happening in this [DispatcherScheduler]. + * + * Running tasks, too, get launched under this scope, because [shutdown] should cancel the running tasks as well. + */ + private val scope = CoroutineScope(schedulerJob + dispatcher) + + /** + * The counter of created workers, for their pretty-printing. + */ + private val workerCounter = atomic(1L) + + override fun scheduleDirect(block: Runnable, delay: Long, unit: TimeUnit): Disposable = + scope.scheduleTask(block, unit.toMillis(delay)) { task -> + Runnable { scope.launch { task() } } + } + + override fun createWorker(): Worker = DispatcherWorker(workerCounter.getAndIncrement(), dispatcher, schedulerJob) + + override fun shutdown() { + schedulerJob.cancel() + } + + private class DispatcherWorker( + private val counter: Long, + private val dispatcher: CoroutineDispatcher, + parentJob: Job + ) : Worker() { + + private val workerJob = SupervisorJob(parentJob) + private val workerScope = CoroutineScope(workerJob + dispatcher) + private val blockChannel = Channel Unit>(Channel.UNLIMITED) + + init { + workerScope.launch { + blockChannel.consumeEach { + it() + } + } + } + + override fun schedule(block: Runnable, delay: Long, unit: TimeUnit): Disposable = + workerScope.scheduleTask(block, unit.toMillis(delay)) { task -> + Runnable { blockChannel.trySend(task) } + } + + override fun isDisposed(): Boolean = !workerScope.isActive + + override fun dispose() { + blockChannel.close() + workerJob.cancel() + } + + override fun toString(): String = "$dispatcher (worker $counter, ${if (isDisposed) "disposed" else "active"})" + } + + override fun toString(): String = dispatcher.toString() +} + +private typealias Task = suspend () -> Unit + +/** + * Schedule [block] so that an adapted version of it, wrapped in [adaptForScheduling], executes after [delayMillis] + * milliseconds. + */ +private fun CoroutineScope.scheduleTask( + block: Runnable, + delayMillis: Long, + adaptForScheduling: (Task) -> Runnable +): Disposable { + val ctx = coroutineContext + var handle: DisposableHandle? = null + val disposable = Disposables.fromRunnable { + // null if delay <= 0 + handle?.dispose() + } + val decoratedBlock = RxJavaPlugins.onSchedule(block) + suspend fun task() { + if (disposable.isDisposed) return + try { + runInterruptible { + decoratedBlock.run() + } + } catch (e: Throwable) { + handleUndeliverableException(e, ctx) + } + } + + val toSchedule = adaptForScheduling(::task) + if (!isActive) return Disposables.disposed() + if (delayMillis <= 0) { + toSchedule.run() + } else { + @Suppress("INVISIBLE_MEMBER") + ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it } + } + return disposable +} /** * Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler]. @@ -45,8 +172,10 @@ public class SchedulerCoroutineDispatcher( /** @suppress */ override fun toString(): String = scheduler.toString() + /** @suppress */ override fun equals(other: Any?): Boolean = other is SchedulerCoroutineDispatcher && other.scheduler === scheduler + /** @suppress */ override fun hashCode(): Int = System.identityHashCode(scheduler) -} +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt index 30266e3e50..7e1d335028 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt @@ -12,7 +12,7 @@ import kotlin.coroutines.* class ObservableCompletionStressTest : TestBase() { private val N_REPEATS = 10_000 * stressTestMultiplier - private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = rxObservable(context) { + private fun range(context: CoroutineContext, start: Int, count: Int) = rxObservable(context) { for (x in start until start + count) send(x) } @@ -33,4 +33,4 @@ class ObservableCompletionStressTest : TestBase() { } } } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt index 074fcf4900..7023211450 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* import org.junit.Test import java.io.* import kotlin.test.* @@ -47,6 +48,29 @@ class ObservableMultiTest : TestBase() { } } + @Test + fun testConcurrentStressOnSend() { + val n = 10_000 * stressTestMultiplier + val observable = rxObservable { + newCoroutineContext(coroutineContext) + // concurrent emitters (many coroutines) + val jobs = List(n) { + // launch + launch(Dispatchers.Default) { + val i = it + select { + onSend(i) {} + } + } + } + jobs.forEach { it.join() } + } + checkSingleValue(observable.toList()) { list -> + assertEquals(n, list.size) + assertEquals((0 until n).toList(), list.sorted()) + } + } + @Test fun testIteratorResendUnconfined() { val n = 10_000 * stressTestMultiplier @@ -88,4 +112,4 @@ class ObservableMultiTest : TestBase() { assertEquals("OK", it) } } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt b/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt new file mode 100644 index 0000000000..ea33a9fa58 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx2 + +import kotlinx.coroutines.* +import org.junit.* +import java.util.concurrent.* + +class SchedulerStressTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxCachedThreadScheduler-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + /** + * Test that we don't get an OOM if we schedule many jobs at once. + * It's expected that if you don't dispose you'd see an OOM error. + */ + @Test + fun testSchedulerDisposed(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableDisposed(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerDisposed(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableDisposed(worker::schedule) + } + + private suspend fun testRunnableDisposed(block: RxSchedulerBlockNoDelay) { + val n = 2000 * stressTestMultiplier + repeat(n) { + val a = ByteArray(1000000) //1MB + val disposable = block(Runnable { + keepMe(a) + expectUnreached() + }) + disposable.dispose() + yield() // allow the scheduled task to observe that it was disposed + } + } + + /** + * Test function that holds a reference. Used for testing OOM situations + */ + private fun keepMe(a: ByteArray) { + Thread.sleep(a.size / (a.size + 1) + 10L) + } + + /** + * Test that we don't get an OOM if we schedule many delayed jobs at once. It's expected that if you don't dispose that you'd + * see a OOM error. + */ + @Test + fun testSchedulerDisposedDuringDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableDisposedDuringDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerDisposedDuringDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableDisposedDuringDelay(worker::schedule) + } + + private fun testRunnableDisposedDuringDelay(block: RxSchedulerBlockWithDelay) { + val n = 2000 * stressTestMultiplier + repeat(n) { + val a = ByteArray(1000000) //1MB + val delayMillis: Long = 10 + val disposable = block(Runnable { + keepMe(a) + expectUnreached() + }, delayMillis, TimeUnit.MILLISECONDS) + disposable.dispose() + } + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt b/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt index 26dbe8f4cf..194186718d 100644 --- a/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt @@ -4,10 +4,18 @@ package kotlinx.coroutines.rx2 -import io.reactivex.schedulers.Schedulers +import io.reactivex.* +import io.reactivex.disposables.* +import io.reactivex.plugins.* +import io.reactivex.schedulers.* import kotlinx.coroutines.* -import org.junit.Before +import kotlinx.coroutines.sync.* +import org.junit.* import org.junit.Test +import java.lang.Runnable +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.* import kotlin.test.* class SchedulerTest : TestBase() { @@ -17,7 +25,7 @@ class SchedulerTest : TestBase() { } @Test - fun testIoScheduler(): Unit = runBlocking { + fun testIoScheduler(): Unit = runTest { expect(1) val mainThread = Thread.currentThread() withContext(Schedulers.io().asCoroutineDispatcher()) { @@ -31,4 +39,458 @@ class SchedulerTest : TestBase() { } finish(4) } -} \ No newline at end of file + + /** Tests [toString] implementations of [CoroutineDispatcher.asScheduler] and its [Scheduler.Worker]. */ + @Test + fun testSchedulerToString() { + val name = "Dispatchers.Default" + val scheduler = Dispatchers.Default.asScheduler() + assertContains(scheduler.toString(), name) + val worker = scheduler.createWorker() + val activeWorkerName = worker.toString() + assertContains(worker.toString(), name) + worker.dispose() + val disposedWorkerName = worker.toString() + assertNotEquals(activeWorkerName, disposedWorkerName) + } + + private fun runSchedulerTest(nThreads: Int = 1, action: (Scheduler) -> Unit) { + val future = CompletableFuture() + try { + newFixedThreadPoolContext(nThreads, "test").use { dispatcher -> + RxJavaPlugins.setErrorHandler { + if (!future.completeExceptionally(it)) { + handleUndeliverableException(it, dispatcher) + } + } + action(dispatcher.asScheduler()) + } + } finally { + RxJavaPlugins.setErrorHandler(null) + } + future.complete(Unit) + future.getNow(Unit) // rethrow any encountered errors + } + + private fun ensureSeparateThread(schedule: (Runnable, Long, TimeUnit) -> Unit, scheduleNoDelay: (Runnable) -> Unit) { + val mainThread = Thread.currentThread() + val cdl1 = CountDownLatch(1) + val cdl2 = CountDownLatch(1) + expect(1) + val thread = AtomicReference(null) + fun checkThread() { + val current = Thread.currentThread() + thread.getAndSet(current)?.let { assertEquals(it, current) } + } + schedule({ + assertNotSame(mainThread, Thread.currentThread()) + checkThread() + cdl2.countDown() + }, 300, TimeUnit.MILLISECONDS) + scheduleNoDelay { + expect(2) + checkThread() + assertNotSame(mainThread, Thread.currentThread()) + cdl1.countDown() + } + cdl1.await() + cdl2.await() + finish(3) + } + + /** + * Tests [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler] on a single-threaded dispatcher. + */ + @Test + fun testSingleThreadedDispatcherDirect(): Unit = runSchedulerTest(1) { + ensureSeparateThread(it::scheduleDirect, it::scheduleDirect) + } + + /** + * Tests [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler] running its tasks on the correct thread. + */ + @Test + fun testSingleThreadedWorker(): Unit = runSchedulerTest(1) { + val worker = it.createWorker() + ensureSeparateThread(worker::schedule, worker::schedule) + } + + private fun checkCancelling(schedule: (Runnable, Long, TimeUnit) -> Disposable) { + // cancel the task before it has a chance to run. + val handle1 = schedule({ + throw IllegalStateException("should have been successfully cancelled") + }, 10_000, TimeUnit.MILLISECONDS) + handle1.dispose() + // cancel the task after it started running. + val cdl1 = CountDownLatch(1) + val cdl2 = CountDownLatch(1) + val handle2 = schedule({ + cdl1.countDown() + cdl2.await() + if (Thread.interrupted()) + throw IllegalStateException("cancelling the task should not interrupt the thread") + }, 100, TimeUnit.MILLISECONDS) + cdl1.await() + handle2.dispose() + cdl2.countDown() + } + + /** + * Test cancelling [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testCancellingDirect(): Unit = runSchedulerTest { + checkCancelling(it::scheduleDirect) + } + + /** + * Test cancelling [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testCancellingWorker(): Unit = runSchedulerTest { + val worker = it.createWorker() + checkCancelling(worker::schedule) + } + + /** + * Test shutting down [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testShuttingDown() { + val n = 5 + runSchedulerTest(nThreads = n) { scheduler -> + val cdl1 = CountDownLatch(n) + val cdl2 = CountDownLatch(1) + val cdl3 = CountDownLatch(n) + repeat(n) { + scheduler.scheduleDirect { + cdl1.countDown() + try { + cdl2.await() + } catch (e: InterruptedException) { + // this is the expected outcome + cdl3.countDown() + } + } + } + cdl1.await() + scheduler.shutdown() + if (!cdl3.await(1, TimeUnit.SECONDS)) { + cdl2.countDown() + error("the tasks were not cancelled when the scheduler was shut down") + } + } + } + + /** Tests that there are no uncaught exceptions if [Disposable.dispose] on a worker happens when tasks are present. */ + @Test + fun testDisposingWorker() = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + yield() // so that the worker starts waiting on the channel + assertFalse(worker.isDisposed) + worker.dispose() + assertTrue(worker.isDisposed) + } + + /** Tests trying to use a [Scheduler.Worker]/[Scheduler] after [Scheduler.Worker.dispose]/[Scheduler.shutdown]. */ + @Test + fun testSchedulingAfterDisposing() = runSchedulerTest { + expect(1) + val worker = it.createWorker() + // use CDL to ensure that the worker has properly initialized + val cdl1 = CountDownLatch(1) + setScheduler(2, 3) + val disposable1 = worker.schedule { + cdl1.countDown() + } + cdl1.await() + expect(4) + assertFalse(disposable1.isDisposed) + setScheduler(6, -1) + // check that the worker automatically disposes of the tasks after being disposed + assertFalse(worker.isDisposed) + worker.dispose() + assertTrue(worker.isDisposed) + expect(5) + val disposable2 = worker.schedule { + expectUnreached() + } + assertTrue(disposable2.isDisposed) + setScheduler(7, 8) + // ensure that the scheduler still works + val cdl2 = CountDownLatch(1) + val disposable3 = it.scheduleDirect { + cdl2.countDown() + } + cdl2.await() + expect(9) + assertFalse(disposable3.isDisposed) + // check that the scheduler automatically disposes of the tasks after being shut down + it.shutdown() + setScheduler(10, -1) + val disposable4 = it.scheduleDirect { + expectUnreached() + } + assertTrue(disposable4.isDisposed) + RxJavaPlugins.setScheduleHandler(null) + finish(11) + } + + @Test + fun testSchedulerWithNoDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithNoDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerWithNoDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithNoDelay(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableWithNoDelay(block: RxSchedulerBlockNoDelay) { + expect(1) + suspendCancellableCoroutine { + block(Runnable { + expect(2) + it.resume(Unit) + }) + } + yield() + finish(3) + } + + @Test + fun testSchedulerWithDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect, 300) + } + + @Test + fun testSchedulerWorkerWithDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule, 300) + } + + @Test + fun testSchedulerWithZeroDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerWithZeroDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableWithDelay(block: RxSchedulerBlockWithDelay, delayMillis: Long = 0) { + expect(1) + suspendCancellableCoroutine { + block({ + expect(2) + it.resume(Unit) + }, delayMillis, TimeUnit.MILLISECONDS) + } + finish(3) + } + + @Test + fun testAsSchedulerWithNegativeDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect, -1) + } + + @Test + fun testAsSchedulerWorkerWithNegativeDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule, -1) + } + + @Test + fun testSchedulerImmediateDispose(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableImmediateDispose(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerImmediateDispose(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableImmediateDispose(scheduler.createWorker()::schedule) + } + + private fun testRunnableImmediateDispose(block: RxSchedulerBlockNoDelay) { + val disposable = block { + expectUnreached() + } + disposable.dispose() + } + + @Test + fun testConvertDispatcherToOriginalScheduler(): Unit = runTest { + val originalScheduler = Schedulers.io() + val dispatcher = originalScheduler.asCoroutineDispatcher() + val scheduler = dispatcher.asScheduler() + assertSame(originalScheduler, scheduler) + } + + @Test + fun testConvertSchedulerToOriginalDispatcher(): Unit = runTest { + val originalDispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = originalDispatcher.asScheduler() + val dispatcher = scheduler.asCoroutineDispatcher() + assertSame(originalDispatcher, dispatcher) + } + + @Test + fun testSchedulerExpectRxPluginsCall(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCall(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerExpectRxPluginsCall(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCall(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableExpectRxPluginsCall(block: RxSchedulerBlockNoDelay) { + expect(1) + setScheduler(2, 4) + suspendCancellableCoroutine { + block(Runnable { + expect(5) + it.resume(Unit) + }) + expect(3) + } + RxJavaPlugins.setScheduleHandler(null) + finish(6) + } + + @Test + fun testSchedulerExpectRxPluginsCallWithDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCallDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerExpectRxPluginsCallWithDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableExpectRxPluginsCallDelay(worker::schedule) + } + + private suspend fun testRunnableExpectRxPluginsCallDelay(block: RxSchedulerBlockWithDelay) { + expect(1) + setScheduler(2, 4) + suspendCancellableCoroutine { + block({ + expect(5) + it.resume(Unit) + }, 10, TimeUnit.MILLISECONDS) + expect(3) + } + RxJavaPlugins.setScheduleHandler(null) + finish(6) + } + + private fun setScheduler(expectedCountOnSchedule: Int, expectCountOnRun: Int) { + RxJavaPlugins.setScheduleHandler { + expect(expectedCountOnSchedule) + Runnable { + expect(expectCountOnRun) + it.run() + } + } + } + + /** + * Tests that [Scheduler.Worker] runs all work sequentially. + */ + @Test + fun testWorkerSequentialOrdering() = runTest { + expect(1) + val scheduler = Dispatchers.Default.asScheduler() + val worker = scheduler.createWorker() + val iterations = 100 + for (i in 0..iterations) { + worker.schedule { + expect(2 + i) + } + } + suspendCoroutine { + worker.schedule { + it.resume(Unit) + } + } + finish((iterations + 2) + 1) + } + + /** + * Test that ensures that delays are actually respected (tasks scheduled sooner in the future run before tasks scheduled later, + * even when the later task is submitted before the earlier one) + */ + @Test + fun testSchedulerRespectsDelays(): Unit = runTest { + val scheduler = Dispatchers.Default.asScheduler() + testRunnableRespectsDelays(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerRespectsDelays(): Unit = runTest { + val scheduler = Dispatchers.Default.asScheduler() + testRunnableRespectsDelays(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableRespectsDelays(block: RxSchedulerBlockWithDelay) { + expect(1) + val semaphore = Semaphore(2, 2) + block({ + expect(3) + semaphore.release() + }, 100, TimeUnit.MILLISECONDS) + block({ + expect(2) + semaphore.release() + }, 1, TimeUnit.MILLISECONDS) + semaphore.acquire() + semaphore.acquire() + finish(4) + } + + /** + * Tests that cancelling a runnable in one worker doesn't affect work in another scheduler. + * + * This is part of expected behavior documented. + */ + @Test + fun testMultipleWorkerCancellation(): Unit = runTest { + expect(1) + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + suspendCancellableCoroutine { + val workerOne = scheduler.createWorker() + workerOne.schedule({ + expect(3) + it.resume(Unit) + }, 50, TimeUnit.MILLISECONDS) + val workerTwo = scheduler.createWorker() + workerTwo.schedule({ + expectUnreached() + }, 1000, TimeUnit.MILLISECONDS) + workerTwo.dispose() + expect(2) + } + finish(4) + } +} + +typealias RxSchedulerBlockNoDelay = (Runnable) -> Disposable +typealias RxSchedulerBlockWithDelay = (Runnable, Long, TimeUnit) -> Disposable \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index f6f3f1d06d..5776214b0a 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -58,7 +58,9 @@ public final class kotlinx/coroutines/rx3/RxObservableKt { } public final class kotlinx/coroutines/rx3/RxSchedulerKt { - public static final fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/rx3/SchedulerCoroutineDispatcher; + public static final fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/CoroutineDispatcher; + public static final synthetic fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/rx3/SchedulerCoroutineDispatcher; + public static final fun asScheduler (Lkotlinx/coroutines/CoroutineDispatcher;)Lio/reactivex/rxjava3/core/Scheduler; } public final class kotlinx/coroutines/rx3/RxSingleKt { diff --git a/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt index 2a14cf7c6c..754dd79484 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt @@ -81,7 +81,7 @@ public suspend fun MaybeSource.awaitSingle(): T = awaitSingleOrNull() ?: */ @Deprecated( message = "Deprecated in favor of awaitSingleOrNull()", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull()") ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun MaybeSource.await(): T? = awaitSingleOrNull() @@ -104,7 +104,7 @@ public suspend fun MaybeSource.await(): T? = awaitSingleOrNull() */ @Deprecated( message = "Deprecated in favor of awaitSingleOrNull()", - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default") ) // Warning since 1.5, error in 1.6, hidden in 1.7 public suspend fun MaybeSource.awaitOrDefault(default: T): T = awaitSingleOrNull() ?: default diff --git a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt index 57007bbdd4..1c5f7c0a63 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.selects.* import kotlinx.coroutines.sync.* import kotlin.coroutines.* import kotlinx.coroutines.internal.* +import kotlinx.coroutines.intrinsics.* /** * Creates cold [observable][Observable] that will run a given [block] in a coroutine. @@ -95,10 +96,22 @@ private class RxObservableCoroutine( element: T, block: suspend (SendChannel) -> R ) { - mutex.onLock.registerSelectClause2(select, null) { + val clause = suspend { doLockedNext(element)?.let { throw it } block(this) } + + // This is the default replacement proposed in onLock replacement + launch(start = CoroutineStart.UNDISPATCHED) { + mutex.lock() + // Already selected -- bail out + if (!select.trySelect()) { + mutex.unlock() + return@launch + } + + clause.startCoroutineCancellable(select.completion) + } } // assert: mutex.isLocked() diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt index 24c3f11834..abaf02450a 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt @@ -4,16 +4,144 @@ package kotlinx.coroutines.rx3 -import io.reactivex.rxjava3.core.Scheduler +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import io.reactivex.rxjava3.plugins.* +import kotlinx.atomicfu.* import kotlinx.coroutines.* -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.* +import java.util.concurrent.* +import kotlin.coroutines.* /** * Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher] * and provides native support of [delay] and [withTimeout]. */ -public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this) +public fun Scheduler.asCoroutineDispatcher(): CoroutineDispatcher = + if (this is DispatcherScheduler) { + dispatcher + } else { + SchedulerCoroutineDispatcher(this) + } + +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.2, binary compatibility with earlier versions") +@JvmName("asCoroutineDispatcher") +public fun Scheduler.asCoroutineDispatcher0(): SchedulerCoroutineDispatcher = + SchedulerCoroutineDispatcher(this) + +/** + * Converts an instance of [CoroutineDispatcher] to an implementation of [Scheduler]. + */ +public fun CoroutineDispatcher.asScheduler(): Scheduler = + if (this is SchedulerCoroutineDispatcher) { + scheduler + } else { + DispatcherScheduler(this) + } + +private class DispatcherScheduler(@JvmField val dispatcher: CoroutineDispatcher) : Scheduler() { + + private val schedulerJob = SupervisorJob() + + /** + * The scope for everything happening in this [DispatcherScheduler]. + * + * Running tasks, too, get launched under this scope, because [shutdown] should cancel the running tasks as well. + */ + private val scope = CoroutineScope(schedulerJob + dispatcher) + + /** + * The counter of created workers, for their pretty-printing. + */ + private val workerCounter = atomic(1L) + + override fun scheduleDirect(block: Runnable, delay: Long, unit: TimeUnit): Disposable = + scope.scheduleTask(block, unit.toMillis(delay)) { task -> + Runnable { scope.launch { task() } } + } + + override fun createWorker(): Worker = DispatcherWorker(workerCounter.getAndIncrement(), dispatcher, schedulerJob) + + override fun shutdown() { + schedulerJob.cancel() + } + + private class DispatcherWorker( + private val counter: Long, + private val dispatcher: CoroutineDispatcher, + parentJob: Job + ) : Worker() { + + private val workerJob = SupervisorJob(parentJob) + private val workerScope = CoroutineScope(workerJob + dispatcher) + private val blockChannel = Channel Unit>(Channel.UNLIMITED) + + init { + workerScope.launch { + blockChannel.consumeEach { + it() + } + } + } + + override fun schedule(block: Runnable, delay: Long, unit: TimeUnit): Disposable = + workerScope.scheduleTask(block, unit.toMillis(delay)) { task -> + Runnable { blockChannel.trySend(task) } + } + + override fun isDisposed(): Boolean = !workerScope.isActive + + override fun dispose() { + blockChannel.close() + workerJob.cancel() + } + + override fun toString(): String = "$dispatcher (worker $counter, ${if (isDisposed) "disposed" else "active"})" + } + + override fun toString(): String = dispatcher.toString() +} + +private typealias Task = suspend () -> Unit + +/** + * Schedule [block] so that an adapted version of it, wrapped in [adaptForScheduling], executes after [delayMillis] + * milliseconds. + */ +private fun CoroutineScope.scheduleTask( + block: Runnable, + delayMillis: Long, + adaptForScheduling: (Task) -> Runnable +): Disposable { + val ctx = coroutineContext + var handle: DisposableHandle? = null + val disposable = Disposable.fromRunnable { + // null if delay <= 0 + handle?.dispose() + } + val decoratedBlock = RxJavaPlugins.onSchedule(block) + suspend fun task() { + if (disposable.isDisposed) return + try { + runInterruptible { + decoratedBlock.run() + } + } catch (e: Throwable) { + handleUndeliverableException(e, ctx) + } + } + + val toSchedule = adaptForScheduling(::task) + if (!isActive) return Disposable.disposed() + if (delayMillis <= 0) { + toSchedule.run() + } else { + @Suppress("INVISIBLE_MEMBER") + ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it } + } + return disposable +} /** * Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler]. @@ -45,8 +173,10 @@ public class SchedulerCoroutineDispatcher( /** @suppress */ override fun toString(): String = scheduler.toString() + /** @suppress */ override fun equals(other: Any?): Boolean = other is SchedulerCoroutineDispatcher && other.scheduler === scheduler + /** @suppress */ override fun hashCode(): Int = System.identityHashCode(scheduler) } diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt index b4adf7af27..d7c799db1c 100644 --- a/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx3 import io.reactivex.rxjava3.core.* import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* import org.junit.Test import java.io.* import kotlin.test.* @@ -34,7 +35,7 @@ class ObservableMultiTest : TestBase() { // concurrent emitters (many coroutines) val jobs = List(n) { // launch - launch { + launch(Dispatchers.Default) { val i = it send(i) } @@ -47,6 +48,29 @@ class ObservableMultiTest : TestBase() { } } + @Test + fun testConcurrentStressOnSend() { + val n = 10_000 * stressTestMultiplier + val observable = rxObservable { + newCoroutineContext(coroutineContext) + // concurrent emitters (many coroutines) + val jobs = List(n) { + // launch + launch(Dispatchers.Default) { + val i = it + select { + onSend(i) {} + } + } + } + jobs.forEach { it.join() } + } + checkSingleValue(observable.toList()) { list -> + assertEquals(n, list.size) + assertEquals((0 until n).toList(), list.sorted()) + } + } + @Test fun testIteratorResendUnconfined() { val n = 10_000 * stressTestMultiplier diff --git a/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt b/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt new file mode 100644 index 0000000000..5abb511d72 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import org.junit.* +import java.util.concurrent.* + +class SchedulerStressTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxCachedThreadScheduler-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + /** + * Test that we don't get an OOM if we schedule many jobs at once. + * It's expected that if you don't dispose you'd see an OOM error. + */ + @Test + fun testSchedulerDisposed(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableDisposed(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerDisposed(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableDisposed(worker::schedule) + } + + private suspend fun testRunnableDisposed(block: RxSchedulerBlockNoDelay) { + val n = 2000 * stressTestMultiplier + repeat(n) { + val a = ByteArray(1000000) //1MB + val disposable = block(Runnable { + keepMe(a) + expectUnreached() + }) + disposable.dispose() + yield() // allow the scheduled task to observe that it was disposed + } + } + + /** + * Test function that holds a reference. Used for testing OOM situations + */ + private fun keepMe(a: ByteArray) { + Thread.sleep(a.size / (a.size + 1) + 10L) + } + + /** + * Test that we don't get an OOM if we schedule many delayed jobs at once. It's expected that if you don't dispose that you'd + * see a OOM error. + */ + @Test + fun testSchedulerDisposedDuringDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableDisposedDuringDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerDisposedDuringDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableDisposedDuringDelay(worker::schedule) + } + + private fun testRunnableDisposedDuringDelay(block: RxSchedulerBlockWithDelay) { + val n = 2000 * stressTestMultiplier + repeat(n) { + val a = ByteArray(1000000) //1MB + val delayMillis: Long = 10 + val disposable = block(Runnable { + keepMe(a) + expectUnreached() + }, delayMillis, TimeUnit.MILLISECONDS) + disposable.dispose() + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt index 9e95c213d0..c966cdfd08 100644 --- a/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt +++ b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt @@ -4,10 +4,18 @@ package kotlinx.coroutines.rx3 -import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import io.reactivex.rxjava3.plugins.* +import io.reactivex.rxjava3.schedulers.* import kotlinx.coroutines.* -import org.junit.Before +import kotlinx.coroutines.sync.* +import org.junit.* import org.junit.Test +import java.lang.Runnable +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.* import kotlin.test.* class SchedulerTest : TestBase() { @@ -17,7 +25,7 @@ class SchedulerTest : TestBase() { } @Test - fun testIoScheduler(): Unit = runBlocking { + fun testIoScheduler(): Unit = runTest { expect(1) val mainThread = Thread.currentThread() withContext(Schedulers.io().asCoroutineDispatcher()) { @@ -31,4 +39,458 @@ class SchedulerTest : TestBase() { } finish(4) } + + /** Tests [toString] implementations of [CoroutineDispatcher.asScheduler] and its [Scheduler.Worker]. */ + @Test + fun testSchedulerToString() { + val name = "Dispatchers.Default" + val scheduler = Dispatchers.Default.asScheduler() + assertContains(scheduler.toString(), name) + val worker = scheduler.createWorker() + val activeWorkerName = worker.toString() + assertContains(worker.toString(), name) + worker.dispose() + val disposedWorkerName = worker.toString() + assertNotEquals(activeWorkerName, disposedWorkerName) + } + + private fun runSchedulerTest(nThreads: Int = 1, action: (Scheduler) -> Unit) { + val future = CompletableFuture() + try { + newFixedThreadPoolContext(nThreads, "test").use { dispatcher -> + RxJavaPlugins.setErrorHandler { + if (!future.completeExceptionally(it)) { + handleUndeliverableException(it, dispatcher) + } + } + action(dispatcher.asScheduler()) + } + } finally { + RxJavaPlugins.setErrorHandler(null) + } + future.complete(Unit) + future.getNow(Unit) // rethrow any encountered errors + } + + private fun ensureSeparateThread(schedule: (Runnable, Long, TimeUnit) -> Unit, scheduleNoDelay: (Runnable) -> Unit) { + val mainThread = Thread.currentThread() + val cdl1 = CountDownLatch(1) + val cdl2 = CountDownLatch(1) + expect(1) + val thread = AtomicReference(null) + fun checkThread() { + val current = Thread.currentThread() + thread.getAndSet(current)?.let { assertEquals(it, current) } + } + schedule({ + assertNotSame(mainThread, Thread.currentThread()) + checkThread() + cdl2.countDown() + }, 300, TimeUnit.MILLISECONDS) + scheduleNoDelay { + expect(2) + checkThread() + assertNotSame(mainThread, Thread.currentThread()) + cdl1.countDown() + } + cdl1.await() + cdl2.await() + finish(3) + } + + /** + * Tests [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler] on a single-threaded dispatcher. + */ + @Test + fun testSingleThreadedDispatcherDirect(): Unit = runSchedulerTest(1) { + ensureSeparateThread(it::scheduleDirect, it::scheduleDirect) + } + + /** + * Tests [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler] running its tasks on the correct thread. + */ + @Test + fun testSingleThreadedWorker(): Unit = runSchedulerTest(1) { + val worker = it.createWorker() + ensureSeparateThread(worker::schedule, worker::schedule) + } + + private fun checkCancelling(schedule: (Runnable, Long, TimeUnit) -> Disposable) { + // cancel the task before it has a chance to run. + val handle1 = schedule({ + throw IllegalStateException("should have been successfully cancelled") + }, 10_000, TimeUnit.MILLISECONDS) + handle1.dispose() + // cancel the task after it started running. + val cdl1 = CountDownLatch(1) + val cdl2 = CountDownLatch(1) + val handle2 = schedule({ + cdl1.countDown() + cdl2.await() + if (Thread.interrupted()) + throw IllegalStateException("cancelling the task should not interrupt the thread") + }, 100, TimeUnit.MILLISECONDS) + cdl1.await() + handle2.dispose() + cdl2.countDown() + } + + /** + * Test cancelling [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testCancellingDirect(): Unit = runSchedulerTest { + checkCancelling(it::scheduleDirect) + } + + /** + * Test cancelling [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testCancellingWorker(): Unit = runSchedulerTest { + val worker = it.createWorker() + checkCancelling(worker::schedule) + } + + /** + * Test shutting down [CoroutineDispatcher.asScheduler]. + */ + @Test + fun testShuttingDown() { + val n = 5 + runSchedulerTest(nThreads = n) { scheduler -> + val cdl1 = CountDownLatch(n) + val cdl2 = CountDownLatch(1) + val cdl3 = CountDownLatch(n) + repeat(n) { + scheduler.scheduleDirect { + cdl1.countDown() + try { + cdl2.await() + } catch (e: InterruptedException) { + // this is the expected outcome + cdl3.countDown() + } + } + } + cdl1.await() + scheduler.shutdown() + if (!cdl3.await(1, TimeUnit.SECONDS)) { + cdl2.countDown() + error("the tasks were not cancelled when the scheduler was shut down") + } + } + } + + /** Tests that there are no uncaught exceptions if [Disposable.dispose] on a worker happens when tasks are present. */ + @Test + fun testDisposingWorker() = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + yield() // so that the worker starts waiting on the channel + assertFalse(worker.isDisposed) + worker.dispose() + assertTrue(worker.isDisposed) + } + + /** Tests trying to use a [Scheduler.Worker]/[Scheduler] after [Scheduler.Worker.dispose]/[Scheduler.shutdown]. */ + @Test + fun testSchedulingAfterDisposing() = runSchedulerTest { + expect(1) + val worker = it.createWorker() + // use CDL to ensure that the worker has properly initialized + val cdl1 = CountDownLatch(1) + setScheduler(2, 3) + val disposable1 = worker.schedule { + cdl1.countDown() + } + cdl1.await() + expect(4) + assertFalse(disposable1.isDisposed) + setScheduler(6, -1) + // check that the worker automatically disposes of the tasks after being disposed + assertFalse(worker.isDisposed) + worker.dispose() + assertTrue(worker.isDisposed) + expect(5) + val disposable2 = worker.schedule { + expectUnreached() + } + assertTrue(disposable2.isDisposed) + setScheduler(7, 8) + // ensure that the scheduler still works + val cdl2 = CountDownLatch(1) + val disposable3 = it.scheduleDirect { + cdl2.countDown() + } + cdl2.await() + expect(9) + assertFalse(disposable3.isDisposed) + // check that the scheduler automatically disposes of the tasks after being shut down + it.shutdown() + setScheduler(10, -1) + val disposable4 = it.scheduleDirect { + expectUnreached() + } + assertTrue(disposable4.isDisposed) + RxJavaPlugins.setScheduleHandler(null) + finish(11) + } + + @Test + fun testSchedulerWithNoDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithNoDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerWithNoDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithNoDelay(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableWithNoDelay(block: RxSchedulerBlockNoDelay) { + expect(1) + suspendCancellableCoroutine { + block(Runnable { + expect(2) + it.resume(Unit) + }) + } + yield() + finish(3) + } + + @Test + fun testSchedulerWithDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect, 300) + } + + @Test + fun testSchedulerWorkerWithDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule, 300) + } + + @Test + fun testSchedulerWithZeroDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerWithZeroDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableWithDelay(block: RxSchedulerBlockWithDelay, delayMillis: Long = 0) { + expect(1) + suspendCancellableCoroutine { + block({ + expect(2) + it.resume(Unit) + }, delayMillis, TimeUnit.MILLISECONDS) + } + finish(3) + } + + @Test + fun testAsSchedulerWithNegativeDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler::scheduleDirect, -1) + } + + @Test + fun testAsSchedulerWorkerWithNegativeDelay(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableWithDelay(scheduler.createWorker()::schedule, -1) + } + + @Test + fun testSchedulerImmediateDispose(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableImmediateDispose(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerImmediateDispose(): Unit = runTest { + val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler() + testRunnableImmediateDispose(scheduler.createWorker()::schedule) + } + + private fun testRunnableImmediateDispose(block: RxSchedulerBlockNoDelay) { + val disposable = block { + expectUnreached() + } + disposable.dispose() + } + + @Test + fun testConvertDispatcherToOriginalScheduler(): Unit = runTest { + val originalScheduler = Schedulers.io() + val dispatcher = originalScheduler.asCoroutineDispatcher() + val scheduler = dispatcher.asScheduler() + assertSame(originalScheduler, scheduler) + } + + @Test + fun testConvertSchedulerToOriginalDispatcher(): Unit = runTest { + val originalDispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = originalDispatcher.asScheduler() + val dispatcher = scheduler.asCoroutineDispatcher() + assertSame(originalDispatcher, dispatcher) + } + + @Test + fun testSchedulerExpectRxPluginsCall(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCall(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerExpectRxPluginsCall(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCall(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableExpectRxPluginsCall(block: RxSchedulerBlockNoDelay) { + expect(1) + setScheduler(2, 4) + suspendCancellableCoroutine { + block(Runnable { + expect(5) + it.resume(Unit) + }) + expect(3) + } + RxJavaPlugins.setScheduleHandler(null) + finish(6) + } + + @Test + fun testSchedulerExpectRxPluginsCallWithDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + testRunnableExpectRxPluginsCallDelay(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerExpectRxPluginsCallWithDelay(): Unit = runTest { + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + val worker = scheduler.createWorker() + testRunnableExpectRxPluginsCallDelay(worker::schedule) + } + + private suspend fun testRunnableExpectRxPluginsCallDelay(block: RxSchedulerBlockWithDelay) { + expect(1) + setScheduler(2, 4) + suspendCancellableCoroutine { + block({ + expect(5) + it.resume(Unit) + }, 10, TimeUnit.MILLISECONDS) + expect(3) + } + RxJavaPlugins.setScheduleHandler(null) + finish(6) + } + + private fun setScheduler(expectedCountOnSchedule: Int, expectCountOnRun: Int) { + RxJavaPlugins.setScheduleHandler { + expect(expectedCountOnSchedule) + Runnable { + expect(expectCountOnRun) + it.run() + } + } + } + + /** + * Tests that [Scheduler.Worker] runs all work sequentially. + */ + @Test + fun testWorkerSequentialOrdering() = runTest { + expect(1) + val scheduler = Dispatchers.Default.asScheduler() + val worker = scheduler.createWorker() + val iterations = 100 + for (i in 0..iterations) { + worker.schedule { + expect(2 + i) + } + } + suspendCoroutine { + worker.schedule { + it.resume(Unit) + } + } + finish((iterations + 2) + 1) + } + + /** + * Test that ensures that delays are actually respected (tasks scheduled sooner in the future run before tasks scheduled later, + * even when the later task is submitted before the earlier one) + */ + @Test + fun testSchedulerRespectsDelays(): Unit = runTest { + val scheduler = Dispatchers.Default.asScheduler() + testRunnableRespectsDelays(scheduler::scheduleDirect) + } + + @Test + fun testSchedulerWorkerRespectsDelays(): Unit = runTest { + val scheduler = Dispatchers.Default.asScheduler() + testRunnableRespectsDelays(scheduler.createWorker()::schedule) + } + + private suspend fun testRunnableRespectsDelays(block: RxSchedulerBlockWithDelay) { + expect(1) + val semaphore = Semaphore(2, 2) + block({ + expect(3) + semaphore.release() + }, 100, TimeUnit.MILLISECONDS) + block({ + expect(2) + semaphore.release() + }, 1, TimeUnit.MILLISECONDS) + semaphore.acquire() + semaphore.acquire() + finish(4) + } + + /** + * Tests that cancelling a runnable in one worker doesn't affect work in another scheduler. + * + * This is part of expected behavior documented. + */ + @Test + fun testMultipleWorkerCancellation(): Unit = runTest { + expect(1) + val dispatcher = currentDispatcher() as CoroutineDispatcher + val scheduler = dispatcher.asScheduler() + suspendCancellableCoroutine { + val workerOne = scheduler.createWorker() + workerOne.schedule({ + expect(3) + it.resume(Unit) + }, 50, TimeUnit.MILLISECONDS) + val workerTwo = scheduler.createWorker() + workerTwo.schedule({ + expectUnreached() + }, 1000, TimeUnit.MILLISECONDS) + workerTwo.dispose() + expect(2) + } + finish(4) + } } + +typealias RxSchedulerBlockNoDelay = (Runnable) -> Disposable +typealias RxSchedulerBlockWithDelay = (Runnable, Long, TimeUnit) -> Disposable \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 44effa7c20..f0a764898b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,7 @@ pluginManagement { // JMH id "net.ltgt.apt" version "0.21" - id "me.champeau.gradle.jmh" version "0.5.2" + id "me.champeau.gradle.jmh" version "0.5.3" } repositories { @@ -18,7 +18,6 @@ pluginManagement { } rootProject.name = 'kotlinx.coroutines' -enableFeaturePreview('GRADLE_METADATA') def module(String path) { int i = path.lastIndexOf('/') @@ -59,5 +58,3 @@ module('ui/kotlinx-coroutines-swing') if (!build_snapshot_train) { module('js/example-frontend-js') } - -module('integration-testing') diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 71b2d69c5c..ddc39bdd64 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts index 2fd0b81479..625ce728b1 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts @@ -2,10 +2,17 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +project.configureAar() + dependencies { - kotlinCompilerPluginClasspathMain(project(":kotlinx-coroutines-core")) + configureAarUnpacking() + testImplementation("com.google.android:android:${version("android")}") testImplementation("org.robolectric:robolectric:${version("robolectric")}") + // Required by robolectric + testImplementation("androidx.test:core:1.2.0") + testImplementation("androidx.test:monitor:1.2.0") + testImplementation(project(":kotlinx-coroutines-test")) testImplementation(project(":kotlinx-coroutines-android")) } diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt index bcc12d5441..676ee4310d 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt @@ -26,6 +26,7 @@ class InitMainDispatcherBeforeRobolectricTestRunner(testClass: Class<*>) : Robol @Config(manifest = Config.NONE, sdk = [28]) @RunWith(InitMainDispatcherBeforeRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.LEGACY) class CustomizedRobolectricTest : TestBase() { @Test fun testComponent() { @@ -52,4 +53,4 @@ class CustomizedRobolectricTest : TestBase() { mainLooper.unPause() assertTrue(component.launchCompleted) } -} \ No newline at end of file +} diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt index eab6fc17fb..99744f897f 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt @@ -15,6 +15,7 @@ import kotlin.test.* @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE, sdk = [28]) +@LooperMode(LooperMode.Mode.LEGACY) open class FirstRobolectricTest { @Test fun testComponent() { diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts index 9cec1dc9f0..7618c529f7 100644 --- a/ui/kotlinx-coroutines-android/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -2,22 +2,32 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +import kotlinx.kover.api.* + configurations { create("r8") } repositories { mavenCentral() - jcenter() // https://youtrack.jetbrains.com/issue/IDEA-261387 } + +project.configureAar() + dependencies { + configureAarUnpacking() + compileOnly("com.google.android:android:${version("android")}") compileOnly("androidx.annotation:annotation:${version("androidx_annotation")}") testImplementation("com.google.android:android:${version("android")}") testImplementation("org.robolectric:robolectric:${version("robolectric")}") - testImplementation("org.smali:baksmali:${version("baksmali")}") + // Required by robolectric + testImplementation("androidx.test:core:1.2.0") + testImplementation("androidx.test:monitor:1.2.0") + + testImplementation("org.smali:baksmali:${version("baksmali")}") "r8"("com.android.tools.build:builder:7.1.0-alpha01") } @@ -103,3 +113,8 @@ open class RunR8 : JavaExec() { } } +tasks.withType { + extensions.configure { + excludes = excludes + listOf("com.android.*", "android.*") // Exclude robolectric-generated classes + } +} diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro index c7cd15fe11..ef42483f56 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro @@ -1,5 +1,6 @@ # When editing this file, update the following files as well: -# - META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro +# - META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro # - META-INF/proguard/coroutines.pro -keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} +-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;} diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro index 0d04990ad9..cf317c41e3 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro @@ -9,8 +9,6 @@ boolean ANDROID_DETECTED return true; } --keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} - # Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher -assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt { boolean SUPPORT_MISSING return false; @@ -21,4 +19,4 @@ boolean getASSERTIONS_ENABLED() return false; boolean getDEBUG() return false; boolean getRECOVER_STACK_TRACES() return false; -} \ No newline at end of file +} diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro deleted file mode 100644 index 549d0e85a1..0000000000 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro +++ /dev/null @@ -1,9 +0,0 @@ -# When editing this file, update the following files as well: -# - META-INF/com.android.tools/proguard/coroutines.pro -# - META-INF/proguard/coroutines.pro - --keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} - --assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader { - boolean ANDROID_DETECTED return true; -} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro new file mode 100644 index 0000000000..1aa2b114c1 --- /dev/null +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro @@ -0,0 +1,10 @@ +# When editing this file, update the following files as well for AGP 3.6.0+: +# - META-INF/com.android.tools/proguard/coroutines.pro +# - META-INF/proguard/coroutines.pro + +# After R8 3.0.0 (or probably sometime before that), R8 learned how to optimize +# classes mentioned in META-INF/services files, and explicitly -keeping them +# disables these optimizations. +# https://github.com/Kotlin/kotlinx.coroutines/issues/3111 +-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} +-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;} diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro index 6c918d49e7..087f1ce8e7 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro @@ -2,6 +2,7 @@ # When editing this file, update the following files as well for AGP 3.6.0+: # - META-INF/com.android.tools/proguard/coroutines.pro -# - META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro +# - META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro -keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} +-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;} diff --git a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt index 7d06752cba..0bc603ea1e 100644 --- a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt +++ b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt @@ -5,12 +5,10 @@ package kotlinx.coroutines.android import android.os.* -import androidx.annotation.* import kotlinx.coroutines.* import java.lang.reflect.* import kotlin.coroutines.* -@Keep internal class AndroidExceptionPreHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { @@ -34,20 +32,21 @@ internal class AndroidExceptionPreHandler : override fun handleException(context: CoroutineContext, exception: Throwable) { /* - * If we are on old SDK, then use Android's `Thread.getUncaughtExceptionPreHandler()` that ensures that - * an exception is logged before crashing the application. + * Android Oreo introduced private API for a global pre-handler for uncaught exceptions, to ensure that the + * exceptions are logged even if the default uncaught exception handler is replaced by the app. The pre-handler + * is invoked from the Thread's private dispatchUncaughtException() method, so our manual invocation of the + * Thread's uncaught exception handler bypasses the pre-handler in Android Oreo, and uncaught coroutine + * exceptions are not logged. This issue was addressed in Android Pie, which added a check in the default + * uncaught exception handler to invoke the pre-handler if it was not invoked already (see + * https://android-review.googlesource.com/c/platform/frameworks/base/+/654578/). So the issue is present only + * in Android Oreo. * - * Since Android Pie default uncaught exception handler always ensures that exception is logged without interfering with - * pre-handler, so reflection hack is no longer needed. - * - * See https://android-review.googlesource.com/c/platform/frameworks/base/+/654578/ + * We're fixing this by manually invoking the pre-handler using reflection, if running on an Android Oreo SDK + * version (26 and 27). */ - val thread = Thread.currentThread() - if (Build.VERSION.SDK_INT >= 28) { - thread.uncaughtExceptionHandler.uncaughtException(thread, exception) - } else { + if (Build.VERSION.SDK_INT in 26..27) { (preHandler()?.invoke(null) as? Thread.UncaughtExceptionHandler) - ?.uncaughtException(thread, exception) + ?.uncaughtException(Thread.currentThread(), exception) } } } diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index ca8dd0d0ca..5e33169dd1 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -51,8 +51,10 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay { internal class AndroidDispatcherFactory : MainDispatcherFactory { - override fun createDispatcher(allFactories: List) = - HandlerContext(Looper.getMainLooper().asHandler(async = true)) + override fun createDispatcher(allFactories: List): MainCoroutineDispatcher { + val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available") + return HandlerContext(mainLooper.asHandler(async = true)) + } override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used" @@ -188,7 +190,7 @@ public suspend fun awaitFrame(): Long { postFrameCallback(choreographer, cont) } } - // post into looper thread thread to figure it out + // post into looper thread to figure it out return suspendCancellableCoroutine { cont -> Dispatchers.Main.dispatch(EmptyCoroutineContext, Runnable { updateChoreographerAndPostFrameCallback(cont) diff --git a/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt new file mode 100644 index 0000000000..1220797009 --- /dev/null +++ b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.android + +import kotlinx.coroutines.* +import org.junit.Test +import org.junit.runner.* +import org.robolectric.* +import org.robolectric.annotation.* +import kotlin.test.* + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [27]) +@LooperMode(LooperMode.Mode.LEGACY) +class AndroidExceptionPreHandlerTest : TestBase() { + @Test + fun testUnhandledException() = runTest { + val previousHandler = Thread.getDefaultUncaughtExceptionHandler() + try { + Thread.setDefaultUncaughtExceptionHandler { _, e -> + expect(3) + assertIs(e) + } + expect(1) + GlobalScope.launch(Dispatchers.Main) { + expect(2) + throw TestException() + }.join() + finish(4) + } finally { + Thread.setDefaultUncaughtExceptionHandler(previousHandler) + } + } +} diff --git a/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt b/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt index a1f0a03d4a..a5b5ec95ee 100644 --- a/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt +++ b/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt @@ -13,6 +13,7 @@ import org.robolectric.annotation.* @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE, sdk = [28]) +@LooperMode(LooperMode.Mode.LEGACY) class DisabledHandlerTest : TestBase() { private var delegateToSuper = false diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt new file mode 100644 index 0000000000..c2091f339f --- /dev/null +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.android + +import android.os.* +import kotlinx.coroutines.* +import org.junit.Test +import org.junit.runner.* +import org.robolectric.* +import org.robolectric.Shadows.* +import org.robolectric.annotation.* +import org.robolectric.shadows.* +import org.robolectric.util.* +import java.util.concurrent.* +import kotlin.test.* + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [28]) +@LooperMode(LooperMode.Mode.LEGACY) +class HandlerDispatcherAsyncTest : TestBase() { + + /** + * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a + * result we only test its behavior on the newest API level and assert that it uses async + * messages. We rely on the other tests to exercise the variance of the mechanism that the main + * dispatcher uses to ensure it has correct behavior on all API levels. + */ + @Test + fun mainIsAsync() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(Dispatchers.Main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi14() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi16() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun asyncMessagesApi28() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun noAsyncMessagesIfNotRequested() = runTest { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher() + + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = launch(main) { + expect(2) + } + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + job.join(mainLooper) + } + + @Test + fun testToString() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName") + assertEquals("testName", main.toString()) + assertEquals("testName.immediate", main.immediate.toString()) + assertEquals("testName.immediate", main.immediate.immediate.toString()) + } + + private suspend fun Job.join(mainLooper: ShadowLooper) { + expect(1) + mainLooper.unPause() + join() + finish(3) + } + + // TODO compile against API 23+ so this can be invoked without reflection. + private val Looper.queue: MessageQueue + get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue + + // TODO compile against API 22+ so this can be invoked without reflection. + private val Message.isAsynchronous: Boolean + get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean +} diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt index 5128a74caf..fe97ae8d27 100644 --- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.android @@ -9,156 +9,131 @@ import kotlinx.coroutines.* import org.junit.Test import org.junit.runner.* import org.robolectric.* -import org.robolectric.Shadows.* import org.robolectric.annotation.* import org.robolectric.shadows.* -import org.robolectric.util.* +import java.util.concurrent.* import kotlin.test.* @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE, sdk = [28]) +@LooperMode(LooperMode.Mode.LEGACY) class HandlerDispatcherTest : TestBase() { - - /** - * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a - * result we only test its behavior on the newest API level and assert that it uses async - * messages. We rely on the other tests to exercise the variance of the mechanism that the main - * dispatcher uses to ensure it has correct behavior on all API levels. - */ @Test - fun mainIsAsync() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val mainLooper = shadowOf(Looper.getMainLooper()) - mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(Dispatchers.Main) { + fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) { + expect(1) + // launch in the immediate dispatcher + launch(Dispatchers.Main.immediate) { expect(2) + yield() + expect(4) } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(3) // after yield + yield() // yield back + finish(5) } @Test - fun asyncMessagesApi14() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) - mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) - } - - val message = mainMessageQueue.head - assertFalse(message.isAsynchronous) - job.join(mainLooper) + fun testMainDispatcherToString() { + assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) } @Test - fun asyncMessagesApi16() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testDefaultDelayIsNotDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + assertFalse { mainLooper.scheduler.areAnyRunnable() } - val job = launch(main) { - expect(2) + val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + expect(1) + delay(Long.MAX_VALUE) + expectUnreached() } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(0, mainLooper.scheduler.size()) + job.cancelAndJoin() + finish(3) } @Test - fun asyncMessagesApi28() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testWithTimeoutIsDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) + assertFalse { mainLooper.scheduler.areAnyRunnable() } + val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + withTimeout(1) { + expect(1) + hang { expect(3) } + } + expectUnreached() } - - val message = mainMessageQueue.head - assertTrue(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(1, mainLooper.scheduler.size()) + // Schedule cancellation + mainLooper.runToEndOfTasks() + job.join() + finish(4) } @Test - fun noAsyncMessagesIfNotRequested() = runTest { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - - val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher() - - val mainLooper = shadowOf(Looper.getMainLooper()) + fun testDelayDelegatedToMain() = runTest { + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) mainLooper.pause() - val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) - - val job = launch(main) { - expect(2) + val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + expect(1) + delay(1) + expect(3) } - - val message = mainMessageQueue.head - assertFalse(message.isAsynchronous) - job.join(mainLooper) + expect(2) + assertEquals(1, mainLooper.scheduler.size()) + // Schedule cancellation + mainLooper.runToEndOfTasks() + job.join() + finish(4) } @Test - fun testToString() { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) - val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName") - assertEquals("testName", main.toString()) - assertEquals("testName.immediate", main.immediate.toString()) - assertEquals("testName.immediate", main.immediate.immediate.toString()) - } + fun testAwaitFrame() = runTest { + doTestAwaitFrame() - private suspend fun Job.join(mainLooper: ShadowLooper) { - expect(1) - mainLooper.unPause() - join() - finish(3) - } + reset() - // TODO compile against API 23+ so this can be invoked without reflection. - private val Looper.queue: MessageQueue - get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue + // Now the second test: we cannot test it separately because we're caching choreographer in HandlerDispatcher + doTestAwaitWithDetectedChoreographer() + } - // TODO compile against API 22+ so this can be invoked without reflection. - private val Message.isAsynchronous: Boolean - get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean + private fun CoroutineScope.doTestAwaitFrame() { + ShadowChoreographer.setPostFrameCallbackDelay(100) + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) + mainLooper.pause() + launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + expect(1) + awaitFrame() + expect(5) + } + expect(2) + // Run choreographer detection + mainLooper.runOneTask() + expect(3) + mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS) + expect(4) + mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS) + finish(6) + } - @Test - fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) { - expect(1) - // launch in the immediate dispatcher - launch(Dispatchers.Main.immediate) { - expect(2) - yield() + private fun CoroutineScope.doTestAwaitWithDetectedChoreographer() { + ShadowChoreographer.setPostFrameCallbackDelay(100) + val mainLooper = Shadows.shadowOf(Looper.getMainLooper()) + launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) { + expect(1) + awaitFrame() expect(4) } - expect(3) // after yield - yield() // yield back + // Run choreographer detection + expect(2) + mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS) + expect(3) + mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS) finish(5) } - - @Test - fun testMainDispatcherToString() { - assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) - assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) - } } diff --git a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt index 5d60d641aa..47beb85bbf 100644 --- a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt +++ b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt @@ -11,7 +11,6 @@ import java.io.* import java.util.stream.* import kotlin.test.* -@Ignore class R8ServiceLoaderOptimizationTest : TestBase() { private val r8Dex = File(System.getProperty("dexPath")!!).asDexFile() private val r8DexNoOptim = File(System.getProperty("noOptimDexPath")!!).asDexFile() diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts index 9e30590cea..f9f66249eb 100644 --- a/ui/kotlinx-coroutines-javafx/build.gradle.kts +++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts @@ -6,17 +6,20 @@ plugins { id("org.openjfx.javafxplugin") version "0.0.9" } +configurations { + register("javafx") + named("compileOnly") { + extendsFrom(configurations["javafx"]) + } + named("testImplementation") { + extendsFrom(configurations["javafx"]) + } +} + javafx { version = version("javafx") modules = listOf("javafx.controls") - configuration = "compileOnly" -} - -sourceSets { - test.configure { - compileClasspath += configurations.compileOnly - runtimeClasspath += configurations.compileOnly - } + configuration = "javafx" } val JDK_18: String? by lazy { diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index 0a35cbf22e..d158fb745a 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -10,7 +10,6 @@ import javafx.event.* import javafx.util.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* -import kotlinx.coroutines.javafx.JavaFx.delay import java.lang.UnsupportedOperationException import java.lang.reflect.* import java.util.concurrent.* @@ -35,22 +34,18 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay { /** @suppress */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler { + val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) { with(continuation) { resumeUndispatched(Unit) } - }) + } continuation.invokeOnCancellation { timeline.stop() } } /** @suppress */ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler { + val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) { block.run() - }) - return object : DisposableHandle { - override fun dispose() { - timeline.stop() - } } + return DisposableHandle { timeline.stop() } } private fun schedule(time: Long, unit: TimeUnit, handler: EventHandler): Timeline = diff --git a/ui/kotlinx-coroutines-swing/build.gradle.kts b/ui/kotlinx-coroutines-swing/build.gradle.kts index b8ca906adf..157ce401b2 100644 --- a/ui/kotlinx-coroutines-swing/build.gradle.kts +++ b/ui/kotlinx-coroutines-swing/build.gradle.kts @@ -3,5 +3,5 @@ */ dependencies { - testCompile(project(":kotlinx-coroutines-jdk8")) + testImplementation(project(":kotlinx-coroutines-jdk8")) } diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index d2d9b78658..3b43483dbc 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -74,6 +74,16 @@ private object ImmediateSwingDispatcher : SwingDispatcher() { * Dispatches execution onto Swing event dispatching thread and provides native [delay] support. */ internal object Swing : SwingDispatcher() { + + /* A workaround so that the dispatcher's initialization crashes with an exception if running in a headless + environment. This is needed so that this broken dispatcher is not used as the source of delays. */ + init { + Timer(1) { }.apply { + isRepeats = false + start() + } + } + override val immediate: MainCoroutineDispatcher get() = ImmediateSwingDispatcher