diff --git a/CHANGES.md b/CHANGES.md
index 4ac7156672..dc14a5e99a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,20 @@
# Change log for kotlinx.coroutines
+## Version 1.8.0-RC
+
+* Implement the library for the Web Assembly (Wasm) for JavaScript (#3713). Thanks @igoriakovlev!
+* On Android, ensure that `Dispatchers.Main != Dispatchers.Main.immediate` (#3545, #3963).
+* `kotlinx-coroutines-debug` is published with the incorrect Java 9 module info (#3944).
+* Major Kotlin version update: was 1.8.20, became 1.9.21.
+* `kotlinx-coroutines-test`: set the default timeout of `runTest` to 60 seconds, added the ability to configure it on the JVM with the `kotlinx.coroutines.test.default_timeout=10s` (#3800).
+* `kotlinx-coroutines-test`: fixed a bug that could lead to not all uncaught exceptions being reported after some tests failed (#3800).
+* `delay(Duration)` rounds nanoseconds up to whole milliseconds and not down (#3920). Thanks @kevincianfarini!
+* `Dispatchers.Default` and the default thread for background work are guaranteed to use the same context classloader as the object containing it them (#3832).
+* It is guaranteed that by the time `SharedFlow.collect` suspends for the first time, it's registered as a subscriber for that `SharedFlow` (#3885). Before, it was also true, but not documented.
+* Atomicfu version is updated to 0.23.1, and Kotlin/Native atomic transformations are enabled, reducing the footprint of coroutine-heavy code (#3954).
+* Added a workaround for miscompilation of `withLock` on JS (#3881). Thanks @CLOVIS-AI!
+* Small tweaks and documentation fixes.
+
## Version 1.7.3
* Disabled the publication of the multiplatform library metadata for the old (1.6 and earlier) KMP Gradle plugin (#3809).
diff --git a/README.md b/README.md
index 2cc23f644f..a053a0df1a 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,12 @@
[](https://kotlinlang.org/docs/components-stability.html)
[](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
[](https://www.apache.org/licenses/LICENSE-2.0)
-[](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.3)
-[](http://kotlinlang.org)
+[](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.8.0-RC)
+[](http://kotlinlang.org)
[](https://kotlinlang.slack.com/messages/coroutines/)
Library support for Kotlin coroutines with [multiplatform](#multiplatform) support.
-This is a companion version for the Kotlin `1.8.20` release.
+This is a companion version for the Kotlin `1.9.21` release.
```kotlin
suspend fun main() = coroutineScope {
@@ -85,7 +85,7 @@ Add dependencies (you can also add other modules that you need):
org.jetbrains.kotlinxkotlinx-coroutines-core
- 1.7.3
+ 1.8.0-RC
```
@@ -93,7 +93,7 @@ And make sure that you use the latest Kotlin version:
```xml
- 1.8.20
+ 1.9.21
```
@@ -103,7 +103,7 @@ Add dependencies (you can also add other modules that you need):
```kotlin
dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC")
}
```
@@ -112,10 +112,10 @@ And make sure that you use the latest Kotlin version:
```kotlin
plugins {
// For build.gradle.kts (Kotlin DSL)
- kotlin("jvm") version "1.8.20"
+ kotlin("jvm") version "1.9.21"
// For build.gradle (Groovy DSL)
- id "org.jetbrains.kotlin.jvm" version "1.8.20"
+ id "org.jetbrains.kotlin.jvm" version "1.9.21"
}
```
@@ -133,7 +133,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android)
module as a dependency when using `kotlinx.coroutines` on Android:
```kotlin
-implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
```
This gives you access to the Android [Dispatchers.Main]
@@ -168,7 +168,7 @@ In common code that should get compiled for different platforms, you can add a d
```kotlin
commonMain {
dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC")
}
}
```
@@ -180,7 +180,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://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.7.3)
+[`kotlinx-coroutines-core-js`](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.8.0-RC)
(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
@@ -215,7 +215,7 @@ See [Contributing Guidelines](CONTRIBUTING.md).
[Dispatchers.IO]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-i-o.html
[asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
[Promise.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await.html
-[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html
+[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/[js]promise.html
[Window.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
diff --git a/build.gradle b/build.gradle
index e7d405e124..4f7a1554c7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,8 +6,6 @@
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
-import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
-import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.dokka.gradle.DokkaTaskPartial
@@ -63,7 +61,6 @@ buildscript {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version"
classpath "org.jetbrains.kotlinx:kotlinx-knit:$knit_version"
- classpath "com.github.node-gradle:gradle-node-plugin:$gradle_node_version"
classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$binary_compatibility_validator_version"
classpath "ru.vyarus:gradle-animalsniffer-plugin:1.5.4" // Android API check
classpath "org.jetbrains.kotlin:atomicfu:$kotlin_version"
@@ -160,8 +157,12 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != core
apply from: rootProject.file("gradle/compile-native-multiplatform.gradle")
}
+ apply from: rootProject.file("gradle/compile-jsAndWasmShared-multiplatform.gradle")
+
apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
- apply from: rootProject.file("gradle/publish-npm-js.gradle")
+
+ apply from: rootProject.file("gradle/compile-wasm-multiplatform.gradle")
+
kotlin.sourceSets.commonMain.dependencies {
api project(":$coreModule")
}
@@ -181,6 +182,7 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != core
apply plugin: "bom-conventions"
apply plugin: "java-modularity-conventions"
+apply plugin: "version-file-conventions"
if (build_snapshot_train) {
println "Hacking test tasks, removing stress and flaky tests"
@@ -256,40 +258,6 @@ 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"]
- } 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
@@ -303,7 +271,7 @@ allprojects {
// --------------- Configure sub-projects that are published ---------------
-def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm", true)
+def publishTasks = getTasksByName("publish", true)
task deploy(dependsOn: publishTasks)
@@ -372,3 +340,17 @@ if (CacheRedirector.enabled) {
nodeJsExtension.nodeDownloadBaseUrl = CacheRedirector.maybeRedirect(nodeJsExtension.nodeDownloadBaseUrl)
}
}
+
+// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4
+// check it here:
+// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt
+extensions.findByType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.class).with {
+ // canary nodejs that supports recent Wasm GC changes
+ it.nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
+ it.nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
+}
+
+// Drop this when node js version become stable
+tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask).configureEach {
+ args.add("--ignore-engines")
+}
diff --git a/buildSrc/src/main/kotlin/CommunityProjectsBuild.kt b/buildSrc/src/main/kotlin/CommunityProjectsBuild.kt
index 155c9e48ac..81a25f8d96 100644
--- a/buildSrc/src/main/kotlin/CommunityProjectsBuild.kt
+++ b/buildSrc/src/main/kotlin/CommunityProjectsBuild.kt
@@ -15,7 +15,7 @@ private val LOGGER: Logger = Logger.getLogger("Kotlin settings logger")
* Functions in this file are responsible for configuring kotlinx.coroutines build against a custom dev version
* of Kotlin compiler.
* Such configuration is used in a composite community build of Kotlin in order to check whether not-yet-released changes
- * are compatible with our libraries (aka "integration testing that substitues lack of unit testing").
+ * are compatible with our libraries (aka "integration testing that substitutes lack of unit testing").
*/
/**
diff --git a/buildSrc/src/main/kotlin/VersionFile.kt b/buildSrc/src/main/kotlin/VersionFile.kt
new file mode 100644
index 0000000000..ef0ef0faa1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/VersionFile.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+import org.gradle.api.*
+import org.gradle.api.tasks.*
+
+/**
+ * Adds 'module_name.version' file to the project's JAR META-INF
+ * for the better toolability. See #2941
+ */
+object VersionFile {
+ fun registerVersionFileTask(project: Project): TaskProvider {
+ val versionFile = project.layout.buildDirectory.file("${project.name.replace('-', '_')}.version")
+ val version = project.version.toString()
+ return project.tasks.register("versionFileTask") {
+ outputs.file(versionFile)
+ doLast {
+ versionFile.get().asFile.writeText(version)
+ }
+ }
+ }
+
+ fun fromVersionFile(target: AbstractCopyTask, versionFileTask: TaskProvider) {
+ target.from(versionFileTask) {
+ into("META-INF")
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/configure-compilation-conventions.gradle.kts b/buildSrc/src/main/kotlin/configure-compilation-conventions.gradle.kts
index 9e22b4515c..ce8e599106 100644
--- a/buildSrc/src/main/kotlin/configure-compilation-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/configure-compilation-conventions.gradle.kts
@@ -10,7 +10,7 @@ configure(subprojects) {
if (name in sourceless) return@configure
apply(plugin = "kotlinx-atomicfu")
tasks.withType>().configureEach {
- val isMainTaskName = name == "compileKotlin" || name == "compileKotlinJvm"
+ val isMainTaskName = name.startsWith("compileKotlin")
kotlinOptions {
languageVersion = getOverriddenKotlinLanguageVersion(project)
apiVersion = getOverriddenKotlinApiVersion(project)
@@ -19,8 +19,11 @@ configure(subprojects) {
}
val newOptions =
listOf(
- "-progressive", "-Xno-param-assertions", "-Xno-receiver-assertions",
- "-Xno-call-assertions"
+ "-progressive",
+ "-Xno-param-assertions",
+ "-Xno-receiver-assertions",
+ "-Xexpect-actual-classes",
+ "-Xno-call-assertions",
) + optInAnnotations.map { "-opt-in=$it" }
freeCompilerArgs = freeCompilerArgs + newOptions
}
@@ -28,4 +31,4 @@ configure(subprojects) {
}
val KotlinCommonOptions.versionsAreNotOverridden: Boolean
- get() = languageVersion == null && apiVersion == null
\ No newline at end of file
+ get() = languageVersion == null && apiVersion == null
diff --git a/buildSrc/src/main/kotlin/kotlin-js-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-js-conventions.gradle.kts
index c1897ca749..5d3902f60a 100644
--- a/buildSrc/src/main/kotlin/kotlin-js-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/kotlin-js-conventions.gradle.kts
@@ -5,6 +5,7 @@
// Platform-specific configuration to compile JS modules
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile
+import org.jetbrains.kotlin.gradle.targets.js.*
plugins {
kotlin("js")
@@ -15,7 +16,7 @@ dependencies {
}
kotlin {
- js(LEGACY) {
+ js(IR) {
moduleName = project.name.removeSuffix("-js")
}
@@ -35,6 +36,5 @@ tasks.withType {
kotlinOptions {
moduleKind = "umd"
sourceMap = true
- metaInfo = true
}
}
diff --git a/buildSrc/src/main/kotlin/version-file-conventions.gradle.kts b/buildSrc/src/main/kotlin/version-file-conventions.gradle.kts
new file mode 100644
index 0000000000..587e184b30
--- /dev/null
+++ b/buildSrc/src/main/kotlin/version-file-conventions.gradle.kts
@@ -0,0 +1,17 @@
+import org.gradle.api.tasks.bundling.*
+
+configure(subprojects.filter { !unpublished.contains(it.name) && it.name !in sourceless }) {
+ val project = this
+ val jarTaskName = when {
+ project.name == "kotlinx-coroutines-debug" -> {
+ project.apply(plugin = "com.github.johnrengelman.shadow")
+ "shadowJar"
+ }
+ isMultiplatform -> "jvmJar"
+ else -> "jar"
+ }
+ val versionFileTask = VersionFile.registerVersionFileTask(project)
+ tasks.withType(Jar::class.java).named(jarTaskName) {
+ VersionFile.fromVersionFile(this, versionFileTask)
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 3d9431be0a..ee5891fc7a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,14 +3,14 @@
#
# Kotlin
-version=1.7.3-SNAPSHOT
+version=1.8.0-RC-SNAPSHOT
group=org.jetbrains.kotlinx
-kotlin_version=1.8.20
+kotlin_version=1.9.21
# Dependencies
junit_version=4.12
junit5_version=5.7.0
-atomicfu_version=0.21.0
+atomicfu_version=0.23.1
knit_version=0.5.0-Beta
html_version=0.7.2
lincheck_version=2.18.1
@@ -33,18 +33,6 @@ androidx_annotation_version=1.1.0
robolectric_version=4.9
baksmali_version=2.2.7
-# JS
-kotlin.js.compiler=both
-gradle_node_version=3.1.1
-node_version=10.0.0
-npm_version=5.7.1
-mocha_version=6.2.2
-mocha_headless_chrome_version=1.8.2
-mocha_teamcity_reporter_version=3.0.0
-source_map_support_version=0.5.16
-jsdom_version=15.2.1
-jsdom_global_version=3.0.2
-
# Settings
kotlin.incremental.multiplatform=true
kotlin.native.ignoreDisabledTargets=true
@@ -58,3 +46,4 @@ kotlinx.atomicfu.enableJvmIrTransformation=true
# When the flag below is set to `true`, AtomicFU cannot process
# usages of `moveForward` in `ConcurrentLinkedList.kt` correctly.
kotlinx.atomicfu.enableJsIrTransformation=false
+kotlinx.atomicfu.enableNativeIrTransformation=true
diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle
index c6fc757c7d..1935fbf197 100644
--- a/gradle/compile-js-multiplatform.gradle
+++ b/gradle/compile-js-multiplatform.gradle
@@ -2,79 +2,25 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-apply from: rootProject.file('gradle/node-js.gradle')
-
kotlin {
js {
moduleName = project.name
-
- // In 1.4.x it has in `both` and `legacy` mode and js() is of type `KotlinJsTarget`
- // `irTarget` is non-null in `both` mode
- // and contains appropriate `irTarget` with type `KotlinJsIrTarget`
- // `irTarget` is null in `legacy` mode
- if (it.irTarget != null) {
- irTarget.nodejs()
- irTarget.compilations['main']?.dependencies {
- api "org.jetbrains.kotlinx:atomicfu-js:$atomicfu_version"
- }
+ nodejs()
+ compilations['main']?.dependencies {
+ api "org.jetbrains.kotlinx:atomicfu-js:$atomicfu_version"
}
}
sourceSets {
+ jsMain {
+ dependsOn(jsAndWasmSharedMain)
+ }
+ jsTest {
+ dependsOn(jsAndWasmSharedTest)
+ }
+
jsTest.dependencies {
api "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version"
}
}
}
-
-// When source sets are configured
-apply from: rootProject.file('gradle/test-mocha-js.gradle')
-
-def compileJsLegacy = tasks.hasProperty("compileKotlinJsLegacy")
- ? compileKotlinJsLegacy
- : compileKotlinJs
-
-def compileTestJsLegacy = tasks.hasProperty("compileTestKotlinJsLegacy")
- ? compileTestKotlinJsLegacy
- : compileTestKotlinJs
-
-compileJsLegacy.configure {
- kotlinOptions.metaInfo = true
- kotlinOptions.sourceMap = true
- kotlinOptions.moduleKind = 'umd'
-
- kotlinOptions {
- // drop -js suffix from outputFile
- def baseName = project.name - "-js"
- outputFile = new File(outputFileProperty.get().parent, baseName + ".js")
- }
-}
-
-compileTestJsLegacy.configure {
- kotlinOptions.metaInfo = true
- kotlinOptions.sourceMap = true
- kotlinOptions.moduleKind = 'umd'
-}
-
-
-task populateNodeModules(type: Copy, dependsOn: compileTestJsLegacy) {
- // we must copy output that is transformed by atomicfu
- from(kotlin.js().compilations.main.output.allOutputs)
- into node.nodeProjectDir.dir("node_modules")
-
- def configuration = configurations.hasProperty("jsLegacyTestRuntimeClasspath")
- ? configurations.jsLegacyTestRuntimeClasspath
- : configurations.jsTestRuntimeClasspath
-
- from(files {
- configuration.collect { File file ->
- file.name.endsWith(".jar") ?
- zipTree(file.absolutePath).matching {
- include '*.js'
- include '*.js.map'
- } : files()
- }
- }.builtBy(configuration))
-}
-
-npmInstall.dependsOn populateNodeModules
diff --git a/gradle/compile-jsAndWasmShared-multiplatform.gradle b/gradle/compile-jsAndWasmShared-multiplatform.gradle
new file mode 100644
index 0000000000..8f489019a8
--- /dev/null
+++ b/gradle/compile-jsAndWasmShared-multiplatform.gradle
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+kotlin {
+ sourceSets {
+ jsAndWasmSharedMain {
+ dependsOn(commonMain)
+ }
+ jsAndWasmSharedTest {
+ dependsOn(commonTest)
+ }
+ }
+}
+
+// Disable intermediate sourceSet compilation because we do not need js-wasmJs artifact
+tasks.configureEach {
+ if (name == 'compileJsAndWasmSharedMainKotlinMetadata') {
+ enabled = false
+ }
+}
diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle
index 3b2758854f..4f74cf5278 100644
--- a/gradle/compile-native-multiplatform.gradle
+++ b/gradle/compile-native-multiplatform.gradle
@@ -41,10 +41,6 @@ kotlin {
addTarget(presets.androidNativeX64)
addTarget(presets.mingwX64)
addTarget(presets.watchosDeviceArm64)
-
- // Deprecated, but were provided by coroutine; can be removed only when K/N drops the target
- addTarget(presets.iosArm32)
- addTarget(presets.watchosX86)
}
sourceSets {
diff --git a/gradle/compile-wasm-multiplatform.gradle b/gradle/compile-wasm-multiplatform.gradle
new file mode 100644
index 0000000000..3692c6d6b8
--- /dev/null
+++ b/gradle/compile-wasm-multiplatform.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+kotlin {
+ wasmJs {
+ moduleName = project.name
+ nodejs()
+ compilations['main']?.dependencies {
+ api "org.jetbrains.kotlinx:atomicfu-wasm-js:$atomicfu_version"
+ }
+ }
+
+ sourceSets {
+ wasmJsMain {
+ dependsOn(jsAndWasmSharedMain)
+ }
+ wasmJsTest {
+ dependsOn(jsAndWasmSharedTest)
+ }
+
+ wasmJsTest.dependencies {
+ api "org.jetbrains.kotlin:kotlin-test-wasm-js:$kotlin_version"
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle/node-js.gradle b/gradle/node-js.gradle
deleted file mode 100644
index 5eddc5fa37..0000000000
--- a/gradle/node-js.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-apply plugin: 'com.github.node-gradle.node'
-
-node {
- version = "$node_version"
- npmVersion = "$npm_version"
- download = true
- nodeProjectDir = file(buildDir)
-}
-
-// Configures testing for JS modules
-
-task prepareNodePackage(type: Copy) {
- from("npm") {
- include 'package.json'
- // Postpone expansion of package.json until we configure version property in build.gradle
- def copySpec = it
- afterEvaluate {
- copySpec.expand(project.properties + [kotlinDependency: ""])
- }
- }
- from("npm") {
- exclude 'package.json'
- }
- into node.nodeProjectDir
-}
-
-npmInstall.dependsOn prepareNodePackage
-
-// Workaround the problem with Node downloading
-repositories.whenObjectAdded {
- if (it instanceof IvyArtifactRepository) {
- metadataSources {
- artifact()
- }
- }
-}
diff --git a/gradle/publish-npm-js.gradle b/gradle/publish-npm-js.gradle
deleted file mode 100644
index 9d4152770c..0000000000
--- a/gradle/publish-npm-js.gradle
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-def prop(name, defVal) {
- def value = project.properties[name]
- if (value == null) return defVal
- return value
-}
-
-def distTag(version) {
- def i = version.indexOf('-')
- if (i > 0) return version.substring(i + 1)
- return "latest"
-}
-
-def npmTemplateDir = file("$projectDir/npm")
-def npmDeployDir = file("$buildDir/npm")
-
-def authToken = prop("kotlin.npmjs.auth.token", "")
-def dryRun = prop("dryRun", "false")
-
-def jsLegacy = kotlin.targets.hasProperty("jsLegacy")
- ? kotlin.targets.jsLegacy
- : kotlin.targets.js
-
-// Note: publish transformed files using dependency on sourceSets.main.output
-task preparePublishNpm(type: Copy) {
- from(npmTemplateDir) {
- // Postpone expansion of package.json until we configure version property in build.gradle
- def copySpec = it
- afterEvaluate {
- copySpec.expand(project.properties + [kotlinDependency: "\"kotlin\": \"$kotlin_version\""])
- }
- }
- // we must publish output that is transformed by atomicfu
- from(jsLegacy.compilations.main.output.allOutputs)
- into npmDeployDir
-}
-
-task publishNpm(type: NpmTask, dependsOn: [preparePublishNpm]) {
- workingDir = npmDeployDir
-
- def npmDeployTag = distTag(version)
- def deployArgs = ['publish',
- "--//registry.npmjs.org/:_authToken=$authToken",
- "--tag=$npmDeployTag"]
- if (dryRun == "true") {
- println("$npmDeployDir \$ npm arguments: $deployArgs")
- args = ['pack']
- } else {
- args = deployArgs
- }
-}
diff --git a/gradle/publish.gradle b/gradle/publish.gradle
index f3b1561dde..9984ae8e70 100644
--- a/gradle/publish.gradle
+++ b/gradle/publish.gradle
@@ -2,8 +2,6 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-import org.gradle.util.VersionNumber
-
// Configures publishing of Maven artifacts to Maven Central
apply plugin: 'maven-publish'
@@ -15,10 +13,6 @@ def isMultiplatform = project.name == "kotlinx-coroutines-core" || project.name
def isBom = project.name == "kotlinx-coroutines-bom"
if (!isBom) {
- if (project.name == "kotlinx-coroutines-debug") {
- apply plugin: "com.github.johnrengelman.shadow"
- }
-
// empty xxx-javadoc.jar
task javadocJar(type: Jar) {
archiveClassifier = 'javadoc'
diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle
deleted file mode 100644
index 1ec297e415..0000000000
--- a/gradle/test-mocha-js.gradle
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-// -- Testing with Mocha under Node
-
-task installDependenciesMochaNode(type: NpmTask, dependsOn: [npmInstall]) {
- args = ['install',
- "mocha@$mocha_version",
- "source-map-support@$source_map_support_version",
- '--no-save']
- if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
-}
-
-def compileJsLegacy = tasks.hasProperty("compileKotlinJsLegacy")
- ? compileKotlinJsLegacy
- : compileKotlinJs
-
-def compileTestJsLegacy = tasks.hasProperty("compileTestKotlinJsLegacy")
- ? compileTestKotlinJsLegacy
- : compileTestKotlinJs
-
-// todo: use atomicfu-transformed test files here (not critical)
-task testMochaNode(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaNode]) {
- script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha")
- args = [compileTestJsLegacy.outputFileProperty.get().path, '--require', 'source-map-support/register']
- if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
-}
-
-def jsLegacyTestTask = project.tasks.findByName('jsLegacyTest') ? jsLegacyTest : jsTest
-
-// TODO
-//jsLegacyTestTask.dependsOn testMochaNode
-
-// -- Testing with Mocha under headless Chrome
-
-task installDependenciesMochaChrome(type: NpmTask, dependsOn: [npmInstall]) {
- args = ['install',
- "mocha@$mocha_version",
- "mocha-headless-chrome@$mocha_headless_chrome_version",
- "kotlin@$kotlin_version",
- "kotlin-test@$kotlin_version",
- '--no-save']
- if (project.hasProperty("teamcity")) args.addAll([
- "mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
-}
-
-def mochaChromeTestPage = file("$buildDir/test-page.html")
-
-task prepareMochaChrome(dependsOn: [compileTestJsLegacy, installDependenciesMochaChrome]) {
- outputs.file(mochaChromeTestPage)
-}
-
-prepareMochaChrome.doLast {
- def nodeProjDir = node.nodeProjectDir.getAsFile().get()
- mochaChromeTestPage.text = """
-
-
- Mocha Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- """
-}
-
-task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) {
- script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha-headless-chrome/bin/start")
- args = [compileTestJsLegacy.outputFileProperty.get().path, '--file', mochaChromeTestPage]
- if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
-}
-
-// todo: Commented out because mocha-headless-chrome does not work on TeamCity
-//jsTest.dependsOn testMochaChrome
-
-// -- Testing with Mocha under jsdom
-
-task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) {
- args = ['install',
- "mocha@$mocha_version",
- "jsdom@$jsdom_version",
- "jsdom-global@$jsdom_global_version",
- "source-map-support@$source_map_support_version",
- '--no-save']
- if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
-}
-
-task testMochaJsdom(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaJsdom]) {
- script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha")
- args = [compileTestJsLegacy.outputFileProperty.get().path, '--require', 'source-map-support/register', '--require', 'jsdom-global/register']
- if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
-}
-
-// TODO
-//jsLegacyTestTask.dependsOn testMochaJsdom
diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle
index 26ee9d99dc..1a231afbdf 100644
--- a/integration-testing/build.gradle
+++ b/integration-testing/build.gradle
@@ -179,3 +179,8 @@ compileKotlin {
jvmTarget = "1.8"
}
}
+
+// Drop this when node js version become stable
+tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask.class).configureEach {
+ it.args.add("--ignore-engines")
+}
diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties
index 7aeb10ac0a..f74fc7d901 100644
--- a/integration-testing/gradle.properties
+++ b/integration-testing/gradle.properties
@@ -1,5 +1,5 @@
-kotlin_version=1.8.20
-coroutines_version=1.7.3-SNAPSHOT
+kotlin_version=1.9.21
+coroutines_version=1.8.0-RC-SNAPSHOT
asm_version=9.3
kotlin.code.style=official
diff --git a/integration-testing/smokeTest/build.gradle b/integration-testing/smokeTest/build.gradle
index 26cd02b600..ad6a485ed2 100644
--- a/integration-testing/smokeTest/build.gradle
+++ b/integration-testing/smokeTest/build.gradle
@@ -3,10 +3,10 @@ plugins {
}
repositories {
- // Coroutines from the outer project are published by previous CI buils step
- mavenLocal()
mavenCentral()
maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" }
+ // Coroutines from the outer project are published by previous CI buils step
+ mavenLocal()
}
kotlin {
@@ -14,6 +14,9 @@ kotlin {
js(IR) {
nodejs()
}
+ wasmJs() {
+ nodejs()
+ }
sourceSets {
commonMain {
@@ -34,6 +37,11 @@ kotlin {
implementation kotlin('test-js')
}
}
+ wasmJsTest {
+ dependencies {
+ implementation kotlin('test-wasm-js')
+ }
+ }
jvmTest {
dependencies {
implementation kotlin('test')
@@ -50,3 +58,11 @@ kotlin {
}
}
+// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4
+// check it here:
+// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt
+rootProject.extensions.findByType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.class).with {
+ // canary nodejs that supports recent Wasm GC changes
+ it.nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
+ it.nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
+}
\ No newline at end of file
diff --git a/js/example-frontend-js/build.gradle.kts b/js/example-frontend-js/build.gradle.kts
index 1cc587b740..ec718a28ad 100644
--- a/js/example-frontend-js/build.gradle.kts
+++ b/js/example-frontend-js/build.gradle.kts
@@ -3,22 +3,22 @@
*/
kotlin {
- js(LEGACY) {
+ js(IR) {
binaries.executable()
browser {
- distribution {
- directory = directory.parentFile.resolve("dist")
- }
- commonWebpackConfig {
+ distribution(Action {
+ outputDirectory.set(outputDirectory.get().asFile.parentFile.resolve("dist"))
+ })
+ commonWebpackConfig(Action {
cssSupport {
enabled.set(true)
}
- }
- testTask {
+ })
+ testTask(Action {
useKarma {
useChromeHeadless()
}
- }
+ })
}
}
}
diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
index 234bf10420..ad5e68cba3 100644
--- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
+++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
@@ -264,6 +264,7 @@ public final class kotlinx/coroutines/CoroutineStart : java/lang/Enum {
public static final field DEFAULT Lkotlinx/coroutines/CoroutineStart;
public static final field LAZY Lkotlinx/coroutines/CoroutineStart;
public static final field UNDISPATCHED Lkotlinx/coroutines/CoroutineStart;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
public final fun invoke (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)V
public final fun invoke (Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V
public final fun isLazy ()Z
@@ -320,7 +321,7 @@ public abstract interface annotation class kotlinx/coroutines/DelicateCoroutines
}
public final class kotlinx/coroutines/DispatchedCoroutine {
- public static final fun get_decision$FU ()Ljava/util/concurrent/atomic/AtomicIntegerFieldUpdater;
+ public static final synthetic fun get_decision$volatile$FU$kotlinx_coroutines_core ()Ljava/util/concurrent/atomic/AtomicIntegerFieldUpdater;
}
public abstract class kotlinx/coroutines/DispatchedTask : kotlinx/coroutines/scheduling/Task {
@@ -672,6 +673,7 @@ public final class kotlinx/coroutines/channels/BufferOverflow : java/lang/Enum {
public static final field DROP_LATEST Lkotlinx/coroutines/channels/BufferOverflow;
public static final field DROP_OLDEST Lkotlinx/coroutines/channels/BufferOverflow;
public static final field SUSPEND Lkotlinx/coroutines/channels/BufferOverflow;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/channels/BufferOverflow;
public static fun values ()[Lkotlinx/coroutines/channels/BufferOverflow;
}
@@ -912,6 +914,7 @@ public final class kotlinx/coroutines/channels/TickerChannelsKt {
public final class kotlinx/coroutines/channels/TickerMode : java/lang/Enum {
public static final field FIXED_DELAY Lkotlinx/coroutines/channels/TickerMode;
public static final field FIXED_PERIOD Lkotlinx/coroutines/channels/TickerMode;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/channels/TickerMode;
public static fun values ()[Lkotlinx/coroutines/channels/TickerMode;
}
@@ -1183,6 +1186,7 @@ public final class kotlinx/coroutines/flow/SharingCommand : java/lang/Enum {
public static final field START Lkotlinx/coroutines/flow/SharingCommand;
public static final field STOP Lkotlinx/coroutines/flow/SharingCommand;
public static final field STOP_AND_RESET_REPLAY_CACHE Lkotlinx/coroutines/flow/SharingCommand;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/flow/SharingCommand;
public static fun values ()[Lkotlinx/coroutines/flow/SharingCommand;
}
diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle
index 6b0d6c2c09..fbd62e0cf8 100644
--- a/kotlinx-coroutines-core/build.gradle
+++ b/kotlinx-coroutines-core/build.gradle
@@ -2,6 +2,10 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+plugins {
+ id 'org.jetbrains.kotlinx.benchmark' version '0.4.9'
+}
+
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'org.jetbrains.dokka'
@@ -15,8 +19,11 @@ if (rootProject.ext.native_targets_enabled) {
apply from: rootProject.file("gradle/compile-native-multiplatform.gradle")
}
+apply from: rootProject.file("gradle/compile-jsAndWasmShared-multiplatform.gradle")
+
apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
-apply from: rootProject.file('gradle/publish-npm-js.gradle')
+
+apply from: rootProject.file("gradle/compile-wasm-multiplatform.gradle")
apply from: rootProject.file('gradle/dokka.gradle.kts')
apply from: rootProject.file('gradle/publish.gradle')
@@ -25,9 +32,8 @@ apply from: rootProject.file('gradle/publish.gradle')
TARGETS SOURCE SETS
------- ----------------------------------------------
-
- js -----------------------------------------------------+
- |
+ wasmJs \----------> jsAndWasmShared --------------------+
+ js / |
V
jvmCore\ --------> jvm ---------> concurrent -------> common
jdk8 / ^
@@ -107,8 +113,8 @@ if (rootProject.ext.native_targets_enabled) {
kotlin {
/*
* Configure two test runs:
- * 1) New memory model, Main thread
- * 2) New memory model, BG thread (required for Dispatchers.Main tests on Darwin)
+ * 1) Main thread
+ * 2) BG thread (required for Dispatchers.Main tests on Darwin)
*
* All new MM targets are build with optimize = true to have stress tests properly run.
*/
@@ -117,15 +123,13 @@ kotlin {
optimized = true
// Test for memory leaks using a special entry point that does not exit but returns from main
freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"]
- binaryOptions["memoryModel"] = "experimental"
}
- binaries.test("workerWithNewMM", [DEBUG]) {
+ binaries.test("workerTest", [DEBUG]) {
def thisTest = it
optimized = true
freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"]
- binaryOptions["memoryModel"] = "experimental"
- testRuns.create("workerWithNewMM") {
+ testRuns.create("workerTest") {
setExecutionSourceFrom(thisTest)
executionTask.configure { targetName = "$targetName worker with new MM" }
}
@@ -168,6 +172,13 @@ kotlin {
// For animal sniffer
withJava()
+ compilations.create('benchmark') { associateWith(compilations.main) }
+ }
+}
+
+benchmark {
+ targets {
+ register("jvmBenchmark")
}
}
@@ -234,7 +245,7 @@ kotlin.sourceSets {
kotlin.sourceSets.configureEach {
// Do not apply 'ExperimentalForeignApi' where we have allWarningsAsErrors set
- if (it.name in ["jvmMain", "jsMain", "concurrentMain", "commonMain"]) return
+ if (it.name in ["jvmMain", "jvmCoreMain", "jsMain", 'wasmJsMain', 'jsAndWasmSharedMain', "concurrentMain", "commonMain"]) return
languageSettings {
optIn('kotlinx.cinterop.ExperimentalForeignApi')
optIn('kotlin.experimental.ExperimentalNativeApi')
@@ -324,7 +335,9 @@ task jvmLincheckTestAdditional(type: Test, dependsOn: compileTestKotlinJvm) {
static void configureJvmForLincheck(task, additional = false) {
task.minHeapSize = '1g'
task.maxHeapSize = '4g' // we may need more space for building an interleaving tree in the model checking mode
+ // https://github.com/JetBrains/lincheck#java-9
task.jvmArgs = ['--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED', // required for transformation
+ '--add-exports', 'java.base/sun.security.action=ALL-UNNAMED',
'--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode
// Adjust internal algorithmic parameters to increase the testing quality instead of performance.
var segmentSize = additional ? '2' : '1'
@@ -364,7 +377,9 @@ koverReport {
"kotlinx.coroutines.debug.*", // Tested by debug module
"kotlinx.coroutines.channels.ChannelsKt__DeprecatedKt.*", // Deprecated
"kotlinx.coroutines.scheduling.LimitingDispatcher", // Deprecated
- "kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher" // Deprecated
+ "kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher", // Deprecated
+ "_COROUTINE._CREATION", // For IDE navigation
+ "_COROUTINE._BOUNDARY", // For IDE navigation
)
}
}
diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt
index ba06d9778d..313e87314d 100644
--- a/kotlinx-coroutines-core/common/src/Delay.kt
+++ b/kotlinx-coroutines-core/common/src/Delay.kt
@@ -7,6 +7,7 @@ package kotlinx.coroutines
import kotlinx.coroutines.selects.*
import kotlin.coroutines.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.nanoseconds
/**
* This dispatcher _feature_ is implemented by [CoroutineDispatcher] implementations that natively support
@@ -106,7 +107,7 @@ internal interface DelayWithTimeoutDiagnostics : Delay {
public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}
/**
- * Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
+ * Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time.
* If the given [timeMillis] is non-positive, this function returns immediately.
*
* This suspending function is cancellable.
@@ -133,7 +134,7 @@ public suspend fun delay(timeMillis: Long) {
}
/**
- * Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time.
+ * Delays coroutine for at least the given [duration] without blocking a thread and resumes it after the specified time.
* If the given [duration] is non-positive, this function returns immediately.
*
* This suspending function is cancellable.
@@ -154,8 +155,10 @@ public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMilli
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
/**
- * Convert this duration to its millisecond value.
- * Positive durations are coerced at least `1`.
+ * Convert this duration to its millisecond value. Durations which have a nanosecond component less than
+ * a single millisecond will be rounded up to the next largest millisecond.
*/
-internal fun Duration.toDelayMillis(): Long =
- if (this > Duration.ZERO) inWholeMilliseconds.coerceAtLeast(1) else 0
+internal fun Duration.toDelayMillis(): Long = when (isPositive()) {
+ true -> plus(999_999L.nanoseconds).inWholeMilliseconds
+ false -> 0L
+}
diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
index 8d9eed21bc..53c03be91e 100644
--- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt
+++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
@@ -6,6 +6,7 @@ package kotlinx.coroutines
import kotlinx.atomicfu.*
import kotlinx.coroutines.internal.*
+import kotlin.concurrent.Volatile
import kotlin.coroutines.*
import kotlin.jvm.*
diff --git a/kotlinx-coroutines-core/common/src/Exceptions.common.kt b/kotlinx-coroutines-core/common/src/Exceptions.common.kt
index 6d5442dfdc..27d11353e7 100644
--- a/kotlinx-coroutines-core/common/src/Exceptions.common.kt
+++ b/kotlinx-coroutines-core/common/src/Exceptions.common.kt
@@ -14,7 +14,6 @@ public class CompletionHandlerException(message: String, cause: Throwable) : Run
public expect open class CancellationException(message: String?) : IllegalStateException
-@Suppress("FunctionName", "NO_ACTUAL_FOR_EXPECT")
public expect fun CancellationException(message: String?, cause: Throwable?) : CancellationException
internal expect class JobCancellationException(
diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt
index e7a58ccdc4..64b6a69595 100644
--- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt
+++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt
@@ -147,12 +147,14 @@ private open class BroadcastCoroutine(
override val channel: SendChannel
get() = this
+ @Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
final override fun cancel(cause: Throwable?): Boolean {
cancelInternal(cause ?: defaultCancellationException())
return true
}
+ @Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
final override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}
diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt
index e3c3a30666..a967b673cc 100644
--- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt
@@ -136,6 +136,7 @@ public class ConflatedBroadcastChannel private constructor(
*
* This channel is created by `BroadcastChannel(capacity)` factory function invocation.
*/
+@Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING", "MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_WHEN_NO_EXPLICIT_OVERRIDE_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
internal class BroadcastChannelImpl(
/**
* Buffer capacity; [Channel.CONFLATED] when this broadcast is conflated.
diff --git a/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt b/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt
index 4fc7d4384d..9224ae8fce 100644
--- a/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt
@@ -1582,7 +1582,12 @@ internal open class BufferedChannel(
* When [hasNext] suspends, this field stores the corresponding
* continuation. The [tryResumeHasNext] and [tryResumeHasNextOnClosedChannel]
* function resume this continuation when the [hasNext] invocation should complete.
+ *
+ * This property is the subject to bening data race:
+ * It is nulled-out on both completion and cancellation paths that
+ * could happen concurrently.
*/
+ @BenignDataRace
private var continuation: CancellableContinuationImpl? = null
// `hasNext()` is just a special receive operation.
@@ -1690,8 +1695,11 @@ internal open class BufferedChannel(
}
fun tryResumeHasNextOnClosedChannel() {
- // Read the current continuation and clean
- // the corresponding field to avoid memory leaks.
+ /*
+ * Read the current continuation of the suspended `hasNext()` call and clean the corresponding field to avoid memory leaks.
+ * While this nulling out is unnecessary, it eliminates memory leaks (through the continuation)
+ * if the channel iterator accidentally remains GC-reachable after the channel is closed.
+ */
val cont = this.continuation!!
this.continuation = null
// Update the `hasNext()` internal result and inform
diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt
index 3fcf388a67..7b6bd02605 100644
--- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt
+++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt
@@ -21,12 +21,14 @@ internal open class ChannelCoroutine(
cancelInternal(defaultCancellationException())
}
+ @Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
final override fun cancel(cause: Throwable?): Boolean {
cancelInternal(defaultCancellationException())
return true
}
+ @Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
final override fun cancel(cause: CancellationException?) {
if (isCancelled) return // Do not create an exception if the coroutine (-> the channel) is already cancelled
cancelInternal(cause ?: defaultCancellationException())
diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
index b4833fead6..588560e2c3 100644
--- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
+++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
@@ -137,6 +137,16 @@ public interface SharedFlow : Flow {
* **A shared flow never completes**. A call to [Flow.collect] or any other terminal operator
* on a shared flow never completes normally.
*
+ * It is guaranteed that, by the time the first suspension happens, [collect] has already subscribed to the
+ * [SharedFlow] and is eligible for receiving emissions. In particular, the following code will always print `1`:
+ * ```
+ * val flow = MutableSharedFlow()
+ * launch(start = CoroutineStart.UNDISPATCHED) {
+ * flow.collect { println(1) }
+ * }
+ * flow.emit(1)
+ * ```
+ *
* @see [Flow.collect] for implementation and inheritance details.
*/
override suspend fun collect(collector: FlowCollector): Nothing
@@ -221,7 +231,11 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector {
* .launchIn(scope) // launch it
* ```
*
- * Implementation note: the resulting flow **does not** conflate subscription count.
+ * Usually, [StateFlow] conflates values, but [subscriptionCount] is not conflated.
+ * This is done so that any subscribers that need to be notified when subscribers appear do
+ * reliably observe it. With conflation, if a single subscriber appeared and immediately left, those
+ * collecting [subscriptionCount] could fail to notice it due to `0` immediately conflating the
+ * subscription count.
*/
public val subscriptionCount: StateFlow
diff --git a/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.common.kt b/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.common.kt
index 723a322be3..68d34f264c 100644
--- a/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.common.kt
+++ b/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.common.kt
@@ -10,6 +10,8 @@ import kotlinx.coroutines.internal.ScopeCoroutine
import kotlin.coroutines.*
import kotlin.jvm.*
+// Collector that ensures exception transparency and context preservation on a best-effort basis.
+// See an explanation in SafeCollector JVM actualization.
internal expect class SafeCollector(
collector: FlowCollector,
collectContext: CoroutineContext
diff --git a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
index 848a42c867..c06fcaf7b2 100644
--- a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
+++ b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
@@ -12,3 +12,15 @@ internal expect class ReentrantLock() {
internal expect inline fun ReentrantLock.withLock(action: () -> T): T
internal expect fun identitySet(expectedSize: Int): MutableSet
+
+/**
+ * Annotation indicating that the marked property is the subject of benign data race.
+ * LLVM does not support this notion, so on K/N platforms we alias it into `@Volatile` to prevent potential OoTA.
+ *
+ * The purpose of this annotation is not to save an extra-volatile on JVM platform, but rather to explicitly emphasize
+ * that data-race is benign.
+ */
+@OptionalExpectation
+@Target(AnnotationTarget.FIELD)
+@OptIn(ExperimentalMultiplatform::class)
+internal expect annotation class BenignDataRace()
diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt
index 3ac3cb6f27..79acdbd1c2 100644
--- a/kotlinx-coroutines-core/common/src/selects/Select.kt
+++ b/kotlinx-coroutines-core/common/src/selects/Select.kt
@@ -372,7 +372,12 @@ internal open class SelectImplementation(
/**
* List of clauses waiting on this `select` instance.
+ *
+ * This property is the subject to bening data race: concurrent cancellation might null-out this property
+ * while [trySelect] operation reads it and iterates over its content.
+ * A logical race is resolved by the consensus on [state] property.
*/
+ @BenignDataRace
private var clauses: MutableList? = ArrayList(2)
/**
@@ -407,7 +412,13 @@ internal open class SelectImplementation(
* one that stores either result when the clause is successfully registered ([inRegistrationPhase] is `true`),
* or [DisposableHandle] instance when the clause is completed during registration ([inRegistrationPhase] is `false`).
* Yet, this optimization is omitted for code simplicity.
+ *
+ * This property is the subject to benign data race:
+ * [Cleanup][cleanup] procedure can be invoked both as part of the completion sequence
+ * and as a cancellation handler triggered by an external cancellation.
+ * In both scenarios, [NO_RESULT] is written to this property via race.
*/
+ @BenignDataRace
private var internalResult: Any? = NO_RESULT
/**
@@ -621,9 +632,8 @@ internal open class SelectImplementation(
// try to resume the continuation.
this.internalResult = internalResult
if (cont.tryResume(onCancellation)) return TRY_SELECT_SUCCESSFUL
- // If the resumption failed, we need to clean
- // the [result] field to avoid memory leaks.
- this.internalResult = null
+ // If the resumption failed, we need to clean the [result] field to avoid memory leaks.
+ this.internalResult = NO_RESULT
return TRY_SELECT_CANCELLED
}
}
diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt
index 1fc2b41194..b51b75f740 100644
--- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt
+++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt
@@ -122,12 +122,18 @@ public suspend inline fun Mutex.withLock(owner: Any? = null, action: () -> T
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
+ // Cannot use 'finally' in this function because of KT-58685
+ // See kotlinx.coroutines.sync.MutexTest.testWithLockJsMiscompilation
+
lock(owner)
- try {
- return action()
- } finally {
+ val result = try {
+ action()
+ } catch (e: Throwable) {
unlock(owner)
+ throw e
}
+ unlock(owner)
+ return result
}
diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt
index 9f30721df5..f8a4791f27 100644
--- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt
+++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt
@@ -83,12 +83,18 @@ public suspend inline fun Semaphore.withPermit(action: () -> T): T {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
+ // Cannot use 'finally' in this function because of KT-58685
+ // See kotlinx.coroutines.sync.SemaphoreTest.testWithPermitJsMiscompilation
+
acquire()
- try {
- return action()
- } finally {
+ val result = try {
+ action()
+ } catch (e: Throwable) {
release()
+ throw e
}
+ release()
+ return result
}
@Suppress("UNCHECKED_CAST")
diff --git a/kotlinx-coroutines-core/common/test/AsyncTest.kt b/kotlinx-coroutines-core/common/test/AsyncTest.kt
index 2096a4d69e..3fff252318 100644
--- a/kotlinx-coroutines-core/common/test/AsyncTest.kt
+++ b/kotlinx-coroutines-core/common/test/AsyncTest.kt
@@ -8,7 +8,6 @@ package kotlinx.coroutines
import kotlin.test.*
-@Suppress("DEPRECATION") // cancel(cause)
class AsyncTest : TestBase() {
@Test
diff --git a/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt b/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt
new file mode 100644
index 0000000000..e2ea43dd3c
--- /dev/null
+++ b/kotlinx-coroutines-core/common/test/DurationToMillisTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+import kotlin.test.*
+import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.nanoseconds
+import kotlin.time.Duration.Companion.seconds
+
+class DurationToMillisTest {
+
+ @Test
+ fun testNegativeDurationCoercedToZeroMillis() {
+ assertEquals(0L, (-1).seconds.toDelayMillis())
+ }
+
+ @Test
+ fun testZeroDurationCoercedToZeroMillis() {
+ assertEquals(0L, 0.seconds.toDelayMillis())
+ }
+
+ @Test
+ fun testOneNanosecondCoercedToOneMillisecond() {
+ assertEquals(1L, 1.nanoseconds.toDelayMillis())
+ }
+
+ @Test
+ fun testOneSecondCoercedTo1000Milliseconds() {
+ assertEquals(1_000L, 1.seconds.toDelayMillis())
+ }
+
+ @Test
+ fun testMixedComponentDurationRoundedUpToNextMillisecond() {
+ assertEquals(999L, (998.milliseconds + 75909.nanoseconds).toDelayMillis())
+ }
+
+ @Test
+ fun testOneExtraNanosecondRoundedUpToNextMillisecond() {
+ assertEquals(999L, (998.milliseconds + 1.nanoseconds).toDelayMillis())
+ }
+
+ @Test
+ fun testInfiniteDurationCoercedToLongMaxValue() {
+ assertEquals(Long.MAX_VALUE, Duration.INFINITE.toDelayMillis())
+ }
+
+ @Test
+ fun testNegativeInfiniteDurationCoercedToZero() {
+ assertEquals(0L, (-Duration.INFINITE).toDelayMillis())
+ }
+
+ @Test
+ fun testNanosecondOffByOneInfinityDoesNotOverflow() {
+ assertEquals(Long.MAX_VALUE / 1_000_000, (Long.MAX_VALUE - 1L).nanoseconds.toDelayMillis())
+ }
+
+ @Test
+ fun testMillisecondOffByOneInfinityDoesNotIncrement() {
+ assertEquals((Long.MAX_VALUE / 2) - 1, ((Long.MAX_VALUE / 2) - 1).milliseconds.toDelayMillis())
+ }
+
+ @Test
+ fun testOutOfBoundsNanosecondsButFiniteDoesNotIncrement() {
+ val milliseconds = Long.MAX_VALUE / 10
+ assertEquals(milliseconds, milliseconds.milliseconds.toDelayMillis())
+ }
+}
diff --git a/kotlinx-coroutines-core/common/test/MainDispatcherTestBase.kt b/kotlinx-coroutines-core/common/test/MainDispatcherTestBase.kt
new file mode 100644
index 0000000000..baf56121f8
--- /dev/null
+++ b/kotlinx-coroutines-core/common/test/MainDispatcherTestBase.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.test.*
+
+abstract class MainDispatcherTestBase: TestBase() {
+
+ open fun shouldSkipTesting(): Boolean = false
+
+ open suspend fun spinTest(testBody: Job) {
+ testBody.join()
+ }
+
+ abstract fun isMainThread(): Boolean?
+
+ /** Runs the given block as a test, unless [shouldSkipTesting] indicates that the environment is not suitable. */
+ fun runTestOrSkip(block: suspend CoroutineScope.() -> Unit): TestResult {
+ // written as a block body to make the need to return `TestResult` explicit
+ return runTest {
+ if (shouldSkipTesting()) return@runTest
+ val testBody = launch(Dispatchers.Default) {
+ block()
+ }
+ spinTest(testBody)
+ }
+ }
+
+ /** Tests the [toString] behavior of [Dispatchers.Main] and [MainCoroutineDispatcher.immediate] */
+ @Test
+ fun testMainDispatcherToString() {
+ assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
+ assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
+ }
+
+ /** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier,
+ * even if the immediate dispatcher was entered from the main thread. */
+ @Test
+ fun testMainDispatcherOrderingInMainThread() = runTestOrSkip {
+ withContext(Dispatchers.Main) {
+ testMainDispatcherOrdering()
+ }
+ }
+
+ /** Tests that the tasks scheduled earlier from [MainCoroutineDispatcher.immediate] will be executed earlier
+ * if the immediate dispatcher was entered from outside the main thread. */
+ @Test
+ fun testMainDispatcherOrderingOutsideMainThread() = runTestOrSkip {
+ testMainDispatcherOrdering()
+ }
+
+ /** Tests that [Dispatchers.Main] and its [MainCoroutineDispatcher.immediate] are treated as different values. */
+ @Test
+ fun testHandlerDispatcherNotEqualToImmediate() {
+ assertNotEquals(Dispatchers.Main, Dispatchers.Main.immediate)
+ }
+
+ /** Tests that [Dispatchers.Main] shares its queue with [MainCoroutineDispatcher.immediate]. */
+ @Test
+ fun testImmediateDispatcherYield() = runTestOrSkip {
+ withContext(Dispatchers.Main) {
+ expect(1)
+ checkIsMainThread()
+ // launch in the immediate dispatcher
+ launch(Dispatchers.Main.immediate) {
+ expect(2)
+ yield()
+ expect(4)
+ }
+ expect(3) // after yield
+ yield() // yield back
+ expect(5)
+ }
+ finish(6)
+ }
+
+ /** Tests that entering [MainCoroutineDispatcher.immediate] from [Dispatchers.Main] happens immediately. */
+ @Test
+ fun testEnteringImmediateFromMain() = runTestOrSkip {
+ withContext(Dispatchers.Main) {
+ expect(1)
+ val job = launch { expect(3) }
+ withContext(Dispatchers.Main.immediate) {
+ expect(2)
+ }
+ job.join()
+ }
+ finish(4)
+ }
+
+ /** Tests that dispatching to [MainCoroutineDispatcher.immediate] is required from and only from dispatchers
+ * other than the main dispatchers and that it's always required for [Dispatchers.Main] itself. */
+ @Test
+ fun testDispatchRequirements() = runTestOrSkip {
+ checkDispatchRequirements()
+ withContext(Dispatchers.Main) {
+ checkDispatchRequirements()
+ withContext(Dispatchers.Main.immediate) {
+ checkDispatchRequirements()
+ }
+ checkDispatchRequirements()
+ }
+ checkDispatchRequirements()
+ }
+
+ private suspend fun checkDispatchRequirements() {
+ isMainThread()?.let { assertNotEquals(it, Dispatchers.Main.immediate.isDispatchNeeded(currentCoroutineContext())) }
+ assertTrue(Dispatchers.Main.isDispatchNeeded(currentCoroutineContext()))
+ assertTrue(Dispatchers.Default.isDispatchNeeded(currentCoroutineContext()))
+ }
+
+ /** Tests that launching a coroutine in [MainScope] will execute it in the main thread. */
+ @Test
+ fun testLaunchInMainScope() = runTestOrSkip {
+ var executed = false
+ withMainScope {
+ launch {
+ checkIsMainThread()
+ executed = true
+ }.join()
+ if (!executed) throw AssertionError("Should be executed")
+ }
+ }
+
+ /** Tests that a failure in [MainScope] will not propagate upwards. */
+ @Test
+ fun testFailureInMainScope() = runTestOrSkip {
+ var exception: Throwable? = null
+ withMainScope {
+ launch(CoroutineExceptionHandler { ctx, e -> exception = e }) {
+ checkIsMainThread()
+ throw TestException()
+ }.join()
+ }
+ if (exception!! !is TestException) throw AssertionError("Expected TestException, but had $exception")
+ }
+
+ /** Tests cancellation in [MainScope]. */
+ @Test
+ fun testCancellationInMainScope() = runTestOrSkip {
+ withMainScope {
+ cancel()
+ launch(start = CoroutineStart.ATOMIC) {
+ checkIsMainThread()
+ delay(Long.MAX_VALUE)
+ }.join()
+ }
+ }
+
+ private suspend fun withMainScope(block: suspend CoroutineScope.() -> R): R {
+ MainScope().apply {
+ return block().also { coroutineContext[Job]!!.cancelAndJoin() }
+ }
+ }
+
+ private suspend fun testMainDispatcherOrdering() {
+ withContext(Dispatchers.Main.immediate) {
+ expect(1)
+ launch(Dispatchers.Main) {
+ expect(2)
+ }
+ withContext(Dispatchers.Main) {
+ finish(3)
+ }
+ }
+ }
+
+ abstract class WithRealTimeDelay : MainDispatcherTestBase() {
+ abstract fun scheduleOnMainQueue(block: () -> Unit)
+
+ /** Tests that after a delay, the execution gets back to the main thread. */
+ @Test
+ fun testDelay() = runTestOrSkip {
+ expect(1)
+ checkNotMainThread()
+ scheduleOnMainQueue { expect(2) }
+ withContext(Dispatchers.Main) {
+ checkIsMainThread()
+ expect(3)
+ scheduleOnMainQueue { expect(4) }
+ delay(100)
+ checkIsMainThread()
+ expect(5)
+ }
+ checkNotMainThread()
+ finish(6)
+ }
+
+ /** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much slower. */
+ @Test
+ fun testWithTimeoutContextDelayNoTimeout() = runTestOrSkip {
+ expect(1)
+ withTimeout(1000) {
+ withContext(Dispatchers.Main) {
+ checkIsMainThread()
+ expect(2)
+ delay(100)
+ checkIsMainThread()
+ expect(3)
+ }
+ }
+ checkNotMainThread()
+ finish(4)
+ }
+
+ /** Tests that [Dispatchers.Main] is in agreement with the default time source: it's not much faster. */
+ @Test
+ fun testWithTimeoutContextDelayTimeout() = runTestOrSkip {
+ expect(1)
+ assertFailsWith {
+ withTimeout(300) {
+ withContext(Dispatchers.Main) {
+ checkIsMainThread()
+ expect(2)
+ delay(1000)
+ expectUnreached()
+ }
+ }
+ expectUnreached()
+ }
+ checkNotMainThread()
+ finish(3)
+ }
+
+ /** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much faster. */
+ @Test
+ fun testWithContextTimeoutDelayNoTimeout() = runTestOrSkip {
+ expect(1)
+ withContext(Dispatchers.Main) {
+ withTimeout(1000) {
+ checkIsMainThread()
+ expect(2)
+ delay(100)
+ checkIsMainThread()
+ expect(3)
+ }
+ }
+ checkNotMainThread()
+ finish(4)
+ }
+
+ /** Tests that the timeout of [Dispatchers.Main] is in agreement with its [delay]: it's not much slower. */
+ @Test
+ fun testWithContextTimeoutDelayTimeout() = runTestOrSkip {
+ expect(1)
+ assertFailsWith {
+ withContext(Dispatchers.Main) {
+ withTimeout(100) {
+ checkIsMainThread()
+ expect(2)
+ delay(1000)
+ expectUnreached()
+ }
+ }
+ expectUnreached()
+ }
+ checkNotMainThread()
+ finish(3)
+ }
+ }
+
+ fun checkIsMainThread() { isMainThread()?.let { check(it) } }
+ fun checkNotMainThread() { isMainThread()?.let { check(!it) } }
+}
diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt
index 06e71b45b5..3b9aeefab6 100644
--- a/kotlinx-coroutines-core/common/test/TestBase.common.kt
+++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt
@@ -39,6 +39,7 @@ public expect open class TestBase constructor() {
public fun finish(index: Int)
public fun ensureFinished() // Ensures that 'finish' was invoked
public fun reset() // Resets counter and finish flag. Workaround for parametrized tests absence in common
+ public fun println(message: Any?)
public fun runTest(
expected: ((Throwable) -> Boolean)? = null,
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
index cf83a50b0f..19b70032f3 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
@@ -236,4 +236,9 @@ class ShareInTest : TestBase() {
assertEquals(239, shared.first())
j.cancel()
}
+
+ @Test
+ fun testSubscriptionByFirstSuspensionInSharedFlow() = runTest {
+ testSubscriptionByFirstSuspensionInCollect(flowOf(1).stateIn(this@runTest), emit = { })
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
index 98e04f00e8..bb36c0ef9a 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
@@ -818,4 +818,24 @@ class SharedFlowTest : TestBase() {
j2.cancelAndJoin()
assertEquals(0, flow.subscriptionCount.first())
}
+
+ @Test
+ fun testSubscriptionByFirstSuspensionInSharedFlow() = runTest {
+ testSubscriptionByFirstSuspensionInCollect(MutableSharedFlow()) { emit(it) }
+ }
+}
+
+/**
+ * Check that, by the time [SharedFlow.collect] suspends for the first time, its subscription is already active.
+ */
+inline fun> CoroutineScope.testSubscriptionByFirstSuspensionInCollect(flow: T, emit: T.(Int) -> Unit) {
+ var received = 0
+ val job = launch(start = CoroutineStart.UNDISPATCHED) {
+ flow.collect {
+ received = it
+ }
+ }
+ flow.emit(1)
+ assertEquals(1, received)
+ job.cancel()
}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt
index be4f8c536b..a77f091b4f 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt
@@ -181,4 +181,9 @@ class StateFlowTest : TestBase() {
state.update { it + 3 }
assertEquals(5, state.value)
}
+
+ @Test
+ fun testSubscriptionByFirstSuspensionInStateFlow() = runTest {
+ testSubscriptionByFirstSuspensionInCollect(MutableStateFlow(0)) { value = it; yield() }
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt
index d0e76c461e..a27489bb4a 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt
@@ -86,4 +86,9 @@ class StateInTest : TestBase() {
assertFailsWith { flow.stateIn(CoroutineScope(currentCoroutineContext() + Job() + ceh)) }
finish(3)
}
+
+ @Test
+ fun testSubscriptionByFirstSuspensionInStateFlow() = runTest {
+ testSubscriptionByFirstSuspensionInCollect(flowOf(1).stateIn(this@runTest)) { }
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/sync/MutexTest.kt b/kotlinx-coroutines-core/common/test/sync/MutexTest.kt
index b4acd94e9c..2f45bb50d5 100644
--- a/kotlinx-coroutines-core/common/test/sync/MutexTest.kt
+++ b/kotlinx-coroutines-core/common/test/sync/MutexTest.kt
@@ -148,4 +148,20 @@ class MutexTest : TestBase() {
assertFailsWith { mutex.lock(owner) }
assertFailsWith { select { mutex.onLock(owner) {} } }
}
+
+ @Test
+ fun testWithLockJsMiscompilation() = runTest {
+ // This is a reproducer for KT-58685
+ // On Kotlin/JS IR, the compiler miscompiles calls to 'unlock' in an inlined finally
+ // This is visible on the withLock function
+ // Until the compiler bug is fixed, this test case checks that we do not suffer from it
+ val mutex = Mutex()
+ assertFailsWith {
+ try {
+ mutex.withLock { null } ?: throw IndexOutOfBoundsException() // should throw…
+ } catch (e: Exception) {
+ throw e // …but instead fails here
+ }
+ }
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt b/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt
index b4ff88b895..9a1ed01a69 100644
--- a/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt
+++ b/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt
@@ -168,4 +168,20 @@ class SemaphoreTest : TestBase() {
assertFailsWith { Semaphore(1, -1) }
assertFailsWith { Semaphore(1, 2) }
}
-}
\ No newline at end of file
+
+ @Test
+ fun testWithPermitJsMiscompilation() = runTest {
+ // This is a reproducer for KT-58685
+ // On Kotlin/JS IR, the compiler miscompiles calls to 'release' in an inlined finally
+ // This is visible on the withPermit function
+ // Until the compiler bug is fixed, this test case checks that we do not suffer from it
+ val semaphore = Semaphore(1)
+ assertFailsWith {
+ try {
+ semaphore.withPermit { null } ?: throw IndexOutOfBoundsException() // should throw…
+ } catch (e: Exception) {
+ throw e // …but instead fails here
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt
index 00888499c6..533fba6436 100644
--- a/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt
+++ b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt
@@ -85,7 +85,8 @@ public actual open class LockFreeLinkedListNode {
}
// LINEARIZABLE. Returns next non-removed Node
- public actual val nextNode: Node get() = next.unwrap()
+ public actual val nextNode: Node get() =
+ next.let { (it as? Removed)?.ref ?: it as Node } // unwraps the `next` node
// LINEARIZABLE WHEN THIS NODE IS NOT REMOVED:
// Returns prev non-removed Node, makes sure prev is correct (prev.next === this)
@@ -323,9 +324,6 @@ private class Removed(@JvmField val ref: Node) {
override fun toString(): String = "Removed[$ref]"
}
-@PublishedApi
-internal fun Any.unwrap(): Node = (this as? Removed)?.ref ?: this as Node
-
/**
* Head (sentinel) item of the linked list that is never removed.
*
@@ -350,6 +348,7 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
// optimization: because head is never removed, we don't have to read _next.value to check these:
override val isRemoved: Boolean get() = false
+
override fun nextIfRemoved(): Node? = null
internal fun validate() {
diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
index 232b3e271b..2d80a90730 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 fun createDefaultDispatcher(): CoroutineDispatcher = when {
+internal actual 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/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
index 8ddb903339..c94985b1c8 100644
--- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt
+++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
@@ -4,50 +4,36 @@
package kotlinx.coroutines
-import kotlinx.coroutines.internal.*
import org.w3c.dom.*
-import kotlin.coroutines.*
import kotlin.js.Promise
-private const val MAX_DELAY = Int.MAX_VALUE.toLong()
+public actual typealias W3CWindow = Window
-private fun delayToInt(timeMillis: Long): Int =
- timeMillis.coerceIn(0, MAX_DELAY).toInt()
+internal actual fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int =
+ setTimeout(window, handler, timeout)
-internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay {
- inner class ScheduledMessageQueue : MessageQueue() {
- internal val processQueue: dynamic = { process() }
+internal actual fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int =
+ setTimeout(handler, timeout)
- override fun schedule() {
- scheduleQueueProcessing()
- }
+internal actual fun w3cClearTimeout(window: W3CWindow, handle: Int) =
+ window.clearTimeout(handle)
- override fun reschedule() {
- setTimeout(processQueue, 0)
- }
- }
-
- internal val messageQueue = ScheduledMessageQueue()
+internal actual fun w3cClearTimeout(handle: Int) =
+ clearTimeout(handle)
- abstract fun scheduleQueueProcessing()
+internal actual class ScheduledMessageQueue actual constructor(private val dispatcher: SetTimeoutBasedDispatcher) : MessageQueue() {
+ internal val processQueue: dynamic = { process() }
- override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
- parallelism.checkParallelism()
- return this
+ actual override fun schedule() {
+ dispatcher.scheduleQueueProcessing()
}
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- messageQueue.enqueue(block)
+ actual override fun reschedule() {
+ setTimeout(processQueue, 0)
}
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val handle = setTimeout({ block.run() }, delayToInt(timeMillis))
- return ClearTimeout(handle)
- }
-
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) {
- val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
- continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler)
+ internal actual fun setTimeout(timeout: Int) {
+ setTimeout(processQueue, timeout)
}
}
@@ -57,48 +43,7 @@ internal object NodeDispatcher : SetTimeoutBasedDispatcher() {
}
}
-internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() {
- override fun scheduleQueueProcessing() {
- setTimeout(messageQueue.processQueue, 0)
- }
-}
-
-private open class ClearTimeout(protected val handle: Int) : CancelHandler(), DisposableHandle {
-
- override fun dispose() {
- clearTimeout(handle)
- }
-
- override fun invoke(cause: Throwable?) {
- dispose()
- }
-
- override fun toString(): String = "ClearTimeout[$handle]"
-}
-
-internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay {
- private val queue = WindowMessageQueue(window)
-
- override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block)
-
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) {
- val handle = window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
- continuation.invokeOnCancellation(handler = WindowClearTimeout(handle).asHandler)
- }
-
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis))
- return WindowClearTimeout(handle)
- }
-
- private inner class WindowClearTimeout(handle: Int) : ClearTimeout(handle) {
- override fun dispose() {
- window.clearTimeout(handle)
- }
- }
-}
-
-private class WindowMessageQueue(private val window: Window) : MessageQueue() {
+internal actual class WindowMessageQueue actual constructor(private val window: W3CWindow) : MessageQueue() {
private val messageName = "dispatchCoroutine"
init {
@@ -110,61 +55,20 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() {
}, true)
}
- override fun schedule() {
+ actual override fun schedule() {
Promise.resolve(Unit).then({ process() })
}
- override fun reschedule() {
+ actual override fun reschedule() {
window.postMessage(messageName, "*")
}
}
-/**
- * An abstraction over JS scheduling mechanism that leverages micro-batching of dispatched blocks without
- * paying the cost of JS callbacks scheduling on every dispatch.
- *
- * Queue uses two scheduling mechanisms:
- * 1) [schedule] is used to schedule the initial processing of the message queue.
- * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch
- * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop.
- * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks.
- *
- * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size.
- */
-internal abstract class MessageQueue : MutableList by ArrayDeque() {
- val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
- private var scheduled = false
-
- abstract fun schedule()
-
- abstract fun reschedule()
-
- fun enqueue(element: Runnable) {
- add(element)
- if (!scheduled) {
- scheduled = true
- schedule()
- }
- }
-
- fun process() {
- try {
- // limit number of processed messages
- repeat(yieldEvery) {
- val element = removeFirstOrNull() ?: return@process
- element.run()
- }
- } finally {
- if (isEmpty()) {
- scheduled = false
- } else {
- reschedule()
- }
- }
- }
-}
-
// We need to reference global setTimeout and clearTimeout so that it works on Node.JS as opposed to
// using them via "window" (which only works in browser)
private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int
+
private external fun clearTimeout(handle: Int = definedExternally)
+
+private fun setTimeout(window: Window, handler: () -> Unit, timeout: Int): Int =
+ window.setTimeout(handler, timeout)
diff --git a/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt
index 675cc4a67a..097f4bb607 100644
--- a/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt
+++ b/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt
@@ -1,26 +1,12 @@
/*
- * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 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.*
-
-private val platformExceptionHandlers_ = mutableSetOf()
-
-internal actual val platformExceptionHandlers: Collection
- get() = platformExceptionHandlers_
-
-internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExceptionHandler) {
- platformExceptionHandlers_ += callback
-}
internal actual fun propagateExceptionFinalResort(exception: Throwable) {
// log exception
- console.error(exception)
-}
-
-internal actual class DiagnosticCoroutineContextException actual constructor(context: CoroutineContext) :
- RuntimeException(context.toString())
-
+ console.error(exception.toString())
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt
deleted file mode 100644
index 7ca6a242b2..0000000000
--- a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt
+++ /dev/null
@@ -1,32 +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 ImmediateDispatcherTest : TestBase() {
-
- @Test
- fun testImmediate() = runTest {
- expect(1)
- val job = launch { expect(3) }
- withContext(Dispatchers.Main.immediate) {
- expect(2)
- }
- job.join()
- finish(4)
- }
-
- @Test
- fun testMain() = runTest {
- expect(1)
- val job = launch { expect(2) }
- withContext(Dispatchers.Main) {
- expect(3)
- }
- job.join()
- finish(4)
- }
-}
diff --git a/kotlinx-coroutines-core/js/test/TestBase.kt b/kotlinx-coroutines-core/js/test/TestBase.kt
index f0e3a2dc7a..83f6cfd67d 100644
--- a/kotlinx-coroutines-core/js/test/TestBase.kt
+++ b/kotlinx-coroutines-core/js/test/TestBase.kt
@@ -26,8 +26,7 @@ public actual open class TestBase actual constructor() {
* Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
* complete successfully even if this exception is consumed somewhere in the test.
*/
- @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
- public actual fun error(message: Any, cause: Throwable? = null): Nothing {
+ public actual fun error(message: Any, cause: Throwable?): Nothing {
if (cause != null) console.log(cause)
val exception = IllegalStateException(
if (cause == null) message.toString() else "$message; caused by $cause")
@@ -78,6 +77,10 @@ public actual open class TestBase actual constructor() {
finished = false
}
+ actual fun println(message: Any?) {
+ kotlin.io.println(message)
+ }
+
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun runTest(
expected: ((Throwable) -> Boolean)? = null,
diff --git a/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/CloseableCoroutineDispatcher.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/CloseableCoroutineDispatcher.kt
diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt
similarity index 95%
rename from kotlinx-coroutines-core/js/src/Dispatchers.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt
index 1304c5a9e5..622344b577 100644
--- a/kotlinx-coroutines-core/js/src/Dispatchers.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt
@@ -6,6 +6,8 @@ package kotlinx.coroutines
import kotlin.coroutines.*
+internal expect fun createDefaultDispatcher(): CoroutineDispatcher
+
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
public actual val Main: MainCoroutineDispatcher
diff --git a/kotlinx-coroutines-core/js/src/EventLoop.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/EventLoop.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/EventLoop.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/EventLoop.kt
diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Exceptions.kt
similarity index 86%
rename from kotlinx-coroutines-core/js/src/Exceptions.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/Exceptions.kt
index da9979b603..2295f93709 100644
--- a/kotlinx-coroutines-core/js/src/Exceptions.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/Exceptions.kt
@@ -12,6 +12,11 @@ package kotlinx.coroutines
*/
public actual typealias CancellationException = kotlin.coroutines.cancellation.CancellationException
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@kotlin.internal.LowPriorityInOverloadResolution
+public actual fun CancellationException(message: String?, cause: Throwable?): CancellationException =
+ CancellationException(message, cause)
+
/**
* Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed
* without cause, or with a cause or exception that is not [CancellationException]
diff --git a/kotlinx-coroutines-core/js/src/Runnable.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Runnable.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/Runnable.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/Runnable.kt
diff --git a/kotlinx-coroutines-core/js/src/SchedulerTask.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/SchedulerTask.kt
similarity index 73%
rename from kotlinx-coroutines-core/js/src/SchedulerTask.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/SchedulerTask.kt
index c0ecc4f2da..f8569ba505 100644
--- a/kotlinx-coroutines-core/js/src/SchedulerTask.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/SchedulerTask.kt
@@ -6,10 +6,11 @@ package kotlinx.coroutines
internal actual abstract class SchedulerTask : Runnable
-@Suppress("ACTUAL_WITHOUT_EXPECT")
-internal actual typealias SchedulerTaskContext = Unit
+internal actual interface SchedulerTaskContext { }
-internal actual val SchedulerTask.taskContext: SchedulerTaskContext get() = Unit
+private object TaskContext: SchedulerTaskContext { }
+
+internal actual val SchedulerTask.taskContext: SchedulerTaskContext get() = TaskContext
@Suppress("NOTHING_TO_INLINE")
internal actual inline fun SchedulerTaskContext.afterTask() {}
diff --git a/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/FlowExceptions.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/FlowExceptions.kt
diff --git a/kotlinx-coroutines-core/js/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/SafeCollector.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/flow/internal/SafeCollector.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/SafeCollector.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Concurrent.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/Concurrent.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/Concurrent.kt
diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt
new file mode 100644
index 0000000000..0612922edd
--- /dev/null
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2016-2022 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.*
+
+private val platformExceptionHandlers_ = mutableSetOf()
+
+internal actual val platformExceptionHandlers: Collection
+ get() = platformExceptionHandlers_
+
+internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExceptionHandler) {
+ platformExceptionHandlers_ += callback
+}
+
+internal actual class DiagnosticCoroutineContextException actual constructor(context: CoroutineContext) :
+ RuntimeException(context.toString())
+
diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt
new file mode 100644
index 0000000000..b93c0b35f0
--- /dev/null
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2016-2023 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 kotlin.coroutines.*
+
+public expect abstract class W3CWindow
+internal expect fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int
+internal expect fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int
+internal expect fun w3cClearTimeout(handle: Int)
+internal expect fun w3cClearTimeout(window: W3CWindow, handle: Int)
+
+internal expect class ScheduledMessageQueue(dispatcher: SetTimeoutBasedDispatcher) : MessageQueue {
+ override fun schedule()
+ override fun reschedule()
+ internal fun setTimeout(timeout: Int)
+}
+
+internal expect class WindowMessageQueue(window: W3CWindow) : MessageQueue {
+ override fun schedule()
+ override fun reschedule()
+}
+
+private const val MAX_DELAY = Int.MAX_VALUE.toLong()
+
+private fun delayToInt(timeMillis: Long): Int =
+ timeMillis.coerceIn(0, MAX_DELAY).toInt()
+
+internal abstract class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay {
+ internal val messageQueue = ScheduledMessageQueue(this)
+
+ abstract fun scheduleQueueProcessing()
+
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ return this
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ messageQueue.enqueue(block)
+ }
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ val handle = w3cSetTimeout({ block.run() }, delayToInt(timeMillis))
+ return ClearTimeout(handle)
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) {
+ val handle = w3cSetTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
+ continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler)
+ }
+}
+
+internal class WindowDispatcher(private val window: W3CWindow) : CoroutineDispatcher(), Delay {
+ private val queue = WindowMessageQueue(window)
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block)
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) {
+ val handle = w3cSetTimeout(window, { with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
+ continuation.invokeOnCancellation(handler = WindowClearTimeout(handle).asHandler)
+ }
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ val handle = w3cSetTimeout(window, block::run, delayToInt(timeMillis))
+ return WindowClearTimeout(handle)
+ }
+
+ private inner class WindowClearTimeout(handle: Int) : ClearTimeout(handle) {
+ override fun dispose() {
+ w3cClearTimeout(window, handle)
+ }
+ }
+}
+
+internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() {
+ override fun scheduleQueueProcessing() {
+ messageQueue.setTimeout(0)
+ }
+}
+
+private open class ClearTimeout(protected val handle: Int) : CancelHandler(), DisposableHandle {
+ override fun dispose() {
+ w3cClearTimeout(handle)
+ }
+
+ override fun invoke(cause: Throwable?) {
+ dispose()
+ }
+
+ override fun toString(): String = "ClearTimeout[$handle]"
+}
+
+
+/**
+ * An abstraction over JS scheduling mechanism that leverages micro-batching of dispatched blocks without
+ * paying the cost of JS callbacks scheduling on every dispatch.
+ *
+ * Queue uses two scheduling mechanisms:
+ * 1) [schedule] is used to schedule the initial processing of the message queue.
+ * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch
+ * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop.
+ * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks.
+ *
+ * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size.
+ */
+internal abstract class MessageQueue : MutableList by ArrayDeque() {
+ val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
+ private var scheduled = false
+
+ abstract fun schedule()
+
+ abstract fun reschedule()
+
+ fun enqueue(element: Runnable) {
+ add(element)
+ if (!scheduled) {
+ scheduled = true
+ schedule()
+ }
+ }
+
+ fun process() {
+ try {
+ // limit number of processed messages
+ repeat(yieldEvery) {
+ val element = removeFirstOrNull() ?: return@process
+ element.run()
+ }
+ } finally {
+ if (isEmpty()) {
+ scheduled = false
+ } else {
+ reschedule()
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt
similarity index 96%
rename from kotlinx-coroutines-core/js/src/internal/LinkedList.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt
index de5d491121..c4b1fddd03 100644
--- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt
@@ -9,8 +9,8 @@ package kotlinx.coroutines.internal
import kotlinx.coroutines.*
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.** */
diff --git a/kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/LocalAtomics.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/LocalAtomics.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/ProbesSupport.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ProbesSupport.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/ProbesSupport.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ProbesSupport.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/StackTraceRecovery.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/StackTraceRecovery.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt
similarity index 91%
rename from kotlinx-coroutines-core/js/src/internal/Synchronized.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt
index 05db52854f..91c422f237 100644
--- a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt
@@ -10,7 +10,7 @@ import kotlinx.coroutines.*
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
-public actual typealias SynchronizedObject = Any
+public actual open class SynchronizedObject
/**
* @suppress **This an internal API and should not be used from general code.**
diff --git a/kotlinx-coroutines-core/js/src/internal/SystemProps.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/SystemProps.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/SystemProps.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/SystemProps.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/ThreadContext.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadContext.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/src/internal/ThreadContext.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadContext.kt
diff --git a/kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt
similarity index 92%
rename from kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt
index c8dd09683f..8800e281e3 100644
--- a/kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt
+++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt
@@ -11,4 +11,4 @@ internal actual class CommonThreadLocal {
actual fun set(value: T) { this.value = value }
}
-internal actual fun commonThreadLocal(name: Symbol): CommonThreadLocal = CommonThreadLocal()
+internal actual fun commonThreadLocal(name: Symbol): CommonThreadLocal = CommonThreadLocal()
\ No newline at end of file
diff --git a/kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt
new file mode 100644
index 0000000000..ac249560b9
--- /dev/null
+++ b/kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016-2023 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.test.*
+
+class ImmediateDispatcherTest : MainDispatcherTestBase.WithRealTimeDelay() {
+
+ /** Tests that [MainCoroutineDispatcher.immediate] doesn't require dispatches from the test context. */
+ @Test
+ fun testImmediate() = runTest {
+ expect(1)
+ val job = launch { expect(3) }
+ assertFalse(Dispatchers.Main.immediate.isDispatchNeeded(currentCoroutineContext()))
+ withContext(Dispatchers.Main.immediate) {
+ expect(2)
+ }
+ job.join()
+ finish(4)
+ }
+
+ @Test
+ fun testMain() = runTest {
+ expect(1)
+ val job = launch { expect(2) }
+ withContext(Dispatchers.Main) {
+ expect(3)
+ }
+ job.join()
+ finish(4)
+ }
+
+ override fun isMainThread(): Boolean? = null
+
+ override fun scheduleOnMainQueue(block: () -> Unit) {
+ Dispatchers.Default.dispatch(EmptyCoroutineContext, Runnable { block() })
+ }
+}
diff --git a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/test/MessageQueueTest.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt
diff --git a/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt
diff --git a/kotlinx-coroutines-core/js/test/internal/LinkedListTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/internal/LinkedListTest.kt
similarity index 100%
rename from kotlinx-coroutines-core/js/test/internal/LinkedListTest.kt
rename to kotlinx-coroutines-core/jsAndWasmShared/test/internal/LinkedListTest.kt
diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin
index 9d171f3a7a..950dcf45be 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/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
index 4d23aff3f7..3229502b32 100644
--- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
+++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
@@ -135,6 +135,13 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
private fun createThreadSync(): Thread {
return _thread ?: Thread(this, THREAD_NAME).apply {
_thread = this
+ /*
+ * `DefaultExecutor` is a global singleton that creates its thread lazily.
+ * To isolate the classloaders properly, we are inherting the context classloader from
+ * the singleton itself instead of using parent' thread one
+ * in order not to accidentally capture temporary application classloader.
+ */
+ contextClassLoader = this@DefaultExecutor.javaClass.classLoader
isDaemon = true
start()
}
diff --git a/kotlinx-coroutines-core/jvm/src/EventLoop.kt b/kotlinx-coroutines-core/jvm/src/EventLoop.kt
index 7d1078cf6f..147d62c4f7 100644
--- a/kotlinx-coroutines-core/jvm/src/EventLoop.kt
+++ b/kotlinx-coroutines-core/jvm/src/EventLoop.kt
@@ -9,6 +9,7 @@ import kotlinx.coroutines.scheduling.*
import kotlinx.coroutines.scheduling.CoroutineScheduler
internal actual abstract class EventLoopImplPlatform: EventLoop() {
+
protected abstract val thread: Thread
protected actual fun unpark() {
diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt
index 48b4788cc5..9c9f5bf441 100644
--- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt
+++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt
@@ -17,7 +17,6 @@ public actual typealias CancellationException = java.util.concurrent.Cancellatio
/**
* Creates a cancellation exception with a specified message and [cause].
*/
-@Suppress("FunctionName")
public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException =
CancellationException(message).apply { initCause(cause) }
diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt
index e8a9152e09..6e2bef6680 100644
--- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt
+++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt
@@ -178,6 +178,7 @@ private class LazyActorCoroutine(
return super.trySend(element)
}
+ @Suppress("MULTIPLE_DEFAULTS_INHERITED_FROM_SUPERTYPES_DEPRECATION_WARNING") // do not remove the MULTIPLE_DEFAULTS suppression: required in K2
override fun close(cause: Throwable?): Boolean {
// close the channel _first_
val closed = super.close(cause)
diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
index c6b7ea92ce..04ffed9717 100644
--- a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
+++ b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
@@ -13,9 +13,25 @@ import kotlin.coroutines.jvm.internal.*
@Suppress("UNCHECKED_CAST")
private val emitFun =
FlowCollector::emit as Function3, Any?, Continuation, Any?>
-/*
- * Implementor of ContinuationImpl (that will be preserved as ABI nearly forever)
- * in order to properly control 'intercepted()' lifecycle.
+
+/**
+ * A safe collector is an instance of [FlowCollector] that ensures that neither context preservation
+ * nor exception transparency invariants are broken. Instances of [SafeCollector] are used in flow
+ * operators that provide raw access to the [FlowCollector] e.g. [Flow.transform].
+ * Mechanically, each [emit] call captures [currentCoroutineContext], ensures it is not different from the
+ * previously caught one and proceeds further. If an exception is thrown from the downstream,
+ * it is caught, and any further attempts to [emit] lead to the [IllegalStateException].
+ *
+ * ### Performance hacks
+ *
+ * Implementor of [ContinuationImpl] (that will be preserved as ABI nearly forever)
+ * in order to properly control `intercepted()` lifecycle.
+ * The safe collector implements [ContinuationImpl] to pretend it *is* a state-machine of its own `emit` method.
+ * It is [ContinuationImpl] and not any other [Continuation] subclass because only [ContinuationImpl] supports `intercepted()` caching.
+ * This is the most performance-sensitive place in the overall flow pipeline, because otherwise safe collector is forced to allocate
+ * a state machine on each element being emitted for each intermediate stage where the safe collector is present.
+ *
+ * See a comment to [emit] for the explanation of what and how is being optimized.
*/
@Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "UNCHECKED_CAST")
internal actual class SafeCollector actual constructor(
@@ -23,7 +39,7 @@ internal actual class SafeCollector actual constructor(
@JvmField internal actual val collectContext: CoroutineContext
) : FlowCollector, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext), CoroutineStackFrame {
- override val callerFrame: CoroutineStackFrame? get() = completion as? CoroutineStackFrame
+ override val callerFrame: CoroutineStackFrame? get() = completion_ as? CoroutineStackFrame
override fun getStackTraceElement(): StackTraceElement? = null
@@ -32,20 +48,20 @@ internal actual class SafeCollector actual constructor(
// Either context of the last emission or wrapper 'DownstreamExceptionContext'
private var lastEmissionContext: CoroutineContext? = null
- // Completion if we are currently suspended or within completion body or null otherwise
- private var completion: Continuation? = null
+ // Completion if we are currently suspended or within completion_ body or null otherwise
+ private var completion_: Continuation? = null
/*
* This property is accessed in two places:
* * ContinuationImpl invokes this in its `releaseIntercepted` as `context[ContinuationInterceptor]!!`
- * * When we are within a callee, it is used to create its continuation object with this collector as completion
+ * * When we are within a callee, it is used to create its continuation object with this collector as completion_
*/
override val context: CoroutineContext
get() = lastEmissionContext ?: EmptyCoroutineContext
override fun invokeSuspend(result: Result): Any {
result.onFailure { lastEmissionContext = DownstreamExceptionContext(it, context) }
- completion?.resumeWith(result as Result)
+ completion_?.resumeWith(result as Result)
return COROUTINE_SUSPENDED
}
@@ -56,11 +72,15 @@ internal actual class SafeCollector actual constructor(
/**
* This is a crafty implementation of state-machine reusing.
- * First it checks that it is not used concurrently (which we explicitly prohibit) and
- * then just cache an instance of the completion in order to avoid extra allocation on each emit,
+ *
+ * First it checks that it is not used concurrently (which we explicitly prohibit), and
+ * then just caches an instance of the completion_ in order to avoid extra allocation on each emit,
* making it effectively garbage-free on its hot-path.
+ *
+ * See `emit` overload.
*/
actual override suspend fun emit(value: T) {
+ // NB: it is a tail-call, so we are sure `uCont` is the completion of the emit's **caller**.
return suspendCoroutineUninterceptedOrReturn sc@{ uCont ->
try {
emit(uCont, value)
@@ -74,23 +94,33 @@ internal actual class SafeCollector actual constructor(
}
}
+ /**
+ * Here we use the following trick:
+ * - Perform all the required checks
+ * - Having a non-intercepted, non-cancellable caller's `uCont`, we leverage our implementation knowledge
+ * and invoke `collector.emit(T)` as `collector.emit(value: T, completion: Continuation), passing `this`
+ * as the completion. We also setup `this` state, so if the `completion.resume` is invoked, we are
+ * invoking `uCont.resume` properly in accordance with `ContinuationImpl`/`BaseContinuationImpl` internal invariants.
+ *
+ * Note that in such scenarios, `collector.emit` completion is the current instance of SafeCollector and thus is reused.
+ */
private fun emit(uCont: Continuation, value: T): Any? {
val currentContext = uCont.context
currentContext.ensureActive()
- // This check is triggered once per flow on happy path.
+ // This check is triggered once per flow on a happy path.
val previousContext = lastEmissionContext
if (previousContext !== currentContext) {
checkContext(currentContext, previousContext, value)
lastEmissionContext = currentContext
}
- completion = uCont
+ completion_ = uCont
val result = emitFun(collector as FlowCollector, value, this as Continuation)
/*
* If the callee hasn't suspended, that means that it won't (it's forbidden) call 'resumeWith` (-> `invokeSuspend`)
* and we don't have to retain a strong reference to it to avoid memory leaks.
*/
if (result != COROUTINE_SUSPENDED) {
- completion = null
+ completion_ = null
}
return result
}
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
index 5ca3de5726..84fa474a3d 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
@@ -593,6 +593,13 @@ internal class CoroutineScheduler(
internal inner class Worker private constructor() : Thread() {
init {
isDaemon = true
+ /*
+ * `Dispatchers.Default` is used as *the* dispatcher in the containerized environments,
+ * isolated by their own classloaders. Workers are populated lazily, thus we are inheriting
+ * `Dispatchers.Default` context class loader here instead of using parent' thread one
+ * in order not to accidentally capture temporary application classloader.
+ */
+ contextClassLoader = this@CoroutineScheduler.javaClass.classLoader
}
// guarded by scheduler lock, index in workers array, 0 when not in array (terminated)
diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt
index f9e5466b44..5947eb710f 100644
--- a/kotlinx-coroutines-core/jvm/test/TestBase.kt
+++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt
@@ -75,12 +75,11 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
*/
private lateinit var previousOut: PrintStream
- /**
+ /**
* Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
* complete successfully even if this exception is consumed somewhere in the test.
*/
- @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
- public actual fun error(message: Any, cause: Throwable? = null): Nothing {
+ public actual fun error(message: Any, cause: Throwable?): Nothing {
throw makeError(message, cause)
}
@@ -154,7 +153,7 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
}
})
- fun println(message: Any?) {
+ actual fun println(message: Any?) {
if (disableOutCheck) kotlin.io.println(message)
else previousOut.println(message)
}
@@ -212,7 +211,7 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
DefaultScheduler.restore()
}
- @Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun runTest(
expected: ((Throwable) -> Boolean)? = null,
unhandled: List<(Throwable) -> Boolean> = emptyList(),
diff --git a/kotlinx-coroutines-core/jvmBenchmark/README.md b/kotlinx-coroutines-core/jvmBenchmark/README.md
new file mode 100644
index 0000000000..a89761a245
--- /dev/null
+++ b/kotlinx-coroutines-core/jvmBenchmark/README.md
@@ -0,0 +1,15 @@
+## kotlinx-coroutines-core benchmarks
+
+This source-set contains benchmarks that leverage `internal` API (e.g. `suspendCancellableCoroutineReusable`)
+and thus cannot be written in `benchmarks` module.
+
+This is an interim solution unless we introduce clear separation of responsibilities in benchmark modules
+and decide on their usability.
+
+
+### Usage
+
+```
+./gradlew :kotlinx-coroutines-core:jvmBenchmarkBenchmarkJar
+java -jar kotlinx-coroutines-core/build/benchmarks/jvmBenchmark/jars/kotlinx-coroutines-core-jvmBenchmark-jmh-*-JMH.jar
+```
diff --git a/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/BenchmarkUtils.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/BenchmarkUtils.kt
new file mode 100644
index 0000000000..714aad114c
--- /dev/null
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/BenchmarkUtils.kt
@@ -0,0 +1,17 @@
+/*
+ * 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.*
+
+public fun doGeomDistrWork(work: Int) {
+ // We use geometric distribution here. We also checked on macbook pro 13" (2017) that the resulting work times
+ // are distributed geometrically, see https://github.com/Kotlin/kotlinx.coroutines/pull/1464#discussion_r355705325
+ val p = 1.0 / work
+ val r = ThreadLocalRandom.current()
+ while (true) {
+ if (r.nextDouble() < p) break
+ }
+}
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/SemaphoreBenchmark.kt
similarity index 91%
rename from benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/SemaphoreBenchmark.kt
index 6826b7a1a3..df82690771 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/SemaphoreBenchmark.kt
@@ -1,10 +1,9 @@
/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-package benchmarks
+package kotlinx.coroutines
-import benchmarks.common.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.scheduling.*
@@ -80,8 +79,7 @@ open class SemaphoreBenchmark {
}
enum class SemaphoreBenchDispatcherCreator(val create: (parallelism: Int) -> CoroutineDispatcher) {
- // FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }),
- @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+ FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }),
DEFAULT({ parallelism -> ExperimentalCoroutineDispatcher(corePoolSize = parallelism, maxPoolSize = parallelism) })
}
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/ChannelProducerConsumerBenchmark.kt
similarity index 94%
rename from benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/ChannelProducerConsumerBenchmark.kt
index 0aa218e824..7bc0d2b2be 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/ChannelProducerConsumerBenchmark.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.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-package benchmarks
+package kotlinx.coroutines.channels
-import benchmarks.common.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.scheduling.*
import kotlinx.coroutines.selects.select
import org.openjdk.jmh.annotations.*
import java.lang.Integer.max
+import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Phaser
import java.util.concurrent.TimeUnit
@@ -136,8 +136,7 @@ open class ChannelProducerConsumerBenchmark {
}
enum class DispatcherCreator(val create: (parallelism: Int) -> CoroutineDispatcher) {
- //FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }),
- @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+ FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }),
DEFAULT({ parallelism -> ExperimentalCoroutineDispatcher(corePoolSize = parallelism, maxPoolSize = parallelism) })
}
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SelectBenchmark.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SelectBenchmark.kt
similarity index 90%
rename from benchmarks/src/jmh/kotlin/benchmarks/tailcall/SelectBenchmark.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SelectBenchmark.kt
index cb4d39eed6..e2bc9e4abf 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SelectBenchmark.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SelectBenchmark.kt
@@ -1,8 +1,8 @@
/*
- * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-package benchmarks.tailcall
+package kotlinx.coroutines.channels
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannel.kt
similarity index 92%
rename from benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannel.kt
index 1f71d8dc19..1afb0f0464 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannel.kt
@@ -1,14 +1,13 @@
/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-package benchmarks.tailcall
+package kotlinx.coroutines.channels
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
-@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
public abstract class SimpleChannel {
companion object {
const val NULL_SURROGATE: Int = -1
@@ -83,13 +82,11 @@ class CancellableChannel : SimpleChannel() {
}
class CancellableReusableChannel : SimpleChannel() {
- @Suppress("INVISIBLE_MEMBER")
override suspend fun suspendReceive(): Int = suspendCancellableCoroutineReusable {
consumer = it.intercepted()
COROUTINE_SUSPENDED
}
- @Suppress("INVISIBLE_MEMBER")
override suspend fun suspendSend(element: Int) = suspendCancellableCoroutineReusable {
enqueuedValue = element
producer = it.intercepted()
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannelBenchmark.kt
similarity index 92%
rename from benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannelBenchmark.kt
index 9654b6dabe..233cecff4e 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/channels/SimpleChannelBenchmark.kt
@@ -1,8 +1,8 @@
/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-package benchmarks.tailcall
+package kotlinx.coroutines.channels
import kotlinx.coroutines.*
import org.openjdk.jmh.annotations.*
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/flow/TakeWhileBenchmark.kt
similarity index 89%
rename from benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt
rename to kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/flow/TakeWhileBenchmark.kt
index 7501e2c419..6dad29e51f 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt
+++ b/kotlinx-coroutines-core/jvmBenchmark/kotlin/kotlinx/coroutines/flow/TakeWhileBenchmark.kt
@@ -1,16 +1,15 @@
/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-
-package benchmarks.flow
+package kotlinx.coroutines.flow
import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.internal.*
+import kotlinx.coroutines.flow.internal.AbortFlowException
+import kotlinx.coroutines.flow.internal.unsafeFlow
import org.openjdk.jmh.annotations.*
-import java.util.concurrent.TimeUnit
+import java.util.concurrent.*
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt
index 1f1d352dab..e3db7f16bc 100644
--- a/kotlinx-coroutines-core/native/src/Builders.kt
+++ b/kotlinx-coroutines-core/native/src/Builders.kt
@@ -2,7 +2,7 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-@file:OptIn(ExperimentalContracts::class)
+@file:OptIn(ExperimentalContracts::class, ObsoleteWorkersApi::class)
package kotlinx.coroutines
import kotlinx.cinterop.*
diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt
index 25c3c12b78..62e51b26c7 100644
--- a/kotlinx-coroutines-core/native/src/EventLoop.kt
+++ b/kotlinx-coroutines-core/native/src/EventLoop.kt
@@ -2,6 +2,8 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(ObsoleteWorkersApi::class)
+
package kotlinx.coroutines
import kotlin.coroutines.*
@@ -28,4 +30,5 @@ internal class EventLoopImpl: EventLoopImplBase() {
internal actual fun createEventLoop(): EventLoop = EventLoopImpl()
+@Suppress("DEPRECATION")
internal actual fun nanoTime(): Long = getTimeNanos()
diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt
index 1a923c40ff..f9d0f5db73 100644
--- a/kotlinx-coroutines-core/native/src/Exceptions.kt
+++ b/kotlinx-coroutines-core/native/src/Exceptions.kt
@@ -15,6 +15,11 @@ import kotlinx.coroutines.internal.SuppressSupportingThrowableImpl
*/
public actual typealias CancellationException = kotlin.coroutines.cancellation.CancellationException
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@kotlin.internal.LowPriorityInOverloadResolution
+public actual fun CancellationException(message: String?, cause: Throwable?): CancellationException =
+ CancellationException(message, cause)
+
/**
* Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed
* without cause, or with a cause or exception that is not [CancellationException]
diff --git a/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt
index 007d079a8d..548602e146 100644
--- a/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt
+++ b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt
@@ -2,12 +2,15 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(ObsoleteWorkersApi::class)
+
package kotlinx.coroutines
import kotlinx.atomicfu.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
+import kotlin.concurrent.AtomicReference
import kotlin.native.concurrent.*
import kotlin.time.*
import kotlin.time.Duration.Companion.milliseconds
@@ -17,7 +20,6 @@ public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): Closea
return MultiWorkerDispatcher(name, nThreads)
}
-@OptIn(ExperimentalTime::class)
internal class WorkerDispatcher(name: String) : CloseableCoroutineDispatcher(), Delay {
private val worker = Worker.start(name = name)
diff --git a/kotlinx-coroutines-core/native/src/SchedulerTask.kt b/kotlinx-coroutines-core/native/src/SchedulerTask.kt
index 781e32213c..9b2b68a02a 100644
--- a/kotlinx-coroutines-core/native/src/SchedulerTask.kt
+++ b/kotlinx-coroutines-core/native/src/SchedulerTask.kt
@@ -6,10 +6,11 @@ package kotlinx.coroutines
internal actual abstract class SchedulerTask : Runnable
-@Suppress("ACTUAL_WITHOUT_EXPECT")
-internal actual typealias SchedulerTaskContext = Unit
+internal actual interface SchedulerTaskContext { }
-internal actual val SchedulerTask.taskContext: SchedulerTaskContext get() = kotlin.Unit
+private object TaskContext: SchedulerTaskContext { }
+
+internal actual val SchedulerTask.taskContext: SchedulerTaskContext get() = TaskContext
@Suppress("NOTHING_TO_INLINE")
internal actual inline fun SchedulerTaskContext.afterTask() {}
diff --git a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
index 17975e2e7f..0cc78378da 100644
--- a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
+++ b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
@@ -5,11 +5,13 @@
package kotlinx.coroutines.internal
import kotlinx.atomicfu.*
+import kotlinx.cinterop.*
import kotlinx.atomicfu.locks.withLock as withLock2
@Suppress("ACTUAL_WITHOUT_EXPECT")
internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject
+@OptIn(UnsafeNumber::class)
internal actual inline fun ReentrantLock.withLock(action: () -> T): T = this.withLock2(action)
internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet()
@@ -29,3 +31,5 @@ internal open class SuppressSupportingThrowableImpl : Throwable() {
}
}
+@Suppress("ACTUAL_WITHOUT_EXPECT") // This suppress can be removed in 2.0: KT-59355
+internal actual typealias BenignDataRace = kotlin.concurrent.Volatile
diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
index 8a8ecfe393..20fc666229 100644
--- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
+++ b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
@@ -4,6 +4,7 @@
package kotlinx.coroutines.internal
+import kotlinx.cinterop.*
import kotlinx.coroutines.*
import kotlinx.atomicfu.locks.withLock as withLock2
@@ -16,5 +17,6 @@ public actual typealias SynchronizedObject = kotlinx.atomicfu.locks.Synchronized
/**
* @suppress **This an internal API and should not be used from general code.**
*/
+@OptIn(UnsafeNumber::class)
@InternalCoroutinesApi
public actual inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block)
diff --git a/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt b/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt
index 405cbfb6a5..95069ec14d 100644
--- a/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt
+++ b/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt
@@ -4,6 +4,8 @@
package kotlinx.coroutines.internal
+import kotlin.native.concurrent.ThreadLocal
+
internal actual class CommonThreadLocal(private val name: Symbol) {
@Suppress("UNCHECKED_CAST")
actual fun get(): T = Storage[name] as T
diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt
index d7dfeeaeba..ca876969f5 100644
--- a/kotlinx-coroutines-core/native/test/TestBase.kt
+++ b/kotlinx-coroutines-core/native/test/TestBase.kt
@@ -17,16 +17,15 @@ public actual typealias TestResult = Unit
public actual open class TestBase actual constructor() {
public actual val isBoundByJsTestTimeout = false
- private var actionIndex = atomic(0)
- private var finished = atomic(false)
+ private val actionIndex = atomic(0)
+ private val finished = atomic(false)
private var error: Throwable? = null
/**
* Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
* complete successfully even if this exception is consumed somewhere in the test.
*/
- @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
- public actual fun error(message: Any, cause: Throwable? = null): Nothing {
+ public actual fun error(message: Any, cause: Throwable?): Nothing {
val exception = IllegalStateException(message.toString(), cause)
if (error == null) error = exception
throw exception
@@ -74,6 +73,10 @@ public actual open class TestBase actual constructor() {
finished.value = false
}
+ actual fun println(message: Any?) {
+ kotlin.io.println(message)
+ }
+
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public actual fun runTest(
expected: ((Throwable) -> Boolean)? = null,
diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt
index edc0a13ce8..bab9c9094e 100644
--- a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt
+++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt
@@ -2,13 +2,15 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(BetaInteropApi::class)
+
package kotlinx.coroutines
import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import kotlin.coroutines.*
-import kotlin.native.concurrent.*
+import kotlin.concurrent.*
import kotlin.native.internal.NativePtr
internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
@@ -18,6 +20,7 @@ internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoro
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGlobalQueueDispatcher
private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() {
+ @OptIn(UnsafeNumber::class)
override fun dispatch(context: CoroutineContext, block: Runnable) {
autoreleasepool {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.convert(), 0u)) {
@@ -65,7 +68,7 @@ private class DarwinMainDispatcher(
}
override fun toString(): String =
- "MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }"
+ if (invokeImmediately) "Dispatchers.Main.immediate" else "Dispatchers.Main"
}
private typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit
@@ -76,6 +79,7 @@ private val TIMER_DISPOSED = NativePtr.NULL.plus(1)
private class Timer : DisposableHandle {
private val ref = AtomicNativePtr(TIMER_NEW)
+ @OptIn(UnsafeNumber::class)
fun start(timeMillis: Long, timerBlock: TimerBlock) {
val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0
val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock)
diff --git a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt
index 010bd03089..b2a58f5d77 100644
--- a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt
+++ b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt
@@ -6,6 +6,7 @@ package kotlinx.coroutines
import kotlinx.cinterop.*
+@OptIn(BetaInteropApi::class)
internal actual inline fun workerMain(block: () -> Unit) {
autoreleasepool {
block()
diff --git a/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt
index 9904f06c5f..ba5c29cec2 100644
--- a/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt
+++ b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt
@@ -4,126 +4,24 @@
package kotlinx.coroutines
+import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import kotlin.coroutines.*
import kotlin.test.*
-class MainDispatcherTest : TestBase() {
+class MainDispatcherTest : MainDispatcherTestBase.WithRealTimeDelay() {
- private fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
- private fun canTestMainDispatcher() = !isMainThread()
+ override fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
- 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)
- }
+ // skip if already on the main thread, run blocking doesn't really work well with that
+ override fun shouldSkipTesting(): Boolean = isMainThread()
- @Test
- fun testWithContextTimeoutDelayTimeout() = runTestNotOnMainDispatcher {
- expect(1)
- assertFailsWith {
- withContext(Dispatchers.Main) {
- withTimeout(100) {
- assertTrue(isMainThread())
- expect(2)
- delay(1000)
- expectUnreached()
- }
+ override fun scheduleOnMainQueue(block: () -> Unit) {
+ autoreleasepool {
+ dispatch_async(dispatch_get_main_queue()) {
+ block()
}
- expectUnreached()
}
- assertFalse(isMainThread())
- finish(3)
}
}
diff --git a/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt b/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt
new file mode 100644
index 0000000000..4835f7968e
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt
@@ -0,0 +1,22 @@
+/*
+ * 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/wasmJs/src/CoroutineContext.kt b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt
new file mode 100644
index 0000000000..ab37ff88d3
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt
@@ -0,0 +1,51 @@
+/*
+ * 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 org.w3c.dom.*
+import kotlin.coroutines.*
+
+internal external interface JsProcess : JsAny {
+ fun nextTick(handler: () -> Unit)
+}
+
+internal fun tryGetProcess(): JsProcess? =
+ js("(typeof(process) !== 'undefined' && typeof(process.nextTick) === 'function') ? process : null")
+
+internal fun tryGetWindow(): Window? =
+ js("(typeof(window) !== 'undefined' && window != null && typeof(window.addEventListener) === 'function') ? window : null")
+
+internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
+ tryGetProcess()?.let(::NodeDispatcher)
+ ?: tryGetWindow()?.let(::WindowDispatcher)
+ ?: SetTimeoutDispatcher
+
+@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI
+internal actual val DefaultDelay: Delay
+ get() = Dispatchers.Default as Delay
+
+public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
+ val combined = coroutineContext + context
+ return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
+ combined + Dispatchers.Default else combined
+}
+
+public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
+ return this + addedContext
+}
+
+// No debugging facilities on Wasm
+internal actual inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block()
+internal actual inline fun withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block()
+internal actual fun Continuation<*>.toDebugString(): String = toString()
+internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm
+
+internal actual class UndispatchedCoroutine actual constructor(
+ context: CoroutineContext,
+ uCont: Continuation
+) : ScopeCoroutine(context, uCont) {
+ override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
+}
diff --git a/kotlinx-coroutines-core/wasmJs/src/Debug.kt b/kotlinx-coroutines-core/wasmJs/src/Debug.kt
new file mode 100644
index 0000000000..93e253039f
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/Debug.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
+
+internal actual val DEBUG: Boolean = false
+
+internal actual val Any.hexAddress: String
+ get() = this.hashCode().toString()
+
+internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown"
+
+internal actual inline fun assert(value: () -> Boolean) {}
+
+internal external interface Console {
+ fun error(s: String)
+}
+
+internal external val console: Console
\ No newline at end of file
diff --git a/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt b/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt
new file mode 100644
index 0000000000..3dfdeff321
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.w3c.dom.Window
+import kotlin.js.*
+
+public actual typealias W3CWindow = Window
+
+internal actual fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int =
+ setTimeout(window, handler, timeout)
+
+internal actual fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int =
+ setTimeout(handler, timeout)
+
+internal actual fun w3cClearTimeout(window: W3CWindow, handle: Int) =
+ window.clearTimeout(handle)
+
+internal actual fun w3cClearTimeout(handle: Int) =
+ clearTimeout(handle)
+
+internal actual class ScheduledMessageQueue actual constructor(private val dispatcher: SetTimeoutBasedDispatcher) : MessageQueue() {
+ internal val processQueue: () -> Unit = ::process
+
+ actual override fun schedule() {
+ dispatcher.scheduleQueueProcessing()
+ }
+
+ actual override fun reschedule() {
+ setTimeout(processQueue, 0)
+ }
+
+ internal actual fun setTimeout(timeout: Int) {
+ setTimeout(processQueue, timeout)
+ }
+}
+
+internal class NodeDispatcher(private val process: JsProcess) : SetTimeoutBasedDispatcher() {
+ override fun scheduleQueueProcessing() {
+ process.nextTick(messageQueue.processQueue)
+ }
+}
+
+@Suppress("UNUSED_PARAMETER")
+private fun subscribeToWindowMessages(window: Window, process: () -> Unit): Unit = js("""{
+ const handler = (event) => {
+ if (event.source == window && event.data == 'dispatchCoroutine') {
+ event.stopPropagation();
+ process();
+ }
+ }
+ window.addEventListener('message', handler, true);
+}""")
+
+@Suppress("UNUSED_PARAMETER")
+private fun createRescheduleMessagePoster(window: Window): () -> Unit =
+ js("() => window.postMessage('dispatchCoroutine', '*')")
+
+@Suppress("UNUSED_PARAMETER")
+private fun createScheduleMessagePoster(process: () -> Unit): () -> Unit =
+ js("() => Promise.resolve(0).then(process)")
+
+internal actual class WindowMessageQueue actual constructor(window: W3CWindow) : MessageQueue() {
+ private val scheduleMessagePoster = createScheduleMessagePoster(::process)
+ private val rescheduleMessagePoster = createRescheduleMessagePoster(window)
+ init {
+ subscribeToWindowMessages(window, ::process)
+ }
+
+ actual override fun schedule() {
+ scheduleMessagePoster()
+ }
+
+ actual override fun reschedule() {
+ rescheduleMessagePoster()
+ }
+}
+
+// We need to reference global setTimeout and clearTimeout so that it works on Node.JS as opposed to
+// using them via "window" (which only works in browser)
+private external fun setTimeout(handler: () -> Unit, timeout: Int): Int
+
+// d8 doesn't have clearTimeout
+@Suppress("UNUSED_PARAMETER")
+private fun clearTimeout(handle: Int): Unit =
+ js("{ if (typeof clearTimeout !== 'undefined') clearTimeout(handle); }")
+
+@Suppress("UNUSED_PARAMETER")
+private fun setTimeout(window: Window, handler: () -> Unit, timeout: Int): Int =
+ js("window.setTimeout(handler, timeout)")
diff --git a/kotlinx-coroutines-core/wasmJs/src/Promise.kt b/kotlinx-coroutines-core/wasmJs/src/Promise.kt
new file mode 100644
index 0000000000..f20bee3320
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/Promise.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.js.*
+
+@Suppress("UNUSED_PARAMETER")
+internal fun promiseSetDeferred(promise: Promise, deferred: JsAny): Unit =
+ js("promise.deferred = deferred")
+
+@Suppress("UNUSED_PARAMETER")
+internal fun promiseGetDeferred(promise: Promise): JsAny? = js("""{
+ console.assert(promise instanceof Promise, "promiseGetDeferred must receive a promise, but got ", promise);
+ return promise.deferred == null ? null : promise.deferred;
+}""")
+
+
+/**
+ * Starts new coroutine and returns its result as an implementation of [Promise].
+ *
+ * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument.
+ * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
+ * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden
+ * with corresponding [context] element.
+ *
+ * By default, the coroutine is immediately scheduled for execution.
+ * Other options can be specified via `start` parameter. See [CoroutineStart] for details.
+ *
+ * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
+ * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
+ * @param block the coroutine code.
+ */
+public fun CoroutineScope.promise(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> T
+): Promise =
+ async(context, start, block).asPromise()
+
+/**
+ * Converts this deferred value to the instance of [Promise].
+ */
+public fun Deferred.asPromise(): Promise {
+ val promise = Promise { resolve, reject ->
+ invokeOnCompletion {
+ val e = getCompletionExceptionOrNull()
+ if (e != null) {
+ reject(e.toJsReference())
+ } else {
+ resolve(getCompleted()?.toJsReference())
+ }
+ }
+ }
+ promiseSetDeferred(promise, this.toJsReference())
+ return promise
+}
+
+/**
+ * Converts this promise value to the instance of [Deferred].
+ */
+@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE", "UNCHECKED_CAST")
+public fun Promise.asDeferred(): Deferred {
+ val deferred = promiseGetDeferred(this) as? JsReference>
+ return deferred?.get() ?: GlobalScope.async(start = CoroutineStart.UNDISPATCHED) { await() }
+}
+
+/**
+ * Awaits for completion of the promise without blocking.
+ *
+ * This suspending function is cancellable.
+ * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
+ * stops waiting for the promise and 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("UNCHECKED_CAST")
+public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation ->
+ this@await.then(
+ onFulfilled = { cont.resume(it as T); null },
+ onRejected = { cont.resumeWithException(it.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $it")); null }
+ )
+}
diff --git a/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt b/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt
new file mode 100644
index 0000000000..5d6f79fbcd
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+@Suppress("UNCHECKED_CAST")
+internal class CopyOnWriteList : AbstractMutableList() {
+ private var array: Array = arrayOfNulls(0)
+
+ override val size: Int
+ get() = array.size
+
+ override fun add(element: E): Boolean {
+ 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 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 = n + 1)
+ array = update
+ }
+
+ override fun remove(element: E): Boolean {
+ val index = array.indexOf(element as Any)
+ if (index == -1) return false
+ removeAt(index)
+ return true
+ }
+
+ override fun removeAt(index: Int): E {
+ rangeCheck(index)
+ val n = size
+ val element = array[index]
+ val update = arrayOfNulls(n - 1)
+ array.copyInto(destination = update, endIndex = index)
+ array.copyInto(destination = update, destinationOffset = index, startIndex = index + 1, endIndex = n)
+ array = update
+ return element as E
+ }
+
+ 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
+
+ private class IteratorImpl(private val array: Array) : MutableIterator {
+ private var current = 0
+
+ override fun hasNext(): Boolean = current != array.size
+
+ override fun next(): E {
+ 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")
+ }
+}
diff --git a/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt
new file mode 100644
index 0000000000..097f4bb607
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlinx.coroutines.*
+
+internal actual fun propagateExceptionFinalResort(exception: Throwable) {
+ // log exception
+ console.error(exception.toString())
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt
new file mode 100644
index 0000000000..214f8294cd
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.js.*
+import kotlin.test.*
+
+class PromiseTest : TestBase() {
+ @Test
+ fun testPromiseResolvedAsDeferred() = GlobalScope.promise {
+ val promise = Promise> { resolve, _ ->
+ resolve("OK".toJsReference())
+ }
+ val deferred = promise.asDeferred>()
+ assertEquals("OK", deferred.await().get())
+ }
+
+ @Test
+ fun testPromiseRejectedAsDeferred() = GlobalScope.promise {
+ lateinit var promiseReject: (JsAny) -> Unit
+ val promise = Promise { _, reject ->
+ promiseReject = reject
+ }
+ val deferred = promise.asDeferred>()
+ // reject after converting to deferred to avoid "Unhandled promise rejection" warnings
+ promiseReject(TestException("Rejected").toJsReference())
+ try {
+ deferred.await()
+ expectUnreached()
+ } catch (e: Throwable) {
+ assertTrue(e is TestException)
+ assertEquals("Rejected", e.message)
+ }
+ }
+
+ @Test
+ fun testCompletedDeferredAsPromise() = GlobalScope.promise {
+ val deferred = async(start = CoroutineStart.UNDISPATCHED) {
+ // completed right away
+ "OK"
+ }
+ val promise = deferred.asPromise()
+ assertEquals("OK", promise.await())
+ }
+
+ @Test
+ fun testWaitForDeferredAsPromise() = GlobalScope.promise {
+ val deferred = async {
+ // will complete later
+ "OK"
+ }
+ val promise = deferred.asPromise()
+ assertEquals("OK", promise.await()) // await yields main thread to deferred coroutine
+ }
+
+ @Test
+ fun testCancellableAwaitPromise() = GlobalScope.promise {
+ lateinit var r: (JsAny) -> Unit
+ val toAwait = Promise { resolve, _ -> r = resolve }
+ val job = launch(start = CoroutineStart.UNDISPATCHED) {
+ toAwait.await() // suspends
+ }
+ job.cancel() // cancel the job
+ r("fail".toJsString()) // too late, the waiting job was already cancelled
+ }
+
+ @Test
+ fun testAsPromiseAsDeferred() = GlobalScope.promise {
+ val deferred = async { "OK" }
+ val promise = deferred.asPromise()
+ val d2 = promise.asDeferred()
+ assertSame(d2, deferred)
+ assertEquals("OK", d2.await())
+ }
+
+ @Test
+ fun testLeverageTestResult(): TestResult {
+ // Cannot use expect(..) here
+ var seq = 0
+ val result = runTest {
+ ++seq
+ }
+ return result.then {
+ if (seq != 1) error("Unexpected result: $seq")
+ null
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/wasmJs/test/TestBase.kt b/kotlinx-coroutines-core/wasmJs/test/TestBase.kt
new file mode 100644
index 0000000000..08aedb7414
--- /dev/null
+++ b/kotlinx-coroutines-core/wasmJs/test/TestBase.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+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
+
+@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // Counterpart for @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+public actual open class TestBase actual constructor() {
+ public actual val isBoundByJsTestTimeout = true
+ private var actionIndex = 0
+ private var finished = false
+ private var error: Throwable? = null
+ private var lastTestPromise: Promise? = null
+
+ /**
+ * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
+ * complete successfully even if this exception is consumed somewhere in the test.
+ */
+ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+ public actual fun error(message: Any, cause: Throwable? = null): Nothing {
+ if (cause != null) println(cause)
+ val exception = IllegalStateException(
+ if (cause == null) message.toString() else "$message; caused by $cause")
+ if (error == null) error = exception
+ throw exception
+ }
+
+ private fun printError(message: String, cause: Throwable) {
+ if (error == null) error = cause
+ println("$message: $cause")
+ println(cause)
+ }
+
+ /**
+ * Asserts that this invocation is `index`-th in the execution sequence (counting from one).
+ */
+ public actual fun expect(index: Int) {
+ val wasIndex = ++actionIndex
+ check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
+ }
+
+ /**
+ * Asserts that this line is never executed.
+ */
+ public actual fun expectUnreached() {
+ error("Should not be reached")
+ }
+
+ /**
+ * Asserts that this it the last action in the test. It must be invoked by any test that used [expect].
+ */
+ public actual fun finish(index: Int) {
+ expect(index)
+ check(!finished) { "Should call 'finish(...)' at most once" }
+ finished = true
+ }
+
+ /**
+ * Asserts that [finish] was invoked
+ */
+ public actual fun ensureFinished() {
+ require(finished) { "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
+ }
+
+ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+ public actual fun runTest(
+ expected: ((Throwable) -> Boolean)? = null,
+ unhandled: List<(Throwable) -> Boolean> = emptyList(),
+ block: suspend CoroutineScope.() -> Unit
+ ): TestResult {
+ var exCount = 0
+ var ex: Throwable? = null
+ /*
+ * This is an additional sanity check against `runTest` mis-usage on JS.
+ * The only way to write an async test on JS is to return Promise from the test function.
+ * _Just_ launching promise and returning `Unit` won't suffice as the underlying test framework
+ * won't be able to detect an asynchronous failure in a timely manner.
+ * We cannot detect such situations, but we can detect the most common erroneous pattern
+ * in our code base, an attempt to use multiple `runTest` in the same `@Test` method,
+ * which typically is a premise to the same error:
+ * ```
+ * @Test
+ * fun incorrectTestForJs() { // <- promise is not returned
+ * for (parameter in parameters) {
+ * runTest {
+ * runTestForParameter(parameter)
+ * }
+ * }
+ * }
+ * ```
+ */
+ if (lastTestPromise != null) {
+ error("Attempt to run multiple asynchronous test within one @Test method")
+ }
+ val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e ->
+ if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
+ exCount++
+ when {
+ exCount > unhandled.size ->
+ printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e)
+ !unhandled[exCount - 1](e) ->
+ printError("Unhandled exception was unexpected: $e", e)
+ }
+ }).catch { jsE ->
+ val e = jsE.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $jsE")
+ ex = e
+ if (expected != null) {
+ if (!expected(e))
+ error("Unexpected exception", e)
+ } else
+ throw e
+
+ null
+ }.finally {
+ if (ex == null && expected != null) error("Exception was expected but none produced")
+ if (exCount < unhandled.size)
+ error("Too few unhandled exceptions $exCount, expected ${unhandled.size}")
+ error?.let { throw it }
+ check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
+ }
+ lastTestPromise = result
+ return result
+ }
+}
+
+public actual val isJavaAndWindows: Boolean get() = false
\ No newline at end of file
diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md
index 5e385e3c24..a0d6cbafe3 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.7.3.jar`.
+You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.8.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/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
index b671b1a488..11131fad42 100644
--- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
+++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
@@ -39,6 +39,7 @@ public final class kotlinx/coroutines/debug/State : java/lang/Enum {
public static final field CREATED Lkotlinx/coroutines/debug/State;
public static final field RUNNING Lkotlinx/coroutines/debug/State;
public static final field SUSPENDED Lkotlinx/coroutines/debug/State;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State;
public static fun values ()[Lkotlinx/coroutines/debug/State;
}
diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle
deleted file mode 100644
index 42d0b8d0f0..0000000000
--- a/kotlinx-coroutines-debug/build.gradle
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-apply plugin: "com.github.johnrengelman.shadow"
-
-// apply plugin to use autocomplete for Kover DSL
-apply plugin: 'org.jetbrains.kotlinx.kover'
-
-configurations {
- shadowDeps // shaded dependencies, not included into the resulting .pom file
- compileOnly.extendsFrom(shadowDeps)
- runtimeOnly.extendsFrom(shadowDeps)
-}
-
-dependencies {
- compileOnly "junit:junit:$junit_version"
- compileOnly "org.junit.jupiter:junit-jupiter-api:$junit5_version"
- testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
- testImplementation "org.junit.platform:junit-platform-testkit:1.7.0"
- shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version"
- 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"
-}
-
-java {
- /* This is needed to be able to run JUnit5 tests. Otherwise, Gradle complains that it can't find the
- JVM1.6-compatible version of the `junit-jupiter-api` artifact. */
- disableAutoTargetJvm()
-}
-
-// This is required for BlockHound tests to work, see https://github.com/Kotlin/kotlinx.coroutines/issues/3701
-tasks.withType(Test).configureEach {
- if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) {
- jvmArgs += ["-XX:+AllowRedefinitionToAddDeleteMethods"]
- }
-}
-
-jar {
- setEnabled(false)
-}
-
-def shadowJarTask = shadowJar {
- classifier null
- // Shadow only byte buddy, do not package kotlin stdlib
- configurations = [project.configurations.shadowDeps]
- relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy')
-
- manifest {
- attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
- attributes "Can-Redefine-Classes": "true"
- }
-}
-
-configurations {
- artifacts {
- add("apiElements", shadowJarTask)
- add("runtimeElements", shadowJarTask)
- }
-}
-
-koverReport {
- filters {
- excludes {
- // Never used, safety mechanism
- classes("kotlinx.coroutines.debug.internal.NoOpProbesKt")
- }
- }
-}
diff --git a/kotlinx-coroutines-debug/build.gradle.kts b/kotlinx-coroutines-debug/build.gradle.kts
new file mode 100644
index 0000000000..ed8b918b8a
--- /dev/null
+++ b/kotlinx-coroutines-debug/build.gradle.kts
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.*
+import java.net.*
+import java.nio.file.*
+
+plugins {
+ id("com.github.johnrengelman.shadow")
+ id("org.jetbrains.kotlinx.kover") // apply plugin to use autocomplete for Kover DSL
+}
+
+configurations {
+ val shadowDeps by creating
+ compileOnly.configure {
+ extendsFrom(shadowDeps)
+ }
+ runtimeOnly.configure {
+ extendsFrom(shadowDeps)
+ }
+}
+
+val junit_version by properties
+val junit5_version by properties
+val byte_buddy_version by properties
+val blockhound_version by properties
+val jna_version by properties
+
+dependencies {
+ compileOnly("junit:junit:$junit_version")
+ compileOnly("org.junit.jupiter:junit-jupiter-api:$junit5_version")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junit5_version")
+ testImplementation("org.junit.platform:junit-platform-testkit:1.7.0")
+ add("shadowDeps", "net.bytebuddy:byte-buddy:$byte_buddy_version")
+ add("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")
+}
+
+java {
+ /* This is needed to be able to run JUnit5 tests. Otherwise, Gradle complains that it can't find the
+ JVM1.6-compatible version of the `junit-jupiter-api` artifact. */
+ disableAutoTargetJvm()
+}
+
+// This is required for BlockHound tests to work, see https://github.com/Kotlin/kotlinx.coroutines/issues/3701
+tasks.withType().configureEach {
+ if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) {
+ jvmArgs("-XX:+AllowRedefinitionToAddDeleteMethods")
+ }
+}
+
+val jar by tasks.existing(Jar::class) {
+ enabled = false
+}
+
+val shadowJar by tasks.existing(ShadowJar::class) {
+ // Shadow only byte buddy, do not package kotlin stdlib
+ configurations = listOf(project.configurations["shadowDeps"])
+ relocate("net.bytebuddy", "kotlinx.coroutines.repackaged.net.bytebuddy")
+ /* These classifiers are both set to `null` to trick Gradle into thinking that this jar file is both the
+ artifact from the `jar` task and the one from `shadowJar`. Without this, Gradle complains that the artifact
+ from the `jar` task is not present when the compilaton finishes, even if the file with this name exists. */
+ archiveClassifier.convention(null as String?)
+ archiveClassifier.set(null as String?)
+ archiveBaseName.set(jar.flatMap { it.archiveBaseName })
+ archiveVersion.set(jar.flatMap { it.archiveVersion })
+ manifest {
+ attributes(
+ mapOf(
+ "Premain-Class" to "kotlinx.coroutines.debug.AgentPremain",
+ "Can-Redefine-Classes" to "true",
+ "Multi-Release" to "true"
+ )
+ )
+ }
+ // add module-info.class to the META-INF/versions/9/ directory.
+ dependsOn(tasks.compileModuleInfoJava)
+ doLast {
+ // We can't do that directly with the shadowJar task because it doesn't support replacing existing files.
+ val zipPath = this@existing.outputs.files.singleFile.toPath()
+ val zipUri = URI.create("jar:${zipPath.toUri()}")
+ val moduleInfoFilePath = tasks.compileModuleInfoJava.get().outputs.files.asFileTree.matching {
+ include("module-info.class")
+ }.singleFile.toPath()
+ FileSystems.newFileSystem(zipUri, emptyMap()).use { fs ->
+ val moduleInfoFile = fs.getPath("META-INF/versions/9/module-info.class")
+ Files.copy(moduleInfoFilePath, moduleInfoFile, StandardCopyOption.REPLACE_EXISTING)
+ }
+ }
+}
+
+configurations {
+ // shadowJar is already part of the `shadowRuntimeElements` and `shadowApiElements`, but the other subprojects
+ // that depend on `kotlinx-coroutines-debug` look at `runtimeElements` and `apiElements`.
+ artifacts {
+ add("apiElements", shadowJar)
+ add("runtimeElements", shadowJar)
+ }
+}
+
+koverReport {
+ filters {
+ excludes {
+ // Never used, safety mechanism
+ classes("kotlinx.coroutines.debug.internal.NoOpProbesKt")
+ }
+ }
+}
diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt
index 493b85e199..0755f48943 100644
--- a/kotlinx-coroutines-debug/src/DebugProbes.kt
+++ b/kotlinx-coroutines-debug/src/DebugProbes.kt
@@ -51,6 +51,7 @@ public object DebugProbes {
*/
public var sanitizeStackTraces: Boolean
get() = DebugProbesImpl.sanitizeStackTraces
+ @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
set(value) {
DebugProbesImpl.sanitizeStackTraces = value
}
@@ -66,6 +67,7 @@ public object DebugProbes {
*/
public var enableCreationStackTraces: Boolean
get() = DebugProbesImpl.enableCreationStackTraces
+ @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
set(value) {
DebugProbesImpl.enableCreationStackTraces = value
}
@@ -82,6 +84,7 @@ public object DebugProbes {
*/
public var ignoreCoroutinesWithEmptyContext: Boolean
get() = DebugProbesImpl.ignoreCoroutinesWithEmptyContext
+ @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
set(value) {
DebugProbesImpl.ignoreCoroutinesWithEmptyContext = value
}
diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md
index f5dd8e6cb5..6acd168e24 100644
--- a/kotlinx-coroutines-test/README.md
+++ b/kotlinx-coroutines-test/README.md
@@ -26,7 +26,7 @@ Provided [TestDispatcher] implementations:
Add `kotlinx-coroutines-test` to your project test dependencies:
```
dependencies {
- testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0-RC'
}
```
diff --git a/kotlinx-coroutines-test/build.gradle.kts b/kotlinx-coroutines-test/build.gradle.kts
index c968fc4991..220c65019c 100644
--- a/kotlinx-coroutines-test/build.gradle.kts
+++ b/kotlinx-coroutines-test/build.gradle.kts
@@ -16,7 +16,6 @@ kotlin {
targets.withType(KotlinNativeTargetWithTests::class.java).configureEach {
binaries.getTest("DEBUG").apply {
optimized = true
- binaryOptions["memoryModel"] = "experimental"
}
}
@@ -27,4 +26,15 @@ kotlin {
}
}
}
-}
+
+ wasmJs {
+ nodejs {
+ testTask {
+ filter.apply {
+ // https://youtrack.jetbrains.com/issue/KT-61888
+ excludeTest("TestDispatchersTest", "testMainMocking")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt
index f95dabc3d7..26952efa6d 100644
--- a/kotlinx-coroutines-test/common/src/TestBuilders.kt
+++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt
@@ -14,7 +14,6 @@ import kotlin.jvm.*
import kotlin.time.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.internal.*
/**
* A test result.
@@ -122,8 +121,14 @@ public expect class TestResult
*
* #### Timing out
*
- * There's a built-in timeout of 10 seconds for the test body. If the test body doesn't complete within this time,
- * then the test fails with an [AssertionError]. The timeout can be changed by setting the [timeout] parameter.
+ * There's a built-in timeout of 60 seconds for the test body. If the test body doesn't complete within this time,
+ * then the test fails with an [AssertionError]. The timeout can be changed for each test separately by setting the
+ * [timeout] parameter.
+ *
+ * Additionally, setting the `kotlinx.coroutines.test.default_timeout` system property on the
+ * JVM to any string that can be parsed using [Duration.parse] (like `1m`, `30s` or `1500ms`) will change the default
+ * timeout to that value for all tests whose [timeout] is not set explicitly; setting it to anything else will throw an
+ * exception every time [runTest] is invoked.
*
* On timeout, the test body is cancelled so that the test finishes. If the code inside the test body does not
* respond to cancellation, the timeout will not be able to make the test execution stop.
@@ -157,7 +162,7 @@ public expect class TestResult
*/
public fun runTest(
context: CoroutineContext = EmptyCoroutineContext,
- timeout: Duration = DEFAULT_TIMEOUT,
+ timeout: Duration = DEFAULT_TIMEOUT.getOrThrow(),
testBody: suspend TestScope.() -> Unit
): TestResult {
check(context[RunningInRunTest] == null) {
@@ -301,7 +306,7 @@ public fun runTest(
* Performs [runTest] on an existing [TestScope]. See the documentation for [runTest] for details.
*/
public fun TestScope.runTest(
- timeout: Duration = DEFAULT_TIMEOUT,
+ timeout: Duration = DEFAULT_TIMEOUT.getOrThrow(),
testBody: suspend TestScope.() -> Unit
): TestResult = asSpecificImplementation().let { scope ->
scope.enter()
@@ -421,8 +426,15 @@ internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
/**
* The default timeout to use when running a test.
+ *
+ * It's not just a [Duration] but a [Result] so that every access to [runTest]
+ * throws the same clear exception if parsing the environment variable failed.
+ * Otherwise, the parsing error would only be thrown in one tests, while the
+ * other ones would get an incomprehensible `NoClassDefFoundError`.
*/
-internal val DEFAULT_TIMEOUT = 10.seconds
+private val DEFAULT_TIMEOUT: Result = runCatching {
+ systemProperty("kotlinx.coroutines.test.default_timeout", Duration::parse, 60.seconds)
+}
/**
* Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
@@ -571,6 +583,17 @@ internal fun throwAll(head: Throwable?, other: List) {
internal expect fun dumpCoroutines()
+private fun systemProperty(
+ name: String,
+ parse: (String) -> T,
+ default: T,
+): T {
+ val value = systemPropertyImpl(name) ?: return default
+ return parse(value)
+}
+
+internal expect fun systemPropertyImpl(name: String): String?
+
@Deprecated(
"This is for binary compatibility with the `runTest` overload that existed at some point",
level = DeprecationLevel.HIDDEN
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
index 3777cd26f8..d380bb4bd2 100644
--- a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
+++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
@@ -93,7 +93,8 @@ private class UnconfinedTestDispatcherImpl(
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
- @Suppress("INVISIBLE_MEMBER")
+ // do not remove the INVISIBLE_REFERENCE and INVISIBLE_SETTER suppressions: required in K2
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "INVISIBLE_SETTER")
override fun dispatch(context: CoroutineContext, block: Runnable) {
checkSchedulerInContext(scheduler, context)
scheduler.sendDispatchEvent(context)
diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt
index fa6e3930d8..7089255669 100644
--- a/kotlinx-coroutines-test/common/src/TestScope.kt
+++ b/kotlinx-coroutines-test/common/src/TestScope.kt
@@ -231,7 +231,7 @@ internal class TestScopeImpl(context: CoroutineContext) :
* However, we also want [uncaughtExceptions] to be queried after the callback is registered,
* because the exception collector will be able to report the exceptions that arrived before this test but
* after the previous one, and learning about such exceptions as soon is possible is nice. */
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
run { ensurePlatformExceptionHandlerLoaded(ExceptionCollector) }
if (catchNonTestRelatedExceptions) {
ExceptionCollector.addOnExceptionCallback(lock, this::reportException)
@@ -239,6 +239,7 @@ internal class TestScopeImpl(context: CoroutineContext) :
uncaughtExceptions
}
if (exceptions.isNotEmpty()) {
+ ExceptionCollector.removeOnExceptionCallback(lock)
throw UncaughtExceptionsBeforeTest().apply {
for (e in exceptions)
addSuppressed(e)
@@ -287,7 +288,7 @@ internal class TestScopeImpl(context: CoroutineContext) :
if (finished) {
throw throwable
} else {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
for (existingThrowable in uncaughtExceptions) {
// avoid reporting exceptions that already were reported.
if (unwrap(throwable) == unwrap(existingThrowable))
diff --git a/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt b/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt
index 70fcb9487f..6387e791bd 100644
--- a/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt
+++ b/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt
@@ -81,7 +81,7 @@ internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineEx
return executedACallback
}
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override fun handleException(context: CoroutineContext, exception: Throwable) {
if (handleException(exception)) {
throw ExceptionSuccessfullyProcessed
diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
index 411699b9d8..acb45e0899 100644
--- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
+++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
@@ -91,9 +91,9 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher):
}
}
-@Suppress("INVISIBLE_MEMBER")
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
private val defaultDelay
inline get() = DefaultDelay
-@Suppress("INVISIBLE_MEMBER")
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher
diff --git a/kotlinx-coroutines-test/common/test/Helpers.kt b/kotlinx-coroutines-test/common/test/Helpers.kt
index 345c66f91a..db60c5ddf6 100644
--- a/kotlinx-coroutines-test/common/test/Helpers.kt
+++ b/kotlinx-coroutines-test/common/test/Helpers.kt
@@ -47,14 +47,16 @@ fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResu
*/
expect fun testResultChain(block: () -> TestResult, after: (Result) -> TestResult): TestResult
-fun testResultChain(vararg chained: (Result) -> TestResult): TestResult =
+fun testResultChain(vararg chained: (Result) -> TestResult, initialResult: Result = Result.success(Unit)): TestResult =
if (chained.isEmpty()) {
- createTestResult { }
+ createTestResult {
+ initialResult.getOrThrow()
+ }
} else {
testResultChain(block = {
- chained[0](Result.success(Unit))
+ chained[0](initialResult)
}) {
- testResultChain(*chained.drop(1).toTypedArray())
+ testResultChain(*chained.drop(1).toTypedArray(), initialResult = it)
}
}
diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt
index da2bdcfc76..6f31cca349 100644
--- a/kotlinx-coroutines-test/common/test/RunTestTest.kt
+++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt
@@ -166,7 +166,7 @@ class RunTestTest {
it()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val suppressed = unwrap(e).suppressedExceptions
assertEquals(1, suppressed.size, "$suppressed")
assertIs(suppressed[0]).also {
@@ -207,7 +207,7 @@ class RunTestTest {
fn()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val suppressed = unwrap(e).suppressedExceptions
assertEquals(1, suppressed.size, "$suppressed")
assertIs(suppressed[0]).also {
@@ -408,4 +408,69 @@ class RunTestTest {
fun testCoroutineCompletingWithoutDispatch() = runTest(timeout = Duration.INFINITE) {
launch(Dispatchers.Default) { delay(100) }
}
+
+ /**
+ * Tests that [runTest] cleans up the exception handler even if it threw on initialization.
+ *
+ * This test must be run manually, because it writes garbage to the log.
+ *
+ * The JVM-only source set contains a test equivalent to this one that isn't ignored.
+ */
+ @Test
+ @Ignore
+ fun testExceptionCaptorCleanedUpOnPreliminaryExit(): TestResult = testResultChain({
+ // step 1: installing the exception handler
+ println("step 1")
+ runTest { }
+ }, {
+ it.getOrThrow()
+ // step 2: throwing an uncaught exception to be caught by the exception-handling system
+ println("step 2")
+ createTestResult {
+ launch(NonCancellable) { throw TestException("A") }
+ }
+ }, {
+ it.getOrThrow()
+ // step 3: trying to run a test should immediately fail, even before entering the test body
+ println("step 3")
+ try {
+ runTest {
+ fail("unreached")
+ }
+ fail("unreached")
+ } catch (e: UncaughtExceptionsBeforeTest) {
+ val cause = e.suppressedExceptions.single()
+ assertIs(cause)
+ assertEquals("A", cause.message)
+ }
+ // step 4: trying to run a test again should not fail with an exception
+ println("step 4")
+ runTest {
+ }
+ }, {
+ it.getOrThrow()
+ // step 5: throwing an uncaught exception to be caught by the exception-handling system, again
+ println("step 5")
+ createTestResult {
+ launch(NonCancellable) { throw TestException("B") }
+ }
+ }, {
+ it.getOrThrow()
+ // step 6: trying to run a test should immediately fail, again
+ println("step 6")
+ try {
+ runTest {
+ fail("unreached")
+ }
+ fail("unreached")
+ } catch (e: Exception) {
+ val cause = e.suppressedExceptions.single()
+ assertIs(cause)
+ assertEquals("B", cause.message)
+ }
+ // step 7: trying to run a test again should not fail with an exception, again
+ println("step 7")
+ runTest {
+ }
+ })
}
diff --git a/kotlinx-coroutines-test/common/test/TestScopeTest.kt b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
index 433faef7ac..76791365fc 100644
--- a/kotlinx-coroutines-test/common/test/TestScopeTest.kt
+++ b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
@@ -495,9 +495,11 @@ class TestScopeTest {
* Tests that the [TestScope] exception reporting mechanism will report the exceptions that happen between
* different tests.
*
- * This test must be ran manually, because such exceptions still go through the global exception handler
+ * This test must be run manually, because such exceptions still go through the global exception handler
* (as there's no guarantee that another test will happen), and the global exception handler will
* log the exceptions or, on Native, crash the test suite.
+ *
+ * The JVM-only source set contains a test equivalent to this one that isn't ignored.
*/
@Test
@Ignore
diff --git a/kotlinx-coroutines-test/js/src/TestBuilders.kt b/kotlinx-coroutines-test/js/src/TestBuilders.kt
index 97c9da0eee..f1148135d9 100644
--- a/kotlinx-coroutines-test/js/src/TestBuilders.kt
+++ b/kotlinx-coroutines-test/js/src/TestBuilders.kt
@@ -9,6 +9,8 @@ import kotlin.js.*
@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
public actual typealias TestResult = Promise
+internal actual fun systemPropertyImpl(name: String): String? = null
+
internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult =
GlobalScope.promise {
testProcedure()
diff --git a/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt
index 4d865f83c0..d68cecda70 100644
--- a/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt
+++ b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt
@@ -5,7 +5,7 @@
package kotlinx.coroutines.test.internal
import kotlinx.coroutines.*
-@Suppress("INVISIBLE_MEMBER")
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher =
when (val mainDispatcher = Main) {
is TestMainDispatcher -> mainDispatcher
diff --git a/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
index 0521fd22ae..1307e036ba 100644
--- a/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
+++ b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
@@ -15,6 +15,13 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
}
}
+internal actual fun systemPropertyImpl(name: String): String? =
+ try {
+ System.getProperty(name)
+ } catch (e: SecurityException) {
+ null
+ }
+
internal actual fun dumpCoroutines() {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
if (DebugProbesImpl.isInstalled) {
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
index 150055f532..24f2166eb3 100644
--- a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
@@ -55,7 +55,7 @@ public class TestCoroutineExceptionHandler :
private val _lock = SynchronizedObject()
private var _coroutinesCleanedUp = false
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override fun handleException(context: CoroutineContext, exception: Throwable) {
synchronized(_lock) {
if (_coroutinesCleanedUp) {
diff --git a/kotlinx-coroutines-test/jvm/test/UncaughtExceptionsTest.kt b/kotlinx-coroutines-test/jvm/test/UncaughtExceptionsTest.kt
new file mode 100644
index 0000000000..d3802eda14
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/UncaughtExceptionsTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016-2023 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.*
+
+/**
+ * Tests that check the behavior of the test framework when there are stray uncaught exceptions.
+ * These tests are JVM-only because only the JVM allows to set a global uncaught exception handler and validate the
+ * behavior without checking the test logs.
+ * Nevertheless, each test here has a corresponding test in the common source set that can be run manually.
+ */
+class UncaughtExceptionsTest {
+
+ val oldExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
+ val uncaughtExceptions = mutableListOf()
+
+ @BeforeTest
+ fun setUp() {
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
+ uncaughtExceptions.add(throwable)
+ }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ Thread.setDefaultUncaughtExceptionHandler(oldExceptionHandler)
+ }
+
+ @Test
+ fun testReportingStrayUncaughtExceptionsBetweenTests() {
+ TestScopeTest().testReportingStrayUncaughtExceptionsBetweenTests()
+ assertEquals(1, uncaughtExceptions.size, "Expected 1 uncaught exception, but got $uncaughtExceptions")
+ val exception = assertIs(uncaughtExceptions.single())
+ assertEquals("x", exception.message)
+ }
+
+ @Test
+ fun testExceptionCaptorCleanedUpOnPreliminaryExit() {
+ RunTestTest().testExceptionCaptorCleanedUpOnPreliminaryExit()
+ assertEquals(2, uncaughtExceptions.size, "Expected 2 uncaught exceptions, but got $uncaughtExceptions")
+ for (exception in uncaughtExceptions) {
+ assertIs(exception)
+ }
+ assertEquals("A", uncaughtExceptions[0].message)
+ assertEquals("B", uncaughtExceptions[1].message)
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
index ed5b1577f5..4f05c1920f 100644
--- a/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
@@ -92,7 +92,7 @@ class RunTestLegacyScopeTest {
fn()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val suppressed = unwrap(e).suppressedExceptions
assertEquals(1, suppressed.size)
assertIs(suppressed[0]).also {
diff --git a/kotlinx-coroutines-test/native/src/TestBuilders.kt b/kotlinx-coroutines-test/native/src/TestBuilders.kt
index 607dec6a73..2714b45823 100644
--- a/kotlinx-coroutines-test/native/src/TestBuilders.kt
+++ b/kotlinx-coroutines-test/native/src/TestBuilders.kt
@@ -4,7 +4,6 @@
package kotlinx.coroutines.test
import kotlinx.coroutines.*
-import kotlin.native.concurrent.*
@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias TestResult = Unit
@@ -15,4 +14,6 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
}
}
+internal actual fun systemPropertyImpl(name: String): String? = null
+
internal actual fun dumpCoroutines() { }
diff --git a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
index 4d865f83c0..d68cecda70 100644
--- a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
+++ b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
@@ -5,7 +5,7 @@
package kotlinx.coroutines.test.internal
import kotlinx.coroutines.*
-@Suppress("INVISIBLE_MEMBER")
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher =
when (val mainDispatcher = Main) {
is TestMainDispatcher -> mainDispatcher
diff --git a/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt
new file mode 100644
index 0000000000..898750b8bd
--- /dev/null
+++ b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.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.
+ */
+
+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 systemPropertyImpl(name: String): String? = null
+
+internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult =
+ GlobalScope.promise {
+ testProcedure()
+ }
+
+internal actual fun dumpCoroutines() { }
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt
new file mode 100644
index 0000000000..4d865f83c0
--- /dev/null
+++ b/kotlinx-coroutines-test/wasmJs/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/wasmJs/test/Helpers.kt b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt
new file mode 100644
index 0000000000..1a8d63d7a7
--- /dev/null
+++ b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016-2022 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 testResultChain(block: () -> TestResult, after: (Result) -> TestResult): TestResult =
+ block().then(
+ {
+ after(Result.success(Unit))
+ null
+ }, {
+ after(Result.failure(it.toThrowableOrNull() ?: Throwable("Unexpected non-Kotlin exception $it")))
+ null
+ })
+
+actual typealias NoJs = Ignore
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt
new file mode 100644
index 0000000000..087db81eb8
--- /dev/null
+++ b/kotlinx-coroutines-test/wasmJs/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/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt
index ae85e4186a..a516777860 100644
--- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt
@@ -84,7 +84,7 @@ public class PublisherCoroutine(
// Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked
private val mutex: Mutex = Mutex(locked = true)
- @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER")
+ @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override val onSend: SelectClause2> get() = SelectClause2Impl(
clauseObject = this,
regFunc = PublisherCoroutine<*>::registerSelectForSend as RegistrationFunction,
diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt
index 1a527a3c2b..7e6dcefae2 100644
--- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt
@@ -59,7 +59,7 @@ private class PublisherAsFlow(
* It's too counter-intuitive to be public, and moving it to Flow companion
* will also create undesired effect.
*/
- @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
private val requestSize: Long
get() =
if (onBufferOverflow != BufferOverflow.SUSPEND) {
@@ -208,7 +208,7 @@ public class FlowSubscription(
try {
consumeFlow()
} catch (cause: Throwable) {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val unwrappedCause = unwrap(cause)
if (!cancellationRequested || isActive || unwrappedCause !== getCancellationException()) {
try {
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
index ad8fac71d3..2da5096804 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
@@ -70,7 +70,7 @@ private class RxObservableCoroutine(
// Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked
private val mutex: Mutex = Mutex()
- @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER")
+ @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override val onSend: SelectClause2> get() = SelectClause2Impl(
clauseObject = this,
regFunc = RxObservableCoroutine<*>::registerSelectForSend as RegistrationFunction,
@@ -165,7 +165,7 @@ private class RxObservableCoroutine(
if (_signal.value == SIGNALLED)
return
_signal.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val unwrappedCause = cause?.let { unwrap(it) }
if (unwrappedCause == null) {
try {
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
index d7d5f6cfbf..1b65f9b36c 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
@@ -136,7 +136,7 @@ private fun CoroutineScope.scheduleTask(
if (delayMillis <= 0) {
toSchedule.run()
} else {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it }
}
return disposable
@@ -178,4 +178,4 @@ public class SchedulerCoroutineDispatcher(
/** @suppress */
override fun hashCode(): Int = System.identityHashCode(scheduler)
-}
\ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
index 8ea761c979..4123531a69 100644
--- a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
+++ b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
@@ -70,7 +70,7 @@ private class RxObservableCoroutine(
// Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked
private val mutex: Mutex = Mutex()
- @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER")
+ @Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override val onSend: SelectClause2> get() = SelectClause2Impl(
clauseObject = this,
regFunc = RxObservableCoroutine<*>::registerSelectForSend as RegistrationFunction,
@@ -165,7 +165,7 @@ private class RxObservableCoroutine(
if (_signal.value == SIGNALLED)
return
_signal.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
val unwrappedCause = cause?.let { unwrap(it) }
if (unwrappedCause == null) {
try {
diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
index e7f93868b1..2ef0fc1d92 100644
--- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
+++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
@@ -136,7 +136,7 @@ private fun CoroutineScope.scheduleTask(
if (delayMillis <= 0) {
toSchedule.run()
} else {
- @Suppress("INVISIBLE_MEMBER")
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it }
}
return disposable
diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md
index 872926d713..a1f442ec8b 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.7.3"
+implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.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/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
index 7012c23ecd..065bb2af49 100644
--- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
+++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
@@ -127,11 +127,8 @@ internal class HandlerContext private constructor(
name: String? = null
) : this(handler, name, false)
- @Volatile
- private var _immediate: HandlerContext? = if (invokeImmediately) this else null
-
- override val immediate: HandlerContext = _immediate ?:
- HandlerContext(handler, name, true).also { _immediate = it }
+ override val immediate: HandlerContext = if (invokeImmediately) this else
+ HandlerContext(handler, name, true)
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return !invokeImmediately || Looper.myLooper() != handler.looper
@@ -172,8 +169,10 @@ internal class HandlerContext private constructor(
if (invokeImmediately) "$str.immediate" else str
}
- override fun equals(other: Any?): Boolean = other is HandlerContext && other.handler === handler
- override fun hashCode(): Int = System.identityHashCode(handler)
+ override fun equals(other: Any?): Boolean =
+ other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately
+ // inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
+ override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
}
@Volatile
diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
index afe6cff2f6..9fc40d33ac 100644
--- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
+++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
@@ -13,31 +13,14 @@ import org.robolectric.annotation.*
import org.robolectric.shadows.*
import java.util.concurrent.*
import kotlin.test.*
+import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
@RunWith(RobolectricTestRunner::class)
-@Config(manifest = Config.NONE, sdk = [28])
@LooperMode(LooperMode.Mode.LEGACY)
-class HandlerDispatcherTest : TestBase() {
- @Test
- fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) {
- expect(1)
- // launch in the immediate dispatcher
- launch(Dispatchers.Main.immediate) {
- expect(2)
- yield()
- expect(4)
- }
- expect(3) // after yield
- yield() // yield back
- finish(5)
- }
-
- @Test
- fun testMainDispatcherToString() {
- assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
- assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
- }
-
+@Config(manifest = Config.NONE, sdk = [28])
+class HandlerDispatcherTest : MainDispatcherTestBase.WithRealTimeDelay() {
@Test
fun testDefaultDelayIsNotDelegatedToMain() = runTest {
val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
@@ -132,4 +115,20 @@ class HandlerDispatcherTest : TestBase() {
mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS)
finish(5)
}
+
+ override fun isMainThread(): Boolean = Looper.getMainLooper().thread === Thread.currentThread()
+
+ override fun scheduleOnMainQueue(block: () -> Unit) {
+ Handler(Looper.getMainLooper()).post(block)
+ }
+
+ // by default, Robolectric only schedules tasks on the main thread but doesn't run them.
+ // This function nudges it to run them, 10 milliseconds of virtual time at a time.
+ override suspend fun spinTest(testBody: Job) {
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
+ while (testBody.isActive) {
+ Thread.sleep(10, 0)
+ mainLooper.idleFor(10, TimeUnit.MILLISECONDS)
+ }
+ }
}
diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts
index 634423a517..d11304e4a7 100644
--- a/ui/kotlinx-coroutines-javafx/build.gradle.kts
+++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts
@@ -2,15 +2,8 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-buildscript {
- dependencies {
- // this line can be removed when https://github.com/openjfx/javafx-gradle-plugin/pull/135 is released
- classpath("org.javamodularity:moduleplugin:1.8.12")
- }
-}
-
plugins {
- id("org.openjfx.javafxplugin") version "0.0.13"
+ id("org.openjfx.javafxplugin") version "0.0.14"
}
configurations {
@@ -33,7 +26,7 @@ javafx {
tasks {
test {
extensions.configure(org.javamodularity.moduleplugin.extensions.TestModuleOptions::class) {
- addReads["kotlinx.coroutines.core"] = "junit"
+ addReads["kotlinx.coroutines.core"] = "junit,kotlin.test"
addReads["kotlinx.coroutines.javafx"] = "kotlin.test"
}
jvmArgs = listOf(
diff --git a/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt b/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt
index 24c5c132fd..46f6a0249a 100644
--- a/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt
+++ b/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt
@@ -8,60 +8,33 @@ import javafx.application.*
import kotlinx.coroutines.*
import org.junit.*
import org.junit.Test
+import javax.swing.*
import kotlin.test.*
-class JavaFxDispatcherTest : TestBase() {
+class JavaFxDispatcherTest : MainDispatcherTestBase.WithRealTimeDelay() {
@Before
fun setup() {
ignoreLostThreads("JavaFX Application Thread", "Thread-", "QuantumRenderer-", "InvokeLaterDispatcher")
}
- @Test
- fun testDelay() {
+ override fun shouldSkipTesting(): Boolean {
if (!initPlatform()) {
println("Skipping JavaFxTest in headless environment")
- return // ignore test in headless environments
- }
-
- runBlocking {
- expect(1)
- val job = launch(Dispatchers.JavaFx) {
- check(Platform.isFxApplicationThread())
- expect(2)
- delay(100)
- check(Platform.isFxApplicationThread())
- expect(3)
- }
- job.join()
- finish(4)
+ return true // ignore test in headless environments
}
+ return false
}
- @Test
- fun testImmediateDispatcherYield() {
- if (!initPlatform()) {
- println("Skipping JavaFxTest in headless environment")
- return // ignore test in headless environments
- }
+ override fun isMainThread() = Platform.isFxApplicationThread()
- runBlocking(Dispatchers.JavaFx) {
- expect(1)
- check(Platform.isFxApplicationThread())
- // launch in the immediate dispatcher
- launch(Dispatchers.JavaFx.immediate) {
- expect(2)
- yield()
- expect(4)
- }
- expect(3) // after yield
- yield() // yield back
- finish(5)
- }
+ override fun scheduleOnMainQueue(block: () -> Unit) {
+ Platform.runLater { block() }
}
+ /** Tests that the Main dispatcher is in fact the JavaFx one. */
@Test
- fun testMainDispatcherToString() {
- assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
- assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
+ fun testMainIsJavaFx() {
+ assertSame(Dispatchers.JavaFx, Dispatchers.Main)
}
-}
\ No newline at end of file
+
+}
diff --git a/ui/kotlinx-coroutines-swing/test/SwingTest.kt b/ui/kotlinx-coroutines-swing/test/SwingTest.kt
index 7e53e57b17..6d5111ebec 100644
--- a/ui/kotlinx-coroutines-swing/test/SwingTest.kt
+++ b/ui/kotlinx-coroutines-swing/test/SwingTest.kt
@@ -11,96 +11,21 @@ import javax.swing.*
import kotlin.coroutines.*
import kotlin.test.*
-class SwingTest : TestBase() {
+class SwingTest : MainDispatcherTestBase.WithRealTimeDelay() {
@Before
fun setup() {
ignoreLostThreads("AWT-EventQueue-")
}
- @Test
- fun testDelay() = runBlocking {
- expect(1)
- SwingUtilities.invokeLater { expect(2) }
- val job = launch(Dispatchers.Swing) {
- check(SwingUtilities.isEventDispatchThread())
- expect(3)
- SwingUtilities.invokeLater { expect(4) }
- delay(100)
- check(SwingUtilities.isEventDispatchThread())
- expect(5)
- }
- job.join()
- finish(6)
- }
-
- private class SwingComponent(coroutineContext: CoroutineContext = EmptyCoroutineContext) :
- CoroutineScope by MainScope() + coroutineContext
- {
- public var executed = false
- fun testLaunch(): Job = launch {
- check(SwingUtilities.isEventDispatchThread())
- executed = true
- }
- fun testFailure(): Job = launch {
- check(SwingUtilities.isEventDispatchThread())
- throw TestException()
- }
- fun testCancellation() : Job = launch(start = CoroutineStart.ATOMIC) {
- check(SwingUtilities.isEventDispatchThread())
- delay(Long.MAX_VALUE)
- }
- }
-
- @Test
- fun testLaunchInMainScope() = runTest {
- val component = SwingComponent()
- val job = component.testLaunch()
- job.join()
- assertTrue(component.executed)
- component.cancel()
- component.coroutineContext[Job]!!.join()
- }
-
- @Test
- fun testFailureInMainScope() = runTest {
- var exception: Throwable? = null
- val component = SwingComponent(CoroutineExceptionHandler { ctx, e -> exception = e})
- val job = component.testFailure()
- job.join()
- assertTrue(exception!! is TestException)
- component.cancel()
- join(component)
- }
-
- @Test
- fun testCancellationInMainScope() = runTest {
- val component = SwingComponent()
- component.cancel()
- component.testCancellation().join()
- join(component)
- }
-
- private suspend fun join(component: SwingComponent) {
- component.coroutineContext[Job]!!.join()
- }
+ override fun isMainThread() = SwingUtilities.isEventDispatchThread()
- @Test
- fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Swing) {
- expect(1)
- // launch in the immediate dispatcher
- launch(Dispatchers.Swing.immediate) {
- expect(2)
- yield()
- expect(4)
- }
- expect(3) // after yield
- yield() // yield back
- finish(5)
+ override fun scheduleOnMainQueue(block: () -> Unit) {
+ SwingUtilities.invokeLater { block() }
}
+ /** Tests that the Main dispatcher is in fact the JavaFx one. */
@Test
- fun testMainDispatcherToString() {
- assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
- assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
+ fun testMainIsJavaFx() {
+ assertSame(Dispatchers.Swing, Dispatchers.Main)
}
-}
\ No newline at end of file
+}