diff --git a/CHANGES.md b/CHANGES.md index 611e9c9c74..17805e5b7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,53 @@ # Change log for kotlinx.coroutines +## 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). +* `FlowCollector` 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/README.md b/README.md index 5fc8973330..91cbf5362f 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ [![official JetBrains 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.0-RC)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.6.0-RC/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 { @@ -83,7 +83,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.5.2 + 1.6.0-RC ``` @@ -91,7 +91,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.5.30 + 1.6.0 ``` @@ -101,7 +101,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC' } ``` @@ -109,7 +109,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.6.0' } ``` @@ -127,7 +127,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC") } ``` @@ -147,7 +147,7 @@ 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' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-RC' ``` This gives you access to the Android [Dispatchers.Main] @@ -180,7 +180,7 @@ In common code that should get compiled for different platforms, you can add a d ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC") } } ``` @@ -192,7 +192,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.0-RC/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/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index ce0bff1cdf..b7dcb57968 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -46,21 +46,28 @@ 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 { + 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..80e15a1b4f 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt @@ -30,7 +30,7 @@ abstract class ParametrizedDispatcherBase : CoroutineScope { 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..1bec7a3700 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/" } @@ -67,6 +56,7 @@ buildscript { classpath "org.jetbrains.kotlinx:kotlinx-knit:$knit_version" classpath "com.moowork.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.3" // Android API check // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" @@ -102,13 +92,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 +104,13 @@ allprojects { } apply plugin: "binary-compatibility-validator" +apply plugin: 'base' + 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 +124,51 @@ allprojects { */ google() mavenCentral() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } } } // 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 +195,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 +212,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 @@ -245,10 +250,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,7 +301,7 @@ def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm" task deploy(dependsOn: publishTasks) -apply plugin: 'base' +apply plugin: 'animalsniffer-convention' clean.dependsOn gradle.includedBuilds.collect { it.task(':clean') } @@ -302,12 +341,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..c2808c004a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -46,4 +46,5 @@ dependencies { implementation(kotlin("gradle-plugin", version("kotlin"))) implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") implementation("org.jetbrains.dokka:dokka-core:${version("dokka")}") + implementation("ru.vyarus:gradle-animalsniffer-plugin:1.5.3") // Android API check } 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..d19e00ca46 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", "integration-testing") +val internal = setOf("kotlinx.coroutines", "benchmarks", "integration-testing") +// 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/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/animalsniffer-convention.gradle.kts b/buildSrc/src/main/kotlin/animalsniffer-convention.gradle.kts new file mode 100644 index 0000000000..32b4931e57 --- /dev/null +++ b/buildSrc/src/main/kotlin/animalsniffer-convention.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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.* + +subprojects { + // Skip JDK 8 projects or unpublished ones + if (!shouldSniff()) return@subprojects + apply(plugin = "ru.vyarus.animalsniffer") + 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/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/gradle.properties b/gradle.properties index 26e5147c51..1d64c83611 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,14 @@ # # Kotlin -version=1.5.2-SNAPSHOT +version=1.6.0-RC-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.5.30 +kotlin_version=1.6.0 # Dependencies junit_version=4.12 junit5_version=5.7.0 -atomicfu_version=0.16.3 +atomicfu_version=0.17.0 knit_version=0.3.0 html_version=0.7.2 lincheck_version=2.14 @@ -22,9 +22,9 @@ 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.8.0-RC blockhound_version=1.0.2.RELEASE -jna_version=5.5.0 +jna_version=5.9.0 # Android versions android_version=4.1.1.4 @@ -53,7 +53,7 @@ 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 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..a4926f7e61 100644 --- a/gradle/dokka.gradle.kts +++ b/gradle/dokka.gradle.kts @@ -37,7 +37,7 @@ 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.name != "kotlinx-coroutines-core" && project.name != "kotlinx-coroutines-test") { dependsOn(project.configurations["compileClasspath"]) doFirst { // resolve classpath only during execution 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.gradle b/gradle/publish.gradle index 3a0a4224ab..fa2bbb8544 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -12,7 +12,7 @@ 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) { diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index 6efa3a14e6..aea4d45203 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -58,6 +58,7 @@ task npmTest(type: Test) { } task mavenTest(type: Test) { + environment "version", version def sourceSet = sourceSets.mavenTest dependsOn(project(':').getTasksByName("publishToMavenLocal", true)) testClassesDirs = sourceSet.output.classesDirs @@ -81,6 +82,7 @@ task debugAgentTest(type: Test) { jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0]) testClassesDirs = sourceSet.output.classesDirs classpath = sourceSet.runtimeClasspath + systemProperties project.properties.subMap(["overwrite.probes"]) } task coreAgentTest(type: Test) { 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/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index 8f11e0a916..d214cc6b1a 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]. @@ -241,8 +243,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 +286,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 +311,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) } } 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-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/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/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..c06cd358ad 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 @@ -121,8 +120,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..ee4d8bfc09 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -140,6 +140,17 @@ 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 copyForChildCoroutine ()Lkotlinx/coroutines/CopyableThreadContextElement; +} + +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; } @@ -156,6 +167,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 +291,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 +380,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 +459,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 +556,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 +909,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; @@ -900,6 +920,7 @@ public final class kotlinx/coroutines/flow/FlowKt { 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 fun collect (Lkotlinx/coroutines/flow/SharedFlow;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 +979,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 +995,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 +1010,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 +1074,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; } diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index c45ca08cef..0d4d708962 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -70,39 +70,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) } @@ -175,12 +201,6 @@ task checkJdk16() { } } -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,7 +266,7 @@ 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 task.systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2' @@ -257,7 +277,6 @@ 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.*' 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/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..da094e152d 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -12,8 +12,7 @@ import kotlin.coroutines.* */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext -internal expect fun createDefaultDispatcher(): CoroutineDispatcher - +@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..c91e944b91 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -61,6 +61,45 @@ 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 imageProcessingDispatcher = backgroundDispatcher.limitedParallelism(3) + * // At most 1 thread will be doing IO + * private val fileWriterDispatcher = backgroundDispatcher.limitedParallelism(1) + * ``` + * is 6. Yet 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/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt index 49923a92e7..819f205b17 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt @@ -1,13 +1,19 @@ /* * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - package kotlinx.coroutines import kotlin.coroutines.* +import kotlin.jvm.* internal expect fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) +/** + * JVM kludge: trigger loading of all the classes and service loading + * **before** any exception occur because it may be OOM, SOE or VerifyError + */ +internal expect fun initializeDefaultExceptionHandlers() + /** * Helper function for coroutine builder implementations to handle uncaught and unexpected exceptions in coroutines, * that could not be otherwise handled in a normal way through structured concurrency, saving them to a future, and diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 3ed233bfb9..21d2a6e000 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. @@ -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..d95dc52f9f 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) diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt index e6a57c927a..f4f61b25d4 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 @@ -271,7 +276,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 +284,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() @@ -526,3 +531,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..085ef7e8af 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -113,7 +113,13 @@ public interface Job : CoroutineContext.Element { /** * Key for [Job] instance in the coroutine context. */ - public companion object Key : CoroutineContext.Key + public companion object Key : CoroutineContext.Key { + init { + // `Job` will necessarily be accessed early, so this is as good a place as any for the + // initialization logic that we want to happen as soon as possible + initializeDefaultExceptionHandlers() + } + } // ------------ state query ------------ @@ -387,7 +393,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 +401,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/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..68ed5f1e78 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 { 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/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 9c6051d36d..51ed4270c0 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -178,41 +178,6 @@ 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. * diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 0ccd343ead..259f477de0 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -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. diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 6278081a5d..64effbf395 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() diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index d79e203464..41d05a6868 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -129,6 +129,19 @@ 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. + * This method should never be used directly. To emit values from a shared flow into a specific collector, either `collector.emitAll(flow)` or `collect { ... }` extension + * should 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] + */ + @InternalCoroutinesApi + override suspend fun collect(collector: FlowCollector): Nothing } /** @@ -198,6 +211,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 +268,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 +290,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 +349,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..a5ae63667f 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.* /** @@ -204,5 +204,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..ace01e1d44 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -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..e893f44ea5 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -23,6 +23,7 @@ import kotlin.time.* ----- INCLUDE .* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { ----- SUFFIX .* diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt index 608221e09f..30512f407d 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt @@ -60,35 +60,6 @@ public fun Flow.catch(action: suspend FlowCollector.(cause: Throwable) if (exception != null) action(exception) } -/** - * @suppress **Deprecated**: Use `(Throwable) -> Boolean` functional type - */ -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Use (Throwable) -> Boolean functional type", - replaceWith = ReplaceWith("(Throwable) -> Boolean") -) -public typealias ExceptionPredicate = (Throwable) -> Boolean - -/** - * Switches to the [fallback] flow if the original flow throws an exception that matches the [predicate]. - * Cancellation exceptions that were caused by the direct [cancel] call are not handled by this operator. - * - * @suppress **Deprecated**: Use `catch { e -> if (predicate(e)) emitAll(fallback) else throw e }` - */ -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Use catch { e -> if (predicate(e)) emitAll(fallback) else throw e }", - replaceWith = ReplaceWith("catch { e -> if (predicate(e)) emitAll(fallback) else throw e }") -) -public fun Flow.onErrorCollect( - fallback: Flow, - predicate: (Throwable) -> Boolean = { true } -): Flow = catch { e -> - if (!predicate(e)) throw e - emitAll(fallback) -} - /** * Retries collection of the given flow up to [retries] times when an exception that matches the * given [predicate] occurs in the upstream flow. This operator is *transparent* to exceptions that occur @@ -124,16 +95,6 @@ public fun Flow.retry( return retryWhen { cause, attempt -> attempt < retries && predicate(cause) } } -@FlowPreview -@Deprecated(level = DeprecationLevel.HIDDEN, message = "binary compatibility with retries: Int preview version") -public fun Flow.retry( - retries: Int = Int.MAX_VALUE, - predicate: (Throwable) -> Boolean = { true } -): Flow { - require(retries > 0) { "Expected positive amount of retries, but had $retries" } - return retryWhen { cause, attempt -> predicate(cause) && attempt < retries } -} - /** * Retries collection of the given flow when an exception occurs in the upstream flow and the * [predicate] returns true. The predicate also receives an `attempt` number as parameter, @@ -186,6 +147,7 @@ public fun Flow.retryWhen(predicate: suspend FlowCollector.(cause: Thr } // Return exception from upstream or null +@Suppress("NAME_SHADOWING") internal suspend fun Flow.catchImpl( collector: FlowCollector ): Throwable? { @@ -200,6 +162,8 @@ internal suspend fun Flow.catchImpl( } } } catch (e: Throwable) { + // Otherwise, smartcast is impossible + val fromDownstream = fromDownstream /* * First check ensures that we catch an original exception, not one rethrown by an operator. * Seconds check ignores cancellation causes, they cannot be caught. @@ -207,7 +171,41 @@ internal suspend fun Flow.catchImpl( if (e.isSameExceptionAs(fromDownstream) || e.isCancellationCause(coroutineContext)) { throw e // Rethrow exceptions from downstream and cancellation causes } else { - return e // not from downstream + /* + * The exception came from the upstream [semi-] independently. + * For pure failures, when the downstream functions normally, we handle the exception as intended. + * But if the downstream has failed prior to or concurrently + * with the upstream, we forcefully rethrow it, preserving the contextual information and ensuring that it's not lost. + */ + if (fromDownstream == null) { + return e + } + /* + * We consider the upstream exception as the superseding one when both upstream and downstream + * fail, suppressing the downstream exception, and operating similarly to `finally` block with + * the useful addition of adding the original downstream exception to suppressed ones. + * + * That's important for the following scenarios: + * ``` + * flow { + * val resource = ... + * try { + * ... emit as well ... + * } finally { + * resource.close() // Throws in the shutdown sequence when 'collect' already has thrown an exception + * } + * }.catch { } // or retry + * .collect { ... } + * ``` + * when *the downstream* throws. + */ + if (e is CancellationException) { + fromDownstream.addSuppressed(e) + throw fromDownstream + } else { + e.addSuppressed(fromDownstream) + throw e + } } } return null diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index 8fbf1a2b0e..734464b557 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -112,7 +112,6 @@ public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = f * } * ``` */ -@ExperimentalCoroutinesApi public fun Flow.transformWhile( @BuilderInference transform: suspend FlowCollector.(value: T) -> Boolean ): Flow = diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 432160f340..35c44d0895 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -61,7 +61,7 @@ public fun Flow.flatMapConcat(transform: suspend (value: T) -> Flow * its concurrent merging so that only one properly configured channel is used for execution of merging logic. * * @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected - * at the same time. By default it is equal to [DEFAULT_CONCURRENCY]. + * at the same time. By default, it is equal to [DEFAULT_CONCURRENCY]. */ @FlowPreview public fun Flow.flatMapMerge( @@ -71,8 +71,7 @@ public fun Flow.flatMapMerge( map(transform).flattenMerge(concurrency) /** - * Flattens the given flow of flows into a single flow in a sequentially manner, without interleaving nested flows. - * This method is conceptually identical to `flattenMerge(concurrency = 1)` but has faster implementation. + * Flattens the given flow of flows into a single flow in a sequential manner, without interleaving nested flows. * * Inner flows are collected by this operator *sequentially*. */ @@ -90,7 +89,6 @@ public fun Flow>.flattenConcat(): Flow = flow { * Applications of [flowOn], [buffer], and [produceIn] _after_ this operator are fused with * its concurrent merging so that only one properly configured channel is used for execution of merging logic. */ -@ExperimentalCoroutinesApi public fun Iterable>.merge(): Flow { /* * This is a fuseable implementation of the following operator: @@ -114,14 +112,13 @@ public fun Iterable>.merge(): Flow { * Applications of [flowOn], [buffer], and [produceIn] _after_ this operator are fused with * its concurrent merging so that only one properly configured channel is used for execution of merging logic. */ -@ExperimentalCoroutinesApi public fun merge(vararg flows: Flow): Flow = flows.asIterable().merge() /** * Flattens the given flow of flows into a single flow with a [concurrency] limit on the number of * concurrently collected flows. * - * If [concurrency] is more than 1, then inner flows are be collected by this operator *concurrently*. + * If [concurrency] is more than 1, then inner flows are collected by this operator *concurrently*. * With `concurrency == 1` this operator is identical to [flattenConcat]. * * ### Operator fusion @@ -133,7 +130,7 @@ public fun merge(vararg flows: Flow): Flow = flows.asIterable().merge( * and size of its output buffer can be changed by applying subsequent [buffer] operator. * * @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected - * at the same time. By default it is equal to [DEFAULT_CONCURRENCY]. + * at the same time. By default, it is equal to [DEFAULT_CONCURRENCY]. */ @FlowPreview public fun Flow>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index 4fa74d8e50..2b690e3c04 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -197,8 +197,16 @@ private fun CoroutineScope.launchSharing( shared: MutableSharedFlow, started: SharingStarted, initialValue: T -): Job = - launch(context) { // the single coroutine to rule the sharing +): Job { + /* + * Conditional start: in the case when sharing and subscribing happens in the same dispatcher, we want to + * have the following invariants preserved: + * * Delayed sharing strategies have a chance to immediately observe consecutive subscriptions. + * E.g. in the cases like `flow.shareIn(...); flow.take(1)` we want sharing strategy to see the initial subscription + * * Eager sharing does not start immediately, so the subscribers have actual chance to subscribe _prior_ to sharing. + */ + val start = if (started == SharingStarted.Eagerly) CoroutineStart.DEFAULT else CoroutineStart.UNDISPATCHED + return launch(context, start = start) { // the single coroutine to rule the sharing // Optimize common built-in started strategies when { started === SharingStarted.Eagerly -> { @@ -230,6 +238,7 @@ private fun CoroutineScope.launchSharing( } } } +} // -------------------------------- stateIn -------------------------------- diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index a47ae776ca..9b97193227 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -85,7 +85,6 @@ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = transform * * This function is an alias to [runningFold] operator. */ -@ExperimentalCoroutinesApi public fun Flow.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow = runningFold(initial, operation) /** @@ -97,7 +96,6 @@ public fun Flow.scan(initial: R, @BuilderInference operation: suspend * ``` * will produce `[], [1], [1, 2], [1, 2, 3]]`. */ -@ExperimentalCoroutinesApi public fun Flow.runningFold(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow = flow { var accumulator: R = initial emit(accumulator) @@ -118,7 +116,6 @@ public fun Flow.runningFold(initial: R, @BuilderInference operation: s * ``` * will produce `[1, 3, 6, 10]` */ -@ExperimentalCoroutinesApi public fun Flow.runningReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = flow { var accumulator: Any? = NULL collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index 771f8332c3..91d410b9a2 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -73,6 +73,19 @@ public suspend inline fun Flow.collect(crossinline action: suspend (value override suspend fun emit(value: T) = action(value) }) +/** + * Terminal flow operator that collects the given [SharedFlow] with the provided [action]. + * If any exception occurs during `collect` or in the provided flow, this exception is rethrown from this method. + * + * This is a counterpart of a regular [Flow.collect] extension, only different in the return type + * so that any code below `collect` produces a compilation warning. + */ +public suspend inline fun SharedFlow.collect(crossinline action: suspend (value: T) -> Unit): Nothing { + collect(object : FlowCollector { + override suspend fun emit(value: T) = action(value) + }) +} + /** * Terminal flow operator that collects the given flow with a provided [action] that takes the index of an element (zero-based) and the element. * If any exception occurs during collect or in the provided flow, this exception is rethrown from this method. diff --git a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt index 9f2699ae48..fb254a0ebc 100644 --- a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt @@ -8,10 +8,12 @@ package kotlinx.coroutines.internal * Special kind of list intended to be used as collection of subscribers in `ArrayBroadcastChannel` * On JVM it's CopyOnWriteList and on JS it's MutableList. * - * Note that this alias is intentionally not named as CopyOnWriteList to avoid accidental misusage outside of ArrayBroadcastChannel + * Note that this alias is intentionally not named as CopyOnWriteList to avoid accidental misusage outside of the ArrayBroadcastChannel */ internal typealias SubscribersList = MutableList +@Deprecated(message = "Implementation of this primitive is tailored to specific ArrayBroadcastChannel usages on K/N " + + "and K/JS platforms and it is unsafe to use it anywhere else") internal expect fun subscriberList(): SubscribersList internal expect class ReentrantLock() { diff --git a/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt b/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt new file mode 100644 index 0000000000..1df81c9cc4 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.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.internal + +// Ignore JRE requirements for animal-sniffer, compileOnly dependency +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +@OptionalExpectation +internal expect annotation class IgnoreJreRequirement() diff --git a/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt new file mode 100644 index 0000000000..892375b89f --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.jvm.* + +/** + * The result of .limitedParallelism(x) call, a dispatcher + * that wraps the given dispatcher, but limits the parallelism level, while + * trying to emulate fairness. + */ +internal class LimitedDispatcher( + private val dispatcher: CoroutineDispatcher, + private val parallelism: Int +) : CoroutineDispatcher(), Runnable, Delay by (dispatcher as? Delay ?: DefaultDelay) { + + @Volatile + private var runningWorkers = 0 + + private val queue = LockFreeTaskQueue(singleConsumer = false) + + @ExperimentalCoroutinesApi + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + if (parallelism >= this.parallelism) return this + return super.limitedParallelism(parallelism) + } + + override fun run() { + var fairnessCounter = 0 + while (true) { + val task = queue.removeFirstOrNull() + if (task != null) { + try { + task.run() + } catch (e: Throwable) { + handleCoroutineException(EmptyCoroutineContext, e) + } + // 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well + if (++fairnessCounter >= 16 && dispatcher.isDispatchNeeded(this)) { + // Do "yield" to let other views to execute their runnable as well + // Note that we do not decrement 'runningWorkers' as we still committed to do our part of work + dispatcher.dispatch(this, this) + return + } + continue + } + + @Suppress("CAST_NEVER_SUCCEEDS") + synchronized(this as SynchronizedObject) { + --runningWorkers + if (queue.size == 0) return + ++runningWorkers + fairnessCounter = 0 + } + } + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + dispatchInternal(block) { + dispatcher.dispatch(this, this) + } + } + + @InternalCoroutinesApi + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + dispatchInternal(block) { + dispatcher.dispatchYield(this, this) + } + } + + private inline fun dispatchInternal(block: Runnable, dispatch: () -> Unit) { + // Add task to queue so running workers will be able to see that + if (addAndTryDispatching(block)) return + /* + * Protect against the race when the number of workers is enough, + * but one (because of synchronized serialization) attempts to complete, + * and we just observed the number of running workers smaller than the actual + * number (hit right between `--runningWorkers` and `++runningWorkers` in `run()`) + */ + if (!tryAllocateWorker()) return + dispatch() + } + + private fun tryAllocateWorker(): Boolean { + @Suppress("CAST_NEVER_SUCCEEDS") + synchronized(this as SynchronizedObject) { + if (runningWorkers >= parallelism) return false + ++runningWorkers + return true + } + } + + private fun addAndTryDispatching(block: Runnable): Boolean { + queue.addLast(block) + return runningWorkers >= parallelism + } +} + +// Save a few bytecode ops +internal fun Int.checkParallelism() = require(this >= 1) { "Expected positive parallelism level, but got $this" } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index 0e1d1b473a..8b20ade1f0 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -43,7 +43,7 @@ public expect open class LockFreeLinkedListNode() { public expect open class LockFreeLinkedListHead() : LockFreeLinkedListNode { public val isEmpty: Boolean public inline fun forEach(block: (T) -> Unit) - public final override fun remove(): Boolean // Actual return type is Nothing, KT-27534 + public final override fun remove(): Nothing } /** @suppress **This is unstable API and it is subject to change.** */ diff --git a/kotlinx-coroutines-core/common/src/internal/Symbol.kt b/kotlinx-coroutines-core/common/src/internal/Symbol.kt index 84db2ef6cc..b629951fbd 100644 --- a/kotlinx-coroutines-core/common/src/internal/Symbol.kt +++ b/kotlinx-coroutines-core/common/src/internal/Symbol.kt @@ -4,12 +4,14 @@ package kotlinx.coroutines.internal +import kotlin.jvm.* + /** * A symbol class that is used to define unique constants that are self-explanatory in debugger. * * @suppress **This is unstable API and it is subject to change.** */ -internal class Symbol(val symbol: String) { +internal class Symbol(@JvmField val symbol: String) { override fun toString(): String = "<$symbol>" @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index a7172707e2..a313de3d5d 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -186,7 +186,6 @@ public interface SelectInstance { * | [SendChannel] | [send][SendChannel.send] | [onSend][SendChannel.onSend] * | [ReceiveChannel] | [receive][ReceiveChannel.receive] | [onReceive][ReceiveChannel.onReceive] * | [ReceiveChannel] | [receiveCatching][ReceiveChannel.receiveCatching] | [onReceiveCatching][ReceiveChannel.onReceiveCatching] - * | [Mutex] | [lock][Mutex.lock] | [onLock][Mutex.onLock] * | none | [delay] | [onTimeout][SelectBuilder.onTimeout] * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 19584e0981..d2a2fcd41b 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -52,8 +52,7 @@ public interface Mutex { * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. * - * This function can be used in [select] invocation with [onLock] clause. - * Use [tryLock] to try acquire lock without waiting. + * Use [tryLock] to try acquiring a lock without waiting. * * This function is fair; suspended callers are resumed in first-in-first-out order. * @@ -63,10 +62,10 @@ public interface Mutex { public suspend fun lock(owner: Any? = null) /** - * Clause for [select] expression of [lock] suspending function that selects when the mutex is locked. - * Additional parameter for the clause in the `owner` (see [lock]) and when the clause is selected - * the reference to this mutex is passed into the corresponding block. + * Deprecated for removal without built-in replacement. */ + @Deprecated(level = DeprecationLevel.WARNING, message = "Mutex.onLock deprecated without replacement. " + + "For additional details please refer to #2794") // WARNING since 1.6.0 public val onLock: SelectClause2 /** @@ -370,7 +369,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { private abstract inner class LockWaiter( @JvmField val owner: Any? ) : LockFreeLinkedListNode(), DisposableHandle { - private val isTaken = atomic(false) + private val isTaken = atomic(false) fun take(): Boolean = isTaken.compareAndSet(false, true) final override fun dispose() { remove() } abstract fun tryResumeLockWaiter(): Boolean diff --git a/kotlinx-coroutines-core/common/test/DelayDurationTest.kt b/kotlinx-coroutines-core/common/test/DelayDurationTest.kt index 3dd55bde84..1c6c189a44 100644 --- a/kotlinx-coroutines-core/common/test/DelayDurationTest.kt +++ b/kotlinx-coroutines-core/common/test/DelayDurationTest.kt @@ -10,6 +10,8 @@ package kotlinx.coroutines import kotlin.test.* import kotlin.time.* +import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.nanoseconds @ExperimentalTime class DelayDurationTest : TestBase() { diff --git a/kotlinx-coroutines-core/common/test/EmptyContext.kt b/kotlinx-coroutines-core/common/test/EmptyContext.kt index ad78429d2b..97efec34c9 100644 --- a/kotlinx-coroutines-core/common/test/EmptyContext.kt +++ b/kotlinx-coroutines-core/common/test/EmptyContext.kt @@ -7,10 +7,6 @@ package kotlinx.coroutines import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* -suspend fun withEmptyContext(block: suspend () -> T): T { - val baseline = Result.failure(IllegalStateException("Block was suspended")) - var result: Result = baseline - block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { result = it }) - while (result == baseline) yield() - return result.getOrThrow() +suspend fun withEmptyContext(block: suspend () -> T): T = suspendCoroutine { cont -> + block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { cont.resumeWith(it) }) } diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 71c45769cb..8b7024a60a 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -7,11 +7,13 @@ package kotlinx.coroutines import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.test.* public expect val isStressTest: Boolean public expect val stressTestMultiplier: Int +public expect val stressTestMultiplierSqrt: Int /** * The result of a multiplatform asynchronous test. @@ -20,6 +22,8 @@ public expect val stressTestMultiplier: Int @Suppress("NO_ACTUAL_FOR_EXPECT") public expect class TestResult +public expect val isNative: Boolean + public expect open class TestBase constructor() { /* * In common tests we emulate parameterized tests diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt index b5f1bf7bb8..5f39d3200d 100644 --- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -235,7 +235,7 @@ class FlowInvariantsTest : TestBase() { } expectUnreached() } catch (e: IllegalStateException) { - assertTrue(e.message!!.contains("Flow invariant is violated")) + assertTrue(e.message!!.contains("Flow invariant is violated"), "But had: ${e.message}") finish(2) } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt index 447eb73b5d..ad91e49898 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt @@ -144,4 +144,61 @@ class CatchTest : TestBase() { .collect() finish(9) } + + @Test + fun testUpstreamExceptionConcurrentWithDownstream() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw TestException() + } + }.catch { expectUnreached() }.onEach { + expect(2) + throw TestException2() + } + + assertFailsWith(flow) + finish(4) + } + + @Test + fun testUpstreamExceptionConcurrentWithDownstreamCancellation() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw TestException() + } + }.catch { expectUnreached() }.onEach { + expect(2) + throw CancellationException("") + } + + assertFailsWith(flow) + finish(4) + } + + @Test + fun testUpstreamCancellationIsIgnoredWhenDownstreamFails() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw CancellationException("") + } + }.catch { expectUnreached() }.onEach { + expect(2) + throw TestException("") + } + + assertFailsWith(flow) + finish(4) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt index 1c5a305352..dfa2827447 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt @@ -48,10 +48,9 @@ class DropTest : TestBase() { expectUnreached() } }.drop(1) - .map { + .map { expect(4) throw TestException() - 42 }.catch { emit(42) } expect(1) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt index 3de5d54a6d..f52416d823 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt @@ -38,7 +38,6 @@ class FilterTest : TestBase() { }.filter { latch.receive() throw TestException() - true }.catch { emit(42) } assertEquals(42, flow.single()) @@ -74,7 +73,6 @@ class FilterTest : TestBase() { }.filterNot { latch.receive() throw TestException() - true }.catch { emit(42) } assertEquals(42, flow.single()) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt index 4095172dab..f09db120a8 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt @@ -72,7 +72,7 @@ abstract class FlatMapMergeBaseTest : FlatMapBaseTest() { emit(2) expectUnreached() }.flatMap { - if (it == 1) flow { + if (it == 1) flow { expect(5) latch.send(Unit) hang { expect(7) } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt index a92189c45c..f810221848 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt @@ -39,19 +39,14 @@ class FlatMapMergeFastPathTest : FlatMapMergeBaseTest() { @Test fun testCancellationExceptionDownstream() = runTest { - val flow = flow { - emit(1) - hang { expect(2) } - }.flatMapMerge { + val flow = flowOf(1, 2, 3).flatMapMerge { flow { emit(it) - expect(1) throw CancellationException("") } }.buffer(64) - assertFailsWith(flow) - finish(3) + assertEquals(listOf(1, 2, 3), flow.toList()) } @Test diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt index 7470289ece..c2ce346d9b 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt @@ -69,19 +69,14 @@ class FlatMapMergeTest : FlatMapMergeBaseTest() { @Test fun testCancellationExceptionDownstream() = runTest { - val flow = flow { - emit(1) - hang { expect(2) } - }.flatMapMerge { + val flow = flowOf(1, 2, 3).flatMapMerge { flow { emit(it) - expect(1) throw CancellationException("") } } - assertFailsWith(flow) - finish(3) + assertEquals(listOf(1, 2, 3), flow.toList()) } @Test diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt index 084af5b9bb..4ec7cc3cd1 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt @@ -36,4 +36,17 @@ class FlattenConcatTest : FlatMapBaseTest() { consumer.cancelAndJoin() finish(2) } + + @Test + fun testCancellation() = runTest { + val flow = flow { + repeat(5) { + emit(flow { + if (it == 2) throw CancellationException("") + emit(1) + }) + } + } + assertFailsWith(flow.flattenConcat()) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index 68653281cc..8fba8456e8 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -83,7 +83,7 @@ class FlowOnTest : TestBase() { }.map { expect(2) assertEquals("throwing", it) - throw TestException(); it + throw TestException() }.flowOn(NamedDispatchers("throwing")) assertFailsWith(flow) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt index 893811df15..d8bb480054 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt @@ -39,10 +39,9 @@ class MapNotNullTest : TestBase() { } emit(1) } - }.mapNotNull { + }.mapNotNull { latch.receive() throw TestException() - it + 1 }.catch { emit(42) } assertEquals(42, flow.single()) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt index 1248188554..f084798487 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt @@ -45,6 +45,64 @@ abstract class MergeTest : TestBase() { assertEquals(listOf("source"), result) } + @Test + fun testOneSourceCancelled() = runTest { + val flow = flow { + expect(1) + emit(1) + expect(2) + yield() + throw CancellationException("") + } + + val otherFlow = flow { + repeat(5) { + emit(1) + yield() + } + + expect(3) + } + + val result = listOf(flow, otherFlow).merge().toList() + assertEquals(MutableList(6) { 1 }, result) + finish(4) + } + + @Test + fun testOneSourceCancelledNonFused() = runTest { + val flow = flow { + expect(1) + emit(1) + expect(2) + yield() + throw CancellationException("") + } + + val otherFlow = flow { + repeat(5) { + emit(1) + yield() + } + + expect(3) + } + + val result = listOf(flow, otherFlow).nonFuseableMerge().toList() + assertEquals(MutableList(6) { 1 }, result) + finish(4) + } + + private fun Iterable>.nonFuseableMerge(): Flow { + return channelFlow { + forEach { flow -> + launch { + flow.collect { send(it) } + } + } + } + } + @Test fun testIsolatedContext() = runTest { val flow = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt index b8a6b198fb..e5dde1b7fc 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt @@ -104,4 +104,61 @@ class RetryTest : TestBase() { job.cancelAndJoin() finish(3) } -} \ No newline at end of file + + @Test + fun testUpstreamExceptionConcurrentWithDownstream() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw TestException() + } + }.retry { expectUnreached(); true }.onEach { + expect(2) + throw TestException2() + } + + assertFailsWith(flow) + finish(4) + } + + @Test + fun testUpstreamExceptionConcurrentWithDownstreamCancellation() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw TestException() + } + }.retry { expectUnreached(); true }.onEach { + expect(2) + throw CancellationException("") + } + + assertFailsWith(flow) + finish(4) + } + + @Test + fun testUpstreamCancellationIsIgnoredWhenDownstreamFails() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw CancellationException("") + } + }.retry { expectUnreached(); true }.onEach { + expect(2) + throw TestException("") + } + + assertFailsWith(flow) + finish(4) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt index 87bee56f1d..f7741fbd59 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt @@ -250,7 +250,6 @@ class SampleTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt index 62d2322c04..ea8939fed2 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt @@ -88,9 +88,8 @@ class TakeTest : TestBase() { emit(1) } }.take(2) - .map { + .map { throw TestException() - 42 }.catch { emit(42) } assertEquals(42, flow.single()) diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt index 0528e97e7d..c19d52367b 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt @@ -21,7 +21,7 @@ class ShareInConflationTest : TestBase() { op: suspend Flow.(CoroutineScope) -> Flow ) = runTest { expect(1) - // emit all and conflate, then should collect bufferCapacity latest ones + // emit all and conflate, then should collect bufferCapacity the latest ones val done = Job() flow { repeat(n) { i -> @@ -159,4 +159,4 @@ class ShareInConflationTest : TestBase() { checkConflation(1, BufferOverflow.DROP_LATEST) { buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt index db69e2bc06..cf83a50b0f 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -210,4 +210,30 @@ class ShareInTest : TestBase() { stop() } } + + @Test + fun testShouldStart() = runTest { + val flow = flow { + expect(2) + emit(1) + expect(3) + }.shareIn(this, SharingStarted.Lazily) + + expect(1) + flow.onSubscription { throw CancellationException("") } + .catch { e -> assertTrue { e is CancellationException } } + .collect() + yield() + finish(4) + } + + @Test + fun testShouldStartScalar() = runTest { + val j = Job() + val shared = flowOf(239).stateIn(this + j, SharingStarted.Lazily, 42) + assertEquals(42, shared.first()) + yield() + assertEquals(239, shared.first()) + j.cancel() + } } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt index 6e18b38f55..98e04f00e8 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -798,4 +798,24 @@ class SharedFlowTest : TestBase() { job.join() finish(5) } + + @Test + fun testSubscriptionCount() = runTest { + val flow = MutableSharedFlow() + fun startSubscriber() = launch(start = CoroutineStart.UNDISPATCHED) { flow.collect() } + + assertEquals(0, flow.subscriptionCount.first()) + + val j1 = startSubscriber() + assertEquals(1, flow.subscriptionCount.first()) + + val j2 = startSubscriber() + assertEquals(2, flow.subscriptionCount.first()) + + j1.cancelAndJoin() + assertEquals(1, flow.subscriptionCount.first()) + + j2.cancelAndJoin() + assertEquals(0, flow.subscriptionCount.first()) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt index 516bb2e291..3b961c5783 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt @@ -7,6 +7,8 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.test.* import kotlin.time.* +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds class SharingStartedWhileSubscribedTest : TestBase() { @Test // make sure equals works properly, or otherwise other tests don't make sense @@ -30,15 +32,49 @@ class SharingStartedWhileSubscribedTest : TestBase() { @Test fun testDurationParams() { assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO)) - assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(Duration.milliseconds(10))) + assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds)) assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds)) assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE)) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed( replayExpiration = Duration.milliseconds(3) )) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), + SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE)) } -} + @Test + fun testShouldRestart() = runTest { + var started = 0 + val flow = flow { + expect(1 + ++started) + emit(1) + hang { } + }.shareIn(this, SharingStarted.WhileSubscribed(100 /* ms */)) + + expect(1) + flow.first() + delay(200) + flow.first() + finish(4) + coroutineContext.job.cancelChildren() + } + + @Test + fun testImmediateUnsubscribe() = runTest { + val flow = flow { + expect(2) + emit(1) + hang { finish(4) } + }.shareIn(this, SharingStarted.WhileSubscribed(400, 0 /* ms */), 1) + + expect(1) + repeat(5) { + flow.first() + delay(100) + } + expect(3) + coroutineContext.job.cancelChildren() + } +} diff --git a/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt new file mode 100644 index 0000000000..8a6c09231b --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.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 + +import kotlin.coroutines.* + +/** + * Runs a new coroutine and **blocks** the current thread until its completion. + * This function should not be used from a coroutine. It is designed to bridge regular blocking code + * to libraries that are written in suspending style, to be used in `main` functions and in tests. + */ +public expect fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/CompletionHandler.kt b/kotlinx-coroutines-core/concurrent/src/CompletionHandler.kt similarity index 100% rename from kotlinx-coroutines-core/jvm/src/CompletionHandler.kt rename to kotlinx-coroutines-core/concurrent/src/CompletionHandler.kt diff --git a/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt b/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt new file mode 100644 index 0000000000..a2b4241f0f --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +@ExperimentalCoroutinesApi +public expect fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher + +@ExperimentalCoroutinesApi +public expect fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt similarity index 99% rename from kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt rename to kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt index 9bbc6dc9eb..b4b36dad34 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt @@ -7,6 +7,8 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* private typealias Node = LockFreeLinkedListNode @@ -20,9 +22,11 @@ internal const val SUCCESS: Int = 1 internal const val FAILURE: Int = 2 @PublishedApi +@SharedImmutable internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE") @PublishedApi +@SharedImmutable internal val LIST_EMPTY: Any = Symbol("LIST_EMPTY") /** @suppress **This is unstable API and it is subject to change.** */ @@ -616,7 +620,7 @@ public actual open class LockFreeLinkedListNode { assert { next === this._next.value } } - override fun toString(): String = "${this::class.java.simpleName}@${Integer.toHexString(System.identityHashCode(this))}" + override fun toString(): String = "${this::classSimpleName}@${this.hexAddress}" } private class Removed(@JvmField val ref: Node) { @@ -646,7 +650,7 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() { } // just a defensive programming -- makes sure that list head sentinel is never removed - public actual final override fun remove(): Boolean = error("head cannot be removed") + public actual final override fun remove(): Nothing = error("head cannot be removed") // optimization: because head is never removed, we don't have to read _next.value to check these: override val isRemoved: Boolean get() = false diff --git a/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt b/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt new file mode 100644 index 0000000000..8fc4f4efe0 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines + +import kotlinx.coroutines.channels.* +import kotlin.test.* + + +abstract class AbstractDispatcherConcurrencyTest : TestBase() { + + public abstract val dispatcher: CoroutineDispatcher + + @Test + fun testLaunchAndJoin() = runMtTest { + expect(1) + var capturedMutableState = 0 + val job = GlobalScope.launch(dispatcher) { + ++capturedMutableState + expect(2) + } + runBlocking { job.join() } + assertEquals(1, capturedMutableState) + finish(3) + } + + @Test + fun testDispatcherHasOwnThreads() = runMtTest { + val channel = Channel() + GlobalScope.launch(dispatcher) { + channel.send(42) + } + + var result = ChannelResult.failure() + while (!result.isSuccess) { + result = channel.tryReceive() + // Block the thread, wait + } + // Delivery was successful, let's check it + assertEquals(42, result.getOrThrow()) + } + + @Test + fun testDelayInDispatcher() = runMtTest { + expect(1) + val job = GlobalScope.launch(dispatcher) { + expect(2) + delay(100) + expect(3) + } + runBlocking { job.join() } + finish(4) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt b/kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.kt similarity index 98% rename from kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt rename to kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.kt index 2612b84153..74751fcc3f 100644 --- a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.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-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -142,4 +142,4 @@ class AtomicCancellationTest : TestBase() { yield() // to jobToJoin & canceller expect(6) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt b/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt new file mode 100644 index 0000000000..d4252da300 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.exceptions.* +import kotlinx.coroutines.internal.* +import kotlin.test.* + +class ConcurrentExceptionsStressTest : TestBase() { + private val nWorkers = 4 + private val nRepeat = 1000 * stressTestMultiplier + + private var workers: Array = emptyArray() + + @AfterTest + fun tearDown() { + workers.forEach { + it.close() + } + } + + @Test + fun testStress() = runMtTest { + workers = Array(nWorkers) { index -> + newSingleThreadContext("JobExceptionsStressTest-$index") + } + + repeat(nRepeat) { + testOnce() + } + } + + @Suppress("SuspendFunctionOnCoroutineScope") // workaround native inline fun stacktraces + private suspend fun CoroutineScope.testOnce() { + val deferred = async(NonCancellable) { + repeat(nWorkers) { index -> + // Always launch a coroutine even if parent job was already cancelled (atomic start) + launch(workers[index], start = CoroutineStart.ATOMIC) { + randomWait() + throw StressException(index) + } + } + } + deferred.join() + assertTrue(deferred.isCancelled) + val completionException = deferred.getCompletionExceptionOrNull() + val cause = completionException as? StressException + ?: unexpectedException("completion", completionException) + val suppressed = cause.suppressed + val indices = listOf(cause.index) + suppressed.mapIndexed { index, e -> + (e as? StressException)?.index ?: unexpectedException("suppressed $index", e) + } + repeat(nWorkers) { index -> + assertTrue(index in indices, "Exception $index is missing: $indices") + } + assertEquals(nWorkers, indices.size, "Duplicated exceptions in list: $indices") + } + + private fun unexpectedException(msg: String, e: Throwable?): Nothing { + throw IllegalStateException("Unexpected $msg exception", e) + } + + private class StressException(val index: Int) : SuppressSupportingThrowable() +} + diff --git a/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt b/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt new file mode 100644 index 0000000000..a4d40fb2ef --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* + +internal expect open class SuppressSupportingThrowable() : Throwable +expect val Throwable.suppressed: Array +expect fun Throwable.printStackTrace() + +expect fun randomWait() + +expect fun currentThreadName(): String + +inline fun CloseableCoroutineDispatcher.use(block: (CloseableCoroutineDispatcher) -> Unit) { + try { + block(this) + } finally { + close() + } +} diff --git a/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt b/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt new file mode 100644 index 0000000000..a12930cc12 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines + +class DefaultDispatcherConcurrencyTest : AbstractDispatcherConcurrencyTest() { + override val dispatcher: CoroutineDispatcher = Dispatchers.Default +} diff --git a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt b/kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt similarity index 86% rename from kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt index 50d86f32be..431bb697fd 100644 --- a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt @@ -1,11 +1,11 @@ /* - * 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 -import org.junit.* import kotlin.coroutines.* +import kotlin.test.* /** * Test a race between job failure and join. @@ -16,12 +16,12 @@ class JobStructuredJoinStressTest : TestBase() { private val nRepeats = 10_000 * stressTestMultiplier @Test - fun testStressRegularJoin() { + fun testStressRegularJoin() = runMtTest { stress(Job::join) } @Test - fun testStressSuspendCancellable() { + fun testStressSuspendCancellable() = runMtTest { stress { job -> suspendCancellableCoroutine { cont -> job.invokeOnCompletion { cont.resume(Unit) } @@ -30,7 +30,7 @@ class JobStructuredJoinStressTest : TestBase() { } @Test - fun testStressSuspendCancellableReusable() { + fun testStressSuspendCancellableReusable() = runMtTest { stress { job -> suspendCancellableCoroutineReusable { cont -> job.invokeOnCompletion { cont.resume(Unit) } @@ -61,4 +61,4 @@ class JobStructuredJoinStressTest : TestBase() { } finish(2 + nRepeats) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt b/kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt similarity index 66% rename from kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt rename to kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt index de38df6b26..70f6b8ba60 100644 --- a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt @@ -1,16 +1,16 @@ /* * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - package kotlinx.coroutines +import kotlinx.coroutines.exceptions.* import kotlin.coroutines.* import kotlin.test.* class RunBlockingTest : TestBase() { @Test - fun testWithTimeoutBusyWait() = runBlocking { + fun testWithTimeoutBusyWait() = runMtTest { val value = withTimeoutOrNull(10) { while (isActive) { // Busy wait @@ -52,14 +52,14 @@ class RunBlockingTest : TestBase() { } @Test - fun testOtherDispatcher() { + fun testOtherDispatcher() = runMtTest { expect(1) val name = "RunBlockingTest.testOtherDispatcher" val thread = newSingleThreadContext(name) runBlocking(thread) { expect(2) assertSame(coroutineContext[ContinuationInterceptor], thread) - assertTrue(Thread.currentThread().name.contains(name)) + assertTrue(currentThreadName().contains(name)) yield() // should work expect(3) } @@ -67,19 +67,20 @@ class RunBlockingTest : TestBase() { thread.close() } - @Test - fun testCancellation() = newFixedThreadPoolContext(2, "testCancellation").use { - val job = GlobalScope.launch(it) { - runBlocking(coroutineContext) { - while (true) { - yield() + fun testCancellation() = runMtTest { + newFixedThreadPoolContext(2, "testCancellation").use { + val job = GlobalScope.launch(it) { + runBlocking(coroutineContext) { + while (true) { + yield() + } } } - } - runBlocking { - job.cancelAndJoin() + runBlocking { + job.cancelAndJoin() + } } } @@ -104,40 +105,44 @@ class RunBlockingTest : TestBase() { } } - @Test(expected = CancellationException::class) - fun testDispatchOnShutdown() = runBlocking { - expect(1) - val job = launch(NonCancellable) { - try { - expect(2) - delay(Long.MAX_VALUE) - } finally { - finish(4) + @Test + fun testDispatchOnShutdown(): Unit = assertFailsWith { + runBlocking { + expect(1) + val job = launch(NonCancellable) { + try { + expect(2) + delay(Long.MAX_VALUE) + } finally { + finish(4) + } } - } - yield() - expect(3) - coroutineContext.cancel() - job.cancel() - } + yield() + expect(3) + coroutineContext.cancel() + job.cancel() + } + }.let { } - @Test(expected = CancellationException::class) - fun testDispatchOnShutdown2() = runBlocking { - coroutineContext.cancel() - expect(1) - val job = launch(NonCancellable, start = CoroutineStart.UNDISPATCHED) { - try { - expect(2) - delay(Long.MAX_VALUE) - } finally { - finish(4) + @Test + fun testDispatchOnShutdown2(): Unit = assertFailsWith { + runBlocking { + coroutineContext.cancel() + expect(1) + val job = launch(NonCancellable, start = CoroutineStart.UNDISPATCHED) { + try { + expect(2) + delay(Long.MAX_VALUE) + } finally { + finish(4) + } } - } - expect(3) - job.cancel() - } + expect(3) + job.cancel() + } + }.let { } @Test fun testNestedRunBlocking() = runBlocking { @@ -157,21 +162,12 @@ class RunBlockingTest : TestBase() { fun testIncompleteState() { val handle = runBlocking { // See #835 - coroutineContext[Job]!!.invokeOnCompletion { } + coroutineContext[Job]!!.invokeOnCompletion { } } handle.dispose() } - @Test - fun testContract() { - val rb: Int - runBlocking { - rb = 42 - } - rb.hashCode() // unused - } - @Test fun testCancelledParent() { val job = Job() diff --git a/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt b/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt new file mode 100644 index 0000000000..b19bf50ec8 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.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 + +expect fun TestBase.runMtTest( + expected: ((Throwable) -> Boolean)? = null, + unhandled: List<(Throwable) -> Boolean> = emptyList(), + block: suspend CoroutineScope.() -> Unit +): TestResult diff --git a/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt new file mode 100644 index 0000000000..30b1075c0a --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlin.test.* + +/** + * Creates a broadcast channel and repeatedly opens new subscription, receives event, closes it, + * to stress test the logic of opening the subscription + * to broadcast channel while events are being concurrently sent to it. + */ +class BroadcastChannelSubStressTest: TestBase() { + + private val nSeconds = 5 * stressTestMultiplier + private val sentTotal = atomic(0L) + private val receivedTotal = atomic(0L) + + @Test + fun testStress() = runMtTest { + TestBroadcastChannelKind.values().forEach { kind -> + println("--- BroadcastChannelSubStressTest $kind") + val broadcast = kind.create() + val sender = + launch(context = Dispatchers.Default + CoroutineName("Sender")) { + while (isActive) { + broadcast.send(sentTotal.incrementAndGet()) + } + } + val receiver = + launch(context = Dispatchers.Default + CoroutineName("Receiver")) { + var last = -1L + while (isActive) { + val channel = broadcast.openSubscription() + val i = channel.receive() + check(i >= last) { "Last was $last, got $i" } + if (!kind.isConflated) check(i != last) { "Last was $last, got it again" } + receivedTotal.incrementAndGet() + last = i + channel.cancel() + } + } + var prevSent = -1L + repeat(nSeconds) { sec -> + delay(1000) + val curSent = sentTotal.value + println("${sec + 1}: Sent $curSent, received ${receivedTotal.value}") + check(curSent > prevSent) { "Send stalled at $curSent events" } + prevSent = curSent + } + withTimeout(5000) { + sender.cancelAndJoin() + receiver.cancelAndJoin() + } + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt similarity index 89% rename from kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt index 86adfee049..3e38eec362 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt @@ -4,14 +4,14 @@ package kotlinx.coroutines.channels +import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.selects.* -import java.util.concurrent.atomic.* import kotlin.random.* import kotlin.test.* class ChannelCancelUndeliveredElementStressTest : TestBase() { - private val repeatTimes = 10_000 * stressTestMultiplier + private val repeatTimes = (if (isNative) 1_000 else 10_000) * stressTestMultiplier // total counters private var sendCnt = 0 @@ -25,10 +25,10 @@ class ChannelCancelUndeliveredElementStressTest : TestBase() { private var dSendExceptionCnt = 0 private var dTrySendFailedCnt = 0 private var dReceivedCnt = 0 - private val dUndeliveredCnt = AtomicInteger() + private val dUndeliveredCnt = atomic(0) @Test - fun testStress() = runTest { + fun testStress() = runMtTest { repeat(repeatTimes) { val channel = Channel(1) { dUndeliveredCnt.incrementAndGet() } val j1 = launch(Dispatchers.Default) { @@ -43,23 +43,23 @@ class ChannelCancelUndeliveredElementStressTest : TestBase() { joinAll(j1, j2) // All elements must be either received or undelivered (IN every run) - if (dSendCnt - dTrySendFailedCnt != dReceivedCnt + dUndeliveredCnt.get()) { + if (dSendCnt - dTrySendFailedCnt != dReceivedCnt + dUndeliveredCnt.value) { println(" Send: $dSendCnt") println("Send exception: $dSendExceptionCnt") println("trySend failed: $dTrySendFailedCnt") println(" Received: $dReceivedCnt") - println(" Undelivered: ${dUndeliveredCnt.get()}") + println(" Undelivered: ${dUndeliveredCnt.value}") error("Failed") } trySendFailedCnt += dTrySendFailedCnt receivedCnt += dReceivedCnt - undeliveredCnt += dUndeliveredCnt.get() + undeliveredCnt += dUndeliveredCnt.value // clear for next run dSendCnt = 0 dSendExceptionCnt = 0 dTrySendFailedCnt = 0 dReceivedCnt = 0 - dUndeliveredCnt.set(0) + dUndeliveredCnt.value = 0 } // Stats println(" Send: $sendCnt") diff --git a/kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt similarity index 70% rename from kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt index 2b3c05bcc6..5da00d2af2 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt @@ -1,29 +1,28 @@ /* - * Copyright 2016-2018 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.channels +import kotlinx.atomicfu.* import kotlinx.coroutines.* -import org.junit.Test -import java.util.concurrent.atomic.* import kotlin.test.* class ConflatedBroadcastChannelNotifyStressTest : TestBase() { private val nSenders = 2 private val nReceivers = 3 - private val nEvents = 500_000 * stressTestMultiplier + private val nEvents = (if (isNative) 5_000 else 500_000) * stressTestMultiplier private val timeLimit = 30_000L * stressTestMultiplier // 30 sec private val broadcast = ConflatedBroadcastChannel() - private val sendersCompleted = AtomicInteger() - private val receiversCompleted = AtomicInteger() - private val sentTotal = AtomicInteger() - private val receivedTotal = AtomicInteger() + private val sendersCompleted = atomic(0) + private val receiversCompleted = atomic(0) + private val sentTotal = atomic(0) + private val receivedTotal = atomic(0) @Test - fun testStressNotify()= runBlocking { + fun testStressNotify()= runMtTest { println("--- ConflatedBroadcastChannelNotifyStressTest") val senders = List(nSenders) { senderId -> launch(Dispatchers.Default + CoroutineName("Sender$senderId")) { @@ -57,7 +56,7 @@ class ConflatedBroadcastChannelNotifyStressTest : TestBase() { var seconds = 0 while (true) { delay(1000) - println("${++seconds}: Sent ${sentTotal.get()}, received ${receivedTotal.get()}") + println("${++seconds}: Sent ${sentTotal.value}, received ${receivedTotal.value}") } } try { @@ -71,13 +70,13 @@ class ConflatedBroadcastChannelNotifyStressTest : TestBase() { } progressJob.cancel() println("Tested with nSenders=$nSenders, nReceivers=$nReceivers") - println("Completed successfully ${sendersCompleted.get()} sender coroutines") - println("Completed successfully ${receiversCompleted.get()} receiver coroutines") - println(" Sent ${sentTotal.get()} events") - println(" Received ${receivedTotal.get()} events") - assertEquals(nSenders, sendersCompleted.get()) - assertEquals(nReceivers, receiversCompleted.get()) - assertEquals(nEvents, sentTotal.get()) + println("Completed successfully ${sendersCompleted.value} sender coroutines") + println("Completed successfully ${receiversCompleted.value} receiver coroutines") + println(" Sent ${sentTotal.value} events") + println(" Received ${receivedTotal.value} events") + assertEquals(nSenders, sendersCompleted.value) + assertEquals(nReceivers, receiversCompleted.value) + assertEquals(nEvents, sentTotal.value) } private suspend fun waitForEvent(): Int = diff --git a/kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt similarity index 89% rename from kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt index 3b5c36f9e9..f262e78f81 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt @@ -1,16 +1,16 @@ /* - * 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.flow import kotlinx.coroutines.* -import org.junit.* +import kotlin.test.* class CombineStressTest : TestBase() { @Test - public fun testCancellation() = runTest { + fun testCancellation() = runMtTest { withContext(Dispatchers.Default + CoroutineExceptionHandler { _, _ -> expectUnreached() }) { flow { expect(1) @@ -26,7 +26,7 @@ class CombineStressTest : TestBase() { } @Test - public fun testFailure() = runTest { + fun testFailure() = runMtTest { val innerIterations = 100 * stressTestMultiplierSqrt val outerIterations = 10 * stressTestMultiplierSqrt withContext(Dispatchers.Default + CoroutineExceptionHandler { _, _ -> expectUnreached() }) { diff --git a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt similarity index 83% rename from kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt rename to kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt index 269805f9b6..286ba751dd 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt @@ -1,17 +1,18 @@ /* - * Copyright 2016-2020 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.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* import kotlin.test.* class FlowCancellationTest : TestBase() { @Test - fun testEmitIsCooperative() = runTest { + fun testEmitIsCooperative() = runMtTest { val latch = Channel(1) val job = flow { expect(1) @@ -28,7 +29,7 @@ class FlowCancellationTest : TestBase() { } @Test - fun testIsActiveOnCurrentContext() = runTest { + fun testIsActiveOnCurrentContext() = runMtTest { val latch = Channel(1) val job = flow { expect(1) @@ -45,7 +46,7 @@ class FlowCancellationTest : TestBase() { } @Test - fun testFlowWithEmptyContext() = runTest { + fun testFlowWithEmptyContext() = runMtTest { expect(1) withEmptyContext { val flow = flow { @@ -59,4 +60,4 @@ class FlowCancellationTest : TestBase() { } finish(4) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt new file mode 100644 index 0000000000..f2fb41a589 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.random.* +import kotlin.test.* + +// A simplified version of StateFlowStressTest +class StateFlowCommonStressTest : TestBase() { + private val state = MutableStateFlow(0) + + @Test + fun testSingleEmitterAndCollector() = runMtTest { + var collected = 0L + val collector = launch(Dispatchers.Default) { + // collect, but abort and collect again after every 1000 values to stress allocation/deallocation + do { + val batchSize = Random.nextInt(1..1000) + var index = 0 + val cnt = state.onEach { value -> + // the first value in batch is allowed to repeat, but cannot go back + val ok = if (index++ == 0) value >= collected else value > collected + check(ok) { + "Values must be monotonic, but $value is not, was $collected" + } + collected = value + }.take(batchSize).map { 1 }.sum() + } while (cnt == batchSize) + } + + var current = 1L + val emitter = launch { + while (true) { + state.value = current++ + if (current % 1000 == 0L) yield() // make it cancellable + } + } + + delay(3000) + emitter.cancelAndJoin() + collector.cancelAndJoin() + assertTrue { current >= collected / 2 } + } +} diff --git a/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt new file mode 100644 index 0000000000..1e79709481 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +// A simplified version of StateFlowUpdateStressTest +class StateFlowUpdateCommonTest : TestBase() { + private val iterations = 100_000 * stressTestMultiplier + + @Test + fun testUpdate() = doTest { update { it + 1 } } + + @Test + fun testUpdateAndGet() = doTest { updateAndGet { it + 1 } } + + @Test + fun testGetAndUpdate() = doTest { getAndUpdate { it + 1 } } + + private fun doTest(increment: MutableStateFlow.() -> Unit) = runMtTest { + val flow = MutableStateFlow(0) + val j1 = launch(Dispatchers.Default) { + repeat(iterations / 2) { + flow.increment() + } + } + + repeat(iterations / 2) { + flow.increment() + } + + joinAll(j1) + assertEquals(iterations, flow.value) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt similarity index 97% rename from kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt rename to kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt index b9011448cd..7e85d495fc 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt @@ -1,10 +1,9 @@ /* - * 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.internal -import org.junit.Test import kotlin.test.* class LockFreeLinkedListTest { @@ -82,4 +81,4 @@ class LockFreeLinkedListTest { for (i in 0 until n) assertEquals(expected[i], actual[i], "item $i") assertEquals(expected.isEmpty(), list.isEmpty) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt b/kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt similarity index 65% rename from kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt index 200cdc09b0..29c6c34889 100644 --- a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt @@ -1,5 +1,5 @@ /* - * 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.selects @@ -11,61 +11,60 @@ import kotlin.test.* class SelectChannelStressTest: TestBase() { + // Running less iterations on native platforms because of some performance regression + private val iterations = (if (isNative) 1_000 else 1_000_000) * stressTestMultiplier + @Test - fun testSelectSendResourceCleanupArrayChannel() = runTest { + fun testSelectSendResourceCleanupArrayChannel() = runMtTest { val channel = Channel(1) - val n = 10_000_000 * stressTestMultiplier expect(1) channel.send(-1) // fill the buffer, so all subsequent sends cannot proceed - repeat(n) { i -> + repeat(iterations) { i -> select { channel.onSend(i) { expectUnreached() } default { expect(i + 2) } } } - finish(n + 2) + finish(iterations + 2) } @Test - fun testSelectReceiveResourceCleanupArrayChannel() = runTest { + fun testSelectReceiveResourceCleanupArrayChannel() = runMtTest { val channel = Channel(1) - val n = 10_000_000 * stressTestMultiplier expect(1) - repeat(n) { i -> + repeat(iterations) { i -> select { channel.onReceive { expectUnreached() } default { expect(i + 2) } } } - finish(n + 2) + finish(iterations + 2) } @Test - fun testSelectSendResourceCleanupRendezvousChannel() = runTest { + fun testSelectSendResourceCleanupRendezvousChannel() = runMtTest { val channel = Channel(Channel.RENDEZVOUS) - val n = 1_000_000 * stressTestMultiplier expect(1) - repeat(n) { i -> + repeat(iterations) { i -> select { channel.onSend(i) { expectUnreached() } default { expect(i + 2) } } } - finish(n + 2) + finish(iterations + 2) } @Test - fun testSelectReceiveResourceRendezvousChannel() = runTest { + fun testSelectReceiveResourceRendezvousChannel() = runMtTest { val channel = Channel(Channel.RENDEZVOUS) - val n = 1_000_000 * stressTestMultiplier expect(1) - repeat(n) { i -> + repeat(iterations) { i -> select { channel.onReceive { expectUnreached() } default { expect(i + 2) } } } - finish(n + 2) + finish(iterations + 2) } internal fun SelectBuilder.default(block: suspend () -> R) { diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt b/kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.kt similarity index 93% rename from kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.kt index 5489ea5d73..8f649c2fb8 100644 --- a/kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.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-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.selects diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt similarity index 73% rename from kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt index 027f3c514d..73b62aee2e 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt @@ -1,22 +1,49 @@ /* - * Copyright 2016-2018 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.sync import kotlinx.coroutines.* +import kotlinx.coroutines.exceptions.* import kotlinx.coroutines.selects.* import kotlin.test.* class MutexStressTest : TestBase() { + + private val n = (if (isNative) 1_000 else 10_000) * stressTestMultiplier + @Test - fun testStress() = runBlocking(Dispatchers.Default) { - val n = 1000 * stressTestMultiplier + fun testDefaultDispatcher() = runMtTest { testBody(Dispatchers.Default) } + + @Test + fun testSingleThreadContext() = runMtTest { + newSingleThreadContext("testSingleThreadContext").use { + testBody(it) + } + } + + @Test + fun testMultiThreadedContextWithSingleWorker() = runMtTest { + newFixedThreadPoolContext(1, "testMultiThreadedContextWithSingleWorker").use { + testBody(it) + } + } + + @Test + fun testMultiThreadedContext() = runMtTest { + newFixedThreadPoolContext(8, "testMultiThreadedContext").use { + testBody(it) + } + } + + @Suppress("SuspendFunctionOnCoroutineScope") + private suspend fun CoroutineScope.testBody(dispatcher: CoroutineDispatcher) { val k = 100 var shared = 0 val mutex = Mutex() val jobs = List(n) { - launch { + launch(dispatcher) { repeat(k) { mutex.lock() shared++ @@ -29,11 +56,11 @@ class MutexStressTest : TestBase() { } @Test - fun stressUnlockCancelRace() = runTest { + fun stressUnlockCancelRace() = runMtTest { val n = 10_000 * stressTestMultiplier val mutex = Mutex(true) // create a locked mutex newSingleThreadContext("SemaphoreStressTest").use { pool -> - repeat (n) { + repeat(n) { // Initially, we hold the lock and no one else can `lock`, // otherwise it's a bug. assertTrue(mutex.isLocked) @@ -59,11 +86,11 @@ class MutexStressTest : TestBase() { } @Test - fun stressUnlockCancelRaceWithSelect() = runTest { + fun stressUnlockCancelRaceWithSelect() = runMtTest { val n = 10_000 * stressTestMultiplier val mutex = Mutex(true) // create a locked mutex newSingleThreadContext("SemaphoreStressTest").use { pool -> - repeat (n) { + repeat(n) { // Initially, we hold the lock and no one else can `lock`, // otherwise it's a bug. assertTrue(mutex.isLocked) @@ -92,7 +119,7 @@ class MutexStressTest : TestBase() { } @Test - fun testShouldBeUnlockedOnCancellation() = runTest { + fun testShouldBeUnlockedOnCancellation() = runMtTest { val mutex = Mutex() val n = 1000 * stressTestMultiplier repeat(n) { diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt similarity index 69% rename from kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt rename to kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt index 2ceed64b95..c5f2038937 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt @@ -1,18 +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.sync import kotlinx.coroutines.* -import org.junit.Test +import kotlinx.coroutines.exceptions.* import kotlin.test.* class SemaphoreStressTest : TestBase() { + + private val iterations = (if (isNative) 1_000 else 10_000) * stressTestMultiplier + @Test - fun stressTestAsMutex() = runBlocking(Dispatchers.Default) { - val n = 10_000 * stressTestMultiplier + fun testStressTestAsMutex() = runMtTest { + val n = iterations val k = 100 var shared = 0 val semaphore = Semaphore(1) val jobs = List(n) { - launch { + launch(Dispatchers.Default) { repeat(k) { semaphore.acquire() shared++ @@ -25,12 +32,12 @@ class SemaphoreStressTest : TestBase() { } @Test - fun stressTest() = runBlocking(Dispatchers.Default) { - val n = 10_000 * stressTestMultiplier + fun testStress() = runMtTest { + val n = iterations val k = 100 val semaphore = Semaphore(10) val jobs = List(n) { - launch { + launch(Dispatchers.Default) { repeat(k) { semaphore.acquire() semaphore.release() @@ -41,12 +48,33 @@ class SemaphoreStressTest : TestBase() { } @Test - fun stressCancellation() = runBlocking(Dispatchers.Default) { - val n = 10_000 * stressTestMultiplier + fun testStressAsMutex() = runMtTest { + runBlocking(Dispatchers.Default) { + val n = iterations + val k = 100 + var shared = 0 + val semaphore = Semaphore(1) + val jobs = List(n) { + launch { + repeat(k) { + semaphore.acquire() + shared++ + semaphore.release() + } + } + } + jobs.forEach { it.join() } + assertEquals(n * k, shared) + } + } + + @Test + fun testStressCancellation() = runMtTest { + val n = iterations val semaphore = Semaphore(1) semaphore.acquire() repeat(n) { - val job = launch { + val job = launch(Dispatchers.Default) { semaphore.acquire() } yield() @@ -62,8 +90,8 @@ class SemaphoreStressTest : TestBase() { * the semaphore into an incorrect state where permits are leaked. */ @Test - fun stressReleaseCancelRace() = runTest { - val n = 10_000 * stressTestMultiplier + fun testStressReleaseCancelRace() = runMtTest { + val n = iterations val semaphore = Semaphore(1, 1) newSingleThreadContext("SemaphoreStressTest").use { pool -> repeat (n) { @@ -92,7 +120,7 @@ class SemaphoreStressTest : TestBase() { } @Test - fun testShouldBeUnlockedOnCancellation() = runTest { + fun testShouldBeUnlockedOnCancellation() = runMtTest { val semaphore = Semaphore(1) val n = 1000 * stressTestMultiplier repeat(n) { diff --git a/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt new file mode 100644 index 0000000000..0e239a42f7 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +public actual abstract class CloseableCoroutineDispatcher actual constructor() : CoroutineDispatcher() { + public actual abstract fun close() +} diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index a98ea9732d..95cb3c2964 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -12,7 +12,7 @@ private external val navigator: dynamic private const val UNDEFINED = "undefined" internal external val process: dynamic -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { +internal fun createDefaultDispatcher(): CoroutineDispatcher = when { // Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source. // It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md // "It's missing a few semantics, especially around origins, as well as MessageEvent source." diff --git a/kotlinx-coroutines-core/js/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/js/src/CoroutineExceptionHandlerImpl.kt index 54a65e10a6..a4d671dc65 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineExceptionHandlerImpl.kt @@ -6,6 +6,10 @@ package kotlinx.coroutines import kotlin.coroutines.* +internal actual fun initializeDefaultExceptionHandlers() { + // Do nothing +} + internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // log exception console.error(exception) diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 8d3bac3209..3eec5408cc 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -8,8 +8,22 @@ import kotlin.coroutines.* public actual object Dispatchers { public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false) + public actual val Main: MainCoroutineDispatcher + get() = injectedMainDispatcher ?: mainDispatcher public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined + + private val mainDispatcher = JsMainDispatcher(Default, false) + private var injectedMainDispatcher: MainCoroutineDispatcher? = null + + @PublishedApi + internal fun injectMain(dispatcher: MainCoroutineDispatcher) { + injectedMainDispatcher = dispatcher + } + + @PublishedApi + internal fun resetInjectedMain() { + injectedMainDispatcher = null + } } private class JsMainDispatcher( diff --git a/kotlinx-coroutines-core/js/src/EventLoop.kt b/kotlinx-coroutines-core/js/src/EventLoop.kt index b3a1364107..13c336969b 100644 --- a/kotlinx-coroutines-core/js/src/EventLoop.kt +++ b/kotlinx-coroutines-core/js/src/EventLoop.kt @@ -25,3 +25,5 @@ internal actual object DefaultExecutor { private fun unsupported(): Nothing = throw UnsupportedOperationException("runBlocking event loop is not supported") + +internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index 6ad7d41b15..603005d5a4 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -31,6 +31,11 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { abstract fun scheduleQueueProcessing() + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + return this + } + override fun dispatch(context: CoroutineContext, block: Runnable) { messageQueue.enqueue(block) } diff --git a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt index 0a1b03104e..71f652271a 100644 --- a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt @@ -16,3 +16,4 @@ internal class NoOpLock { internal actual fun subscriberList(): SubscribersList = CopyOnWriteList() internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet(expectedSize) + diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 147b31dc3e..d8c07f4e19 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -177,5 +177,5 @@ public open class LinkedListHead : LinkedListNode() { } // just a defensive programming -- makes sure that list head sentinel is never removed - public final override fun remove(): Boolean = throw UnsupportedOperationException() + public final override fun remove(): Nothing = throw UnsupportedOperationException() } diff --git a/kotlinx-coroutines-core/js/test/PromiseTest.kt b/kotlinx-coroutines-core/js/test/PromiseTest.kt index cc1297cd78..6049a908cf 100644 --- a/kotlinx-coroutines-core/js/test/PromiseTest.kt +++ b/kotlinx-coroutines-core/js/test/PromiseTest.kt @@ -16,7 +16,7 @@ class PromiseTest : TestBase() { val deferred = promise.asDeferred() assertEquals("OK", deferred.await()) } - + @Test fun testPromiseRejectedAsDeferred() = GlobalScope.promise { lateinit var promiseReject: (Throwable) -> Unit diff --git a/kotlinx-coroutines-core/js/test/TestBase.kt b/kotlinx-coroutines-core/js/test/TestBase.kt index cc7865ba07..c930c20030 100644 --- a/kotlinx-coroutines-core/js/test/TestBase.kt +++ b/kotlinx-coroutines-core/js/test/TestBase.kt @@ -8,10 +8,13 @@ import kotlin.js.* public actual val isStressTest: Boolean = false public actual val stressTestMultiplier: Int = 1 +public actual val stressTestMultiplierSqrt: Int = 1 @Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") public actual typealias TestResult = Promise +public actual val isNative = false + public actual open class TestBase actual constructor() { public actual val isBoundByJsTestTimeout = true private var actionIndex = 0 diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin index 397aaf67ac..6c1500ae6e 100644 Binary files a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin and b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin differ diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro new file mode 100644 index 0000000000..58509bdf0f --- /dev/null +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro @@ -0,0 +1,24 @@ +# When editing this file, update the following files as well: +# - META-INF/proguard/coroutines.pro +# - META-INF/com.android.tools/r8/coroutines.pro + +# ServiceLoader support +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembers class kotlinx.coroutines.** { + volatile ; +} + +# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater +-keepclassmembers class kotlin.coroutines.SafeContinuation { + volatile ; +} + +# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when +# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. +-dontwarn java.lang.instrument.ClassFileTransformer +-dontwarn sun.misc.SignalHandler +-dontwarn java.lang.instrument.Instrumentation +-dontwarn sun.misc.Signal diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro new file mode 100644 index 0000000000..8a89318229 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro @@ -0,0 +1,20 @@ +# When editing this file, update the following files as well: +# - META-INF/proguard/coroutines.pro +# - META-INF/com.android.tools/proguard/coroutines.pro + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembers class kotlinx.coroutines.** { + volatile ; +} + +# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater +-keepclassmembers class kotlin.coroutines.SafeContinuation { + volatile ; +} + +# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when +# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. +-dontwarn java.lang.instrument.ClassFileTransformer +-dontwarn sun.misc.SignalHandler +-dontwarn java.lang.instrument.Instrumentation +-dontwarn sun.misc.Signal diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro index 1a9ae1c7ff..628a113ecf 100644 --- a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro @@ -1,3 +1,7 @@ +# When editing this file, update the following files as well: +# - META-INF/com.android.tools/proguard/coroutines.pro +# - META-INF/com.android.tools/r8/coroutines.pro + # ServiceLoader support -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index edb4303198..8c4b62b19d 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.kt @@ -35,7 +35,7 @@ import kotlin.coroutines.* * @param block the coroutine code. */ @Throws(InterruptedException::class) -public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { +public actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } diff --git a/kotlinx-coroutines-core/jvm/src/CommonPool.kt b/kotlinx-coroutines-core/jvm/src/CommonPool.kt deleted file mode 100644 index 502630b010..0000000000 --- a/kotlinx-coroutines-core/jvm/src/CommonPool.kt +++ /dev/null @@ -1,143 +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 - -import java.util.concurrent.* -import java.util.concurrent.atomic.* -import kotlin.coroutines.* - -/** - * Represents common pool of shared threads as coroutine dispatcher for compute-intensive tasks. - * - * If there isn't a SecurityManager present it uses [java.util.concurrent.ForkJoinPool] when available, which implements - * efficient work-stealing algorithm for its queues, so every coroutine resumption is dispatched as a separate task even - * when it already executes inside the pool. When available, it wraps `ForkJoinPool.commonPool` and provides a similar - * shared pool where not. - * - * If there is a SecurityManager present (as would be if running inside a Java Web Start context) then a plain thread - * pool is created. This is to work around the fact that ForkJoinPool creates threads that cannot perform - * privileged actions. - */ -internal object CommonPool : ExecutorCoroutineDispatcher() { - - /** - * Name of the property that controls default parallelism level of [CommonPool]. - * If the property is not specified, `Runtime.getRuntime().availableProcessors() - 1` will be used instead (or `1` for single-core JVM). - * Note that until Java 10, if an application is run within a container, - * `Runtime.getRuntime().availableProcessors()` is not aware of container constraints and will return the real number of cores. - */ - private const val DEFAULT_PARALLELISM_PROPERTY_NAME = "kotlinx.coroutines.default.parallelism" - - override val executor: Executor - get() = pool ?: getOrCreatePoolSync() - - // Equals to -1 if not explicitly specified - private val requestedParallelism = run { - val property = Try { System.getProperty(DEFAULT_PARALLELISM_PROPERTY_NAME) } ?: return@run -1 - val parallelism = property.toIntOrNull() - if (parallelism == null || parallelism < 1) { - error("Expected positive number in $DEFAULT_PARALLELISM_PROPERTY_NAME, but has $property") - } - parallelism - } - - private val parallelism: Int - get() = requestedParallelism.takeIf { it > 0 } - ?: (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1) - - // For debug and tests - private var usePrivatePool = false - - @Volatile - private var pool: Executor? = null - - private inline fun Try(block: () -> T) = try { block() } catch (e: Throwable) { null } - - private fun createPool(): ExecutorService { - if (System.getSecurityManager() != null) return createPlainPool() - // Reflection on ForkJoinPool class so that it works on JDK 6 (which is absent there) - val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") } - ?: return createPlainPool() // Fallback to plain thread pool - // Try to use commonPool unless parallelism was explicitly specified or in debug privatePool mode - if (!usePrivatePool && requestedParallelism < 0) { - Try { fjpClass.getMethod("commonPool").invoke(null) as? ExecutorService } - ?.takeIf { isGoodCommonPool(fjpClass, it) } - ?.let { return it } - } - // Try to create private ForkJoinPool instance - Try { fjpClass.getConstructor(Int::class.java).newInstance(parallelism) as? ExecutorService } - ?. let { return it } - // Fallback to plain thread pool - return createPlainPool() - } - - /** - * Checks that this ForkJoinPool's parallelism is at least one to avoid pathological bugs. - */ - internal fun isGoodCommonPool(fjpClass: Class<*>, executor: ExecutorService): Boolean { - // We cannot use getParallelism, since it lies to us (always returns at least 1) - // So we submit a task and check that getPoolSize is at least one after that - // A broken FJP (that is configured for 0 parallelism) would not execute the task and - // would report its pool size as zero. - executor.submit {} - val actual = Try { fjpClass.getMethod("getPoolSize").invoke(executor) as? Int } - ?: return false - return actual >= 1 - } - - private fun createPlainPool(): ExecutorService { - val threadId = AtomicInteger() - return Executors.newFixedThreadPool(parallelism) { - Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true } - } - } - - @Synchronized - private fun getOrCreatePoolSync(): Executor = - pool ?: createPool().also { pool = it } - - override fun dispatch(context: CoroutineContext, block: Runnable) { - try { - (pool ?: getOrCreatePoolSync()).execute(wrapTask(block)) - } catch (e: RejectedExecutionException) { - unTrackTask() - // CommonPool only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. - DefaultExecutor.enqueue(block) - } - } - - // used for tests - @Synchronized - internal fun usePrivatePool() { - shutdown(0) - usePrivatePool = true - pool = null - } - - // used for tests - @Synchronized - internal fun shutdown(timeout: Long) { - (pool as? ExecutorService)?.apply { - shutdown() - if (timeout > 0) - awaitTermination(timeout, TimeUnit.MILLISECONDS) - shutdownNow().forEach { DefaultExecutor.enqueue(it) } - } - pool = Executor { throw RejectedExecutionException("CommonPool was shutdown") } - } - - // used for tests - @Synchronized - internal fun restore() { - shutdown(0) - usePrivatePool = false - pool = null - } - - override fun toString(): String = "CommonPool" - - override fun close(): Unit = error("Close cannot be invoked on CommonPool") -} diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt index e91bb9fd21..d562207f8b 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt @@ -5,23 +5,9 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* -import kotlinx.coroutines.scheduling.* import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.CoroutineStackFrame -internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler" - -internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value -> - when (value) { - null, "", "on" -> true - "off" -> false - else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'") - } -} - -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = - if (useCoroutinesScheduler) DefaultScheduler else CommonPool - /** * Creates context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher nor * [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on). @@ -30,12 +16,31 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = */ @ExperimentalCoroutinesApi public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { - val combined = coroutineContext + context + val combined = coroutineContext.foldCopiesForChildCoroutine() + context val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) debug + Dispatchers.Default else debug } +/** + * Returns the [CoroutineContext] for a child coroutine to inherit. + * + * If any [CopyableThreadContextElement] is in the [this], calls + * [CopyableThreadContextElement.copyForChildCoroutine] on each, returning a new [CoroutineContext] + * by folding the returned copied elements into [this]. + * + * Returns [this] if `this` has zero [CopyableThreadContextElement] in it. + */ +private fun CoroutineContext.foldCopiesForChildCoroutine(): CoroutineContext { + val hasToCopy = fold(false) { result, it -> + result || it is CopyableThreadContextElement<*> + } + if (!hasToCopy) return this + return fold(EmptyCoroutineContext) { combined, it -> + combined + if (it is CopyableThreadContextElement<*>) it.copyForChildCoroutine() else it + } +} + /** * Executes a block using a given coroutine context. */ @@ -153,6 +158,7 @@ internal actual val CoroutineContext.coroutineName: String? get() { private const val DEBUG_THREAD_NAME_SEPARATOR = " @" +@IgnoreJreRequirement // desugared hashcode implementation internal data class CoroutineId( val id: Long ) : ThreadContextElement, AbstractCoroutineContextElement(CoroutineId) { diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt index 6d06969293..4c8c81b8db 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt @@ -22,6 +22,12 @@ private val handlers: List = ServiceLoader.load( CoroutineExceptionHandler::class.java.classLoader ).iterator().asSequence().toList() +internal actual fun initializeDefaultExceptionHandlers() { + // Load CEH and handlers + CoroutineExceptionHandler +} + + internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension handlers for (handler in handlers) { diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index fe020276e5..7b0810c2a1 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -4,10 +4,25 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import java.util.concurrent.* import kotlin.coroutines.* -internal actual val DefaultDelay: Delay = DefaultExecutor +internal actual val DefaultDelay: Delay = initializeDefaultDelay() + +private val defaultMainDelayOptIn = systemProp("kotlinx.coroutines.main.delay", true) + +private fun initializeDefaultDelay(): Delay { + // Opt-out flag + if (!defaultMainDelayOptIn) return DefaultExecutor + val main = Dispatchers.Main + /* + * When we already are working with UI and Main threads, it makes + * no sense to create a separate thread with timer that cannot be controller + * by the UI runtime. + */ + return if (main.isMissing() || main !is Delay) DefaultExecutor else main +} @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { @@ -17,13 +32,13 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { incrementUseCount() // this event loop is never completed } - private const val DEFAULT_KEEP_ALIVE = 1000L // in milliseconds + private const val DEFAULT_KEEP_ALIVE_MS = 1000L // in milliseconds private val KEEP_ALIVE_NANOS = TimeUnit.MILLISECONDS.toNanos( try { - java.lang.Long.getLong("kotlinx.coroutines.DefaultExecutor.keepAlive", DEFAULT_KEEP_ALIVE) + java.lang.Long.getLong("kotlinx.coroutines.DefaultExecutor.keepAlive", DEFAULT_KEEP_ALIVE_MS) } catch (e: SecurityException) { - DEFAULT_KEEP_ALIVE + DEFAULT_KEEP_ALIVE_MS }) @Suppress("ObjectPropertyName") @@ -37,15 +52,39 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { private const val ACTIVE = 1 private const val SHUTDOWN_REQ = 2 private const val SHUTDOWN_ACK = 3 + private const val SHUTDOWN = 4 @Volatile private var debugStatus: Int = FRESH + private val isShutDown: Boolean get() = debugStatus == SHUTDOWN + private val isShutdownRequested: Boolean get() { val debugStatus = debugStatus return debugStatus == SHUTDOWN_REQ || debugStatus == SHUTDOWN_ACK } + actual override fun enqueue(task: Runnable) { + if (isShutDown) shutdownError() + super.enqueue(task) + } + + override fun reschedule(now: Long, delayedTask: DelayedTask) { + // Reschedule on default executor can only be invoked after Dispatchers.shutdown + shutdownError() + } + + private fun shutdownError() { + throw RejectedExecutionException("DefaultExecutor was shut down. " + + "This error indicates that Dispatchers.shutdown() was invoked prior to completion of exiting coroutines, leaving coroutines in incomplete state. " + + "Please refer to Dispatchers.shutdown documentation for more details") + } + + override fun shutdown() { + debugStatus = SHUTDOWN + super.shutdown() + } + /** * All event loops are using DefaultExecutor#invokeOnTimeout to avoid livelock on * ``` @@ -118,9 +157,8 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { return true } - // used for tests - @Synchronized - fun shutdown(timeout: Long) { + @Synchronized // used _only_ for tests + fun shutdownForTests(timeout: Long) { val deadline = System.currentTimeMillis() + timeout if (!isShutdownRequested) debugStatus = SHUTDOWN_REQ // loop while there is anything to do immediately or deadline passes diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index d82598eab4..4b1b03337d 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -21,7 +21,7 @@ public const val IO_PARALLELISM_PROPERTY_NAME: String = "kotlinx.coroutines.io.p public actual object Dispatchers { /** * The default [CoroutineDispatcher] that is used by all standard builders like - * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc + * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc. * if no dispatcher nor any other [ContinuationInterceptor] is specified in their context. * * It is backed by a shared pool of threads on JVM. By default, the maximal level of parallelism used @@ -29,7 +29,7 @@ public actual object Dispatchers { * Level of parallelism X guarantees that no more than X tasks can be executed in this dispatcher in parallel. */ @JvmStatic - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() + public actual val Default: CoroutineDispatcher = DefaultScheduler /** * A coroutine dispatcher that is confined to the Main thread operating with UI objects. @@ -86,7 +86,7 @@ public actual object Dispatchers { * Note that 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 * an optional [CoroutineStart] parameter in coroutine builders like - * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to the + * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to * the value of [CoroutineStart.UNDISPATCHED]. */ @JvmStatic @@ -100,14 +100,30 @@ public actual object Dispatchers { * "`kotlinx.coroutines.io.parallelism`" ([IO_PARALLELISM_PROPERTY_NAME]) system property. * It defaults to the limit of 64 threads or the number of cores (whichever is larger). * - * Moreover, the maximum configurable number of threads is capped by the - * `kotlinx.coroutines.scheduler.max.pool.size` system property. - * If you need a higher number of parallel threads, - * you should use a custom dispatcher backed by your own thread pool. + * ### Elasticity for limited parallelism + * + * `Dispatchers.IO` has a unique property of elasticity: its views + * obtained with [CoroutineDispatcher.limitedParallelism] are + * not restricted by the `Dispatchers.IO` parallelism. Conceptually, there is + * a dispatcher backed by an unlimited pool of threads, and both `Dispatchers.IO` + * and views of `Dispatchers.IO` are actually views of that dispatcher. In practice + * this means that, despite not abiding by `Dispatchers.IO`'s parallelism + * restrictions, its views share threads and resources with it. + * + * In the following example + * ``` + * // 100 threads for MySQL connection + * val myMysqlDbDispatcher = Dispatchers.IO.limitedParallelism(100) + * // 60 threads for MongoDB connection + * val myMongoDbDispatcher = Dispatchers.IO.limitedParallelism(60) + * ``` + * the system may have up to `64 + 100 + 60` threads dedicated to blocking tasks during peak loads, + * but during its steady state there is only a small number of threads shared + * among `Dispatchers.IO`, `myMysqlDbDispatcher` and `myMongoDbDispatcher`. * * ### Implementation note * - * This dispatcher shares threads with the [Default][Dispatchers.Default] dispatcher, so using + * This dispatcher and its views share threads with the [Default][Dispatchers.Default] dispatcher, so using * `withContext(Dispatchers.IO) { ... }` when already running on the [Default][Dispatchers.Default] * dispatcher does not lead to an actual switching to another thread — typically execution * continues in the same thread. @@ -115,5 +131,32 @@ public actual object Dispatchers { * during operations over IO dispatcher. */ @JvmStatic - public val IO: CoroutineDispatcher = DefaultScheduler.IO + public val IO: CoroutineDispatcher = DefaultIoScheduler + + /** + * Shuts down built-in dispatchers, such as [Default] and [IO], + * stopping all the threads associated with them and making them reject all new tasks. + * Dispatcher used as a fallback for time-related operations (`delay`, `withTimeout`) + * and to handle rejected tasks from other dispatchers is also shut down. + * + * This is a **delicate** API. It is not supposed to be called from a general + * application-level code and its invocation is irreversible. + * The invocation of shutdown affects most of the coroutines machinery and + * leaves the coroutines framework in an inoperable state. + * The shutdown method should only be invoked when there are no pending tasks or active coroutines. + * Otherwise, the behavior is unspecified: the call to `shutdown` may throw an exception without completing + * the shutdown, or it may finish successfully, but the remaining jobs will be in a permanent dormant state, + * never completing nor executing. + * + * The main goal of the shutdown is to stop all background threads associated with the coroutines + * framework in order to make kotlinx.coroutines classes unloadable by Java Virtual Machine. + * It is only recommended to be used in containerized environments (OSGi, Gradle plugins system, + * IDEA plugins) at the end of the container lifecycle. + */ + @DelicateCoroutinesApi + public fun shutdown() { + DefaultExecutor.shutdown() + // Also shuts down Dispatchers.IO + DefaultScheduler.shutdown() + } } diff --git a/kotlinx-coroutines-core/jvm/src/EventLoop.kt b/kotlinx-coroutines-core/jvm/src/EventLoop.kt index e49c7dc7e1..1ee651aa41 100644 --- a/kotlinx-coroutines-core/jvm/src/EventLoop.kt +++ b/kotlinx-coroutines-core/jvm/src/EventLoop.kt @@ -13,8 +13,7 @@ internal actual abstract class EventLoopImplPlatform: EventLoop() { unpark(thread) } - protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) { - assert { this !== DefaultExecutor } // otherwise default execution was shutdown with tasks in it (cannot be) + protected actual open fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) { DefaultExecutor.schedule(now, delayedTask) } } @@ -47,3 +46,5 @@ internal actual fun createEventLoop(): EventLoop = BlockingEventLoop(Thread.curr @InternalCoroutinesApi public fun processNextEventInCurrentThread(): Long = ThreadLocalEventLoop.currentOrNull()?.processNextEvent() ?: Long.MAX_VALUE + +internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index 7ea3cc6874..4e98e7bc98 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -37,6 +37,9 @@ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closea public abstract override fun close() } +@ExperimentalCoroutinesApi +public actual typealias CloseableCoroutineDispatcher = ExecutorCoroutineDispatcher + /** * Converts an instance of [ExecutorService] to an implementation of [ExecutorCoroutineDispatcher]. * diff --git a/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt b/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt index 37fd70a23e..1b825cef01 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt @@ -77,6 +77,69 @@ public interface ThreadContextElement : CoroutineContext.Element { public fun restoreThreadContext(context: CoroutineContext, oldState: S) } +/** + * A [ThreadContextElement] copied whenever a child coroutine inherits a context containing it. + * + * When an API uses a _mutable_ `ThreadLocal` for consistency, a [CopyableThreadContextElement] + * can give coroutines "coroutine-safe" write access to that `ThreadLocal`. + * + * A write made to a `ThreadLocal` with a matching [CopyableThreadContextElement] by a coroutine + * will be visible to _itself_ and any child coroutine launched _after_ that write. + * + * Writes will not be visible to the parent coroutine, peer coroutines, or coroutines that happen + * to use the same thread. Writes made to the `ThreadLocal` by the parent coroutine _after_ + * launching a child coroutine will not be visible to that child coroutine. + * + * This can be used to allow a coroutine to use a mutable ThreadLocal API transparently and + * correctly, regardless of the coroutine's structured concurrency. + * + * This example adapts a `ThreadLocal` method trace to be "coroutine local" while the method trace + * is in a coroutine: + * + * ``` + * class TraceContextElement(val traceData: TraceData?) : CopyableThreadContextElement { + * companion object Key : CoroutineContext.Key + * override val key: CoroutineContext.Key + * get() = Key + * + * override fun updateThreadContext(context: CoroutineContext): TraceData? { + * val oldState = traceThreadLocal.get() + * traceThreadLocal.set(data) + * return oldState + * } + * + * override fun restoreThreadContext(context: CoroutineContext, oldData: TraceData?) { + * traceThreadLocal.set(oldState) + * } + * + * override fun copyForChildCoroutine(): CopyableThreadContextElement { + * // Copy from the ThreadLocal source of truth at child coroutine launch time. This makes + * // ThreadLocal writes between resumption of the parent coroutine and the launch of the + * // child coroutine visible to the child. + * return CopyForChildCoroutineElement(traceThreadLocal.get()) + * } + * } + * ``` + * + * A coroutine using this mechanism can safely call Java code that assumes it's called using a + * `Thread`. + */ +@ExperimentalCoroutinesApi +public interface CopyableThreadContextElement : ThreadContextElement { + + /** + * Returns a [CopyableThreadContextElement] to replace `this` `CopyableThreadContextElement` in the child + * coroutine's context that is under construction. + * + * This function is called on the element each time a new coroutine inherits a context containing it, + * and the returned value is folded into the context given to the child. + * + * Since this method is called whenever a new coroutine is launched in a context containing this + * [CopyableThreadContextElement], implementations are performance-sensitive. + */ + public fun copyForChildCoroutine(): CopyableThreadContextElement +} + /** * Wraps [ThreadLocal] into [ThreadContextElement]. The resulting [ThreadContextElement] * maintains the given [value] of the given [ThreadLocal] for coroutine regardless of the actual thread its is resumed on. diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index 99e3b46cce..dc0b7e29c5 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -17,11 +17,12 @@ import java.util.concurrent.atomic.AtomicInteger * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. * - * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools - * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed - * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due - * to coroutine-oriented scheduling policy and thread-switch minimization. - * See [issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) for details. + * This is a **delicate** API. The result of this method is a closeable resource with the + * associated native resources (threads). It should not be allocated in place, + * should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint. + * If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher, + * it is recommended to use [CoroutineDispatcher.limitedParallelism] instead. + * * If you need a completely separate thread-pool with scheduling policy that is based on the standard * JDK executors, use the following expression: * `Executors.newSingleThreadExecutor().asCoroutineDispatcher()`. @@ -29,8 +30,8 @@ import java.util.concurrent.atomic.AtomicInteger * * @param name the base name of the created thread. */ -@ObsoleteCoroutinesApi -public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = +@DelicateCoroutinesApi +public actual fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = newFixedThreadPoolContext(1, name) /** @@ -43,11 +44,12 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. * - * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools - * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed - * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due - * to coroutine-oriented scheduling policy and thread-switch minimization. - * See [issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) for details. + * This is a **delicate** API. The result of this method is a closeable resource with the + * associated native resources (threads). It should not be allocated in place, + * should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint. + * If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher, + * it is recommended to use [CoroutineDispatcher.limitedParallelism] instead. + * * If you need a completely separate thread-pool with scheduling policy that is based on the standard * JDK executors, use the following expression: * `Executors.newFixedThreadPool().asCoroutineDispatcher()`. @@ -56,8 +58,8 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * @param nThreads the number of threads. * @param name the base name of the created threads. */ -@ObsoleteCoroutinesApi -public fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher { +@DelicateCoroutinesApi +public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher { require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" } val threadNo = AtomicInteger() val executor = Executors.newScheduledThreadPool(nThreads) { runnable -> diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index 4657bc7d1e..748f52833f 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -163,7 +163,7 @@ private class LazyActorCoroutine( return super.send(element) } - @Suppress("DEPRECATION") + @Suppress("DEPRECATION", "DEPRECATION_ERROR") override fun offer(element: E): Boolean { start() return super.offer(element) diff --git a/kotlinx-coroutines-core/jvm/src/channels/Channels.kt b/kotlinx-coroutines-core/jvm/src/channels/Channels.kt index 0df8278b77..d7454027fb 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Channels.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Channels.kt @@ -43,11 +43,11 @@ import kotlinx.coroutines.* * ``` */ @Deprecated( - level = DeprecationLevel.WARNING, + level = DeprecationLevel.ERROR, message = "Deprecated in the favour of 'trySendBlocking'. " + "Consider handling the result of 'trySendBlocking' explicitly and rethrow exception if necessary", replaceWith = ReplaceWith("trySendBlocking(element)") -) +) // WARNING in 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0 public fun SendChannel.sendBlocking(element: E) { // fast path if (trySend(element).isSuccess) diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index 8ef0c18217..4b0ce3f31e 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -4,12 +4,13 @@ package kotlinx.coroutines.debug -import kotlinx.coroutines.debug.internal.DebugProbesImpl +import android.annotation.* +import kotlinx.coroutines.debug.internal.* +import org.codehaus.mojo.animal_sniffer.* import sun.misc.* import java.lang.instrument.* import java.lang.instrument.ClassFileTransformer import java.security.* -import android.annotation.* /* * This class is loaded if and only if kotlinx-coroutines-core was used as -javaagent argument, @@ -17,17 +18,16 @@ import android.annotation.* */ @Suppress("unused") @SuppressLint("all") +@IgnoreJRERequirement // Never touched on Android internal object AgentPremain { - public var isInstalledStatically = false - private val enableCreationStackTraces = runCatching { System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() }.getOrNull() ?: DebugProbesImpl.enableCreationStackTraces @JvmStatic - public fun premain(args: String?, instrumentation: Instrumentation) { - isInstalledStatically = true + fun premain(args: String?, instrumentation: Instrumentation) { + AgentInstallationType.isInstalledStatically = true instrumentation.addTransformer(DebugProbesTransformer) DebugProbesImpl.enableCreationStackTraces = enableCreationStackTraces DebugProbesImpl.install() @@ -52,7 +52,7 @@ internal object AgentPremain { * on the fly (-> get rid of ASM dependency). * You can verify its content either by using javap on it or looking at out integration test module. */ - isInstalledStatically = true + AgentInstallationType.isInstalledStatically = true return loader.getResourceAsStream("DebugProbesKt.bin").readBytes() } } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt new file mode 100644 index 0000000000..0e9b26ce02 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +/** + * Object used to differentiate between agent installed statically or dynamically. + * This is done in a separate object so [DebugProbesImpl] can check for static installation + * without having to depend on [kotlinx.coroutines.debug.AgentPremain], which is not compatible with Android. + * Otherwise, access to `AgentPremain.isInstalledStatically` triggers the load of its internal `ClassFileTransformer` + * that is not available on Android. + */ +internal object AgentInstallationType { + internal var isInstalledStatically = false +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt index ffb9c2dae6..b0b2660517 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt @@ -73,7 +73,7 @@ internal class ConcurrentWeakMap( while (true) { cleanWeakRef(weakRefQueue.remove() as HashedWeakRef<*>) } - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { Thread.currentThread().interrupt() } } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 05befc1a59..d358d49d1e 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.internal.ScopeCoroutine import java.io.* @@ -82,7 +81,7 @@ internal object DebugProbesImpl { public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return startWeakRefCleanerThread() - if (AgentPremain.isInstalledStatically) return + if (AgentInstallationType.isInstalledStatically) return dynamicAttach?.invoke(true) // attach } @@ -92,7 +91,7 @@ internal object DebugProbesImpl { stopWeakRefCleanerThread() capturedCoroutinesMap.clear() callerInfoCache.clear() - if (AgentPremain.isInstalledStatically) return + if (AgentInstallationType.isInstalledStatically) return dynamicAttach?.invoke(false) // detach } @@ -103,8 +102,10 @@ internal object DebugProbesImpl { } private fun stopWeakRefCleanerThread() { - weakRefCleanerThread?.interrupt() + val thread = weakRefCleanerThread ?: return weakRefCleanerThread = null + thread.interrupt() + thread.join() } public fun hierarchyToString(job: Job): String = coroutineStateLock.write { @@ -149,10 +150,11 @@ internal object DebugProbesImpl { * Private method that dumps coroutines so that different public-facing method can use * to produce different result types. */ - private inline fun dumpCoroutinesInfoImpl(create: (CoroutineOwner<*>, CoroutineContext) -> R): List = + private inline fun dumpCoroutinesInfoImpl(crossinline create: (CoroutineOwner<*>, CoroutineContext) -> R): List = coroutineStateLock.write { check(isInstalled) { "Debug probes are not installed" } capturedCoroutines + .asSequence() // Stable ordering of coroutines by their sequence number .sortedBy { it.info.sequenceNumber } // Leave in the dump only the coroutines that were not collected while we were dumping them @@ -160,9 +162,86 @@ internal object DebugProbesImpl { // Fuse map and filter into one operation to save an inline if (owner.isFinished()) null else owner.info.context?.let { context -> create(owner, context) } + }.toList() + } + + /* + * This method optimises the number of packages sent by the IDEA debugger + * to a client VM to speed up fetching of coroutine information. + * + * The return value is an array of objects, which consists of four elements: + * 1) A string in a JSON format that stores information that is needed to display + * every coroutine in the coroutine panel in the IDEA debugger. + * 2) An array of last observed threads. + * 3) An array of last observed frames. + * 4) An array of DebugCoroutineInfo. + * + * ### Implementation note + * For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference` + * that does a roundtrip to client VM for *each* field or property read. + * To avoid that, we serialize most of the critical for UI data into a primitives + * to save an exponential number of roundtrips. + * + * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC. + */ + @OptIn(ExperimentalStdlibApi::class) + public fun dumpCoroutinesInfoAsJsonAndReferences(): Array { + val coroutinesInfo = dumpCoroutinesInfo() + val size = coroutinesInfo.size + val lastObservedThreads = ArrayList(size) + val lastObservedFrames = ArrayList(size) + val coroutinesInfoAsJson = ArrayList(size) + for (info in coroutinesInfo) { + val context = info.context + val name = context[CoroutineName.Key]?.name?.toStringWithQuotes() + val dispatcher = context[CoroutineDispatcher.Key]?.toStringWithQuotes() + coroutinesInfoAsJson.add( + """ + { + "name": $name, + "id": ${context[CoroutineId.Key]?.id}, + "dispatcher": $dispatcher, + "sequenceNumber": ${info.sequenceNumber}, + "state": "${info.state}" + } + """.trimIndent() + ) + lastObservedFrames.add(info.lastObservedFrame) + lastObservedThreads.add(info.lastObservedThread) + } + + return arrayOf( + "[${coroutinesInfoAsJson.joinToString()}]", + lastObservedThreads.toTypedArray(), + lastObservedFrames.toTypedArray(), + coroutinesInfo.toTypedArray() + ) + } + + /* + * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC. + */ + public fun enhanceStackTraceWithThreadDumpAsJson(info: DebugCoroutineInfo): String { + val stackTraceElements = enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace) + val stackTraceElementsInfoAsJson = mutableListOf() + for (element in stackTraceElements) { + stackTraceElementsInfoAsJson.add( + """ + { + "declaringClass": "${element.className}", + "methodName": "${element.methodName}", + "fileName": ${element.fileName?.toStringWithQuotes()}, + "lineNumber": ${element.lineNumber} } + """.trimIndent() + ) } + return "[${stackTraceElementsInfoAsJson.joinToString()}]" + } + + private fun Any.toStringWithQuotes() = "\"$this\"" + /* * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3. */ diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt new file mode 100644 index 0000000000..de15225266 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import java.lang.reflect.* +import java.util.* +import java.util.concurrent.locks.* +import kotlin.concurrent.* + +private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1) +private typealias Ctor = (Throwable) -> Throwable? + +private val ctorCache = try { + if (ANDROID_DETECTED) WeakMapCtorCache + else ClassValueCtorCache +} catch (e: Throwable) { + // Fallback on Java 6 or exotic setups + WeakMapCtorCache +} + +@Suppress("UNCHECKED_CAST") +internal fun tryCopyException(exception: E): E? { + // Fast path for CopyableThrowable + if (exception is CopyableThrowable<*>) { + return runCatching { exception.createCopy() as E? }.getOrNull() + } + return ctorCache.get(exception.javaClass).invoke(exception) as E? +} + +private fun createConstructor(clz: Class): Ctor { + val nullResult: Ctor = { null } // Pre-cache class + // Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors) + if (throwableFields != clz.fieldsCountOrDefault(0)) return nullResult + /* + * Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message). + * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. + */ + val constructors = clz.constructors.sortedByDescending { it.parameterTypes.size } + for (constructor in constructors) { + val result = createSafeConstructor(constructor) + if (result != null) return result + } + return nullResult +} + +private fun createSafeConstructor(constructor: Constructor<*>): Ctor? { + val p = constructor.parameterTypes + return when (p.size) { + 2 -> when { + p[0] == String::class.java && p[1] == Throwable::class.java -> + safeCtor { e -> constructor.newInstance(e.message, e) as Throwable } + else -> null + } + 1 -> when (p[0]) { + Throwable::class.java -> + safeCtor { e -> constructor.newInstance(e) as Throwable } + String::class.java -> + safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } } + else -> null + } + 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } } + else -> null + } +} + +private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor = + { e -> runCatching { block(e) }.getOrNull() } + +private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = + kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue) + +private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int { + val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) } + val totalFields = accumulator + fieldsCount + val superClass = superclass ?: return totalFields + return superClass.fieldsCount(totalFields) +} + +internal abstract class CtorCache { + abstract fun get(key: Class): Ctor +} + +private object WeakMapCtorCache : CtorCache() { + private val cacheLock = ReentrantReadWriteLock() + private val exceptionCtors: WeakHashMap, Ctor> = WeakHashMap() + + override fun get(key: Class): Ctor { + cacheLock.read { exceptionCtors[key]?.let { return it } } + cacheLock.write { + exceptionCtors[key]?.let { return it } + return createConstructor(key).also { exceptionCtors[key] = it } + } + } +} + +@IgnoreJreRequirement +private object ClassValueCtorCache : CtorCache() { + private val cache = object : ClassValue() { + override fun computeValue(type: Class<*>?): Ctor { + @Suppress("UNCHECKED_CAST") + return createConstructor(type as Class) + } + } + + override fun get(key: Class): Ctor = cache.get(key) +} diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt deleted file mode 100644 index 60328ebdc0..0000000000 --- a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt +++ /dev/null @@ -1,81 +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.internal - -import kotlinx.coroutines.* -import java.lang.reflect.* -import java.util.* -import java.util.concurrent.locks.* -import kotlin.concurrent.* - -private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1) -private val cacheLock = ReentrantReadWriteLock() -private typealias Ctor = (Throwable) -> Throwable? -// Replace it with ClassValue when Java 6 support is over -private val exceptionCtors: WeakHashMap, Ctor> = WeakHashMap() - -@Suppress("UNCHECKED_CAST") -internal fun tryCopyException(exception: E): E? { - // Fast path for CopyableThrowable - if (exception is CopyableThrowable<*>) { - return runCatching { exception.createCopy() as E? }.getOrNull() - } - // Use cached ctor if found - cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor -> - return cachedCtor(exception) as E? - } - /* - * Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors) - */ - if (throwableFields != exception.javaClass.fieldsCountOrDefault(0)) { - cacheLock.write { exceptionCtors[exception.javaClass] = { null } } - return null - } - /* - * Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message). - * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. - */ - var ctor: Ctor? = null - val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size } - for (constructor in constructors) { - ctor = createConstructor(constructor) - if (ctor != null) break - } - // Store the resulting ctor to cache - cacheLock.write { exceptionCtors[exception.javaClass] = ctor ?: { null } } - return ctor?.invoke(exception) as E? -} - -private fun createConstructor(constructor: Constructor<*>): Ctor? { - val p = constructor.parameterTypes - return when (p.size) { - 2 -> when { - p[0] == String::class.java && p[1] == Throwable::class.java -> - safeCtor { e -> constructor.newInstance(e.message, e) as Throwable } - else -> null - } - 1 -> when (p[0]) { - Throwable::class.java -> - safeCtor { e -> constructor.newInstance(e) as Throwable } - String::class.java -> - safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } } - else -> null - } - 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } } - else -> null - } -} - -private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor = - { e -> runCatching { block(e) }.getOrNull() } - -private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue) - -private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int { - val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) } - val totalFields = accumulator + fieldsCount - val superClass = superclass ?: return totalFields - return superClass.fieldsCount(totalFields) -} diff --git a/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt b/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt new file mode 100644 index 0000000000..41707f7b89 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +@Suppress("ACTUAL_WITHOUT_EXPECT") // Not the same name to WA the bug in the compiler +internal actual typealias IgnoreJreRequirement = org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 2d447413b8..2da633a6b6 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -93,6 +93,9 @@ private class MissingMainCoroutineDispatcher( override fun isDispatchNeeded(context: CoroutineContext): Boolean = missing() + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = + missing() + override suspend fun delay(time: Long) = missing() diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 84d9d9f8df..41f759ce8a 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -968,7 +968,6 @@ internal class CoroutineScheduler( * Checks if the thread is part of a thread pool that supports coroutines. * This function is needed for integration with BlockHound. */ -@Suppress("UNUSED") @JvmName("isSchedulerWorker") internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker @@ -976,7 +975,6 @@ internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Wo * Checks if the thread is running a CPU-bound task. * This function is needed for integration with BlockHound. */ -@Suppress("UNUSED") @JvmName("mayNotBlock") internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker && thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt new file mode 100644 index 0000000000..e5defbafcd --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused") + +package kotlinx.coroutines.scheduling +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import java.util.concurrent.* +import kotlin.coroutines.* + +/** + * This API was "public @InternalApi" and leaked into Ktor enabled-by-default sources. + * Since then, we refactored scheduler sources and its API and decided to get rid of it in + * its current shape. + * + * To preserve backwards compatibility with Ktor 1.x, previous version of the code is + * extracted here as is and isolated from the rest of code base, so R8 can get rid of it. + * + * It should be removed after Ktor 3.0.0 (EOL of Ktor 1.x) around 2022. + */ +@PublishedApi +internal open class ExperimentalCoroutineDispatcher( + private val corePoolSize: Int, + private val maxPoolSize: Int, + private val idleWorkerKeepAliveNs: Long, + private val schedulerName: String = "CoroutineScheduler" +) : ExecutorCoroutineDispatcher() { + public constructor( + corePoolSize: Int = CORE_POOL_SIZE, + maxPoolSize: Int = MAX_POOL_SIZE, + schedulerName: String = DEFAULT_SCHEDULER_NAME + ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName) + + @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN) + public constructor( + corePoolSize: Int = CORE_POOL_SIZE, + maxPoolSize: Int = MAX_POOL_SIZE + ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS) + + override val executor: Executor + get() = coroutineScheduler + + // This is variable for test purposes, so that we can reinitialize from clean state + private var coroutineScheduler = createScheduler() + + override fun dispatch(context: CoroutineContext, block: Runnable): Unit = + try { + coroutineScheduler.dispatch(block) + } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. + DefaultExecutor.dispatch(context, block) + } + + override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = + try { + coroutineScheduler.dispatch(block, tailDispatch = true) + } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. + DefaultExecutor.dispatchYield(context, block) + } + + override fun close(): Unit = coroutineScheduler.close() + + override fun toString(): String { + return "${super.toString()}[scheduler = $coroutineScheduler]" + } + + /** + * Creates a coroutine execution context with limited parallelism to execute tasks which may potentially block. + * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher], + * giving it additional hints to adjust its behaviour. + * + * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel. + */ + fun blocking(parallelism: Int = 16): CoroutineDispatcher { + require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } + return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING) + } + + /** + * Creates a coroutine execution context with limited parallelism to execute CPU-intensive tasks. + * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher], + * giving it additional hints to adjust its behaviour. + * + * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel. + */ + fun limited(parallelism: Int): CoroutineDispatcher { + require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } + require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" } + return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING) + } + + internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) { + try { + coroutineScheduler.dispatch(block, context, tailDispatch) + } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. + // TaskContext shouldn't be lost here to properly invoke before/after task + DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context)) + } + } + + private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName) +} + +private class LimitingDispatcher( + private val dispatcher: ExperimentalCoroutineDispatcher, + private val parallelism: Int, + private val name: String?, + override val taskMode: Int +) : ExecutorCoroutineDispatcher(), TaskContext, Executor { + + private val queue = ConcurrentLinkedQueue() + private val inFlightTasks = atomic(0) + + override val executor: Executor + get() = this + + override fun execute(command: Runnable) = dispatch(command, false) + + override fun close(): Unit = error("Close cannot be invoked on LimitingBlockingDispatcher") + + override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false) + + private fun dispatch(block: Runnable, tailDispatch: Boolean) { + var taskToSchedule = block + while (true) { + // Commit in-flight tasks slot + val inFlight = inFlightTasks.incrementAndGet() + + // Fast path, if parallelism limit is not reached, dispatch task and return + if (inFlight <= parallelism) { + dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch) + return + } + + // Parallelism limit is reached, add task to the queue + queue.add(taskToSchedule) + + /* + * We're not actually scheduled anything, so rollback committed in-flight task slot: + * If the amount of in-flight tasks is still above the limit, do nothing + * If the amount of in-flight tasks is lesser than parallelism, then + * it's a race with a thread which finished the task from the current context, we should resubmit the first task from the queue + * to avoid starvation. + * + * Race example #1 (TN is N-th thread, R is current in-flight tasks number), execution is sequential: + * + * T1: submit task, start execution, R == 1 + * T2: commit slot for next task, R == 2 + * T1: finish T1, R == 1 + * T2: submit next task to local queue, decrement R, R == 0 + * Without retries, task from T2 will be stuck in the local queue + */ + if (inFlightTasks.decrementAndGet() >= parallelism) { + return + } + + taskToSchedule = queue.poll() ?: return + } + } + + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + dispatch(block, tailDispatch = true) + } + + override fun toString(): String { + return name ?: "${super.toString()}[dispatcher = $dispatcher]" + } + + /** + * Tries to dispatch tasks which were blocked due to reaching parallelism limit if there is any. + * + * Implementation note: blocking tasks are scheduled in a fair manner (to local queue tail) to avoid + * non-blocking continuations starvation. + * E.g. for + * ``` + * foo() + * blocking() + * bar() + * ``` + * it's more profitable to execute bar at the end of `blocking` rather than pending blocking task + */ + override fun afterTask() { + var next = queue.poll() + // If we have pending tasks in current blocking context, dispatch first + if (next != null) { + dispatcher.dispatchWithContext(next, this, true) + return + } + inFlightTasks.decrementAndGet() + + /* + * Re-poll again and try to submit task if it's required otherwise tasks may be stuck in the local queue. + * Race example #2 (TN is N-th thread, R is current in-flight tasks number), execution is sequential: + * T1: submit task, start execution, R == 1 + * T2: commit slot for next task, R == 2 + * T1: finish T1, poll queue (it's still empty), R == 2 + * T2: submit next task to the local queue, decrement R, R == 1 + * T1: decrement R, finish. R == 0 + * + * The task from T2 is stuck is the local queue + */ + next = queue.poll() ?: return + dispatch(next, true) + } +} diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index 7227b07c07..d55edec94f 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -4,124 +4,108 @@ package kotlinx.coroutines.scheduling -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import java.util.concurrent.* import kotlin.coroutines.* -/** - * Default instance of coroutine dispatcher. - */ -internal object DefaultScheduler : ExperimentalCoroutineDispatcher() { - val IO: CoroutineDispatcher = LimitingDispatcher( - this, - systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)), - "Dispatchers.IO", - TASK_PROBABLY_BLOCKING - ) +// Instance of Dispatchers.Default +internal object DefaultScheduler : SchedulerCoroutineDispatcher( + CORE_POOL_SIZE, MAX_POOL_SIZE, + IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME +) { + // Shuts down the dispatcher, used only by Dispatchers.shutdown() + internal fun shutdown() { + super.close() + } + // Overridden in case anyone writes (Dispatchers.Default as ExecutorCoroutineDispatcher).close() override fun close() { - throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed") + throw UnsupportedOperationException("Dispatchers.Default cannot be closed") } - override fun toString(): String = DEFAULT_DISPATCHER_NAME + override fun toString(): String = "Dispatchers.Default" +} + +// The unlimited instance of Dispatchers.IO that utilizes all the threads CoroutineScheduler provides +private object UnlimitedIoScheduler : CoroutineDispatcher() { @InternalCoroutinesApi - @Suppress("UNUSED") - public fun toDebugString(): String = super.toString() + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + DefaultScheduler.dispatchWithContext(block, BlockingContext, true) + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + DefaultScheduler.dispatchWithContext(block, BlockingContext, false) + } } -/** - * @suppress **This is unstable API and it is subject to change.** - */ -// TODO make internal (and rename) after complete integration -@InternalCoroutinesApi -public open class ExperimentalCoroutineDispatcher( - private val corePoolSize: Int, - private val maxPoolSize: Int, - private val idleWorkerKeepAliveNs: Long, - private val schedulerName: String = "CoroutineScheduler" -) : ExecutorCoroutineDispatcher() { - public constructor( - corePoolSize: Int = CORE_POOL_SIZE, - maxPoolSize: Int = MAX_POOL_SIZE, - schedulerName: String = DEFAULT_SCHEDULER_NAME - ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName) - - @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN) - public constructor( - corePoolSize: Int = CORE_POOL_SIZE, - maxPoolSize: Int = MAX_POOL_SIZE - ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS) +// Dispatchers.IO +internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor { + + private val default = UnlimitedIoScheduler.limitedParallelism( + systemProp( + IO_PARALLELISM_PROPERTY_NAME, + 64.coerceAtLeast(AVAILABLE_PROCESSORS) + ) + ) override val executor: Executor - get() = coroutineScheduler + get() = this - // This is variable for test purposes, so that we can reinitialize from clean state - private var coroutineScheduler = createScheduler() + override fun execute(command: java.lang.Runnable) = dispatch(EmptyCoroutineContext, command) - override fun dispatch(context: CoroutineContext, block: Runnable): Unit = - try { - coroutineScheduler.dispatch(block) - } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. - DefaultExecutor.dispatch(context, block) - } + @ExperimentalCoroutinesApi + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + // See documentation to Dispatchers.IO for the rationale + return UnlimitedIoScheduler.limitedParallelism(parallelism) + } - override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = - try { - coroutineScheduler.dispatch(block, tailDispatch = true) - } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. - DefaultExecutor.dispatchYield(context, block) - } - - override fun close(): Unit = coroutineScheduler.close() - - override fun toString(): String { - return "${super.toString()}[scheduler = $coroutineScheduler]" + override fun dispatch(context: CoroutineContext, block: Runnable) { + default.dispatch(context, block) } - /** - * Creates a coroutine execution context with limited parallelism to execute tasks which may potentially block. - * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher], - * giving it additional hints to adjust its behaviour. - * - * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel. - */ - public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher { - require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } - return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING) + @InternalCoroutinesApi + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + default.dispatchYield(context, block) } - /** - * Creates a coroutine execution context with limited parallelism to execute CPU-intensive tasks. - * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher], - * giving it additional hints to adjust its behaviour. - * - * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel. - */ - public fun limited(parallelism: Int): CoroutineDispatcher { - require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } - require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" } - return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING) + override fun close() { + error("Cannot be invoked on Dispatchers.IO") } + override fun toString(): String = "Dispatchers.IO" +} + +// Instantiated in tests so we can test it in isolation +internal open class SchedulerCoroutineDispatcher( + private val corePoolSize: Int = CORE_POOL_SIZE, + private val maxPoolSize: Int = MAX_POOL_SIZE, + private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, + private val schedulerName: String = "CoroutineScheduler", +) : ExecutorCoroutineDispatcher() { + + override val executor: Executor + get() = coroutineScheduler + + // This is variable for test purposes, so that we can reinitialize from clean state + private var coroutineScheduler = createScheduler() + + private fun createScheduler() = + CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName) + + override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block) + + override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = + coroutineScheduler.dispatch(block, tailDispatch = true) + internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) { - try { - coroutineScheduler.dispatch(block, context, tailDispatch) - } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. - // TaskContext shouldn't be lost here to properly invoke before/after task - DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context)) - } + coroutineScheduler.dispatch(block, context, tailDispatch) } - private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName) + override fun close() { + coroutineScheduler.close() + } // fot tests only @Synchronized @@ -139,106 +123,3 @@ public open class ExperimentalCoroutineDispatcher( // for tests only internal fun restore() = usePrivateScheduler() // recreate scheduler } - -private class LimitingDispatcher( - private val dispatcher: ExperimentalCoroutineDispatcher, - private val parallelism: Int, - private val name: String?, - override val taskMode: Int -) : ExecutorCoroutineDispatcher(), TaskContext, Executor { - - private val queue = ConcurrentLinkedQueue() - private val inFlightTasks = atomic(0) - - override val executor: Executor - get() = this - - override fun execute(command: Runnable) = dispatch(command, false) - - override fun close(): Unit = error("Close cannot be invoked on LimitingBlockingDispatcher") - - override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false) - - private fun dispatch(block: Runnable, tailDispatch: Boolean) { - var taskToSchedule = block - while (true) { - // Commit in-flight tasks slot - val inFlight = inFlightTasks.incrementAndGet() - - // Fast path, if parallelism limit is not reached, dispatch task and return - if (inFlight <= parallelism) { - dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch) - return - } - - // Parallelism limit is reached, add task to the queue - queue.add(taskToSchedule) - - /* - * We're not actually scheduled anything, so rollback committed in-flight task slot: - * If the amount of in-flight tasks is still above the limit, do nothing - * If the amount of in-flight tasks is lesser than parallelism, then - * it's a race with a thread which finished the task from the current context, we should resubmit the first task from the queue - * to avoid starvation. - * - * Race example #1 (TN is N-th thread, R is current in-flight tasks number), execution is sequential: - * - * T1: submit task, start execution, R == 1 - * T2: commit slot for next task, R == 2 - * T1: finish T1, R == 1 - * T2: submit next task to local queue, decrement R, R == 0 - * Without retries, task from T2 will be stuck in the local queue - */ - if (inFlightTasks.decrementAndGet() >= parallelism) { - return - } - - taskToSchedule = queue.poll() ?: return - } - } - - override fun dispatchYield(context: CoroutineContext, block: Runnable) { - dispatch(block, tailDispatch = true) - } - - override fun toString(): String { - return name ?: "${super.toString()}[dispatcher = $dispatcher]" - } - - /** - * Tries to dispatch tasks which were blocked due to reaching parallelism limit if there is any. - * - * Implementation note: blocking tasks are scheduled in a fair manner (to local queue tail) to avoid - * non-blocking continuations starvation. - * E.g. for - * ``` - * foo() - * blocking() - * bar() - * ``` - * it's more profitable to execute bar at the end of `blocking` rather than pending blocking task - */ - override fun afterTask() { - var next = queue.poll() - // If we have pending tasks in current blocking context, dispatch first - if (next != null) { - dispatcher.dispatchWithContext(next, this, true) - return - } - inFlightTasks.decrementAndGet() - - /* - * Re-poll again and try to submit task if it's required otherwise tasks may be stuck in the local queue. - * Race example #2 (TN is N-th thread, R is current in-flight tasks number), execution is sequential: - * T1: submit task, start execution, R == 1 - * T2: commit slot for next task, R == 2 - * T1: finish T1, poll queue (it's still empty), R == 2 - * T2: submit next task to the local queue, decrement R, R == 1 - * T1: decrement R, finish. R == 0 - * - * The task from T2 is stuck is the local queue - */ - next = queue.poll() ?: return - dispatch(next, true) - } -} diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt index da867c9853..5403cfc1fd 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt @@ -9,10 +9,6 @@ import kotlinx.coroutines.internal.* import java.util.concurrent.* -// TODO most of these fields will be moved to 'object ExperimentalDispatcher' - -// User-visible name -internal const val DEFAULT_DISPATCHER_NAME = "Dispatchers.Default" // Internal debuggability name + thread name prefixes internal const val DEFAULT_SCHEDULER_NAME = "DefaultDispatcher" @@ -22,27 +18,24 @@ internal val WORK_STEALING_TIME_RESOLUTION_NS = systemProp( "kotlinx.coroutines.scheduler.resolution.ns", 100000L ) -@JvmField -internal val BLOCKING_DEFAULT_PARALLELISM = systemProp( - "kotlinx.coroutines.scheduler.blocking.parallelism", 16 -) - -// NOTE: we coerce default to at least two threads to give us chances that multi-threading problems -// get reproduced even on a single-core machine, but support explicit setting of 1 thread scheduler if needed. +/** + * The maximum number of threads allocated for CPU-bound tasks at the default set of dispatchers. + * + * NOTE: we coerce default to at least two threads to give us chances that multi-threading problems + * get reproduced even on a single-core machine, but support explicit setting of 1 thread scheduler if needed + */ @JvmField internal val CORE_POOL_SIZE = systemProp( "kotlinx.coroutines.scheduler.core.pool.size", - AVAILABLE_PROCESSORS.coerceAtLeast(2), // !!! at least two here + AVAILABLE_PROCESSORS.coerceAtLeast(2), minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE ) +/** The maximum number of threads allocated for blocking tasks at the default set of dispatchers. */ @JvmField internal val MAX_POOL_SIZE = systemProp( "kotlinx.coroutines.scheduler.max.pool.size", - (AVAILABLE_PROCESSORS * 128).coerceIn( - CORE_POOL_SIZE, - CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE - ), + CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE, maxValue = CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE ) @@ -69,14 +62,18 @@ internal interface TaskContext { fun afterTask() } -internal object NonBlockingContext : TaskContext { - override val taskMode: Int = TASK_NON_BLOCKING - +private class TaskContextImpl(override val taskMode: Int): TaskContext { override fun afterTask() { - // Nothing for non-blocking context + // Nothing for non-blocking context } } +@JvmField +internal val NonBlockingContext: TaskContext = TaskContextImpl(TASK_NON_BLOCKING) + +@JvmField +internal val BlockingContext: TaskContext = TaskContextImpl(TASK_PROBABLY_BLOCKING) + internal abstract class Task( @JvmField var submissionTime: Long, @JvmField var taskContext: TaskContext diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index 8526ca216a..6a71a4deb4 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -10,29 +10,10 @@ import kotlinx.coroutines.internal.* import java.util.concurrent.* import kotlin.coroutines.* -/** - * This [CoroutineContext] dispatcher can be used to simulate virtual time to speed up - * code, especially tests, that deal with delays and timeouts in Coroutines. - * - * Provide an instance of this TestCoroutineContext when calling the *non-blocking* - * [launch][CoroutineScope.launch] or [async][CoroutineScope.async] - * and then advance time or trigger the actions to make the co-routines execute as soon as possible. - * - * This works much like the *TestScheduler* in RxJava2, which allows to speed up tests that deal - * with non-blocking Rx chains that contain delays, timeouts, intervals and such. - * - * This dispatcher can also handle *blocking* coroutines that are started by [runBlocking]. - * This dispatcher's virtual time will be automatically advanced based on the delayed actions - * within the Coroutine(s). - * - * **Note: This API will become obsolete in future updates due to integration with structured concurrency.** - * See [issue #541](https://github.com/Kotlin/kotlinx.coroutines/issues/541). - * - * @param name A user-readable name for debugging purposes. - */ +/** @suppress */ @Deprecated("This API has been deprecated to integrate with Structured Concurrency.", ReplaceWith("TestCoroutineScope", "kotlin.coroutines.test"), - level = DeprecationLevel.WARNING) + level = DeprecationLevel.ERROR) // ERROR in 1.6.0, removed in 1.7.0 public class TestCoroutineContext(private val name: String? = null) : CoroutineContext { private val uncaughtExceptions = mutableListOf() @@ -264,28 +245,11 @@ private class TimedRunnableObsolete( override fun toString() = "TimedRunnable(time=$time, run=$run)" } -/** - * Executes a block of code in which a unit-test can be written using the provided [TestCoroutineContext]. The provided - * [TestCoroutineContext] is available in the [testBody] as the `this` receiver. - * - * The [testBody] is executed and an [AssertionError] is thrown if the list of unhandled exceptions is not empty and - * contains any exception that is not a [CancellationException]. - * - * If the [testBody] successfully executes one of the [TestCoroutineContext.assertAllUnhandledExceptions], - * [TestCoroutineContext.assertAnyUnhandledException], [TestCoroutineContext.assertUnhandledException] or - * [TestCoroutineContext.assertExceptions], the list of unhandled exceptions will have been cleared and this method will - * not throw an [AssertionError]. - * - * **Note: This API will become obsolete in future updates due to integration with structured concurrency.** - * See [issue #541](https://github.com/Kotlin/kotlinx.coroutines/issues/541). - * - * @param testContext The provided [TestCoroutineContext]. If not specified, a default [TestCoroutineContext] will be - * provided instead. - * @param testBody The code of the unit-test. - */ +/** @suppress */ @Deprecated("This API has been deprecated to integrate with Structured Concurrency.", ReplaceWith("testContext.runBlockingTest(testBody)", "kotlin.coroutines.test"), - level = DeprecationLevel.WARNING) + level = DeprecationLevel.ERROR) // ERROR in 1.6.0, removed in 1.7.0 +@Suppress("DEPRECATION_ERROR") public fun withTestContext(testContext: TestCoroutineContext = TestCoroutineContext(), testBody: TestCoroutineContext.() -> Unit) { with (testContext) { testBody() diff --git a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt b/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt deleted file mode 100644 index 8f9f855477..0000000000 --- a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt +++ /dev/null @@ -1,56 +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 - -import org.junit.Test -import java.lang.reflect.* -import java.util.concurrent.* -import kotlin.test.* - -@Suppress("DEPRECATION") -class CommonPoolTest { - private inline fun Try(block: () -> T) = try { block() } catch (e: Throwable) { null } - - @Test - fun testIsGoodCommonPool() { - // Test only on JDKs that has all we need - val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") } ?: return - val wtfClass = Try { Class.forName("java.util.concurrent.ForkJoinPool${'$'}ForkJoinWorkerThreadFactory") } ?: return - val dwtfClass = Try { Class.forName("java.util.concurrent.ForkJoinPool${'$'}DefaultForkJoinWorkerThreadFactory") } ?: return - // We need private constructor to create "broken" FJP instance - val fjpCtor = Try { fjpClass.getDeclaredConstructor( - Int::class.java, - wtfClass, - Thread.UncaughtExceptionHandler::class.java, - Int::class.java, - String::class.java - ) } ?: return - fjpCtor.isAccessible = true - val dwtfCtor = Try { dwtfClass.getDeclaredConstructor() } ?: return - dwtfCtor.isAccessible = true - // Create bad pool - val fjp0: ExecutorService = createFJP(0, fjpCtor, dwtfCtor) ?: return - assertFalse(CommonPool.isGoodCommonPool(fjpClass, fjp0)) - fjp0.shutdown() - // Create good pool - val fjp1: ExecutorService = createFJP(1, fjpCtor, dwtfCtor) ?: return - assertTrue(CommonPool.isGoodCommonPool(fjpClass, fjp1)) - fjp1.shutdown() - } - - private fun createFJP( - parallelism: Int, - fjpCtor: Constructor, - dwtfCtor: Constructor - ): ExecutorService? = Try { - fjpCtor.newInstance( - parallelism, - dwtfCtor.newInstance(), - Thread.getDefaultUncaughtExceptionHandler(), - 0, - "Worker" - ) - } as? ExecutorService -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt b/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt new file mode 100644 index 0000000000..b46adda90d --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlin.random.* + +actual fun randomWait() { + val n = Random.nextInt(1000) + if (n < 500) return // no wait 50% of time + repeat(n) { + BlackHole.sink *= 3 + } + if (n > 900) Thread.yield() +} + +private object BlackHole { + @Volatile + var sink = 1 +} + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias SuppressSupportingThrowable = Throwable + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +actual fun Throwable.printStackTrace() = printStackTrace() + +actual fun currentThreadName(): String = Thread.currentThread().name diff --git a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt index 179b2e5e6e..52bcce3c69 100644 --- a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt +++ b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt @@ -78,9 +78,8 @@ object FieldWalker { val path = ArrayList() var cur = element while (true) { - val ref = visited.getValue(cur) - if (ref is Ref.RootRef) break - when (ref) { + when (val ref = visited.getValue(cur)) { + Ref.RootRef -> break is Ref.FieldRef -> { cur = ref.parent path += "|${ref.parent.javaClass.simpleName}::${ref.name}" @@ -89,7 +88,9 @@ object FieldWalker { cur = ref.parent path += "[${ref.index}]" } - else -> error("Should not be reached") + else -> { + // Nothing, kludge for IDE + } } } path.reverse() diff --git a/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt b/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt new file mode 100644 index 0000000000..4de1862b0f --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.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 + +import org.junit.* +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +@RunWith(Parameterized::class) +class LimitedParallelismStressTest(private val targetParallelism: Int) : TestBase() { + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun params(): Collection> = listOf(1, 2, 3, 4).map { arrayOf(it) } + } + + @get:Rule + val executor = ExecutorRule(targetParallelism * 2) + private val iterations = 100_000 * stressTestMultiplier + + private val parallelism = AtomicInteger(0) + + private fun checkParallelism() { + val value = parallelism.incrementAndGet() + Thread.yield() + assertTrue { value <= targetParallelism } + parallelism.decrementAndGet() + } + + @Test + fun testLimitedExecutor() = runTest { + val view = executor.limitedParallelism(targetParallelism) + repeat(iterations) { + launch(view) { + checkParallelism() + } + } + } + + @Test + fun testLimitedDispatchersIo() = runTest { + val view = Dispatchers.IO.limitedParallelism(targetParallelism) + repeat(iterations) { + launch(view) { + checkParallelism() + } + } + } + + @Test + fun testLimitedDispatchersIoDispatchYield() = runTest { + val view = Dispatchers.IO.limitedParallelism(targetParallelism) + repeat(iterations) { + launch(view) { + yield() + checkParallelism() + } + } + } + + @Test + fun testLimitedExecutorReachesTargetParallelism() = runTest { + val view = executor.limitedParallelism(targetParallelism) + repeat(iterations) { + val barrier = CyclicBarrier(targetParallelism + 1) + repeat(targetParallelism) { + launch(view) { + barrier.await() + } + } + // Successfully awaited parallelism + 1 + barrier.await() + coroutineContext.job.children.toList().joinAll() + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/LimitedParallelismTest.kt b/kotlinx-coroutines-core/jvm/test/LimitedParallelismTest.kt new file mode 100644 index 0000000000..30c54117a9 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/LimitedParallelismTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import org.junit.Test +import java.util.concurrent.* +import kotlin.coroutines.* +import kotlin.test.* + +class LimitedParallelismTest : TestBase() { + + @Test + fun testParallelismSpec() { + assertFailsWith { Dispatchers.Default.limitedParallelism(0) } + assertFailsWith { Dispatchers.Default.limitedParallelism(-1) } + assertFailsWith { Dispatchers.Default.limitedParallelism(Int.MIN_VALUE) } + Dispatchers.Default.limitedParallelism(Int.MAX_VALUE) + } + + @Test + fun testTaskFairness() = runTest { + val executor = newSingleThreadContext("test") + val view = executor.limitedParallelism(1) + val view2 = executor.limitedParallelism(1) + val j1 = launch(view) { + while (true) { + yield() + } + } + val j2 = launch(view2) { j1.cancel() } + joinAll(j1, j2) + executor.close() + } + + @Test + fun testUnhandledException() = runTest { + var caughtException: Throwable? = null + val executor = Executors.newFixedThreadPool( + 1 + ) { + Thread(it).also { + it.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e -> caughtException = e } + } + }.asCoroutineDispatcher() + val view = executor.limitedParallelism(1) + view.dispatch(EmptyCoroutineContext, Runnable { throw TestException() }) + withContext(view) { + // Verify it is in working state and establish happens-before + } + assertTrue { caughtException is TestException } + executor.close() + } +} diff --git a/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt b/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt new file mode 100644 index 0000000000..057a8bbc96 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines + +import org.junit.* + +class RunBlockingJvmTest : TestBase() { + @Test + fun testContract() { + val rb: Int + runBlocking { + rb = 42 + } + rb.hashCode() // unused + } +} + diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index 61a2c8b8b7..f089241c63 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -20,15 +20,18 @@ private val VERBOSE = systemProp("test.verbose", false) */ public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false -public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1 +public actual val stressTestMultiplierSqrt = if (isStressTest) 5 else 1 private const val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread +public actual val isNative = false + /** * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test. */ public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt + @Suppress("ACTUAL_WITHOUT_EXPECT") public actual typealias TestResult = Unit @@ -152,7 +155,8 @@ public actual open class TestBase(private var disableOutCheck: Boolean) { }) fun println(message: Any?) { - previousOut.println(message) + if (disableOutCheck) kotlin.io.println(message) + else previousOut.println(message) } @Before @@ -199,15 +203,12 @@ public actual open class TestBase(private var disableOutCheck: Boolean) { } fun initPoolsBeforeTest() { - CommonPool.usePrivatePool() DefaultScheduler.usePrivateScheduler() } fun shutdownPoolsAfterTest() { - CommonPool.shutdown(SHUTDOWN_TIMEOUT) DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT) - DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) - CommonPool.restore() + DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) DefaultScheduler.restore() } diff --git a/kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt b/kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt new file mode 100644 index 0000000000..799e559a43 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/TestBaseExtension.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 + +public actual fun TestBase.runMtTest( + expected: ((Throwable) -> Boolean)?, + unhandled: List<(Throwable) -> Boolean>, + block: suspend CoroutineScope.() -> Unit +): TestResult = runTest(expected, unhandled, block) diff --git a/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt b/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt index ea43c7ade2..baba4aa8e6 100644 --- a/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt @@ -54,7 +54,6 @@ class ThreadContextElementTest : TestBase() { assertNull(myThreadLocal.get()) } - @Test fun testWithContext() = runTest { expect(1) @@ -86,6 +85,78 @@ class ThreadContextElementTest : TestBase() { finish(7) } + + @Test + fun testNonCopyableElementReferenceInheritedOnLaunch() = runTest { + var parentElement: MyElement? = null + var inheritedElement: MyElement? = null + + newSingleThreadContext("withContext").use { + withContext(it + MyElement(MyData())) { + parentElement = coroutineContext[MyElement.Key] + launch { + inheritedElement = coroutineContext[MyElement.Key] + } + } + } + + assertSame(inheritedElement, parentElement, + "Inner and outer coroutines did not have the same object reference to a" + + " ThreadContextElement that did not override `copyForChildCoroutine()`") + } + + @Test + fun testCopyableElementCopiedOnLaunch() = runTest { + var parentElement: CopyForChildCoroutineElement? = null + var inheritedElement: CopyForChildCoroutineElement? = null + + newSingleThreadContext("withContext").use { + withContext(it + CopyForChildCoroutineElement(MyData())) { + parentElement = coroutineContext[CopyForChildCoroutineElement.Key] + launch { + inheritedElement = coroutineContext[CopyForChildCoroutineElement.Key] + } + } + } + + assertNotSame(inheritedElement, parentElement, + "Inner coroutine did not copy its copyable ThreadContextElement.") + } + + @Test + fun testCopyableThreadContextElementImplementsWriteVisibility() = runTest { + newFixedThreadPoolContext(nThreads = 4, name = "withContext").use { + val startData = MyData() + withContext(it + CopyForChildCoroutineElement(startData)) { + val forBlockData = MyData() + myThreadLocal.setForBlock(forBlockData) { + assertSame(myThreadLocal.get(), forBlockData) + launch { + assertSame(myThreadLocal.get(), forBlockData) + } + launch { + assertSame(myThreadLocal.get(), forBlockData) + // Modify value in child coroutine. Writes to the ThreadLocal and + // the (copied) ThreadLocalElement's memory are not visible to peer or + // ancestor coroutines, so this write is both threadsafe and coroutinesafe. + val innerCoroutineData = MyData() + myThreadLocal.setForBlock(innerCoroutineData) { + assertSame(myThreadLocal.get(), innerCoroutineData) + } + assertSame(myThreadLocal.get(), forBlockData) // Asserts value was restored. + } + launch { + val innerCoroutineData = MyData() + myThreadLocal.setForBlock(innerCoroutineData) { + assertSame(myThreadLocal.get(), innerCoroutineData) + } + assertSame(myThreadLocal.get(), forBlockData) + } + } + assertSame(myThreadLocal.get(), startData) // Asserts value was restored. + } + } + } } class MyData @@ -114,3 +185,60 @@ class MyElement(val data: MyData) : ThreadContextElement { myThreadLocal.set(oldState) } } + +/** + * A [ThreadContextElement] that implements copy semantics in [copyForChildCoroutine]. + */ +class CopyForChildCoroutineElement(val data: MyData?) : CopyableThreadContextElement { + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key + get() = Key + + override fun updateThreadContext(context: CoroutineContext): MyData? { + val oldState = myThreadLocal.get() + myThreadLocal.set(data) + return oldState + } + + override fun restoreThreadContext(context: CoroutineContext, oldState: MyData?) { + myThreadLocal.set(oldState) + } + + /** + * At coroutine launch time, the _current value of the ThreadLocal_ is inherited by the new + * child coroutine, and that value is copied to a new, unique, ThreadContextElement memory + * reference for the child coroutine to use uniquely. + * + * n.b. the value copied to the child must be the __current value of the ThreadLocal__ and not + * the value initially passed to the ThreadContextElement in order to reflect writes made to the + * ThreadLocal between coroutine resumption and the child coroutine launch point. Those writes + * will be reflected in the parent coroutine's [CopyForChildCoroutineElement] when it yields the + * thread and calls [restoreThreadContext]. + */ + override fun copyForChildCoroutine(): CopyableThreadContextElement { + return CopyForChildCoroutineElement(myThreadLocal.get()) + } +} + +/** + * Calls [block], setting the value of [this] [ThreadLocal] for the duration of [block]. + * + * When a [CopyForChildCoroutineElement] for `this` [ThreadLocal] is used within a + * [CoroutineContext], a ThreadLocal set this way will have the "correct" value expected lexically + * at every statement reached, whether that statement is reached immediately, across suspend and + * redispatch within one coroutine, or within a child coroutine. Writes made to the `ThreadLocal` + * by child coroutines will not be visible to the parent coroutine. Writes made to the `ThreadLocal` + * by the parent coroutine _after_ launching a child coroutine will not be visible to that child + * coroutine. + */ +private inline fun ThreadLocal.setForBlock( + value: ThreadLocalT, + crossinline block: () -> OutputT +) { + val priorValue = get() + set(value) + block() + set(priorValue) +} + diff --git a/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt b/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt index bd9a185f6a..b4bc96ebdd 100644 --- a/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt +++ b/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt @@ -11,14 +11,14 @@ import java.util.concurrent.locks.* private const val SHUTDOWN_TIMEOUT = 1000L internal inline fun withVirtualTimeSource(log: PrintStream? = null, block: () -> Unit) { - DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) // shutdown execution with old time source (in case it was working) + DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) // shutdown execution with old time source (in case it was working) val testTimeSource = VirtualTimeSource(log) timeSource = testTimeSource DefaultExecutor.ensureStarted() // should start with new time source try { block() } finally { - DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) + DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) testTimeSource.shutdown() timeSource = null // restore time source } diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt deleted file mode 100644 index 221120af79..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt +++ /dev/null @@ -1,70 +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.channels - -import kotlinx.coroutines.* -import org.junit.* -import org.junit.runner.* -import org.junit.runners.* -import java.util.concurrent.atomic.* - -/** - * Creates a broadcast channel and repeatedly opens new subscription, receives event, closes it, - * to stress test the logic of opening the subscription - * to broadcast channel while events are being concurrently sent to it. - */ -@RunWith(Parameterized::class) -class BroadcastChannelSubStressTest( - private val kind: TestBroadcastChannelKind -) : TestBase() { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun params(): Collection> = - TestBroadcastChannelKind.values().map { arrayOf(it) } - } - - private val nSeconds = 5 * stressTestMultiplier - private val broadcast = kind.create() - - private val sentTotal = AtomicLong() - private val receivedTotal = AtomicLong() - - @Test - fun testStress() = runBlocking { - println("--- BroadcastChannelSubStressTest $kind") - val sender = - launch(context = Dispatchers.Default + CoroutineName("Sender")) { - while (isActive) { - broadcast.send(sentTotal.incrementAndGet()) - } - } - val receiver = - launch(context = Dispatchers.Default + CoroutineName("Receiver")) { - var last = -1L - while (isActive) { - val channel = broadcast.openSubscription() - val i = channel.receive() - check(i >= last) { "Last was $last, got $i" } - if (!kind.isConflated) check(i != last) { "Last was $last, got it again" } - receivedTotal.incrementAndGet() - last = i - channel.cancel() - } - } - var prevSent = -1L - repeat(nSeconds) { sec -> - delay(1000) - val curSent = sentTotal.get() - println("${sec + 1}: Sent $curSent, received ${receivedTotal.get()}") - check(curSent > prevSent) { "Send stalled at $curSent events" } - prevSent = curSent - } - withTimeout(5000) { - sender.cancelAndJoin() - receiver.cancelAndJoin() - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt index a6345cc55b..7e55f2e602 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt @@ -25,7 +25,10 @@ class ChannelSendReceiveStressTest( fun params(): Collection> = listOf(1, 2, 10).flatMap { nSenders -> listOf(1, 10).flatMap { nReceivers -> - TestChannelKind.values().map { arrayOf(it, nSenders, nReceivers) } + TestChannelKind.values() + // Workaround for bug that won't be fixed unless new channel implementation, see #2443 + .filter { it != TestChannelKind.LINKED_LIST } + .map { arrayOf(it, nSenders, nReceivers) } } } } diff --git a/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt deleted file mode 100644 index a054175412..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt +++ /dev/null @@ -1,29 +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.channels - -import kotlinx.coroutines.* -import org.junit.* - -class RandevouzChannelStressTest : TestBase() { - - @Test - fun testStress() = runTest { - val n = 100_000 * stressTestMultiplier - val q = Channel(Channel.RENDEZVOUS) - val sender = launch { - for (i in 1..n) q.send(i) - expect(2) - } - val receiver = launch { - for (i in 1..n) check(q.receive() == i) - expect(3) - } - expect(1) - sender.join() - receiver.join() - finish(4) - } -} diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt index d2a5d536b2..1bc3791cb5 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay01 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt index f74422e6b4..ac48af9ad5 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay02 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt index edaea74258..1fa9dc46be 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay03 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt index a19e6cb181..8c98684ed9 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration01 import kotlin.time.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt index 10ba88a54d..27ee7a88f5 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration02 import kotlin.time.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt index 5fa980a6f8..8a53979c33 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration03 import kotlin.time.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.time.Duration.Companion.milliseconds fun main() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt index 13023e3122..4849f52071 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt @@ -15,7 +15,7 @@ import kotlin.test.* * but run only under JDK 1.8 */ @Suppress("ConflictingExtensionProperty") -val Throwable.suppressed: Array get() { +actual val Throwable.suppressed: Array get() { val method = this::class.java.getMethod("getSuppressed") ?: error("This test can only be run using JDK 1.7") @Suppress("UNCHECKED_CAST") return method.invoke(this) as Array diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt new file mode 100644 index 0000000000..41fe090382 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class FlowSuppressionTest : TestBase() { + @Test + fun testSuppressionForPrimaryException() = runTest { + val flow = flow { + try { + emit(1) + } finally { + throw TestException() + } + }.catch { expectUnreached() }.onEach { throw TestException2() } + + try { + flow.collect() + } catch (e: Throwable) { + assertIs(e) + assertIs(e.suppressed[0]) + } + } + + @Test + fun testSuppressionForPrimaryExceptionRetry() = runTest { + val flow = flow { + try { + emit(1) + } finally { + throw TestException() + } + }.retry { expectUnreached(); true }.onEach { throw TestException2() } + + try { + flow.collect() + } catch (e: Throwable) { + assertIs(e) + assertIs(e.suppressed[0]) + + } + } + + @Test + fun testCancellationSuppression() = runTest { + val flow = flow { + try { + expect(1) + emit(1) + } finally { + expect(3) + throw CancellationException("") + } + }.catch { expectUnreached() }.onEach { + expect(2) + throw TestException("") + } + + try { + flow.collect() + } catch (e: Throwable) { + assertIs(e) + assertIs(e.suppressed[0]) + } + finish(4) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt index 349b7c8121..62a217cefa 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt @@ -71,7 +71,7 @@ class SharedFlowStressTest : TestBase() { var lastProduced = 0L var lastConsumed = 0L for (sec in 1..nSeconds) { - delay(1.seconds) + delay(Duration.seconds(1)) val produced = totalProduced.value val consumed = totalConsumed.value println("$sec sec: produced = $produced; consumed = $consumed") @@ -84,4 +84,4 @@ class SharedFlowStressTest : TestBase() { jobs.forEach { it.join() } println("total: produced = ${totalProduced.value}; consumed = ${totalConsumed.value}") } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt index 7d346bdc33..25c0c98314 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt @@ -189,5 +189,9 @@ class SharingStressTest : TestBase() { var count = 0L } - private fun log(msg: String) = println("${testStarted.elapsedNow().toLongMilliseconds()} ms: $msg") -} \ No newline at end of file + private fun log(msg: String) = println("${testStarted.elapsedNow().inWholeMilliseconds} ms: $msg") + + private fun MutableStateFlow.increment(delta: Int) { + update { it + delta } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt index 2e61ec6bce..80c98030d9 100644 --- a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt +++ b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt @@ -26,9 +26,8 @@ private val OUT_ENABLED = systemProp("guide.tests.sout", false) fun test(name: String, block: () -> R): List = outputException(name) { try { captureOutput(name, stdoutEnabled = OUT_ENABLED) { log -> - CommonPool.usePrivatePool() DefaultScheduler.usePrivateScheduler() - DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) + DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) resetCoroutineId() val threadsBefore = currentThreads() try { @@ -39,15 +38,13 @@ fun test(name: String, block: () -> R): List = outputException(name) } finally { // the shutdown log.println("--- shutting down") - CommonPool.shutdown(SHUTDOWN_TIMEOUT) DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT) shutdownDispatcherPools(SHUTDOWN_TIMEOUT) - DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) // the last man standing -- cleanup all pending tasks + DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) // the last man standing -- cleanup all pending tasks } checkTestThreads(threadsBefore) // check thread if the main completed successfully } } finally { - CommonPool.restore() DefaultScheduler.restore() } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt index 9c17e6988d..864ecdc087 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt @@ -10,7 +10,7 @@ import java.util.* import java.util.concurrent.* class BlockingCoroutineDispatcherTerminationStressTest : TestBase() { - private val baseDispatcher = ExperimentalCoroutineDispatcher( + private val baseDispatcher = SchedulerCoroutineDispatcher( 2, 20, TimeUnit.MILLISECONDS.toNanos(10) ) diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt index fe09440f59..f8830feeef 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt @@ -125,71 +125,6 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { checkPoolThreadsCreated(101..100 + CORES_COUNT) } - @Test - fun testBlockingFairness() = runBlocking { - corePoolSize = 1 - maxPoolSize = 1 - - val blocking = blockingDispatcher(1) - val task = async(dispatcher) { - expect(1) - - val nonBlocking = async(dispatcher) { - expect(3) - } - - val firstBlocking = async(blocking) { - expect(2) - } - - val secondBlocking = async(blocking) { - // Already have 1 queued blocking task, so this one wouldn't be scheduled to head - expect(4) - } - - listOf(firstBlocking, nonBlocking, secondBlocking).joinAll() - finish(5) - } - - task.await() - } - - @Test - fun testBoundedBlockingFairness() = runBlocking { - corePoolSize = 1 - maxPoolSize = 1 - - val blocking = blockingDispatcher(2) - val task = async(dispatcher) { - expect(1) - - val nonBlocking = async(dispatcher) { - expect(3) - } - - val firstBlocking = async(blocking) { - expect(4) - } - - val secondNonBlocking = async(dispatcher) { - expect(5) - } - - val secondBlocking = async(blocking) { - expect(2) // <- last submitted blocking is executed first - } - - val thirdBlocking = async(blocking) { - expect(6) // parallelism level is reached before this task - } - - listOf(firstBlocking, nonBlocking, secondBlocking, secondNonBlocking, thirdBlocking).joinAll() - finish(7) - } - - task.await() - } - @Test(timeout = 1_000) fun testYield() = runBlocking { corePoolSize = 1 diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt index 3280527f2a..3b3e085047 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt @@ -18,7 +18,7 @@ class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase() val iterations = 1000 * stressTestMultiplier repeat(iterations) { // Create a dispatcher every iteration to increase probability of race - val dispatcher = ExperimentalCoroutineDispatcher(CORES_COUNT) + val dispatcher = SchedulerCoroutineDispatcher(CORES_COUNT) val blockingDispatcher = dispatcher.blocking(100) val blockingBarrier = CyclicBarrier(CORES_COUNT * 3 + 1) diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt index 3cd77da74a..c95415a8df 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt @@ -134,7 +134,7 @@ class CoroutineDispatcherTest : SchedulerTestBase() { val initialCount = Thread.getAllStackTraces().keys.asSequence() .count { it is CoroutineScheduler.Worker && it.name.contains("SomeTestName") } assertEquals(0, initialCount) - val dispatcher = ExperimentalCoroutineDispatcher(1, 1, IDLE_WORKER_KEEP_ALIVE_NS, "SomeTestName") + val dispatcher = SchedulerCoroutineDispatcher(1, 1, IDLE_WORKER_KEEP_ALIVE_NS, "SomeTestName") dispatcher.use { launch(dispatcher) { }.join() diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt index 473b429283..a50867d61c 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt @@ -22,15 +22,13 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() { fun params(): Collection> = Mode.values().map { arrayOf(it) } } - private val N_REPEAT = 2 * stressTestMultiplier private val MAX_LEVEL = 5 private val N_COROS = (1 shl (MAX_LEVEL + 1)) - 1 private val N_THREADS = 4 private val rnd = Random() - private lateinit var closeableDispatcher: ExperimentalCoroutineDispatcher - private lateinit var dispatcher: ExecutorCoroutineDispatcher - private var closeIndex = -1 + private lateinit var closeableDispatcher: SchedulerCoroutineDispatcher + private lateinit var dispatcher: CoroutineDispatcher private val started = atomic(0) private val finished = atomic(0) @@ -44,20 +42,12 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() { } } - @Test - fun testRacingClose() { - repeat(N_REPEAT) { - closeIndex = rnd.nextInt(N_COROS) - launchCoroutines() - } - } - private fun launchCoroutines() = runBlocking { - closeableDispatcher = ExperimentalCoroutineDispatcher(N_THREADS) + closeableDispatcher = SchedulerCoroutineDispatcher(N_THREADS) dispatcher = when (mode) { Mode.CPU -> closeableDispatcher - Mode.CPU_LIMITED -> closeableDispatcher.limited(N_THREADS) as ExecutorCoroutineDispatcher - Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS) as ExecutorCoroutineDispatcher + Mode.CPU_LIMITED -> closeableDispatcher.limitedParallelism(N_THREADS) + Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS) } started.value = 0 finished.value = 0 @@ -68,20 +58,16 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() { assertEquals(N_COROS, finished.value) } + // Index and level are used only for debugging purpose private fun CoroutineScope.launchChild(index: Int, level: Int): Job = launch(start = CoroutineStart.ATOMIC) { started.incrementAndGet() try { - if (index == closeIndex) closeableDispatcher.close() if (level < MAX_LEVEL) { launchChild(2 * index + 1, level + 1) launchChild(2 * index + 2, level + 1) } else { if (rnd.nextBoolean()) { delay(1000) - val t = Thread.currentThread() - if (!t.name.contains("DefaultDispatcher-worker")) { - val a = 2 - } } else { yield() } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt index cb49f054ce..7aefd4f75c 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt @@ -15,7 +15,7 @@ import kotlin.coroutines.* import kotlin.test.* class CoroutineSchedulerStressTest : TestBase() { - private var dispatcher: ExperimentalCoroutineDispatcher = ExperimentalCoroutineDispatcher() + private var dispatcher: SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher() private val observedThreads = ConcurrentHashMap() private val tasksNum = 500_000 * stressMemoryMultiplier() diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt index b0a5954b70..9d41c05d26 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt @@ -106,22 +106,22 @@ class CoroutineSchedulerTest : TestBase() { @Test(expected = IllegalArgumentException::class) fun testNegativeCorePoolSize() { - ExperimentalCoroutineDispatcher(-1, 4) + SchedulerCoroutineDispatcher(-1, 4) } @Test(expected = IllegalArgumentException::class) fun testNegativeMaxPoolSize() { - ExperimentalCoroutineDispatcher(1, -4) + SchedulerCoroutineDispatcher(1, -4) } @Test(expected = IllegalArgumentException::class) fun testCorePoolSizeGreaterThanMaxPoolSize() { - ExperimentalCoroutineDispatcher(4, 1) + SchedulerCoroutineDispatcher(4, 1) } @Test fun testSelfClose() { - val dispatcher = ExperimentalCoroutineDispatcher(1, 1) + val dispatcher = SchedulerCoroutineDispatcher(1, 1) val latch = CountDownLatch(1) dispatcher.dispatch(EmptyCoroutineContext, Runnable { dispatcher.close(); latch.countDown() @@ -131,7 +131,7 @@ class CoroutineSchedulerTest : TestBase() { @Test fun testInterruptionCleanup() { - ExperimentalCoroutineDispatcher(1, 1).use { + SchedulerCoroutineDispatcher(1, 1).use { val executor = it.executor var latch = CountDownLatch(1) executor.execute { @@ -171,4 +171,4 @@ class CoroutineSchedulerTest : TestBase() { private class TaskContextImpl(override val taskMode: Int) : TaskContext { override fun afterTask() {} } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt new file mode 100644 index 0000000000..56c669547c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.scheduling + +import kotlinx.coroutines.* +import org.junit.Test +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +class DefaultDispatchersTest : TestBase() { + + private /*const*/ val EXPECTED_PARALLELISM = 64 + + @Test(timeout = 10_000L) + fun testLimitedParallelismIsSeparatedFromDefaultIo() = runTest { + val barrier = CyclicBarrier(EXPECTED_PARALLELISM + 1) + val ioBlocker = CountDownLatch(1) + repeat(EXPECTED_PARALLELISM) { + launch(Dispatchers.IO) { + barrier.await() + ioBlocker.await() + } + } + + barrier.await() // Ensure all threads are occupied + barrier.reset() + val limited = Dispatchers.IO.limitedParallelism(EXPECTED_PARALLELISM) + repeat(EXPECTED_PARALLELISM) { + launch(limited) { + barrier.await() + } + } + barrier.await() + ioBlocker.countDown() + } + + @Test(timeout = 10_000L) + fun testDefaultDispatcherIsSeparateFromIO() = runTest { + val ioBarrier = CyclicBarrier(EXPECTED_PARALLELISM + 1) + val ioBlocker = CountDownLatch(1) + repeat(EXPECTED_PARALLELISM) { + launch(Dispatchers.IO) { + ioBarrier.await() + ioBlocker.await() + } + } + + ioBarrier.await() // Ensure all threads are occupied + val parallelism = Runtime.getRuntime().availableProcessors() + val defaultBarrier = CyclicBarrier(parallelism + 1) + repeat(parallelism) { + launch(Dispatchers.Default) { + defaultBarrier.await() + } + } + defaultBarrier.await() + ioBlocker.countDown() + } + + @Test + fun testHardCapOnParallelism() = runTest { + val iterations = 100_000 * stressTestMultiplierSqrt + val concurrency = AtomicInteger() + repeat(iterations) { + launch(Dispatchers.IO) { + val c = concurrency.incrementAndGet() + assertTrue("Got: $c") { c <= EXPECTED_PARALLELISM } + concurrency.decrementAndGet() + } + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt index b4924277b5..e5705803c0 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt @@ -10,11 +10,6 @@ import java.util.concurrent.* class LimitingDispatcherTest : SchedulerTestBase() { - @Test(expected = IllegalArgumentException::class) - fun testTooLargeView() { - view(corePoolSize + 1) - } - @Test(expected = IllegalArgumentException::class) fun testNegativeView() { view(-1) diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt index dd969bdd37..fc4436f418 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.scheduling -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import org.junit.* @@ -61,11 +60,11 @@ abstract class SchedulerTestBase : TestBase() { protected var maxPoolSize = 1024 protected var idleWorkerKeepAliveNs = IDLE_WORKER_KEEP_ALIVE_NS - private var _dispatcher: ExperimentalCoroutineDispatcher? = null + private var _dispatcher: SchedulerCoroutineDispatcher? = null protected val dispatcher: CoroutineDispatcher get() { if (_dispatcher == null) { - _dispatcher = ExperimentalCoroutineDispatcher( + _dispatcher = SchedulerCoroutineDispatcher( corePoolSize, maxPoolSize, idleWorkerKeepAliveNs @@ -86,7 +85,7 @@ abstract class SchedulerTestBase : TestBase() { protected fun view(parallelism: Int): CoroutineDispatcher { val intitialize = dispatcher - return _dispatcher!!.limited(parallelism) + return _dispatcher!!.limitedParallelism(parallelism) } @After @@ -98,3 +97,17 @@ abstract class SchedulerTestBase : TestBase() { } } } + +internal fun SchedulerCoroutineDispatcher.blocking(parallelism: Int = 16): CoroutineDispatcher { + return object : CoroutineDispatcher() { + + @InternalCoroutinesApi + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + this@blocking.dispatchWithContext(block, BlockingContext, true) + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + this@blocking.dispatchWithContext(block, BlockingContext, false) + } + }.limitedParallelism(parallelism) +} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt index 6a66da9f5c..743b4a617f 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt @@ -13,8 +13,8 @@ class SharingWorkerClassTest : SchedulerTestBase() { @Test fun testSharedThread() = runTest { - val dispatcher = ExperimentalCoroutineDispatcher(1, schedulerName = "first") - val dispatcher2 = ExperimentalCoroutineDispatcher(1, schedulerName = "second") + val dispatcher = SchedulerCoroutineDispatcher(1, schedulerName = "first") + val dispatcher2 = SchedulerCoroutineDispatcher(1, schedulerName = "second") try { withContext(dispatcher) { @@ -39,7 +39,7 @@ class SharingWorkerClassTest : SchedulerTestBase() { val cores = Runtime.getRuntime().availableProcessors() repeat(cores + 1) { CoroutineScope(Dispatchers.Default).launch { - ExperimentalCoroutineDispatcher(1).close() + SchedulerCoroutineDispatcher(1).close() }.join() } } diff --git a/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt b/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt deleted file mode 100644 index 4a6f4d24df..0000000000 --- a/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt +++ /dev/null @@ -1,404 +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 TestCoroutineContextTest { - private val injectedContext = TestCoroutineContext() - - @After - fun tearDown() { - injectedContext.cancelAllActions() - } - - @Test - fun testDelayWithLaunch() = withTestContext(injectedContext) { - val delay = 1000L - - var executed = false - launch { - suspendedDelayedAction(delay) { - executed = true - } - } - - advanceTimeBy(delay / 2) - assertFalse(executed) - - advanceTimeBy(delay / 2) - assertTrue(executed) - } - - @Test - fun testTimeJumpWithLaunch() = withTestContext(injectedContext) { - val delay = 1000L - - var executed = false - launch { - suspendedDelayedAction(delay) { - executed = true - } - } - - advanceTimeTo(delay / 2) - assertFalse(executed) - - advanceTimeTo(delay) - assertTrue(executed) - } - - @Test - fun testDelayWithAsync() = withTestContext(injectedContext) { - val delay = 1000L - - var executed = false - async { - suspendedDelayedAction(delay) { - executed = true - } - } - - advanceTimeBy(delay / 2) - assertFalse(executed) - - advanceTimeBy(delay / 2) - assertTrue(executed) - } - - @Test - fun testDelayWithRunBlocking() = withTestContext(injectedContext) { - val delay = 1000L - - var executed = false - runBlocking { - suspendedDelayedAction(delay) { - executed = true - } - } - - assertTrue(executed) - assertEquals(delay, now()) - } - - private suspend fun suspendedDelayedAction(delay: Long, action: () -> Unit) { - delay(delay) - action() - } - - @Test - fun testDelayedFunctionWithRunBlocking() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 16 - - val result = runBlocking { - suspendedDelayedFunction(delay) { - expectedValue - } - } - - assertEquals(expectedValue, result) - assertEquals(delay, now()) - } - - @Test - fun testDelayedFunctionWithAsync() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 16 - - val deferred = async { - suspendedDelayedFunction(delay) { - expectedValue - } - } - - advanceTimeBy(delay / 2) - try { - deferred.getCompleted() - fail("The Job should not have been completed yet.") - } catch (e: Exception) { - // Success. - } - - advanceTimeBy(delay / 2) - assertEquals(expectedValue, deferred.getCompleted()) - } - - private suspend fun TestCoroutineContext.suspendedDelayedFunction(delay: Long, function: () -> T): T { - delay(delay / 4) - return async { - delay((delay / 4) * 3) - function() - }.await() - } - - @Test - fun testBlockingFunctionWithRunBlocking() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 16 - val result = runBlocking { - suspendedBlockingFunction(delay) { - expectedValue - } - } - assertEquals(expectedValue, result) - assertEquals(delay, now()) - } - - @Test - fun testBlockingFunctionWithAsync() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 16 - var now = 0L - val deferred = async { - suspendedBlockingFunction(delay) { - expectedValue - } - } - now += advanceTimeBy((delay / 4) - 1) - assertEquals((delay / 4) - 1, now) - assertEquals(now, now()) - try { - deferred.getCompleted() - fail("The Job should not have been completed yet.") - } catch (e: Exception) { - // Success. - } - now += advanceTimeBy(1) - assertEquals(delay, now()) - assertEquals(now, now()) - assertEquals(expectedValue, deferred.getCompleted()) - } - - private suspend fun TestCoroutineContext.suspendedBlockingFunction(delay: Long, function: () -> T): T { - delay(delay / 4) - return runBlocking { - delay((delay / 4) * 3) - function() - } - } - - @Test - fun testTimingOutFunctionWithAsyncAndNoTimeout() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 67 - - val result = async { - suspendedTimingOutFunction(delay, delay + 1) { - expectedValue - } - } - - triggerActions() - assertEquals(expectedValue, result.getCompleted()) - } - - @Test - fun testTimingOutFunctionWithAsyncAndTimeout() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 67 - - val result = async { - suspendedTimingOutFunction(delay, delay) { - expectedValue - } - } - - triggerActions() - assertTrue(result.getCompletionExceptionOrNull() is TimeoutCancellationException) - } - - @Test - fun testTimingOutFunctionWithRunBlockingAndTimeout() = withTestContext(injectedContext) { - val delay = 1000L - val expectedValue = 67 - - try { - runBlocking { - suspendedTimingOutFunction(delay, delay) { - expectedValue - } - } - fail("Expected TimeoutCancellationException to be thrown.") - } catch (e: TimeoutCancellationException) { - // Success - } catch (e: Throwable) { - fail("Expected TimeoutCancellationException to be thrown: $e") - } - } - - private suspend fun TestCoroutineContext.suspendedTimingOutFunction(delay: Long, timeOut: Long, function: () -> T): T { - return runBlocking { - withTimeout(timeOut) { - delay(delay / 2) - val ret = function() - delay(delay / 2) - ret - } - } - } - - @Test(expected = AssertionError::class) - fun testWithTestContextThrowingAnAssertionError() = withTestContext(injectedContext) { - val expectedError = IllegalAccessError("hello") - - launch { - throw expectedError - } - - triggerActions() - } - - @Test - fun testExceptionHandlingWithLaunch() = withTestContext(injectedContext) { - val expectedError = IllegalAccessError("hello") - - launch { - throw expectedError - } - - triggerActions() - assertUnhandledException { it === expectedError} - } - - @Test - fun testExceptionHandlingWithLaunchingChildCoroutines() = withTestContext(injectedContext) { - val delay = 1000L - val expectedError = TestException("hello") - val expectedValue = 12 - - launch { - suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true) - } - - advanceTimeBy(delay) - assertUnhandledException { it === expectedError} - } - - @Test - fun testExceptionHandlingWithAsyncAndDontWaitForException() = withTestContext(injectedContext) { - val delay = 1000L - val expectedError = IllegalAccessError("hello") - val expectedValue = 12 - - val result = async { - suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, false) - } - - advanceTimeBy(delay) - - assertNull(result.getCompletionExceptionOrNull()) - assertEquals(expectedValue, result.getCompleted()) - } - - @Test - fun testExceptionHandlingWithAsyncAndWaitForException() = withTestContext(injectedContext) { - val delay = 1000L - val expectedError = TestException("hello") - val expectedValue = 12 - - val result = async { - suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true) - } - - advanceTimeBy(delay) - - val e = result.getCompletionExceptionOrNull() - assertTrue(expectedError === e, "Expected to be thrown: '$expectedError' but was '$e'") - } - - @Test - fun testExceptionHandlingWithRunBlockingAndDontWaitForException() = withTestContext(injectedContext) { - val delay = 1000L - val expectedError = IllegalAccessError("hello") - val expectedValue = 12 - - val result = runBlocking { - suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, false) - } - - advanceTimeBy(delay) - - assertEquals(expectedValue, result) - } - - @Test - fun testExceptionHandlingWithRunBlockingAndWaitForException() = withTestContext(injectedContext) { - val delay = 1000L - val expectedError = TestException("hello") - val expectedValue = 12 - - try { - runBlocking { - suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true) - } - fail("Expected to be thrown: '$expectedError'") - } catch (e: AssertionError) { - throw e - } catch (e: Throwable) { - assertTrue(expectedError === e, "Expected to be thrown: '$expectedError' but was '$e'") - } - } - - private suspend fun TestCoroutineContext.suspendedAsyncWithExceptionAfterDelay(delay: Long, exception: Throwable, value: T, await: Boolean): T { - val deferred = async { - delay(delay - 1) - throw exception - } - - if (await) { - deferred.await() - } - return value - } - - @Test - fun testCancellationException() = withTestContext { - val job = launch { - delay(1000) - } - - advanceTimeBy(500) - job.cancel() - assertAllUnhandledExceptions { it is CancellationException } - } - - @Test - fun testCancellationExceptionNotThrownByWithTestContext() = withTestContext { - val job = launch { - delay(1000) - } - - advanceTimeBy(500) - job.cancel() - } -} - - -/* Some helper functions */ -// todo: deprecate, replace, see https://github.com/Kotlin/kotlinx.coroutines/issues/541 -private fun TestCoroutineContext.launch( - start: CoroutineStart = CoroutineStart.DEFAULT, - parent: Job? = null, - block: suspend CoroutineScope.() -> Unit -) = - GlobalScope.launch(this + (parent ?: EmptyCoroutineContext), start, block) - -// todo: deprecate, replace, see https://github.com/Kotlin/kotlinx.coroutines/issues/541 -private fun TestCoroutineContext.async( - start: CoroutineStart = CoroutineStart.DEFAULT, - parent: Job? = null, - block: suspend CoroutineScope.() -> T - -) = - GlobalScope.async(this + (parent ?: EmptyCoroutineContext), start, block) - -private fun TestCoroutineContext.runBlocking( - block: suspend CoroutineScope.() -> T -) = runBlocking(this, block) diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index 30c187fa96..030e876d2b 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -2,11 +2,14 @@ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines import kotlinx.cinterop.* import platform.posix.* +import kotlin.contracts.* import kotlin.coroutines.* +import kotlin.native.concurrent.* /** * Runs new coroutine and **blocks** current thread _interruptibly_ until its completion. @@ -30,10 +33,13 @@ import kotlin.coroutines.* * @param context context of the coroutine. The default value is an implementation of [EventLoop]. * @param block the coroutine code. */ -public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { +public actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } val contextInterceptor = context[ContinuationInterceptor] val eventLoop: EventLoop? - var newContext: CoroutineContext = context // todo: kludge for data flow analysis error + val newContext: CoroutineContext if (contextInterceptor == null) { // create or use private event loop if no dispatcher is specified eventLoop = ThreadLocalEventLoop.eventLoop @@ -54,20 +60,33 @@ private class BlockingCoroutine( parentContext: CoroutineContext, private val eventLoop: EventLoop? ) : AbstractCoroutine(parentContext, true, true) { + private val joinWorker = Worker.current + override val isScopedCoroutine: Boolean get() = true + override fun afterCompletion(state: Any?) { + // wake up blocked thread + if (joinWorker != Worker.current) { + // Unpark waiting worker + joinWorker.executeAfter(0L, {}) // send an empty task to unpark the waiting event loop + } + } + @Suppress("UNCHECKED_CAST") - fun joinBlocking(): T = memScoped { + fun joinBlocking(): T { try { eventLoop?.incrementUseCount() - val timespec = alloc() while (true) { - val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE + var parkNanos: Long + // Workaround for bug in BE optimizer that cannot eliminate boxing here + if (eventLoop != null) { + parkNanos = eventLoop.processNextEvent() + } else { + parkNanos = Long.MAX_VALUE + } // note: process next even may loose unpark flag, so check if completed before parking if (isCompleted) break - timespec.tv_sec = (parkNanos / 1000000000L).convert() // 1e9 ns -> sec - timespec.tv_nsec = (parkNanos % 1000000000L).convert() // % 1e9 - nanosleep(timespec.ptr, null) + joinWorker.park(parkNanos / 1000L, true) } } finally { // paranoia eventLoop?.decrementUseCount() @@ -75,6 +94,6 @@ private class BlockingCoroutine( // now return result val state = state.unboxState() (state as? CompletedExceptionally)?.let { throw it.cause } - state as T + return state as T } } diff --git a/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt new file mode 100644 index 0000000000..0e239a42f7 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +public actual abstract class CloseableCoroutineDispatcher actual constructor() : CoroutineDispatcher() { + public actual abstract fun close() +} diff --git a/kotlinx-coroutines-core/native/src/CompletionHandler.kt b/kotlinx-coroutines-core/native/src/CompletionHandler.kt deleted file mode 100644 index 4835f7968e..0000000000 --- a/kotlinx-coroutines-core/native/src/CompletionHandler.kt +++ /dev/null @@ -1,22 +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 - -import kotlinx.coroutines.internal.* - -internal actual abstract class CompletionHandlerBase actual constructor() : LockFreeLinkedListNode(), CompletionHandler { - actual abstract override fun invoke(cause: Throwable?) -} - -internal actual inline val CompletionHandlerBase.asHandler: CompletionHandler get() = this - -internal actual abstract class CancelHandlerBase actual constructor() : CompletionHandler { - actual abstract override fun invoke(cause: Throwable?) -} - -internal actual inline val CancelHandlerBase.asHandler: CompletionHandler get() = this - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun CompletionHandler.invokeIt(cause: Throwable?) = invoke(cause) diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index 47afd8aded..c3a376b2be 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -8,33 +8,45 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.native.concurrent.* -private fun takeEventLoop(): EventLoopImpl = - ThreadLocalEventLoop.currentOrNull() as? EventLoopImpl ?: - error("There is no event loop. Use runBlocking { ... } to start one.") - internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { - override fun dispatch(context: CoroutineContext, block: Runnable) = - takeEventLoop().dispatch(context, block) - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = - takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = - takeEventLoop().invokeOnTimeout(timeMillis, block, context) - - actual fun enqueue(task: Runnable): Unit = loopWasShutDown() -} -internal fun loopWasShutDown(): Nothing = error("Cannot execute task because event loop was shut down") + private val delegate = WorkerDispatcher(name = "Dispatchers.Default") + + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkState() + delegate.dispatch(context, block) + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + checkState() + delegate.scheduleResumeAfterDelay(timeMillis, continuation) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + checkState() + return delegate.invokeOnTimeout(timeMillis, block, context) + } + + actual fun enqueue(task: Runnable): Unit { + checkState() + delegate.dispatch(EmptyCoroutineContext, task) + } + + private fun checkState() { + if (multithreadingSupported) return + error("DefaultExecutor should never be invoked in K/N with disabled new memory model. The top-most 'runBlocking' event loop has been shutdown") + } +} -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = - DefaultExecutor +internal expect fun createDefaultDispatcher(): CoroutineDispatcher @SharedImmutable -internal actual val DefaultDelay: Delay = DefaultExecutor +internal actual val DefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { val combined = coroutineContext + context - return if (combined !== DefaultExecutor && combined[ContinuationInterceptor] == null) - combined + DefaultExecutor else combined + return if (combined !== DefaultDelay && combined[ContinuationInterceptor] == null) + combined + (DefaultDelay as CoroutineContext.Element) else combined } // No debugging facilities on native diff --git a/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt index b0aa86339a..d97743b4bf 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt @@ -5,8 +5,14 @@ package kotlinx.coroutines import kotlin.coroutines.* +import kotlin.native.* +internal actual fun initializeDefaultExceptionHandlers() { + // Do nothing +} + +@OptIn(ExperimentalStdlibApi::class) internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // log exception - exception.printStackTrace() + processUnhandledException(exception) } diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index 4e5facfeee..6c51a03463 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -4,15 +4,54 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.multithreadingSupported import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default) + public actual val Default: CoroutineDispatcher = createDefaultDispatcherBasedOnMm() + public actual val Main: MainCoroutineDispatcher + get() = injectedMainDispatcher ?: mainDispatcher public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing + + private val mainDispatcher = createMainDispatcher(Default) + + private var injectedMainDispatcher: MainCoroutineDispatcher? = null + + @PublishedApi + internal fun injectMain(dispatcher: MainCoroutineDispatcher) { + if (!multithreadingSupported) { + throw IllegalStateException("Dispatchers.setMain is not supported in Kotlin/Native when new memory model is disabled") + } + injectedMainDispatcher = dispatcher + } + + @PublishedApi + internal fun resetInjectedMain() { + injectedMainDispatcher = null + } +} + +internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher + +private fun createDefaultDispatcherBasedOnMm(): CoroutineDispatcher { + return if (multithreadingSupported) createDefaultDispatcher() + else OldDefaultExecutor +} + +private fun takeEventLoop(): EventLoopImpl = + ThreadLocalEventLoop.currentOrNull() as? EventLoopImpl ?: + error("There is no event loop. Use runBlocking { ... } to start one.") + +internal object OldDefaultExecutor : CoroutineDispatcher(), Delay { + override fun dispatch(context: CoroutineContext, block: Runnable) = + takeEventLoop().dispatch(context, block) + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = + takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + takeEventLoop().invokeOnTimeout(timeMillis, block, context) } -private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { +internal class OldMainDispatcher(private val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { override val immediate: MainCoroutineDispatcher get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native") override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt index 925cbe9971..f4e5b8c9c4 100644 --- a/kotlinx-coroutines-core/native/src/EventLoop.kt +++ b/kotlinx-coroutines-core/native/src/EventLoop.kt @@ -4,18 +4,38 @@ package kotlinx.coroutines +import kotlinx.cinterop.* +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.internal.multithreadingSupported +import platform.posix.* import kotlin.coroutines.* +import kotlin.native.concurrent.* import kotlin.system.* -internal actual abstract class EventLoopImplPlatform: EventLoop() { - protected actual fun unpark() { /* does nothing */ } - protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask): Unit = - loopWasShutDown() +internal actual abstract class EventLoopImplPlatform : EventLoop() { + + private val current = Worker.current + + protected actual fun unpark() { + current.executeAfter(0L, {})// send an empty task to unpark the waiting event loop + } + + protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) { + if (multithreadingSupported) { + DefaultExecutor.invokeOnTimeout(now, delayedTask, EmptyCoroutineContext) + } else { + error("Cannot execute task because event loop was shut down") + } + } } internal class EventLoopImpl: EventLoopImplBase() { - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = - scheduleInvokeOnTimeout(timeMillis, block) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + if (!multithreadingSupported) { + return scheduleInvokeOnTimeout(timeMillis, block) + } + return DefaultDelay.invokeOnTimeout(timeMillis, block, context) + } } internal actual fun createEventLoop(): EventLoop = EventLoopImpl() diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index da9979b603..1a923c40ff 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -4,6 +4,9 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.internal.SuppressSupportingThrowableImpl + /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending. * It indicates _normal_ cancellation of a coroutine. @@ -31,7 +34,9 @@ internal actual class JobCancellationException public actual constructor( } @Suppress("NOTHING_TO_INLINE") -internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } +internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { + if (this is SuppressSupportingThrowableImpl) addSuppressed(other) +} // For use in tests internal actual val RECOVER_STACK_TRACES: Boolean = false diff --git a/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt new file mode 100644 index 0000000000..d991fc169e --- /dev/null +++ b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.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 + +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* + +@ExperimentalCoroutinesApi +public actual fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher { + if (!multithreadingSupported) throw IllegalStateException("This API is only supported for experimental K/N memory model") + return WorkerDispatcher(name) +} + +public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher { + if (!multithreadingSupported) throw IllegalStateException("This API is only supported for experimental K/N memory model") + require(nThreads >= 1) { "Expected at least one thread, but got: $nThreads"} + return MultiWorkerDispatcher(name, nThreads) +} + +internal class WorkerDispatcher(name: String) : CloseableCoroutineDispatcher(), Delay { + private val worker = Worker.start(name = name) + + override fun dispatch(context: CoroutineContext, block: Runnable) { + worker.executeAfter(0L) { block.run() } + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + worker.executeAfter(timeMillis.toMicrosSafe()) { + with(continuation) { resumeUndispatched(Unit) } + } + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + // No API to cancel on timeout + worker.executeAfter(timeMillis.toMicrosSafe()) { block.run() } + return NonDisposableHandle + } + + override fun close() { + worker.requestTermination().result // Note: calling "result" blocks + } + + private fun Long.toMicrosSafe(): Long { + val result = this * 1000 + return if (result > this) result else Long.MAX_VALUE + } +} + +private class MultiWorkerDispatcher(name: String, workersCount: Int) : CloseableCoroutineDispatcher() { + private val tasksQueue = Channel(Channel.UNLIMITED) + private val workers = Array(workersCount) { Worker.start(name = "$name-$it") } + + init { + workers.forEach { w -> w.executeAfter(0L) { workerRunLoop() } } + } + + private fun workerRunLoop() = runBlocking { + for (task in tasksQueue) { + // TODO error handling + task.run() + } + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + // TODO handle rejections + tasksQueue.trySend(block) + } + + override fun close() { + tasksQueue.close() + workers.forEach { it.requestTermination().result } + } +} diff --git a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt index b379c6ac15..1db6c3bd86 100644 --- a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt @@ -4,15 +4,34 @@ package kotlinx.coroutines.internal -internal actual typealias ReentrantLock = NoOpLock +import kotlinx.atomicfu.* +import kotlin.native.concurrent.* +import kotlinx.atomicfu.locks.withLock as withLock2 -internal actual inline fun ReentrantLock.withLock(action: () -> T) = action() +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject -internal class NoOpLock { - fun tryLock() = true - fun unlock(): Unit {} -} +internal actual inline fun ReentrantLock.withLock(action: () -> T): T = this.withLock2(action) internal actual fun subscriberList(): MutableList = CopyOnWriteList() internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet() + + +// "Suppress-supporting throwable" is currently used for tests only +internal open class SuppressSupportingThrowableImpl : Throwable() { + private val _suppressed = atomic>(emptyArray()) + + val suppressed: Array + get() = _suppressed.value + + fun addSuppressed(other: Throwable) { + _suppressed.update { current -> + current + other + } + } +} + +@SharedImmutable +@OptIn(ExperimentalStdlibApi::class) +internal val multithreadingSupported: Boolean = kotlin.native.isExperimentalMM() diff --git a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt index 30f063a517..2896c2eac5 100644 --- a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt +++ b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt @@ -4,101 +4,76 @@ package kotlinx.coroutines.internal +import kotlinx.atomicfu.* + @Suppress("UNCHECKED_CAST") -internal class CopyOnWriteList(private var array: Array = arrayOfNulls(4)) : AbstractMutableList() { +internal class CopyOnWriteList : AbstractMutableList() { + + private val _array = atomic(arrayOfNulls(0)) + private var array: Array + get() = _array.value + set(value) { _array.value = value } - private var _size = 0 - override val size: Int get() = _size + override val size: Int + get() = array.size override fun add(element: E): Boolean { - val newSize = if (_size == array.size) array.size * 2 else array.size - val update = array.copyOf(newSize) - update[_size++] = element + val n = size + val update = array.copyOf(n + 1) + update[n] = element array = update return true } override fun add(index: Int, element: E) { rangeCheck(index) - val update = arrayOfNulls(if (array.size == _size) array.size * 2 else array.size) - array.copyInto( - destination = update, - endIndex = index - ) + val n = size + val update = arrayOfNulls(n + 1) + array.copyInto(destination = update, endIndex = index) update[index] = element - array.copyInto( - destination = update, - destinationOffset = index + 1, - startIndex = index, - endIndex = _size + 1 - ) - ++_size + array.copyInto(destination = update, destinationOffset = index + 1, startIndex = index, endIndex = n + 1) array = update } override fun remove(element: E): Boolean { val index = array.indexOf(element as Any) - if (index == -1) { - return false - } - + if (index == -1) return false removeAt(index) return true } override fun removeAt(index: Int): E { rangeCheck(index) - modCount++ - val n = array.size + val n = size val element = array[index] - val update = arrayOfNulls(n) - array.copyInto( - destination = update, - endIndex = index - ) - array.copyInto( - destination = update, - destinationOffset = index, - startIndex = index + 1, - endIndex = n - ) + val update = arrayOfNulls(n - 1) + array.copyInto(destination = update, endIndex = index) + array.copyInto(destination = update, destinationOffset = index, startIndex = index + 1, endIndex = n) array = update - --_size return element as E } - override fun iterator(): MutableIterator = IteratorImpl(array as Array, size) - + override fun iterator(): MutableIterator = IteratorImpl(array as Array) override fun listIterator(): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") - override fun listIterator(index: Int): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") - override fun isEmpty(): Boolean = size == 0 - override fun set(index: Int, element: E): E = throw UnsupportedOperationException("Operation is not supported") + override fun get(index: Int): E = array[rangeCheck(index)] as E - override fun get(index: Int): E = array[rangeCheck(index)]!! as E - - private class IteratorImpl(private var array: Array, private val size: Int) : MutableIterator { - + private class IteratorImpl(private val array: Array) : MutableIterator { private var current = 0 - override fun hasNext(): Boolean = current != size + override fun hasNext(): Boolean = current != array.size override fun next(): E { - if (!hasNext()) { - throw NoSuchElementException() - } - - return array[current++]!! + if (!hasNext()) throw NoSuchElementException() + return array[current++] } override fun remove() = throw UnsupportedOperationException("Operation is not supported") } private fun rangeCheck(index: Int) = index.apply { - if (index < 0 || index >= _size) { - throw IndexOutOfBoundsException("index: $index, size: $size") - } + if (index < 0 || index >= size) throw IndexOutOfBoundsException("index: $index, size: $size") } } diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt deleted file mode 100644 index a8aed04461..0000000000 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -@file:Suppress("NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") - -package kotlinx.coroutines.internal - -private typealias Node = LinkedListNode - -/** @suppress **This is unstable API and it is subject to change.** */ -@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // :TODO: Remove when fixed: https://youtrack.jetbrains.com/issue/KT-23703 -public actual typealias LockFreeLinkedListNode = LinkedListNode - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual typealias LockFreeLinkedListHead = LinkedListHead - -/** @suppress **This is unstable API and it is subject to change.** */ -public open class LinkedListNode { - @PublishedApi internal var _next = this - @PublishedApi internal var _prev = this - @PublishedApi internal var _removed: Boolean = false - - public inline val nextNode get() = _next - public inline val prevNode get() = _prev - public inline val isRemoved get() = _removed - - public fun addLast(node: Node) { - val prev = this._prev - node._next = this - node._prev = prev - prev._next = node - this._prev = node - } - - public open fun remove(): Boolean { - if (_removed) return false - val prev = this._prev - val next = this._next - prev._next = next - next._prev = prev - _removed = true - return true - } - - public fun addOneIfEmpty(node: Node): Boolean { - if (_next !== this) return false - addLast(node) - return true - } - - public inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean { - if (!condition()) return false - addLast(node) - return true - } - - public inline fun addLastIfPrev(node: Node, predicate: (Node) -> Boolean): Boolean { - if (!predicate(_prev)) return false - addLast(node) - return true - } - - public inline fun addLastIfPrevAndIf( - node: Node, - predicate: (Node) -> Boolean, // prev node predicate - crossinline condition: () -> Boolean // atomically checked condition - ): Boolean { - if (!predicate(_prev)) return false - if (!condition()) return false - addLast(node) - return true - } - - public fun helpRemove() {} // no-op without multithreading - - public fun removeFirstOrNull(): Node? { - val next = _next - if (next === this) return null - check(next.remove()) { "Should remove" } - return next - } - - public inline fun removeFirstIfIsInstanceOfOrPeekIf(predicate: (T) -> Boolean): T? { - val next = _next - if (next === this) return null - if (next !is T) return null - if (predicate(next)) return next - check(next.remove()) { "Should remove" } - return next - } -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual open class AddLastDesc actual constructor( - actual val queue: Node, - actual val node: T -) : AbstractAtomicDesc() { - override val affectedNode: Node get() = queue._prev - actual override fun finishPrepare(prepareOp: PrepareOp) {} - override fun onComplete() = queue.addLast(node) - actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual open class RemoveFirstDesc actual constructor( - actual val queue: LockFreeLinkedListNode -) : AbstractAtomicDesc() { - @Suppress("UNCHECKED_CAST") - actual val result: T get() = affectedNode as T - override val affectedNode: Node = queue.nextNode - actual override fun finishPrepare(prepareOp: PrepareOp) {} - override fun onComplete() { queue.removeFirstOrNull() } - actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual abstract class AbstractAtomicDesc : AtomicDesc() { - protected abstract val affectedNode: Node - actual abstract fun finishPrepare(prepareOp: PrepareOp) - protected abstract fun onComplete() - - actual open fun onPrepare(prepareOp: PrepareOp): Any? { - finishPrepare(prepareOp) - return null - } - - actual open fun onRemoved(affected: Node) {} - - actual final override fun prepare(op: AtomicOp<*>): Any? { - val affected = affectedNode - val failure = failure(affected) - if (failure != null) return failure - @Suppress("UNCHECKED_CAST") - return onPrepare(PrepareOp(affected, this, op)) - } - - actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete() - protected actual open fun failure(affected: LockFreeLinkedListNode): Any? = null // Never fails by default - protected actual open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean = false // Always succeeds - protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual class PrepareOp( - actual val affected: LockFreeLinkedListNode, - actual val desc: AbstractAtomicDesc, - actual override val atomicOp: AtomicOp<*> -): OpDescriptor() { - override fun perform(affected: Any?): Any? = null - actual fun finishPrepare() {} -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public open class LinkedListHead : LinkedListNode() { - public val isEmpty get() = _next === this - - /** - * Iterates over all elements in this list of a specified type. - */ - public inline fun forEach(block: (T) -> Unit) { - var cur: Node = _next - while (cur != this) { - if (cur is T) block(cur) - cur = cur._next - } - } - - // just a defensive programming -- makes sure that list head sentinel is never removed - public final override fun remove(): Boolean = throw UnsupportedOperationException() -} diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt index dcbb20217d..edbd3fde0c 100644 --- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt @@ -5,16 +5,16 @@ package kotlinx.coroutines.internal import kotlinx.coroutines.* +import kotlinx.atomicfu.locks.withLock as withLock2 /** * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual typealias SynchronizedObject = Any +public actual typealias SynchronizedObject = kotlinx.atomicfu.locks.SynchronizedObject /** * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = - block() +public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block) diff --git a/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt b/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt new file mode 100644 index 0000000000..639b5fb174 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.atomicfu.* +import kotlinx.coroutines.internal.* +import platform.posix.* +import kotlin.native.concurrent.* +import kotlin.random.* + +actual fun randomWait() { + val n = Random.nextInt(1000) + if (n < 500) return // no wait 50% of time + repeat(n) { + BlackHole.sink *= 3 + } + // use the BlackHole value somehow, so even if the compiler gets smarter, it won't remove the object + val sinkValue = if (BlackHole.sink > 16) 1 else 0 + if (n + sinkValue > 900) sched_yield() +} + +@ThreadLocal +private object BlackHole { + var sink = 1 +} + +internal actual typealias SuppressSupportingThrowable = SuppressSupportingThrowableImpl + +actual val Throwable.suppressed: Array + get() = (this as? SuppressSupportingThrowableImpl)?.suppressed ?: emptyArray() + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +actual fun Throwable.printStackTrace() = printStackTrace() + +actual fun currentThreadName(): String = Worker.current.name diff --git a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt index a39a59e134..b0409935da 100644 --- a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt +++ b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt @@ -8,22 +8,6 @@ import kotlin.coroutines.* import kotlin.test.* class DelayExceptionTest : TestBase() { - private object Dispatcher : CoroutineDispatcher() { - override fun isDispatchNeeded(context: CoroutineContext): Boolean = true - override fun dispatch(context: CoroutineContext, block: Runnable) { block.run() } - } - - private lateinit var exception: Throwable - - - @Test - fun testThrowsTce() { - CoroutineScope(Dispatcher + CoroutineExceptionHandler { _, e -> exception = e }).launch { - delay(10) - } - - assertTrue(exception is IllegalStateException) - } @Test fun testMaxDelay() = runBlocking { diff --git a/kotlinx-coroutines-core/native/test/RunBlockingTest.kt b/kotlinx-coroutines-core/native/test/RunBlockingTest.kt deleted file mode 100644 index c5d08af5f3..0000000000 --- a/kotlinx-coroutines-core/native/test/RunBlockingTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines -import kotlin.test.* - -class RunBlockingTest : TestBase() { - - @Test - fun testIncompleteState() { - val handle = runBlocking { - coroutineContext[Job]!!.invokeOnCompletion { } - } - - handle.dispose() - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt index 4ffa6c0b11..6fef4752a8 100644 --- a/kotlinx-coroutines-core/native/test/TestBase.kt +++ b/kotlinx-coroutines-core/native/test/TestBase.kt @@ -4,16 +4,21 @@ package kotlinx.coroutines +import kotlinx.atomicfu.* + public actual val isStressTest: Boolean = false public actual val stressTestMultiplier: Int = 1 +public actual val stressTestMultiplierSqrt: Int = 1 + +public actual val isNative = true @Suppress("ACTUAL_WITHOUT_EXPECT") public actual typealias TestResult = Unit public actual open class TestBase actual constructor() { public actual val isBoundByJsTestTimeout = false - private var actionIndex = 0 - private var finished = false + private var actionIndex = atomic(0) + private var finished = atomic(false) private var error: Throwable? = null /** @@ -36,7 +41,7 @@ public actual open class TestBase actual constructor() { * Asserts that this invocation is `index`-th in the execution sequence (counting from one). */ public actual fun expect(index: Int) { - val wasIndex = ++actionIndex + val wasIndex = actionIndex.incrementAndGet() check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } } @@ -52,21 +57,21 @@ public actual open class TestBase actual constructor() { */ public actual fun finish(index: Int) { expect(index) - check(!finished) { "Should call 'finish(...)' at most once" } - finished = true + check(!finished.value) { "Should call 'finish(...)' at most once" } + finished.value = true } /** * Asserts that [finish] was invoked */ - public actual fun ensureFinished() { - require(finished) { "finish(...) should be caller prior to this check" } + actual fun ensureFinished() { + require(finished.value) { "finish(...) should be caller prior to this check" } } - public actual fun reset() { - check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" } - actionIndex = 0 - finished = false + actual fun reset() { + check(actionIndex.value == 0 || finished.value) { "Expecting that 'finish(...)' was invoked, but it was not" } + actionIndex.value = 0 + finished.value = false } @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") @@ -78,7 +83,7 @@ public actual open class TestBase actual constructor() { var exCount = 0 var ex: Throwable? = null try { - runBlocking(block = block, context = CoroutineExceptionHandler { context, e -> + runBlocking(block = block, context = CoroutineExceptionHandler { _, e -> if (e is CancellationException) return@CoroutineExceptionHandler // are ignored exCount++ when { diff --git a/kotlinx-coroutines-core/native/test/TestBaseExtension.kt b/kotlinx-coroutines-core/native/test/TestBaseExtension.kt new file mode 100644 index 0000000000..fde2cde5cf --- /dev/null +++ b/kotlinx-coroutines-core/native/test/TestBaseExtension.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. + */ + +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* + +actual fun TestBase.runMtTest( + expected: ((Throwable) -> Boolean)?, + unhandled: List<(Throwable) -> Boolean>, + block: suspend CoroutineScope.() -> Unit +) { + if (!multithreadingSupported) return + return runTest(expected, unhandled, block) +} diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt index d6b5fad182..8252ca656a 100644 --- a/kotlinx-coroutines-core/native/test/WorkerTest.kt +++ b/kotlinx-coroutines-core/native/test/WorkerTest.kt @@ -23,12 +23,12 @@ class WorkerTest : TestBase() { } @Test - fun testLaunchInWorkerTroughGlobalScope() { + fun testLaunchInWorkerThroughGlobalScope() { val worker = Worker.start() worker.execute(TransferMode.SAFE, { }) { runBlocking { CoroutineScope(EmptyCoroutineContext).launch { - delay(1) + delay(10) }.join() } }.result diff --git a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt b/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt index 6c1fddfc5f..44ddf471d7 100644 --- a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt +++ b/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt @@ -10,11 +10,11 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class LinkedListTest { - data class IntNode(val i: Int) : LinkedListNode() + data class IntNode(val i: Int) : LockFreeLinkedListNode() @Test fun testSimpleAddLastRemove() { - val list = LinkedListHead() + val list = LockFreeLinkedListHead() assertContents(list) val n1 = IntNode(1).apply { list.addLast(this) } assertContents(list, 1) @@ -35,7 +35,7 @@ class LinkedListTest { assertContents(list) } - private fun assertContents(list: LinkedListHead, vararg expected: Int) { + private fun assertContents(list: LockFreeLinkedListHead, vararg expected: Int) { val n = expected.size val actual = IntArray(n) var index = 0 diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt new file mode 100644 index 0000000000..ace20422f6 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.cinterop.* +import kotlinx.coroutines.internal.* +import platform.CoreFoundation.* +import platform.darwin.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* +import kotlin.native.internal.NativePtr + +internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain() + +internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = + if (multithreadingSupported) DarwinMainDispatcher(false) else OldMainDispatcher(Dispatchers.Default) + +internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGlobalQueueDispatcher + +private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + autoreleasepool { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.convert(), 0)) { + block.run() + } + } + } +} + +private class DarwinMainDispatcher( + private val invokeImmediately: Boolean +) : MainCoroutineDispatcher(), Delay { + + override val immediate: MainCoroutineDispatcher = + if (invokeImmediately) this else DarwinMainDispatcher(true) + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = !(invokeImmediately && isMainThread()) + + override fun dispatch(context: CoroutineContext, block: Runnable) { + autoreleasepool { + dispatch_async(dispatch_get_main_queue()) { + block.run() + } + } + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val timer = Timer() + val timerBlock: TimerBlock = { + timer.dispose() + continuation.resume(Unit) + } + timer.start(timeMillis, timerBlock) + continuation.disposeOnCancellation(timer) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + val timer = Timer() + val timerBlock: TimerBlock = { + timer.dispose() + block.run() + } + timer.start(timeMillis, timerBlock) + return timer + } + + override fun toString(): String = + "MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }" +} + +private typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit + +private val TIMER_NEW = NativePtr.NULL +private val TIMER_DISPOSED = NativePtr.NULL.plus(1) + +private class Timer : DisposableHandle { + private val ref = AtomicNativePtr(TIMER_NEW) + + fun start(timeMillis: Long, timerBlock: TimerBlock) { + val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0 + val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock) + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes) + if (!ref.compareAndSet(TIMER_NEW, timer.rawValue)) { + // dispose was already called concurrently + release(timer) + } + } + + override fun dispose() { + while (true) { + val ptr = ref.value + if (ptr == TIMER_DISPOSED) return + if (ref.compareAndSet(ptr, TIMER_DISPOSED)) { + if (ptr != TIMER_NEW) release(interpretCPointer(ptr)) + return + } + } + } + + private fun release(timer: CFRunLoopTimerRef?) { + CFRunLoopRemoveTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes) + CFRelease(timer) + } +} + +internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit): Unit = autoreleasepool { block() } diff --git a/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt new file mode 100644 index 0000000000..d460bd6e6e --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* +import platform.CoreFoundation.* +import platform.darwin.* +import kotlin.coroutines.* +import kotlin.test.* + +class MainDispatcherTest : TestBase() { + + private fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain() + private fun canTestMainDispatcher() = !isMainThread() && multithreadingSupported + + private fun runTestNotOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) { + // skip if already on the main thread, run blocking doesn't really work well with that + if (!canTestMainDispatcher()) return + runTest(block = block) + } + + @Test + fun testDispatchNecessityCheckWithMainImmediateDispatcher() = runTestNotOnMainDispatcher { + val main = Dispatchers.Main.immediate + assertTrue(main.isDispatchNeeded(EmptyCoroutineContext)) + withContext(Dispatchers.Default) { + assertTrue(main.isDispatchNeeded(EmptyCoroutineContext)) + withContext(Dispatchers.Main) { + assertFalse(main.isDispatchNeeded(EmptyCoroutineContext)) + } + assertTrue(main.isDispatchNeeded(EmptyCoroutineContext)) + } + } + + @Test + fun testWithContext() = runTestNotOnMainDispatcher { + expect(1) + assertFalse(isMainThread()) + withContext(Dispatchers.Main) { + assertTrue(isMainThread()) + expect(2) + } + assertFalse(isMainThread()) + finish(3) + } + + @Test + fun testWithContextDelay() = runTestNotOnMainDispatcher { + expect(1) + withContext(Dispatchers.Main) { + assertTrue(isMainThread()) + expect(2) + delay(100) + assertTrue(isMainThread()) + expect(3) + } + assertFalse(isMainThread()) + finish(4) + } + + @Test + fun testWithTimeoutContextDelayNoTimeout() = runTestNotOnMainDispatcher { + expect(1) + withTimeout(1000) { + withContext(Dispatchers.Main) { + assertTrue(isMainThread()) + expect(2) + delay(100) + assertTrue(isMainThread()) + expect(3) + } + } + assertFalse(isMainThread()) + finish(4) + } + + @Test + fun testWithTimeoutContextDelayTimeout() = runTestNotOnMainDispatcher { + expect(1) + assertFailsWith { + withTimeout(100) { + withContext(Dispatchers.Main) { + assertTrue(isMainThread()) + expect(2) + delay(1000) + expectUnreached() + } + } + expectUnreached() + } + assertFalse(isMainThread()) + finish(3) + } + + @Test + fun testWithContextTimeoutDelayNoTimeout() = runTestNotOnMainDispatcher { + expect(1) + withContext(Dispatchers.Main) { + withTimeout(1000) { + assertTrue(isMainThread()) + expect(2) + delay(100) + assertTrue(isMainThread()) + expect(3) + } + } + assertFalse(isMainThread()) + finish(4) + } + + @Test + fun testWithContextTimeoutDelayTimeout() = runTestNotOnMainDispatcher { + expect(1) + assertFailsWith { + withContext(Dispatchers.Main) { + withTimeout(100) { + assertTrue(isMainThread()) + expect(2) + delay(1000) + expectUnreached() + } + } + expectUnreached() + } + assertFalse(isMainThread()) + finish(3) + } +} diff --git a/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt new file mode 100644 index 0000000000..517190d0a3 --- /dev/null +++ b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.coroutines.* + +internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = + MissingMainDispatcher + +internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DefaultDispatcher + +private object DefaultDispatcher : CoroutineDispatcher() { + + // Delegated, so users won't be able to downcast and call 'close' + // The precise number of threads cannot be obtained until KT-48179 is implemented, 4 is just "good enough" number. + private val ctx = newFixedThreadPoolContext(4, "Dispatchers.Default") + + override fun dispatch(context: CoroutineContext, block: Runnable) { + ctx.dispatch(context, block) + } +} + +private object MissingMainDispatcher : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher + get() = notImplemented() + override fun dispatch(context: CoroutineContext, block: Runnable) = notImplemented() + override fun isDispatchNeeded(context: CoroutineContext): Boolean = notImplemented() + override fun dispatchYield(context: CoroutineContext, block: Runnable) = notImplemented() + + private fun notImplemented(): Nothing = TODO("Dispatchers.Main is missing on the current platform") +} + +internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block() diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index cd71f580f0..e02db4c93d 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.5.2.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.6.0-RC.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index 43d94d1841..4830670d24 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -27,6 +27,7 @@ dependencies { shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" compileOnly "io.projectreactor.tools:blockhound:$blockhound_version" testImplementation "io.projectreactor.tools:blockhound:$blockhound_version" + testImplementation "com.google.code.gson:gson:2.8.6" api "net.java.dev.jna:jna:$jna_version" api "net.java.dev.jna:jna-platform:$jna_version" } diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt index 190476c41a..9cafffb038 100644 --- a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt +++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.scheduling.* import reactor.blockhound.* import reactor.blockhound.integration.* -@Suppress("UNUSED") public class CoroutinesBlockHoundIntegration : BlockHoundIntegration { override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) { @@ -19,6 +18,9 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration { allowServiceLoaderInvocationsOnInit() allowBlockingCallsInReflectionImpl() allowBlockingCallsInDebugProbes() + allowBlockingCallsInWorkQueue() + // Stacktrace recovery cache is guarded by lock + allowBlockingCallsInside("kotlinx.coroutines.internal.ExceptionsConstructorKt", "tryCopyException") /* The predicates that define that BlockHound should only report blocking calls from threads that are part of the coroutine thread pool and currently execute a CPU-bound coroutine computation. */ addDynamicThreadPredicate { isSchedulerWorker(it) } @@ -60,6 +62,14 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration { } } + /** + * Allow blocking calls inside [kotlinx.coroutines.scheduling.WorkQueue] + */ + private fun BlockHound.Builder.allowBlockingCallsInWorkQueue() { + /** uses [Thread.yield] in a benign way. */ + allowBlockingCallsInside("kotlinx.coroutines.scheduling.WorkQueue", "addLast") + } + /** * Allows blocking inside [kotlinx.coroutines.internal.ThreadSafeHeap]. */ @@ -133,7 +143,7 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration { */ private fun BlockHound.Builder.allowBlockingCallsInConflatedChannel() { for (method in listOf("offerInternal", "offerSelectInternal", "pollInternal", "pollSelectInternal", - "onCancelIdempotent")) + "onCancelIdempotent", "isEmpty", "enqueueReceiveInternal")) { allowBlockingCallsInside("kotlinx.coroutines.channels.ConflatedChannel", method) } diff --git a/kotlinx-coroutines-debug/test/BlockHoundTest.kt b/kotlinx-coroutines-debug/test/BlockHoundTest.kt index 571daca12f..3f58878525 100644 --- a/kotlinx-coroutines-debug/test/BlockHoundTest.kt +++ b/kotlinx-coroutines-debug/test/BlockHoundTest.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.channels.* import org.junit.* import reactor.blockhound.* +@Suppress("UnusedEquals", "DeferredResultUnused", "BlockingMethodInNonBlockingContext") class BlockHoundTest : TestBase() { @Before @@ -12,21 +13,21 @@ class BlockHoundTest : TestBase() { } @Test(expected = BlockingOperationError::class) - fun shouldDetectBlockingInDefault() = runTest { + fun testShouldDetectBlockingInDefault() = runTest { withContext(Dispatchers.Default) { Thread.sleep(1) } } @Test - fun shouldNotDetectBlockingInIO() = runTest { + fun testShouldNotDetectBlockingInIO() = runTest { withContext(Dispatchers.IO) { Thread.sleep(1) } } @Test - fun shouldNotDetectNonblocking() = runTest { + fun testShouldNotDetectNonblocking() = runTest { withContext(Dispatchers.Default) { val a = 1 val b = 2 @@ -54,7 +55,7 @@ class BlockHoundTest : TestBase() { } @Test - fun testChannelsNotBeingConsideredBlocking() = runTest { + fun testChannelNotBeingConsideredBlocking() = runTest { withContext(Dispatchers.Default) { // Copy of kotlinx.coroutines.channels.ArrayChannelTest.testSimple val q = Channel(1) @@ -74,6 +75,24 @@ class BlockHoundTest : TestBase() { } } + @Test + fun testConflatedChannelsNotBeingConsideredBlocking() = runTest { + withContext(Dispatchers.Default) { + val q = Channel(Channel.CONFLATED) + check(q.isEmpty) + check(!q.isClosedForReceive) + check(!q.isClosedForSend) + val sender = launch { + q.send(1) + } + val receiver = launch { + q.receive() == 1 + } + sender.join() + receiver.join() + } + } + @Test(expected = BlockingOperationError::class) fun testReusingThreadsFailure() = runTest { val n = 100 diff --git a/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt new file mode 100644 index 0000000000..4808470eb6 --- /dev/null +++ b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.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. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package kotlinx.coroutines.debug + +import com.google.gson.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +@ExperimentalStdlibApi +class DumpCoroutineInfoAsJsonAndReferencesTest : DebugTestBase() { + private data class CoroutineInfoFromJson( + val name: String?, + val id: Long?, + val dispatcher: String?, + val sequenceNumber: Long?, + val state: String? + ) + + @Test + fun testDumpOfUnnamedCoroutine() = + runTestWithNamedDeferred(name = null) + + @Test + fun testDumpOfNamedCoroutine() = + runTestWithNamedDeferred("Name") + + @Test + fun testDumpWithNoCoroutines() { + val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences() + assertEquals(dumpResult.size, 4) + assertIsEmptyArray(dumpResult[1]) + assertIsEmptyArray(dumpResult[2]) + assertIsEmptyArray(dumpResult[3]) + } + + private fun assertIsEmptyArray(obj: Any) = + assertTrue(obj is Array<*> && obj.isEmpty()) + + private fun runTestWithNamedDeferred(name: String?) = runTest { + val context = if (name == null) EmptyCoroutineContext else CoroutineName(name) + val deferred = async(context) { + suspendingMethod() + assertTrue(true) + } + yield() + verifyDump() + deferred.cancelAndJoin() + } + + private suspend fun suspendingMethod() { + delay(Long.MAX_VALUE) + } + + private fun verifyDump() { + val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences() + + assertEquals(dumpResult.size, 4) + + val coroutinesInfoAsJsonString = dumpResult[0] + val lastObservedThreads = dumpResult[1] + val lastObservedFrames = dumpResult[2] + val coroutinesInfo = dumpResult[3] + + assertTrue(coroutinesInfoAsJsonString is String) + assertTrue(lastObservedThreads is Array<*>) + assertTrue(lastObservedFrames is Array<*>) + assertTrue(coroutinesInfo is Array<*>) + + val coroutinesInfoFromJson = Gson().fromJson(coroutinesInfoAsJsonString, Array::class.java) + + val size = coroutinesInfo.size + assertTrue(size != 0) + assertEquals(size, coroutinesInfoFromJson.size) + assertEquals(size, lastObservedFrames.size) + assertEquals(size, lastObservedThreads.size) + + for (i in 0 until size) { + val info = coroutinesInfo[i] + val infoFromJson = coroutinesInfoFromJson[i] + assertTrue(info is DebugCoroutineInfo) + assertEquals(info.lastObservedThread, lastObservedThreads[i]) + assertEquals(info.lastObservedFrame, lastObservedFrames[i]) + assertEquals(info.sequenceNumber, infoFromJson.sequenceNumber) + assertEquals(info.state, infoFromJson.state) + val context = info.context + assertEquals(context[CoroutineName.Key]?.name, infoFromJson.name) + assertEquals(context[CoroutineId.Key]?.id, infoFromJson.id) + assertEquals(context[CoroutineDispatcher.Key]?.toString(), infoFromJson.dispatcher) + } + } +} diff --git a/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt b/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt new file mode 100644 index 0000000000..fcf9f1a9a9 --- /dev/null +++ b/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package kotlinx.coroutines.debug + +import com.google.gson.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import kotlin.test.* + +class EnhanceStackTraceWithTreadDumpAsJsonTest : DebugTestBase() { + private data class StackTraceElementInfoFromJson( + val declaringClass: String, + val methodName: String, + val fileName: String?, + val lineNumber: Int + ) + + @Test + fun testEnhancedStackTraceFormatWithDeferred() = runTest { + val deferred = async { + suspendingMethod() + assertTrue(true) + } + yield() + + val coroutineInfo = DebugProbesImpl.dumpCoroutinesInfo() + assertEquals(coroutineInfo.size, 2) + val info = coroutineInfo[1] + val enhancedStackTraceAsJsonString = DebugProbesImpl.enhanceStackTraceWithThreadDumpAsJson(info) + val enhancedStackTraceFromJson = Gson().fromJson(enhancedStackTraceAsJsonString, Array::class.java) + val enhancedStackTrace = DebugProbesImpl.enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace) + assertEquals(enhancedStackTrace.size, enhancedStackTraceFromJson.size) + for ((frame, frameFromJson) in enhancedStackTrace.zip(enhancedStackTraceFromJson)) { + assertEquals(frame.className, frameFromJson.declaringClass) + assertEquals(frame.methodName, frameFromJson.methodName) + assertEquals(frame.fileName, frameFromJson.fileName) + assertEquals(frame.lineNumber, frameFromJson.lineNumber) + } + + deferred.cancelAndJoin() + } + + private suspend fun suspendingMethod() { + delay(Long.MAX_VALUE) + } +} diff --git a/kotlinx-coroutines-debug/test/StacktraceUtils.kt b/kotlinx-coroutines-debug/test/StacktraceUtils.kt index 8c591ebd44..9cc626f19a 100644 --- a/kotlinx-coroutines-debug/test/StacktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StacktraceUtils.kt @@ -90,7 +90,8 @@ private fun cleanBlockHoundTraces(frames: List): List { public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) - val trace = baos.toString().split("\n\n") + val wholeDump = baos.toString() + val trace = wholeDump.split("\n\n") if (traces.isEmpty()) { val filtered = trace.filter { ignoredCoroutine == null || !it.contains(ignoredCoroutine) } assertEquals(1, filtered.count()) @@ -105,7 +106,7 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) - assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input") + assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input. Whole dump:\n$wholeDump") expected.withIndex().forEach { (index, trace) -> val actualTrace = actual[index].trimStackTrace().sanitizeAddresses() @@ -113,7 +114,7 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val actualLines = cleanBlockHoundTraces(actualTrace.split("\n")) val expectedLines = expectedTrace.split("\n") for (i in expectedLines.indices) { - assertEquals(expectedLines[i], actualLines[i]) + assertEquals(expectedLines[i], actualLines[i], "Whole dump:\n$wholeDump") } } } diff --git a/kotlinx-coroutines-test/MIGRATION.md b/kotlinx-coroutines-test/MIGRATION.md new file mode 100644 index 0000000000..5124864745 --- /dev/null +++ b/kotlinx-coroutines-test/MIGRATION.md @@ -0,0 +1,325 @@ +# Migration to the new kotlinx-coroutines-test API + +In version 1.6.0, the API of the test module changed significantly. +This is a guide for gradually adapting the existing test code to the new API. +This guide is written step-by-step; the idea is to separate the migration into several sets of small changes. + +## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations + +We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that +you don't need to do anything for this section. + +### `UncaughtExceptionCaptor` + +If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler` +was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure +was called. + +We currently don't provide a replacement for this. +However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines +are propagated structurally, which makes uncaught exception handlers less useful. + +If you have a use case for this, please tell us about it at the issue tracker. +Meanwhile, it should be possible to use a custom exception captor, which should only implement +`CoroutineExceptionHandler` now, like this: + +```kotlin +@Test +fun testFoo() = runTest { + val customCaptor = MyUncaughtExceptionCaptor() + launch(customCaptor) { + // ... + } + advanceUntilIdle() + customCaptor.cleanupTestCoroutines() +} +``` + +### `DelayController` + +We don't provide a way to define custom dispatching strategies that support virtual time. +That said, we significantly enhanced this mechanism: +* Using multiple test dispatchers simultaneously is supported. + For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be + passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test + dispatcher. +* Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided. + +If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue +tracker. + +### `TestCoroutineScope` + +This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of +`TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used. +So, there could be two reasons for defining a custom implementation: + +* Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function. + These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and + `ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining + conforming instances. In this case, follow the instructions about replacing them. +* Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else + accepts a `TestCoroutineScope` specifically as an argument. + +## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions` + +It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of +`TestCoroutineExceptionHandler` include: + +* Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions + *yet*. + If there are any, they will be thrown by the cleanup procedure anyway. + We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the + following one. +* Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected. + In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later. + It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be + found by the cleanup procedure are not superseded by the exceptions that are expected. + An example is shown below. + +```kotlin +val exceptions = mutableListOf() +val customCaptor = CoroutineExceptionHandler { ctx, throwable -> + exceptions.add(throwable) // add proper synchronization if the test is multithreaded +} + +@Test +fun testFoo() = runTest { + launch(customCaptor) { + // ... + } + advanceUntilIdle() + // check the list of the caught exceptions +} +``` + +## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope` + +This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`. +If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case, +also pass this scheduler as the argument to the dispatcher. + +## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher` + +* In places where `pauseDispatcher` in its block form is called, replace it with a call to + `withContext(StandardTestDispatcher(testScheduler))` + (`testScheduler` is available as a field of `TestCoroutineScope`, + or `scheduler` is available as a field of `TestCoroutineDispatcher`), + followed by `advanceUntilIdle()`. + This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused + when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`. +* Often, `pauseDispatcher()` in a non-block form is used at the start of the test. + Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`, + if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used, + or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`. + This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming, + instead of the deprecated `TestCoroutineDispatcher`. +* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test. + In this case, attempt to wrap everything until the next `resumeDispatcher()` in + a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of + `StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where + execution happens). + +## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()` + +For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated. +It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the +tasks scheduled *at* `currentTime + n`. + +There is an automatic replacement for this deprecation, which produces correct but inelegant code. + +Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not +encounter this edge case. + +## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())` + +This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with +`TestScope`. + +Significant differences of `runTest` from `runBlockingTest` are each given a section below. + +### It works properly with other dispatchers and asynchronous completions. + +No action on your part is required, other than replacing `runBlocking` with `runTest` as well. + +### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`. + +By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused +variant of `TestCoroutineDispatcher` should be used. +This version of the dispatcher, which can be observed has the property of eagerly entering `launch` and `async` blocks: +code until the first suspension is executed without dispatching. + +We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async` +blocks, but *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide +any guarantees about their dispatching order. + +So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it +did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it +will need to be tweaked. +If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled +at this moment of time to run. + +### The job hierarchy is completely different. + +- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the + created coroutine. +- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`. +- The job passed as an argument is used as a parent job. + +Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a +`SupervisorJob`; this should make the job hierarchy resemble what it used to be. + +```kotlin +@Test +fun testFoo() = runTest { + val deferred = async(SupervisorJob()) { + // test code + } + advanceUntilIdle() + deferred.getCompletionExceptionOrNull()?.let { + throw it + } +} +``` + +### Only a single call to `runTest` is permitted per test. + +In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned +immediately: + +```kotlin +@Test +fun testFoo(): TestResult { + // arbitrary code here + return runTest { + // ... + } +} +``` + +When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported. +Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue +tracker. + +### It uses `TestScope`, not `TestCoroutineScope`, by default. + +There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating +from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and +`TestScope` will not suffice. + +## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest` + +Likely can be done together with the next step. + +Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base. +Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside +the `runTest` block. + +The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup. +If a test must check that no other delays are remaining after it has finished, the following form may help: +```kotlin +runTest { + testBody() + val timeAfterTest = currentTime() + advanceUntilIdle() // run the remaining tasks + assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment +} +``` +Note that this will report time advancement even if the job scheduled at a later point was cancelled. + +It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens +outside the test itself. +In this case, we propose that you write a wrapper of the form: + +```kotlin +fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest { + try { + body() + } finally { + // the usual cleanup procedures that used to happen before `cleanupTestCoroutines` + } +} +``` + +## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope` + +Also, replace `runTestWithLegacyScope` with just `runTest`. +All of this can be done in parallel with replacing `runBlockingTest` with `runTest`. + +This step should remove all uses of `TestCoroutineScope`, explicit or implicit. + +Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be +straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it. +Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest` +handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of +`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them. + +Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`, +and its usages should have been removed during the previous step. + +## Replace `runBlocking` with `runTest` + +Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful. +As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other +threads, like `Dispatchers.IO` or `Dispatchers.Default`. + +## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher` + +`TestCoroutineDispatcher` is a dispatcher with two modes: +* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks. +* ("paused") Behaving like a `StandardTestDispatcher`. + +In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the +implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to +`runTest`. + +Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate. + +## Simplify code by removing unneeded entities + +Likely, now some code has the form + +```kotlin +val dispatcher = StandardTestDispatcher() +val scope = TestScope(dispatcher) + +@BeforeTest +fun setUp() { + Dispatchers.setMain(dispatcher) +} + +@AfterTest +fun tearDown() { + Dispatchers.resetMain() +} + +@Test +fun testFoo() = scope.runTest { + // ... +} +``` + +The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for +`Dispatchers.Main`. + +However, now this can be simplified to just + +```kotlin +@BeforeTest +fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) +} + +@AfterTest +fun tearDown() { + Dispatchers.resetMain() +} + +@Test +fun testFoo() = runTest { + // ... +} +``` + +The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from +the current `Dispatchers.Main`. \ No newline at end of file diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 43ae18f532..91d2b07652 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -2,35 +2,54 @@ Test utilities for `kotlinx.coroutines`. -This package provides testing utilities for effectively testing coroutines. +## Overview + +This package provides utilities for efficiently testing coroutines. + +| Name | Description | +| ---- | ----------- | +| [runTest] | Runs the test code, automatically skipping delays and handling uncaught exceptions. | +| [TestCoroutineScheduler] | The shared source of virtual time, used for controlling execution order and skipping delays. | +| [TestScope] | A [CoroutineScope] that integrates with [runTest], providing access to [TestCoroutineScheduler]. | +| [TestDispatcher] | A [CoroutineDispatcher] that whose delays are controlled by a [TestCoroutineScheduler]. | +| [Dispatchers.setMain] | Mocks the main dispatcher using the provided one. If mocked with a [TestDispatcher], its [TestCoroutineScheduler] is used everywhere by default. | + +Provided [TestDispatcher] implementations: + +| Name | Description | +| ---- | ----------- | +| [StandardTestDispatcher] | A simple dispatcher with no special behavior other than being linked to a [TestCoroutineScheduler]. | +| [UnconfinedTestDispatcher] | A dispatcher that behaves like [Dispatchers.Unconfined]. | ## Using in your project Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0-RC' } ``` -**Do not** depend on this project in your main sources, all utilities are intended and designed to be used only from tests. +**Do not** depend on this project in your main sources, all utilities here are intended and designed to be used only from tests. ## Dispatchers.Main Delegation -`Dispatchers.setMain` will override the `Main` dispatcher in test situations. This is helpful when you want to execute a -test in situations where the platform `Main` dispatcher is not available, or you wish to replace `Dispatchers.Main` with a -testing dispatcher. +`Dispatchers.setMain` will override the `Main` dispatcher in test scenarios. +This is helpful when one wants to execute a test in situations where the platform `Main` dispatcher is not available, +or to replace `Dispatchers.Main` with a testing dispatcher. -Once you have this dependency in the runtime, -[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism will overwrite -[Dispatchers.Main] with a testable implementation. +On the JVM, +the [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism is responsible +for overwriting [Dispatchers.Main] with a testable implementation, which by default will delegate its calls to the real +`Main` dispatcher, if any. -You can override the `Main` implementation using [setMain][setMain] method with any [CoroutineDispatcher] implementation, e.g.: +The `Main` implementation can be overridden using [Dispatchers.setMain][setMain] method with any [CoroutineDispatcher] +implementation, e.g.: ```kotlin class SomeTest { - + private val mainThreadSurrogate = newSingleThreadContext("UI thread") @Before @@ -40,10 +59,10 @@ class SomeTest { @After fun tearDown() { - Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher + Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher mainThreadSurrogate.close() } - + @Test fun testSomeUI() = runBlocking { launch(Dispatchers.Main) { // Will be launched in the mainThreadSurrogate dispatcher @@ -52,372 +71,289 @@ class SomeTest { } } ``` -Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. The testable version of -`Dispatchers.Main` installed by the `ServiceLoader` will delegate to the dispatcher provided by `setMain`. -## runBlockingTest +Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. -To test regular suspend functions or coroutines started with `launch` or `async` use the [runBlockingTest] coroutine -builder that provides extra test control to coroutines. +If `Main` is overridden with a [TestDispatcher], then its [TestCoroutineScheduler] is used when new [TestDispatcher] or +[TestScope] instances are created without [TestCoroutineScheduler] being passed as an argument. -1. Auto-advancing of time for regular suspend functions -2. Explicit time control for testing multiple coroutines -3. Eager execution of `launch` or `async` code blocks -4. Pause, manually advance, and restart the execution of coroutines in a test -5. Report uncaught exceptions as test failures +## runTest -### Testing regular suspend functions +[runTest] is the way to test code that involves coroutines. `suspend` functions can be called inside it. -To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing -coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed. +**IMPORTANT: in order to work with on Kotlin/JS, the result of `runTest` must be immediately `return`-ed from each test.** +The typical invocation of [runTest] thus looks like this: ```kotlin @Test -fun testFoo() = runBlockingTest { // a coroutine with an extra test control - val actual = foo() - // ... +fun testFoo() = runTest { + // code under test } +``` -suspend fun foo() { - delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest - // ... +In more advanced scenarios, it's possible instead to use the following form: +```kotlin +@Test +fun testFoo(): TestResult { + // initialize some test state + return runTest { + // code under test + } } ``` -`runBlockingTest` returns `Unit` so it may be used in a single expression with common testing libraries. +[runTest] is similar to running the code with `runBlocking` on Kotlin/JVM and Kotlin/Native, or launching a new promise +on Kotlin/JS. The main differences are the following: -### Testing `launch` or `async` +* **The calls to `delay` are automatically skipped**, preserving the relative execution order of the tasks. This way, + it's possible to make tests finish more-or-less immediately. +* **Controlling the virtual time**: in case just skipping delays is not sufficient, it's possible to more carefully + guide the execution, advancing the virtual time by a duration, draining the queue of the awaiting tasks, or running + the tasks scheduled at the present moment. +* **Handling uncaught exceptions** spawned in the child coroutines by throwing them at the end of the test. +* **Waiting for asynchronous callbacks**. + Sometimes, especially when working with third-party code, it's impossible to mock all the dispatchers in use. + [runTest] will handle the situations where some code runs in dispatchers not integrated with the test module. -Inside of [runBlockingTest], both [launch] and [async] will start a new coroutine that may run concurrently with the -test case. +## Delay-skipping -To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until -the first call to [delay] or [yield]. +To test regular suspend functions, which may have a delay, just run them inside the [runTest] block. ```kotlin @Test -fun testFooWithLaunch() = runBlockingTest { - foo() - // the coroutine launched by foo() is completed before foo() returns +fun testFoo() = runTest { // a coroutine with an extra test control + val actual = foo() // ... } -fun CoroutineScope.foo() { - // This coroutines `Job` is not shared with the test code - launch { - bar() // executes eagerly when foo() is called due to runBlockingTest - println(1) // executes eagerly when foo() is called due to runBlockingTest - } +suspend fun foo() { + delay(1_000) // when run in `runTest`, will finish immediately instead of delaying + // ... } - -suspend fun bar() {} ``` -`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines -are not able to complete, an [UncompletedCoroutinesError] will be thrown. - -*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters. - -### Testing `launch` or `async` with `delay` +## `launch` and `async` -If the coroutine created by `launch` or `async` calls `delay` then the [runBlockingTest] will not auto-progress time -right away. This allows tests to observe the interaction of multiple coroutines with different delays. +The coroutine dispatcher used for tests is single-threaded, meaning that the child coroutines of the [runTest] block +will run on the thread that started the test, and will never run in parallel. -To control time in the test you can use the [DelayController] interface. The block passed to -[runBlockingTest] can call any method on the `DelayController` interface. +If several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen. +The virtual time will automatically advance to the point of its resumption. ```kotlin @Test -fun testFooWithLaunchAndDelay() = runBlockingTest { - foo() - // the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000) - advanceTimeBy(1_000) // progress time, this will cause the delay to resume - // the coroutine launched by foo has completed here - // ... -} - -suspend fun CoroutineScope.foo() { +fun testWithMultipleDelays() = runTest { launch { - println(1) // executes eagerly when foo() is called due to runBlockingTest - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeBy(1_000) + delay(1_000) + println("1. $currentTime") // 1000 + delay(200) + println("2. $currentTime") // 1200 + delay(2_000) + println("4. $currentTime") // 3200 + } + val deferred = async { + delay(3_000) + println("3. $currentTime") // 3000 + delay(500) + println("5. $currentTime") // 3500 } + deferred.await() } ``` -*Note:* `runBlockingTest` will always attempt to auto-progress time until all coroutines are completed just before -exiting. This is a convenience to avoid having to call [advanceUntilIdle][DelayController.advanceUntilIdle] -as the last line of many common test cases. -If any coroutines cannot complete by advancing time, an [UncompletedCoroutinesError] is thrown. +## Controlling the virtual time -### Testing `withTimeout` using `runBlockingTest` - -Time control can be used to test timeout code. To do so, ensure that the function under test is suspended inside a -`withTimeout` block and advance time until the timeout is triggered. - -Depending on the code, causing the code to suspend may need to use different mocking or fake techniques. For this -example an uncompleted `Deferred` is provided to the function under test via parameter injection. +Inside [runTest], the following operations are supported: +* `currentTime` gets the current virtual time. +* `runCurrent()` runs the tasks that are scheduled at this point of virtual time. +* `advanceUntilIdle()` runs all enqueued tasks until there are no more. +* `advanceTimeBy(timeDelta)` runs the enqueued tasks until the current virtual time advances by `timeDelta`. ```kotlin -@Test(expected = TimeoutCancellationException::class) -fun testFooWithTimeout() = runBlockingTest { - val uncompleted = CompletableDeferred() // this Deferred will never complete - foo(uncompleted) - advanceTimeBy(1_000) // advance time, which will cause the timeout to throw an exception - // ... -} - -fun CoroutineScope.foo(resultDeferred: Deferred) { +@Test +fun testFoo() = runTest { launch { - withTimeout(1_000) { - resultDeferred.await() // await() will suspend forever waiting for uncompleted - // ... - } + println(1) // executes during runCurrent() + delay(1_000) // suspends until time is advanced by at least 1_000 + println(2) // executes during advanceTimeBy(2_000) + delay(500) // suspends until the time is advanced by another 500 ms + println(3) // also executes during advanceTimeBy(2_000) + delay(5_000) // will suspend by another 4_500 ms + println(4) // executes during advanceUntilIdle() } + // the child coroutine has not run yet + runCurrent() + // the child coroutine has called println(1), and is suspended on delay(1_000) + advanceTimeBy(2_000) // progress time, this will cause two calls to `delay` to resume + // the child coroutine has called println(2) and println(3) and suspends for another 4_500 virtual milliseconds + advanceUntilIdle() // will run the child coroutine to completion + assertEquals(6500, currentTime) // the child coroutine finished at virtual time of 6_500 milliseconds } ``` -*Note:* Testing timeouts is simpler with a second coroutine that can be suspended (as in this example). If the -call to `withTimeout` is in a regular suspend function, consider calling `launch` or `async` inside your test body to -create a second coroutine. - -### Using `pauseDispatcher` for explicit execution of `runBlockingTest` +## Using multiple test dispatchers -The eager execution of `launch` and `async` bodies makes many tests easier, but some tests need more fine grained -control of coroutine execution. +The virtual time is controlled by an entity called the [TestCoroutineScheduler], which behaves as the shared source of +virtual time. -To disable eager execution, you can call [pauseDispatcher][DelayController.pauseDispatcher] -to pause the [TestCoroutineDispatcher] that [runBlockingTest] uses. +Several dispatchers can be created that use the same [TestCoroutineScheduler], in which case they will share their +knowledge of the virtual time. -When the dispatcher is paused, all coroutines will be added to a queue instead running. In addition, time will never -auto-progress due to `delay` on a paused dispatcher. +To access the scheduler used for this test, use the [TestScope.testScheduler] property. ```kotlin @Test -fun testFooWithPauseDispatcher() = runBlockingTest { - pauseDispatcher { - foo() - // the coroutine started by foo has not run yet - runCurrent() // the coroutine started by foo advances to delay(1_000) - // the coroutine started by foo has called println(1), and is suspended on delay(1_000) - advanceTimeBy(1_000) // progress time, this will cause the delay to resume - // the coroutine started by foo has called println(2) and has completed here - } - // ... -} - -fun CoroutineScope.foo() { - launch { - println(1) // executes after runCurrent() is called - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeBy(1_000) +fun testWithMultipleDispatchers() = runTest { + val scheduler = testScheduler // the scheduler used for this test + val dispatcher1 = StandardTestDispatcher(scheduler, name = "IO dispatcher") + val dispatcher2 = StandardTestDispatcher(scheduler, name = "Background dispatcher") + launch(dispatcher1) { + delay(1_000) + println("1. $currentTime") // 1000 + delay(200) + println("2. $currentTime") // 1200 + delay(2_000) + println("4. $currentTime") // 3200 + } + val deferred = async(dispatcher2) { + delay(3_000) + println("3. $currentTime") // 3000 + delay(500) + println("5. $currentTime") // 3500 + } + deferred.await() } -} ``` -Using `pauseDispatcher` gives tests explicit control over the progress of time as well as the ability to enqueue all -coroutines. As a best practice consider adding two tests, one paused and one eager, to test coroutines that have -non-trivial external dependencies and side effects in their launch body. - -*Important:* When passed a lambda block, `pauseDispatcher` will resume eager execution immediately after the block. -This will cause time to auto-progress if there are any outstanding `delay` calls that were not resolved before the -`pauseDispatcher` block returned. In advanced situations tests can call [pauseDispatcher][DelayController.pauseDispatcher] -without a lambda block and then explicitly resume the dispatcher with [resumeDispatcher][DelayController.resumeDispatcher]. +**Note: if [Dispatchers.Main] is replaced by a [TestDispatcher], [runTest] will automatically use its scheduler. +This is done so that there is no need to go through the ceremony of passing the correct scheduler to [runTest].** -## Integrating tests with structured concurrency +## Accessing the test coroutine scope -Code that uses structured concurrency needs a [CoroutineScope] in order to launch a coroutine. In order to integrate -[runBlockingTest] with code that uses common structured concurrency patterns tests can provide one (or both) of these -classes to application code. +Structured concurrency ties coroutines to scopes in which they are launched. +[TestScope] is a special coroutine scope designed for testing coroutines, and a new one is automatically created +for [runTest] and used as the receiver for the test body. - | Name | Description | - | ---- | ----------- | - | [TestCoroutineScope] | A [CoroutineScope] which provides detailed control over the execution of coroutines for tests and integrates with [runBlockingTest]. | - | [TestCoroutineDispatcher] | A [CoroutineDispatcher] which can be used for tests and integrates with [runBlockingTest]. | - - Both classes are provided to allow for various testing needs. Depending on the code that's being - tested, it may be easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] will accept - a [TestCoroutineDispatcher] but not a [TestCoroutineScope]. - - [TestCoroutineScope] will always use a [TestCoroutineDispatcher] to execute coroutines. It - also uses [TestCoroutineExceptionHandler] to convert uncaught exceptions into test failures. +However, it can be convenient to access a `CoroutineScope` before the test has started, for example, to perform mocking +of some +parts of the system in `@BeforeTest` via dependency injection. +In these cases, it is possible to manually create [TestScope], the scope for the test coroutines, in advance, +before the test begins. -By providing [TestCoroutineScope] a test case is able to control execution of coroutines, as well as ensure that -uncaught exceptions thrown by coroutines are converted into test failures. +[TestScope] on its own does not automatically run the code launched in it. +In addition, it is stateful in order to keep track of executing coroutines and uncaught exceptions. +Therefore, it is important to ensure that [TestScope.runTest] is called eventually. -### Providing `TestCoroutineScope` from `runBlockingTest` +```kotlin +val scope = TestScope() -In simple cases, tests can use the [TestCoroutineScope] created by [runBlockingTest] directly. +@BeforeTest +fun setUp() { + Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler)) + TestSubject.setScope(scope) +} -```kotlin -@Test -fun testFoo() = runBlockingTest { - foo() // runBlockingTest passed in a TestCoroutineScope as this +@AfterTest +fun tearDown() { + Dispatchers.resetMain() + TestSubject.resetScope() } -fun CoroutineScope.foo() { - launch { // CoroutineScope for launch is the TestCoroutineScope provided by runBlockingTest - // ... - } +@Test +fun testSubject() = scope.runTest { + // the receiver here is `testScope` } ``` -This style is preferred when the `CoroutineScope` is passed through an extension function style. - -### Providing an explicit `TestCoroutineScope` - -In many cases, the direct style is not preferred because [CoroutineScope] may need to be provided through another means -such as dependency injection or service locators. +## Eagerly entering `launch` and `async` blocks -Tests can declare a [TestCoroutineScope] explicitly in the class to support these use cases. +Some tests only test functionality and don't particularly care about the precise order in which coroutines are +dispatched. +In these cases, it can be cumbersome to always call [runCurrent] or [yield] to observe the effects of the coroutines +after they are launched. -Since [TestCoroutineScope] is stateful in order to keep track of executing coroutines and uncaught exceptions, it is -important to ensure that [cleanupTestCoroutines][TestCoroutineScope.cleanupTestCoroutines] is called after every test case. +If [runTest] executes with an [UnconfinedTestDispatcher], the child coroutines launched at the top level are entered +*eagerly*, that is, they don't go through a dispatch until the first suspension. ```kotlin -class TestClass { - private val testScope = TestCoroutineScope() - private lateinit var subject: Subject - - @Before - fun setup() { - // provide the scope explicitly, in this example using a constructor parameter - subject = Subject(testScope) - } - - @After - fun cleanUp() { - testScope.cleanupTestCoroutines() - } - - @Test - fun testFoo() = testScope.runBlockingTest { - // TestCoroutineScope.runBlockingTest uses the Dispatcher and exception handler provided by `testScope` - subject.foo() - } -} - -class Subject(val scope: CoroutineScope) { - fun foo() { - scope.launch { - // launch uses the testScope injected in setup - } +@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. } ``` -*Note:* [TestCoroutineScope], [TestCoroutineDispatcher], and [TestCoroutineExceptionHandler] are interfaces to enable -test libraries to provide library specific integrations. For example, a JUnit4 `@Rule` may call -[Dispatchers.setMain][setMain] then expose [TestCoroutineScope] for use in tests. - -### Providing an explicit `TestCoroutineDispatcher` - -While providing a [TestCoroutineScope] is slightly preferred due to the improved uncaught exception handling, there are -many situations where it is easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] -does not accept a [TestCoroutineScope] and requires a [TestCoroutineDispatcher] to control coroutine execution in -tests. - -The main difference between `TestCoroutineScope` and `TestCoroutineDispatcher` is how uncaught exceptions are handled. -When using `TestCoroutineDispatcher` uncaught exceptions thrown in coroutines will use regular -[coroutine exception handling](https://kotlinlang.org/docs/reference/coroutines/exception-handling.html). -`TestCoroutineScope` will always use `TestCoroutineDispatcher` as it's dispatcher. - -A test can use a `TestCoroutineDispatcher` without declaring an explicit `TestCoroutineScope`. This is preferred -when the class under test allows a test to provide a [CoroutineDispatcher] but does not allow the test to provide a -[CoroutineScope]. - -Since [TestCoroutineDispatcher] is stateful in order to keep track of executing coroutines, it is -important to ensure that [cleanupTestCoroutines][DelayController.cleanupTestCoroutines] is called after every test case. +If this behavior is desirable, but some parts of the test still require accurate dispatching, for example, to ensure +that the code executes on the correct thread, then simply `launch` a new coroutine with the [StandardTestDispatcher]. ```kotlin -class TestClass { - private val testDispatcher = TestCoroutineDispatcher() - - @Before - fun setup() { - // provide the scope explicitly, in this example using a constructor parameter - Dispatchers.setMain(testDispatcher) - } - - @After - fun cleanUp() { - Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() - } - - @Test - fun testFoo() = testDispatcher.runBlockingTest { - // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines - foo() +@Test +fun testEagerlyEnteringSomeChildCoroutines() = runTest(UnconfinedTestDispatcher()) { + var entered1 = false + launch { + entered1 = true } -} + assertTrue(entered1) // `entered1 = true` already executed -fun foo() { - MainScope().launch { - // launch will use the testDispatcher provided by setMain + var entered2 = false + launch(StandardTestDispatcher(testScheduler)) { + // this block and every coroutine launched inside it will explicitly go through the needed dispatches + entered2 = true } + assertFalse(entered2) + runCurrent() // need to explicitly run the dispatched continuation + assertTrue(entered2) } ``` -*Note:* Prefer to provide `TestCoroutineScope` when it does not complicate code since it will also elevate exceptions -to test failures. However, exposing a `CoroutineScope` to callers of a function may lead to complicated code, in which -case this is the preferred pattern. - -### Using `TestCoroutineScope` and `TestCoroutineDispatcher` without `runBlockingTest` +### Using `withTimeout` inside `runTest` -It is supported to use both [TestCoroutineScope] and [TestCoroutineDispatcher] without using the [runBlockingTest] -builder. Tests may need to do this in situations such as introducing multiple dispatchers and library writers may do -this to provide alternatives to `runBlockingTest`. +Timeouts are also susceptible to time control, so the code below will immediately finish. ```kotlin @Test -fun testFooWithAutoProgress() { - val scope = TestCoroutineScope() - scope.foo() - // foo is suspended waiting for time to progress - scope.advanceUntilIdle() - // foo's coroutine will be completed before here -} - -fun CoroutineScope.foo() { - launch { - println(1) // executes eagerly when foo() is called due to TestCoroutineScope - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeUntilIdle +fun testFooWithTimeout() = runTest { + assertFailsWith { + withTimeout(1_000) { + delay(999) + delay(2) + println("this won't be reached") + } } -} +} ``` -## Using time control with `withContext` - -Calls to `withContext(Dispatchers.IO)` or `withContext(Dispatchers.Default)` are common in coroutines based codebases. -Both dispatchers are not designed to interact with `TestCoroutineDispatcher`. +## Virtual time support with other dispatchers -Tests should provide a `TestCoroutineDispatcher` to replace these dispatchers if the `withContext` calls `delay` in the -function under test. For example, a test that calls `veryExpensiveOne` should provide a `TestCoroutineDispatcher` using -either dependency injection, a service locator, or a default parameter. +Calls to `withContext(Dispatchers.IO)`, `withContext(Dispatchers.Default)` ,and `withContext(Dispatchers.Main)` are +common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers +using the virtual time source, so delays will not be skipped in them. ```kotlin -suspend fun veryExpensiveOne() = withContext(Dispatchers.Default) { +suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) { delay(1_000) - 1 // for very expensive values of 1 + 1 } -``` - -In situations where the code inside the `withContext` is very simple, it is not as important to provide a test -dispatcher. The function `veryExpensiveTwo` will behave identically in a `TestCoroutineDispatcher` and -`Dispatchers.Default` after the thread switch for `Dispatchers.Default`. Because `withContext` always returns a value by -directly, there is no need to inject a `TestCoroutineDispatcher` into this function. -```kotlin -suspend fun veryExpensiveTwo() = withContext(Dispatchers.Default) { - 2 // for very expensive values of 2 +fun testExpensiveFunction() = runTest { + val result = veryExpensiveFunction() // will take a whole real-time second to execute + // the virtual time at this point is still 0 } ``` -Tests should provide a `TestCoroutineDispatcher` to code that calls `withContext` to provide time control for -delays, or when execution control is needed to test complex logic. - +Tests should, when possible, replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the +function under test. For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using +either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time. ### Status of the API @@ -426,36 +362,32 @@ This API is experimental and it is may change before migrating out of experiment Changes during experimental may have deprecation applied when possible, but it is not advised to use the API in stable code before it leaves experimental due to possible breaking changes. -If you have any suggestions for improvements to this experimental API please share them them on the +If you have any suggestions for improvements to this experimental API please share them them on the [issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues). -[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..d90a319825 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,43 @@ 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 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 +103,21 @@ 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 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..e6d0c3970d --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt @@ -0,0 +1,252 @@ +/* + * 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, 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. + * + * 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, + testBody: suspend T.() -> Unit, + cleanup: () -> List, +) { + val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!! + /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with + * [TestCoroutineDispatcher], because the event loop is not started. */ + 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) { + try { + cleanup() + } catch (e: UncompletedCoroutinesError) { + // we expect these and will instead throw a more informative exception just below. + emptyList() + }.throwAll() + throw UncompletedCoroutinesError("The test coroutine was not completed after waiting for $dispatchTimeoutMs ms") + } + } + } + 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() +} + +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..d256f27fb0 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt @@ -0,0 +1,231 @@ +/* + * 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.* + +/** + * 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 +// TODO: maybe make this a `TimeSource`? +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. */ + @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 +} + +// 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..3b756b19e9 --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt @@ -0,0 +1,52 @@ +/* + * 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]. + */ +@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..ffd5c01f7a --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestScope.kt @@ -0,0 +1,237 @@ +/* + * 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.* + +/** + * 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 [TestScope.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) + +/** + * 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) } + } + } + } + + 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) \ No newline at end of file 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..a63311b7e4 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/Helpers.kt @@ -0,0 +1,74 @@ +/* + * 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. + */ +@OptIn(ExperimentalTime::class) +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..e063cdacf1 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt @@ -0,0 +1,296 @@ +/* + * 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.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 too low of a dispatch timeout causes crashes. */ + @Test + fun testRunTestWithLargeTimeout() = runTest(dispatchTimeoutMs = 5000) { + withContext(Dispatchers.Default) { + delay(50) + } + } + + /** Tests uncaught exceptions taking priority over dispatch timeout in error reports. */ + @Test + @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native + fun testRunTestTimingOutAndThrowing() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTest(dispatchTimeoutMs = 1) { + coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, IllegalArgumentException()) + 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..203ddc4f11 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt @@ -0,0 +1,323 @@ +/* + * 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 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() + } + } + + 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..66a6c24e8f --- /dev/null +++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt @@ -0,0 +1,99 @@ +/* + * 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 + @NoJs + 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/FailingTests.kt b/kotlinx-coroutines-test/js/test/FailingTests.kt new file mode 100644 index 0000000000..4746a737fa --- /dev/null +++ b/kotlinx-coroutines-test/js/test/FailingTests.kt @@ -0,0 +1,37 @@ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.internal.* +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 { + + private var tearDownEntered = false + + @BeforeTest + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + tearDownEntered = true + } + + /** [TestDispatchersTest.testMainMocking]. */ + @Test + fun testAfterTestIsConcurrent() = runTest { + try { + val mainAtStart = TestMainDispatcher.currentTestDispatcher ?: return@runTest + withContext(Dispatchers.Default) { + // context switch + } + assertNotSame(mainAtStart, TestMainDispatcher.currentTestDispatcher!!) + } finally { + assertTrue(tearDownEntered) + } + } +} 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/src/DelayController.kt b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt similarity index 51% rename from kotlinx-coroutines-test/src/DelayController.kt rename to kotlinx-coroutines-test/jvm/src/migration/DelayController.kt index 6e72222718..e0701ae2cd 100644 --- a/kotlinx-coroutines-test/src/DelayController.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt @@ -1,25 +1,30 @@ /* * 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.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* /** * Control the virtual clock time of a [CoroutineDispatcher]. * - * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher]. + * Testing libraries may expose this interface to the tests instead of [TestCoroutineDispatcher]. */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +@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 // Since 1.2.1, tentatively till 1.3.0 + @ExperimentalCoroutinesApi public val currentTime: Long /** @@ -57,7 +62,7 @@ public interface DelayController { * @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 + @ExperimentalCoroutinesApi public fun advanceTimeBy(delayTimeMillis: Long): Long /** @@ -68,7 +73,7 @@ public interface DelayController { * * @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 + @ExperimentalCoroutinesApi public fun advanceUntilIdle(): Long /** @@ -76,17 +81,17 @@ public interface DelayController { * * Calling this function will never advance the clock. */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + @ExperimentalCoroutinesApi 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 + * @throws AssertionError 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) + @ExperimentalCoroutinesApi + @Throws(AssertionError::class) public fun cleanupTestCoroutines() /** @@ -98,7 +103,11 @@ public interface DelayController { * 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 + @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) /** @@ -107,7 +116,11 @@ public interface DelayController { * 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 + @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() /** @@ -117,13 +130,74 @@ public interface DelayController { * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], * or [advanceUntilIdle]. */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + @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() } -/** - * 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) +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..4524bf2867 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt @@ -0,0 +1,190 @@ +/* + * 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 kotlinx.coroutines.selects.* +import kotlin.coroutines.* +import kotlin.jvm.* + +/** + * 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 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.", 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]. + */ +@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 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]. + */ +@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 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, 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() +} 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..45a3815681 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt @@ -0,0 +1,332 @@ +/* + * 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. + */ +@ExperimentalCoroutinesApi +@Deprecated("Use `TestScope` in combination with `runTest` instead") +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 +public sealed 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. + * + * 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..a76263ddd2 --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt @@ -0,0 +1,277 @@ +/* + * 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.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 -> + assertFailsWith { fn() } + }) { + runTestWithLegacyScope(dispatchTimeoutMs = 1) { + coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, IllegalArgumentException()) + 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/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/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/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..7cc594496e 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; } 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/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/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-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/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/settings.gradle b/settings.gradle index 44effa7c20..d7673a64f9 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 { diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 71b2d69c5c..5b5522beae 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.0-RC" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your 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 =