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 @@ [![Kotlin Stable](https://kotl.in/badges/stable.svg)](https://kotlinlang.org/docs/components-stability.html) [![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.3)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.3) -[![Kotlin](https://img.shields.io/badge/kotlin-1.8.20-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.8.0-RC)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.8.0-RC) +[![Kotlin](https://img.shields.io/badge/kotlin-1.9.21-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for the Kotlin `1.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.kotlinx kotlinx-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 +}