diff --git a/CHANGES.md b/CHANGES.md index 510c35accf..611e9c9c74 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,15 @@ # Change log for kotlinx.coroutines +## Version 1.5.2 + +* Kotlin is updated to 1.5.30. +* New native targets for Apple Silicon are introduced. +* Fixed a bug when `onUndeliveredElement` was incorrectly called on a properly received elements on JS (#2826). +* Fixed `Dispatchers.Default` on React Native, it now fully relies on `setTimeout` instead of stub `process.nextTick`. Thanks to @Legion2 (#2843). +* Optimizations of `Mutex` implementation (#2581). +* `Mutex` implementation is made completely lock-free as stated (#2590). +* Various documentation and guides improvements. Thanks to @MasoodFallahpoor and @Pihanya. + ## Version 1.5.1 * Atomic `update`, `getAndUpdate`, and `updateAndGet` operations of `MutableStateFlow` (#2720). diff --git a/README.md b/README.md index 6437ac72f6..6a13f07aa3 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.1)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.1/pom) -[![Kotlin](https://img.shields.io/badge/kotlin-1.5.20-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2/pom) +[![Kotlin](https://img.shields.io/badge/kotlin-1.5.30-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for the Kotlin `1.5.20` release. +This is a companion version for the Kotlin `1.5.30` release. ```kotlin suspend fun main() = coroutineScope { @@ -83,7 +83,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.5.1 + 1.5.2 ``` @@ -91,7 +91,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.5.20 + 1.5.30 ``` @@ -101,7 +101,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' } ``` @@ -109,7 +109,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.5.20' + ext.kotlin_version = '1.5.30' } ``` @@ -127,7 +127,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") } ``` @@ -147,7 +147,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as a dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' ``` This gives you access to the Android [Dispatchers.Main] @@ -180,7 +180,7 @@ In common code that should get compiled for different platforms, you can add a d ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") } } ``` @@ -192,7 +192,7 @@ Platform-specific dependencies are recommended to be used only for non-multiplat #### JS Kotlin/JS version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.5.1/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.5.2/jar) (follow the link to get the dependency declaration snippet) and as [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) NPM package. #### Native diff --git a/docs/topics/exception-handling.md b/docs/topics/exception-handling.md index 3facd51a22..35e645f4b6 100644 --- a/docs/topics/exception-handling.md +++ b/docs/topics/exception-handling.md @@ -364,6 +364,7 @@ only downwards. This can easily be demonstrated using the following example: import kotlinx.coroutines.* fun main() = runBlocking { +//sampleStart val supervisor = SupervisorJob() with(CoroutineScope(coroutineContext + supervisor)) { // launch the first child -- its exception is ignored for this example (don't do this in practice!) @@ -389,8 +390,10 @@ fun main() = runBlocking { supervisor.cancel() secondChild.join() } +//sampleEnd } ``` +{kotlin-runnable="true" kotlin-min-compiler-version="1.3"} > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt). > @@ -418,6 +421,7 @@ import kotlin.coroutines.* import kotlinx.coroutines.* fun main() = runBlocking { +//sampleStart try { supervisorScope { val child = launch { @@ -436,8 +440,10 @@ fun main() = runBlocking { } catch(e: AssertionError) { println("Caught an assertion error") } +//sampleEnd } ``` +{kotlin-runnable="true" kotlin-min-compiler-version="1.3"} > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt). > @@ -468,6 +474,7 @@ import kotlin.coroutines.* import kotlinx.coroutines.* fun main() = runBlocking { +//sampleStart val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } @@ -479,8 +486,10 @@ fun main() = runBlocking { println("The scope is completing") } println("The scope is completed") +//sampleEnd } ``` +{kotlin-runnable="true" kotlin-min-compiler-version="1.3"} > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt). > diff --git a/gradle.properties b/gradle.properties index 2983dd11a0..26e5147c51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,14 @@ # # Kotlin -version=1.5.1-SNAPSHOT +version=1.5.2-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.5.20 +kotlin_version=1.5.30 # Dependencies junit_version=4.12 junit5_version=5.7.0 -atomicfu_version=0.16.2 +atomicfu_version=0.16.3 knit_version=0.3.0 html_version=0.7.2 lincheck_version=2.14 @@ -22,7 +22,7 @@ rxjava2_version=2.2.8 rxjava3_version=3.0.2 javafx_version=11.0.2 javafx_plugin_version=0.0.8 -binary_compatibility_validator_version=0.6.0 +binary_compatibility_validator_version=0.7.0 blockhound_version=1.0.2.RELEASE jna_version=5.5.0 @@ -56,3 +56,4 @@ jekyll_version=4.0 org.gradle.jvmargs=-Xmx4g kotlin.mpp.enableCompatibilityMetadataVariant=true +kotlin.mpp.stability.nowarn=true diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index 73e99e8465..0a247ede9a 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -25,6 +25,10 @@ kotlin { addTarget(presets.watchosArm64) addTarget(presets.watchosX86) addTarget(presets.watchosX64) + addTarget(presets.iosSimulatorArm64) + addTarget(presets.watchosSimulatorArm64) + addTarget(presets.tvosSimulatorArm64) + addTarget(presets.macosArm64) } sourceSets { diff --git a/gradle/opt-in.gradle b/gradle/opt-in.gradle index bcf6bebe94..22f022dbb5 100644 --- a/gradle/opt-in.gradle +++ b/gradle/opt-in.gradle @@ -3,6 +3,7 @@ */ ext.optInAnnotations = [ + "kotlin.RequiresOptIn", "kotlin.experimental.ExperimentalTypeInference", "kotlin.ExperimentalMultiplatform", "kotlinx.coroutines.DelicateCoroutinesApi", diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index b133b7935d..2c2f1b8ff6 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -81,6 +81,10 @@ public interface CancellableContinuation : Continuation { * Same as [tryResume] but with [onCancellation] handler that called if and only if the value is not * delivered to the caller because of the dispatch in the process, so that atomicity delivery * guaranteed can be provided by having a cancellation fallback. + * + * Implementation note: current implementation always returns RESUME_TOKEN or `null` + * + * @suppress **This is unstable API and it is subject to change.** */ @InternalCoroutinesApi public fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt index f986d78760..beafdaf2ca 100644 --- a/kotlinx-coroutines-core/common/src/CompletableJob.kt +++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt @@ -21,7 +21,7 @@ public interface CompletableJob : Job { * * Subsequent invocations of this function have no effect and always produce `false`. * - * This function transitions this job into _completed- state if it was not completed or cancelled yet. + * This function transitions this job into _completed_ state if it was not completed or cancelled yet. * However, that if this job has children, then it transitions into _completing_ state and becomes _complete_ * once all its children are [complete][isCompleted]. See [Job] for details. */ diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 627318f676..3ed233bfb9 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -49,7 +49,7 @@ import kotlin.coroutines.intrinsics.* * * `CoroutineScope()` uses [Dispatchers.Default] for its coroutines. * * `MainScope()` uses [Dispatchers.Main] for its coroutines. * - * **The key part of custom usage of `CustomScope` is cancelling it and the end of the lifecycle.** + * **The key part of custom usage of `CustomScope` is cancelling it at the end of the lifecycle.** * The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines * is no longer needed. It cancels all the coroutines that might still be running on behalf of it. * @@ -185,7 +185,7 @@ public val CoroutineScope.isActive: Boolean * } * ``` * - * In top-level code, when launching a concurrent operation operation from a non-suspending context, an appropriately + * In top-level code, when launching a concurrent operation from a non-suspending context, an appropriately * confined instance of [CoroutineScope] shall be used instead of a `GlobalScope`. See docs on [CoroutineScope] for * details. * diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index be58213288..9552153aa9 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -168,7 +168,7 @@ public interface Job : CoroutineContext.Element { /** * Starts coroutine related to this job (if any) if it was not started yet. - * The result `true` if this invocation actually started coroutine or `false` + * The result is `true` if this invocation actually started coroutine or `false` * if it was already started or completed. */ public fun start(): Boolean diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index bcf1921594..4751296c87 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -136,6 +136,7 @@ internal abstract class AbstractSendChannel( return sendSuspend(element) } + @Suppress("DEPRECATION") override fun offer(element: E): Boolean { // Temporary migration for offer users who rely on onUndeliveredElement try { diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 5cc8ad8b35..382953efcb 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -171,7 +171,7 @@ private class ChannelAsFlow( */ @Deprecated( level = DeprecationLevel.WARNING, - message = "'BroadcastChannel' is obsolete and all coreresponding operators are deprecated " + + message = "'BroadcastChannel' is obsolete and all corresponding operators are deprecated " + "in the favour of StateFlow and SharedFlow" ) // Since 1.5.0, was @FlowPreview, safe to remove in 1.7.0 public fun BroadcastChannel.asFlow(): Flow = flow { @@ -182,7 +182,7 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * ### Deprecated * * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows. - * [SharedFlow] is a easier-to-use and more flow-centric API for the same purposes, so using + * [SharedFlow] is an easier-to-use and more flow-centric API for the same purposes, so using * [shareIn] operator is preferred. It is not a direct replacement, so please * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb: * diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 9bcf088e95..d79e203464 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -469,7 +469,7 @@ private class SharedFlowImpl( // outside of the lock: register dispose on cancellation emitter?.let { cont.disposeOnCancellation(it) } // outside of the lock: resume slots if needed - for (cont in resumes) cont?.resume(Unit) + for (r in resumes) r?.resume(Unit) } private fun cancelEmitter(emitter: Emitter) = synchronized(this) { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 858c885c1e..83f83e1e15 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -83,7 +83,8 @@ public val FlowCollector<*>.coroutineContext: CoroutineContext get() = noImpl() @Deprecated( - message = "SharedFlow never completes, so this operator has no effect.", + message = "SharedFlow never completes, so this operator typically has not effect, it can only " + + "catch exceptions from 'onSubscribe' operator", level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("this") ) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index d26839f9ea..771f8332c3 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -44,7 +44,7 @@ public suspend fun Flow<*>.collect(): Unit = collect(NopCollector) * .launchIn(uiScope) * ``` * - * Note that resulting value of [launchIn] is not used the provided scope takes care of cancellation. + * Note that the resulting value of [launchIn] is not used and the provided scope takes care of cancellation. */ public fun Flow.launchIn(scope: CoroutineScope): Job = scope.launch { collect() // tail-call diff --git a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt index b91f30d319..2d00768d7c 100644 --- a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt @@ -20,6 +20,7 @@ internal expect fun recoverStackTrace(exception: E, continuation: /** * initCause on JVM, nop on other platforms */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") internal expect fun Throwable.initCause(cause: Throwable) /** diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 7d0a343d95..19584e0981 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.contracts.* -import kotlin.coroutines.* import kotlin.jvm.* import kotlin.native.concurrent.* @@ -124,8 +123,6 @@ private val LOCK_FAIL = Symbol("LOCK_FAIL") @SharedImmutable private val UNLOCK_FAIL = Symbol("UNLOCK_FAIL") @SharedImmutable -private val SELECT_SUCCESS = Symbol("SELECT_SUCCESS") -@SharedImmutable private val LOCKED = Symbol("LOCKED") @SharedImmutable private val UNLOCKED = Symbol("UNLOCKED") @@ -191,7 +188,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable sc@ { cont -> - val waiter = LockCont(owner, cont) + var waiter = LockCont(owner, cont) _state.loop { state -> when (state) { is Empty -> { @@ -210,11 +207,24 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { is LockedQueue -> { val curOwner = state.owner check(curOwner !== owner) { "Already locked by $owner" } - if (state.addLastIf(waiter) { _state.value === state }) { - // added to waiter list! + + state.addLast(waiter) + /* + * If the state has been changed while we were adding the waiter, + * it means that 'unlock' has taken it and _either_ resumed it successfully or just overwritten. + * To rendezvous that, we try to "invalidate" our node and go for retry. + * + * Node has to be re-instantiated as we do not support node re-adding, even to + * another list + */ + if (_state.value === state || !waiter.take()) { + // added to waiter list cont.removeOnCancellation(waiter) return@sc } + + waiter = LockCont(owner, cont) + return@loop } is OpDescriptor -> state.perform(this) // help else -> error("Illegal state $state") @@ -252,8 +262,17 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { is LockedQueue -> { check(state.owner !== owner) { "Already locked by $owner" } val node = LockSelect(owner, select, block) - if (state.addLastIf(node) { _state.value === state }) { - // successfully enqueued + /* + * If the state has been changed while we were adding the waiter, + * it means that 'unlock' has taken it and _either_ resumed it successfully or just overwritten. + * To rendezvous that, we try to "invalidate" our node and go for retry. + * + * Node has to be re-instantiated as we do not support node re-adding, even to + * another list + */ + state.addLast(node) + if (_state.value === state || !node.take()) { + // added to waiter list select.disposeOnSelect(node) return } @@ -300,7 +319,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } } - public override fun unlock(owner: Any?) { + override fun unlock(owner: Any?) { _state.loop { state -> when (state) { is Empty -> { @@ -319,10 +338,9 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { val op = UnlockOp(state) if (_state.compareAndSet(state, op) && op.perform(this) == null) return } else { - val token = (waiter as LockWaiter).tryResumeLockWaiter() - if (token != null) { + if ((waiter as LockWaiter).tryResumeLockWaiter()) { state.owner = waiter.owner ?: LOCKED - waiter.completeResumeLockWaiter(token) + waiter.completeResumeLockWaiter() return } } @@ -352,21 +370,28 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { private abstract inner class LockWaiter( @JvmField val owner: Any? ) : LockFreeLinkedListNode(), DisposableHandle { + private val isTaken = atomic(false) + fun take(): Boolean = isTaken.compareAndSet(false, true) final override fun dispose() { remove() } - abstract fun tryResumeLockWaiter(): Any? - abstract fun completeResumeLockWaiter(token: Any) + abstract fun tryResumeLockWaiter(): Boolean + abstract fun completeResumeLockWaiter() } private inner class LockCont( owner: Any?, - @JvmField val cont: CancellableContinuation + private val cont: CancellableContinuation ) : LockWaiter(owner) { - override fun tryResumeLockWaiter() = cont.tryResume(Unit, idempotent = null) { - // if this continuation gets cancelled during dispatch to the caller, then release the lock - unlock(owner) + + override fun tryResumeLockWaiter(): Boolean { + if (!take()) return false + return cont.tryResume(Unit, idempotent = null) { + // if this continuation gets cancelled during dispatch to the caller, then release the lock + unlock(owner) + } != null } - override fun completeResumeLockWaiter(token: Any) = cont.completeResume(token) - override fun toString(): String = "LockCont[$owner, $cont] for ${this@MutexImpl}" + + override fun completeResumeLockWaiter() = cont.completeResume(RESUME_TOKEN) + override fun toString(): String = "LockCont[$owner, ${cont}] for ${this@MutexImpl}" } private inner class LockSelect( @@ -374,9 +399,8 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { @JvmField val select: SelectInstance, @JvmField val block: suspend (Mutex) -> R ) : LockWaiter(owner) { - override fun tryResumeLockWaiter(): Any? = if (select.trySelect()) SELECT_SUCCESS else null - override fun completeResumeLockWaiter(token: Any) { - assert { token === SELECT_SUCCESS } + override fun tryResumeLockWaiter(): Boolean = take() && select.trySelect() + override fun completeResumeLockWaiter() { block.startCoroutineCancellable(receiver = this@MutexImpl, completion = select.completion) { // if this continuation gets cancelled during dispatch to the caller, then release the lock unlock(owner) diff --git a/kotlinx-coroutines-core/common/test/AsyncLazyTest.kt b/kotlinx-coroutines-core/common/test/AsyncLazyTest.kt index 5b23b64143..cd2401049e 100644 --- a/kotlinx-coroutines-core/common/test/AsyncLazyTest.kt +++ b/kotlinx-coroutines-core/common/test/AsyncLazyTest.kt @@ -76,7 +76,7 @@ class AsyncLazyTest : TestBase() { expected = { it is TestException } ) { expect(1) - val d = async(start = CoroutineStart.LAZY) { + val d = async(start = CoroutineStart.LAZY) { finish(3) throw TestException() } @@ -90,7 +90,7 @@ class AsyncLazyTest : TestBase() { expected = { it is TestException } ) { expect(1) - val d = async(start = CoroutineStart.LAZY) { + val d = async(start = CoroutineStart.LAZY) { expect(3) yield() // this has not effect, because parent coroutine is waiting finish(4) @@ -104,7 +104,7 @@ class AsyncLazyTest : TestBase() { @Test fun testCatchException() = runTest { expect(1) - val d = async(NonCancellable, start = CoroutineStart.LAZY) { + val d = async(NonCancellable, start = CoroutineStart.LAZY) { expect(3) throw TestException() } @@ -184,4 +184,4 @@ class AsyncLazyTest : TestBase() { assertEquals(d.await(), 42) // await shall throw CancellationException expectUnreached() } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/AsyncTest.kt b/kotlinx-coroutines-core/common/test/AsyncTest.kt index 3019ddeab1..2096a4d69e 100644 --- a/kotlinx-coroutines-core/common/test/AsyncTest.kt +++ b/kotlinx-coroutines-core/common/test/AsyncTest.kt @@ -43,7 +43,7 @@ class AsyncTest : TestBase() { @Test fun testSimpleException() = runTest(expected = { it is TestException }) { expect(1) - val d = async { + val d = async { finish(3) throw TestException() } @@ -170,7 +170,7 @@ class AsyncTest : TestBase() { @Test fun testDeferAndYieldException() = runTest(expected = { it is TestException }) { expect(1) - val d = async { + val d = async { expect(3) yield() // no effect, parent waiting finish(4) @@ -266,4 +266,38 @@ class AsyncTest : TestBase() { assertFalse(deferred.isCancelled) } + @Test + fun testAsyncWithFinally() = runTest { + expect(1) + + @Suppress("UNREACHABLE_CODE") + val d = async { + expect(3) + try { + yield() // to main, will cancel + } finally { + expect(6) // will go there on await + return@async "Fail" // result will not override cancellation + } + expectUnreached() + "Fail2" + } + expect(2) + yield() // to async + expect(4) + check(d.isActive && !d.isCompleted && !d.isCancelled) + d.cancel() + check(!d.isActive && !d.isCompleted && d.isCancelled) + check(!d.isActive && !d.isCompleted && d.isCancelled) + expect(5) + try { + d.await() // awaits + expectUnreached() // does not complete normally + } catch (e: Throwable) { + expect(7) + check(e is CancellationException) + } + check(!d.isActive && d.isCompleted && d.isCancelled) + finish(8) + } } diff --git a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt index a41013779a..3881eb2779 100644 --- a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt +++ b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt @@ -140,7 +140,7 @@ class AtomicCancellationCommonTest : TestBase() { val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) - val result = select { // suspends + select { // suspends mutex.onLock { expect(4) "OK" diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index fbfa082555..bff971961b 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt @@ -82,7 +82,7 @@ class CancellableResumeTest : TestBase() { cont.invokeOnCancellation { expect(3) } ctx.cancel() expect(4) - cont.resume("OK") { cause -> + cont.resume("OK") { expect(5) } finish(6) @@ -108,7 +108,7 @@ class CancellableResumeTest : TestBase() { } ctx.cancel() expect(4) - cont.resume("OK") { cause -> + cont.resume("OK") { expect(5) throw TestException3("FAIL") // onCancellation block fails with exception } diff --git a/kotlinx-coroutines-core/common/test/CancelledParentAttachTest.kt b/kotlinx-coroutines-core/common/test/CancelledParentAttachTest.kt index 749bbfc921..9dd61b8012 100644 --- a/kotlinx-coroutines-core/common/test/CancelledParentAttachTest.kt +++ b/kotlinx-coroutines-core/common/test/CancelledParentAttachTest.kt @@ -11,75 +11,105 @@ import kotlin.test.* class CancelledParentAttachTest : TestBase() { @Test - fun testAsync() = CoroutineStart.values().forEach(::testAsyncCancelledParent) + fun testAsync() = runTest { + CoroutineStart.values().forEach { testAsyncCancelledParent(it) } + } - private fun testAsyncCancelledParent(start: CoroutineStart) = - runTest({ it is CancellationException }) { - cancel() - expect(1) - val d = async(start = start) { 42 } - expect(2) - d.invokeOnCompletion { - finish(3) - reset() + private suspend fun testAsyncCancelledParent(start: CoroutineStart) { + try { + withContext(Job()) { + cancel() + expect(1) + val d = async(start = start) { 42 } + expect(2) + d.invokeOnCompletion { + finish(3) + reset() + } } + expectUnreached() + } catch (e: CancellationException) { + // Expected } + } @Test - fun testLaunch() = CoroutineStart.values().forEach(::testLaunchCancelledParent) + fun testLaunch() = runTest { + CoroutineStart.values().forEach { testLaunchCancelledParent(it) } + } - private fun testLaunchCancelledParent(start: CoroutineStart) = - runTest({ it is CancellationException }) { - cancel() - expect(1) - val d = launch(start = start) { } - expect(2) - d.invokeOnCompletion { - finish(3) - reset() + private suspend fun testLaunchCancelledParent(start: CoroutineStart) { + try { + withContext(Job()) { + cancel() + expect(1) + val d = launch(start = start) { } + expect(2) + d.invokeOnCompletion { + finish(3) + reset() + } } + expectUnreached() + } catch (e: CancellationException) { + // Expected } + } @Test - fun testProduce() = - runTest({ it is CancellationException }) { - cancel() - expect(1) - val d = produce { } - expect(2) - (d as Job).invokeOnCompletion { - finish(3) - reset() - } + fun testProduce() = runTest({ it is CancellationException }) { + cancel() + expect(1) + val d = produce { } + expect(2) + (d as Job).invokeOnCompletion { + finish(3) + reset() } + } @Test - fun testBroadcast() = CoroutineStart.values().forEach(::testBroadcastCancelledParent) + fun testBroadcast() = runTest { + CoroutineStart.values().forEach { testBroadcastCancelledParent(it) } + } - private fun testBroadcastCancelledParent(start: CoroutineStart) = - runTest({ it is CancellationException }) { - cancel() - expect(1) - val bc = broadcast(start = start) {} - expect(2) - (bc as Job).invokeOnCompletion { - finish(3) - reset() + private suspend fun testBroadcastCancelledParent(start: CoroutineStart) { + try { + withContext(Job()) { + cancel() + expect(1) + val bc = broadcast(start = start) {} + expect(2) + (bc as Job).invokeOnCompletion { + finish(3) + reset() + } } + expectUnreached() + } catch (e: CancellationException) { + // Expected } + } @Test - fun testScopes() { - testScope { coroutineScope { } } - testScope { supervisorScope { } } - testScope { flowScope { } } - testScope { withTimeout(Long.MAX_VALUE) { } } - testScope { withContext(Job()) { } } - testScope { withContext(CoroutineName("")) { } } + fun testScopes() = runTest { + testScope { coroutineScope { } } + testScope { supervisorScope { } } + testScope { flowScope { } } + testScope { withTimeout(Long.MAX_VALUE) { } } + testScope { withContext(Job()) { } } + testScope { withContext(CoroutineName("")) { } } } - private inline fun testScope(crossinline block: suspend () -> Unit) = runTest({ it is CancellationException }) { - cancel() - block() + private suspend inline fun testScope(crossinline block: suspend () -> Unit) { + try { + withContext(Job()) { + cancel() + block() + } + expectUnreached() + } catch (e: CancellationException) { + // Expected + } } } diff --git a/kotlinx-coroutines-core/common/test/CoroutineDispatcherOperatorFunInvokeTest.kt b/kotlinx-coroutines-core/common/test/CoroutineDispatcherOperatorFunInvokeTest.kt index 6fdd3bbe8b..e6b340cc62 100644 --- a/kotlinx-coroutines-core/common/test/CoroutineDispatcherOperatorFunInvokeTest.kt +++ b/kotlinx-coroutines-core/common/test/CoroutineDispatcherOperatorFunInvokeTest.kt @@ -65,7 +65,6 @@ class CoroutineDispatcherOperatorFunInvokeTest : TestBase() { dispatcher.dispatch(context, block) } - @ExperimentalCoroutinesApi override fun isDispatchNeeded(context: CoroutineContext): Boolean { return dispatcher.isDispatchNeeded(context) } diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 0ba80ee509..71c45769cb 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -13,7 +13,22 @@ import kotlin.test.* public expect val isStressTest: Boolean public expect val stressTestMultiplier: Int +/** + * The result of a multiplatform asynchronous test. + * Aliases into Unit on K/JVM and K/N, and into Promise on K/JS. + */ +@Suppress("NO_ACTUAL_FOR_EXPECT") +public expect class TestResult + public expect open class TestBase constructor() { + /* + * In common tests we emulate parameterized tests + * by iterating over parameters space in the single @Test method. + * This kind of tests is too slow for JS and does not fit into + * the default Mocha timeout, so we're using this flag to bail-out + * and run such tests only on JVM and K/N. + */ + public val isBoundByJsTestTimeout: Boolean public fun error(message: Any, cause: Throwable? = null): Nothing public fun expect(index: Int) public fun expectUnreached() @@ -25,7 +40,7 @@ public expect open class TestBase constructor() { expected: ((Throwable) -> Boolean)? = null, unhandled: List<(Throwable) -> Boolean> = emptyList(), block: suspend CoroutineScope.() -> Unit - ) + ): TestResult } public suspend inline fun hang(onCancellation: () -> Unit) { diff --git a/kotlinx-coroutines-core/common/test/UndispatchedResultTest.kt b/kotlinx-coroutines-core/common/test/UndispatchedResultTest.kt index e262572277..34b8164472 100644 --- a/kotlinx-coroutines-core/common/test/UndispatchedResultTest.kt +++ b/kotlinx-coroutines-core/common/test/UndispatchedResultTest.kt @@ -55,7 +55,7 @@ class UndispatchedResultTest : TestBase() { try { expect(1) // Will cancel its parent - async(context) { + async(context) { expect(2) throw TestException() }.await() diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt index b91a87f0bd..efd55fe231 100644 --- a/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt +++ b/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt @@ -17,7 +17,7 @@ class WithTimeoutDurationTest : TestBase() { @Test fun testBasicNoSuspend() = runTest { expect(1) - val result = withTimeout(10.seconds) { + val result = withTimeout(Duration.seconds(10)) { expect(2) "OK" } @@ -31,7 +31,7 @@ class WithTimeoutDurationTest : TestBase() { @Test fun testBasicSuspend() = runTest { expect(1) - val result = withTimeout(10.seconds) { + val result = withTimeout(Duration.seconds(10)) { expect(2) yield() expect(3) @@ -54,7 +54,7 @@ class WithTimeoutDurationTest : TestBase() { } expect(2) // test that it does not yield to the above job when started - val result = withTimeout(1.seconds) { + val result = withTimeout(Duration.seconds(1)) { expect(3) yield() // yield only now expect(5) @@ -74,7 +74,7 @@ class WithTimeoutDurationTest : TestBase() { fun testYieldBlockingWithTimeout() = runTest( expected = { it is CancellationException } ) { - withTimeout(100.milliseconds) { + withTimeout(Duration.milliseconds(100)) { while (true) { yield() } @@ -87,7 +87,7 @@ class WithTimeoutDurationTest : TestBase() { @Test fun testWithTimeoutChildWait() = runTest { expect(1) - withTimeout(100.milliseconds) { + withTimeout(Duration.milliseconds(100)) { expect(2) // launch child with timeout launch { @@ -102,7 +102,7 @@ class WithTimeoutDurationTest : TestBase() { @Test fun testBadClass() = runTest { val bad = BadClass() - val result = withTimeout(100.milliseconds) { + val result = withTimeout(Duration.milliseconds(100)) { bad } assertSame(bad, result) @@ -118,9 +118,9 @@ class WithTimeoutDurationTest : TestBase() { fun testExceptionOnTimeout() = runTest { expect(1) try { - withTimeout(100.milliseconds) { + withTimeout(Duration.milliseconds(100)) { expect(2) - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) expectUnreached() "OK" } @@ -135,10 +135,10 @@ class WithTimeoutDurationTest : TestBase() { expected = { it is CancellationException } ) { expect(1) - withTimeout(100.milliseconds) { + withTimeout(Duration.milliseconds(100)) { expect(2) try { - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) } catch (e: CancellationException) { finish(3) } @@ -151,10 +151,10 @@ class WithTimeoutDurationTest : TestBase() { fun testSuppressExceptionWithAnotherException() = runTest { expect(1) try { - withTimeout(100.milliseconds) { + withTimeout(Duration.milliseconds(100)) { expect(2) try { - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) } catch (e: CancellationException) { expect(3) throw TestException() @@ -172,7 +172,7 @@ class WithTimeoutDurationTest : TestBase() { fun testNegativeTimeout() = runTest { expect(1) try { - withTimeout(-1.milliseconds) { + withTimeout(-Duration.milliseconds(1)) { expectUnreached() "OK" } @@ -187,7 +187,7 @@ class WithTimeoutDurationTest : TestBase() { expect(1) try { expect(2) - withTimeout(1.seconds) { + withTimeout(Duration.seconds(1)) { expect(3) throw TestException() } diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt index f72bb7d99d..b5777753b6 100644 --- a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt +++ b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt @@ -19,7 +19,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testBasicNoSuspend() = runTest { expect(1) - val result = withTimeoutOrNull(10.seconds) { + val result = withTimeoutOrNull(Duration.seconds(10)) { expect(2) "OK" } @@ -33,7 +33,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testBasicSuspend() = runTest { expect(1) - val result = withTimeoutOrNull(10.seconds) { + val result = withTimeoutOrNull(Duration.seconds(10)) { expect(2) yield() expect(3) @@ -56,7 +56,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { } expect(2) // test that it does not yield to the above job when started - val result = withTimeoutOrNull(1.seconds) { + val result = withTimeoutOrNull(Duration.seconds(1)) { expect(3) yield() // yield only now expect(5) @@ -74,7 +74,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testYieldBlockingWithTimeout() = runTest { expect(1) - val result = withTimeoutOrNull(100.milliseconds) { + val result = withTimeoutOrNull(Duration.milliseconds(100)) { while (true) { yield() } @@ -86,7 +86,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testSmallTimeout() = runTest { val channel = Channel(1) - val value = withTimeoutOrNull(1.milliseconds) { + val value = withTimeoutOrNull(Duration.milliseconds(1)) { channel.receive() } assertNull(value) @@ -94,7 +94,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testThrowException() = runTest(expected = {it is AssertionError}) { - withTimeoutOrNull(Duration.INFINITE) { + withTimeoutOrNull(Duration.INFINITE) { throw AssertionError() } } @@ -103,12 +103,13 @@ class WithTimeoutOrNullDurationTest : TestBase() { fun testInnerTimeout() = runTest( expected = { it is CancellationException } ) { - withTimeoutOrNull(1000.milliseconds) { - withTimeout(10.milliseconds) { + withTimeoutOrNull(Duration.milliseconds(1000)) { + withTimeout(Duration.milliseconds(10)) { while (true) { yield() } } + @Suppress("UNREACHABLE_CODE") expectUnreached() // will timeout } expectUnreached() // will timeout @@ -118,7 +119,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { fun testNestedTimeout() = runTest(expected = { it is TimeoutCancellationException }) { withTimeoutOrNull(Duration.INFINITE) { // Exception from this withTimeout is not suppressed by withTimeoutOrNull - withTimeout(10.milliseconds) { + withTimeout(Duration.milliseconds(10)) { delay(Duration.INFINITE) 1 } @@ -130,9 +131,9 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testOuterTimeout() = runTest { var counter = 0 - val result = withTimeoutOrNull(250.milliseconds) { + val result = withTimeoutOrNull(Duration.milliseconds(250)) { while (true) { - val inner = withTimeoutOrNull(100.milliseconds) { + val inner = withTimeoutOrNull(Duration.milliseconds(100)) { while (true) { yield() } @@ -148,7 +149,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testBadClass() = runTest { val bad = BadClass() - val result = withTimeoutOrNull(100.milliseconds) { + val result = withTimeoutOrNull(Duration.milliseconds(100)) { bad } assertSame(bad, result) @@ -163,9 +164,9 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testNullOnTimeout() = runTest { expect(1) - val result = withTimeoutOrNull(100.milliseconds) { + val result = withTimeoutOrNull(Duration.milliseconds(100)) { expect(2) - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) expectUnreached() "OK" } @@ -176,10 +177,10 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testSuppressExceptionWithResult() = runTest { expect(1) - val result = withTimeoutOrNull(100.milliseconds) { + val result = withTimeoutOrNull(Duration.milliseconds(100)) { expect(2) try { - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) } catch (e: CancellationException) { expect(3) } @@ -193,10 +194,10 @@ class WithTimeoutOrNullDurationTest : TestBase() { fun testSuppressExceptionWithAnotherException() = runTest { expect(1) try { - withTimeoutOrNull(100.milliseconds) { + withTimeoutOrNull(Duration.milliseconds(100)) { expect(2) try { - delay(1000.milliseconds) + delay(Duration.milliseconds(1000)) } catch (e: CancellationException) { expect(3) throw TestException() @@ -215,11 +216,11 @@ class WithTimeoutOrNullDurationTest : TestBase() { @Test fun testNegativeTimeout() = runTest { expect(1) - var result = withTimeoutOrNull(-1.milliseconds) { + var result = withTimeoutOrNull(-Duration.milliseconds(1)) { expectUnreached() } assertNull(result) - result = withTimeoutOrNull(0.milliseconds) { + result = withTimeoutOrNull(Duration.milliseconds(0)) { expectUnreached() } assertNull(result) @@ -231,7 +232,7 @@ class WithTimeoutOrNullDurationTest : TestBase() { expect(1) try { expect(2) - withTimeoutOrNull(1000.milliseconds) { + withTimeoutOrNull(Duration.milliseconds(1000)) { expect(3) throw TestException() } diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt index 40d2758daa..90bcf2dac3 100644 --- a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt +++ b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt @@ -92,7 +92,7 @@ class WithTimeoutOrNullTest : TestBase() { @Test fun testThrowException() = runTest(expected = {it is AssertionError}) { - withTimeoutOrNull(Long.MAX_VALUE) { + withTimeoutOrNull(Long.MAX_VALUE) { throw AssertionError() } } @@ -107,6 +107,7 @@ class WithTimeoutOrNullTest : TestBase() { yield() } } + @Suppress("UNREACHABLE_CODE") expectUnreached() // will timeout } expectUnreached() // will timeout diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt index 5513dab782..f26361f2f8 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt @@ -10,17 +10,19 @@ import kotlin.test.* class ChannelUndeliveredElementTest : TestBase() { @Test - fun testSendSuccessfully() = runAllKindsTest { kind -> - val channel = kind.create { it.cancel() } - val res = Resource("OK") - launch { - channel.send(res) + fun testSendSuccessfully() = runTest { + runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + val res = Resource("OK") + launch { + channel.send(res) + } + val ok = channel.receive() + assertEquals("OK", ok.value) + assertFalse(res.isCancelled) // was not cancelled + channel.close() + assertFalse(res.isCancelled) // still was not cancelled } - val ok = channel.receive() - assertEquals("OK", ok.value) - assertFalse(res.isCancelled) // was not cancelled - channel.close() - assertFalse(res.isCancelled) // still was not cancelled } @Test @@ -86,21 +88,23 @@ class ChannelUndeliveredElementTest : TestBase() { } @Test - fun testSendToClosedChannel() = runAllKindsTest { kind -> - val channel = kind.create { it.cancel() } - channel.close() // immediately close channel - val res = Resource("OK") - assertFailsWith { - channel.send(res) // send fails to closed channel, resource was not delivered + fun testSendToClosedChannel() = runTest { + runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + channel.close() // immediately close channel + val res = Resource("OK") + assertFailsWith { + channel.send(res) // send fails to closed channel, resource was not delivered + } + assertTrue(res.isCancelled) } - assertTrue(res.isCancelled) } - private fun runAllKindsTest(test: suspend CoroutineScope.(TestChannelKind) -> Unit) { + private suspend fun runAllKindsTest(test: suspend CoroutineScope.(TestChannelKind) -> Unit) { for (kind in TestChannelKind.values()) { if (kind.viaBroadcast) continue // does not support onUndeliveredElement try { - runTest { + withContext(Job()) { test(kind) } } catch(e: Throwable) { @@ -119,4 +123,19 @@ class ChannelUndeliveredElementTest : TestBase() { check(!_cancelled.getAndSet(true)) { "Already cancelled" } } } + + @Test + fun testHandlerIsNotInvoked() = runTest { // #2826 + val channel = Channel { + expectUnreached() + } + + expect(1) + launch { + expect(2) + channel.receive() + } + channel.send(Unit) + finish(3) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index b2d957be46..bba5c6bd87 100644 --- a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt +++ b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt @@ -48,7 +48,6 @@ internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : Coroutine originalDispatcher.dispatch(context, block) } - @ExperimentalCoroutinesApi override fun isDispatchNeeded(context: CoroutineContext): Boolean = originalDispatcher.isDispatchNeeded(context) override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt index eedfac2ea3..447eb73b5d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt @@ -133,7 +133,7 @@ class CatchTest : TestBase() { .flowOn(d2) // flowOn with a different dispatcher introduces asynchrony so that all exceptions in the // upstream flows are handled before they go downstream - .onEach { value -> + .onEach { expectUnreached() // already cancelled } .catch { e -> diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index ce75e598e9..aa0893e888 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -205,19 +205,19 @@ class DebounceTest : TestBase() { val flow = flow { expect(3) emit("A") - delay(1500.milliseconds) + delay(Duration.milliseconds(1500)) emit("B") - delay(500.milliseconds) + delay(Duration.milliseconds(500)) emit("C") - delay(250.milliseconds) + delay(Duration.milliseconds(250)) emit("D") - delay(2000.milliseconds) + delay(Duration.milliseconds(2000)) emit("E") expect(4) } expect(2) - val result = flow.debounce(1000.milliseconds).toList() + val result = flow.debounce(Duration.milliseconds(1000)).toList() assertEquals(listOf("A", "D", "E"), result) finish(5) } @@ -296,13 +296,13 @@ class DebounceTest : TestBase() { val flow = flow { expect(3) emit("A") - delay(1500.milliseconds) + delay(Duration.milliseconds(1500)) emit("B") - delay(500.milliseconds) + delay(Duration.milliseconds(500)) emit("C") - delay(250.milliseconds) + delay(Duration.milliseconds(250)) emit("D") - delay(2000.milliseconds) + delay(Duration.milliseconds(2000)) emit("E") expect(4) } @@ -310,9 +310,9 @@ class DebounceTest : TestBase() { expect(2) val result = flow.debounce { if (it == "C") { - 0.milliseconds + Duration.milliseconds(0) } else { - 1000.milliseconds + Duration.milliseconds(1000) } }.toList() diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt index 44376980cd..4095172dab 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt @@ -90,5 +90,5 @@ abstract class FlatMapMergeBaseTest : FlatMapBaseTest() { } @Test - abstract fun testFlatMapConcurrency() + abstract fun testFlatMapConcurrency(): TestResult } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt index bad9db9757..3c1ebfa005 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt @@ -42,7 +42,6 @@ class OnEachTest : TestBase() { }.onEach { latch.receive() throw TestException() - it + 1 }.catch { emit(42) } assertEquals(42, flow.single()) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt index 22a0d4a39a..87bee56f1d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt @@ -268,7 +268,6 @@ class SampleTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) @@ -282,19 +281,19 @@ class SampleTest : TestBase() { val flow = flow { expect(3) emit("A") - delay(1500.milliseconds) + delay(Duration.milliseconds(1500)) emit("B") - delay(500.milliseconds) + delay(Duration.milliseconds(500)) emit("C") - delay(250.milliseconds) + delay(Duration.milliseconds(250)) emit("D") - delay(2000.milliseconds) + delay(Duration.milliseconds(2000)) emit("E") expect(4) } expect(2) - val result = flow.sample(1000.milliseconds).toList() + val result = flow.sample(Duration.milliseconds(1000)).toList() assertEquals(listOf("A", "B", "D"), result) finish(5) } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt index 32d88f3c99..6e18b38f55 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -439,7 +439,8 @@ class SharedFlowTest : TestBase() { } @Test - fun testDifferentBufferedFlowCapacities() { + fun testDifferentBufferedFlowCapacities() = runTest { + if (isBoundByJsTestTimeout) return@runTest // Too slow for JS, bounded by 2 sec. default JS timeout for (replay in 0..10) { for (extraBufferCapacity in 0..5) { if (replay == 0 && extraBufferCapacity == 0) continue // test only buffered shared flows @@ -456,7 +457,7 @@ class SharedFlowTest : TestBase() { } } - private fun testBufferedFlow(sh: MutableSharedFlow, replay: Int) = runTest { + private suspend fun testBufferedFlow(sh: MutableSharedFlow, replay: Int) = withContext(Job()) { reset() expect(1) val n = 100 // initially emitted to fill buffer @@ -601,6 +602,7 @@ class SharedFlowTest : TestBase() { } @Test + @Suppress("DEPRECATION") // 'catch' fun onSubscriptionThrows() = runTest { expect(1) val sh = MutableSharedFlow(1) @@ -678,6 +680,7 @@ class SharedFlowTest : TestBase() { @Test fun testStateFlowModel() = runTest { + if (isBoundByJsTestTimeout) return@runTest // Too slow for JS, bounded by 2 sec. default JS timeout val stateFlow = MutableStateFlow(null) val expect = modelLog(stateFlow) val sharedFlow = MutableSharedFlow( @@ -795,4 +798,4 @@ class SharedFlowTest : TestBase() { job.join() finish(5) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt index bcf626e3e3..516bb2e291 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt @@ -30,11 +30,13 @@ class SharingStartedWhileSubscribedTest : TestBase() { @Test fun testDurationParams() { assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO)) - assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(Duration.milliseconds(10))) assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds)) assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE)) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed(replayExpiration = 3.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed( + replayExpiration = Duration.milliseconds(3) + )) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE)) } diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt index 3c88571378..9a920f1d5f 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt @@ -47,7 +47,6 @@ class FoldTest : TestBase() { latch.receive() expect(4) throw TestException() - 42 // Workaround for KT-30642, return type should not be Nothing } } finish(6) diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt index 99ee1d6641..8ba0b5efbc 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt @@ -30,7 +30,7 @@ class ReduceTest : TestBase() { fun testNullableReduce() = runTest { val flow = flowOf(1, null, null, 2) var invocations = 0 - val sum = flow.reduce { acc, value -> + val sum = flow.reduce { _, value -> ++invocations value } @@ -67,7 +67,6 @@ class ReduceTest : TestBase() { latch.receive() expect(4) throw TestException() - 42 // Workaround for KT-30642, return type should not be Nothing } } finish(6) diff --git a/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt index 66cb72a535..26d6f809b8 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt @@ -14,15 +14,15 @@ class SelectTimeoutDurationTest : TestBase() { fun testBasic() = runTest { expect(1) val result = select { - onTimeout(1000.milliseconds) { + onTimeout(Duration.milliseconds(1000)) { expectUnreached() "FAIL" } - onTimeout(100.milliseconds) { + onTimeout(Duration.milliseconds(100)) { expect(2) "OK" } - onTimeout(500.milliseconds) { + onTimeout(Duration.milliseconds(500)) { expectUnreached() "FAIL" } @@ -35,7 +35,7 @@ class SelectTimeoutDurationTest : TestBase() { fun testZeroTimeout() = runTest { expect(1) val result = select { - onTimeout(1.seconds) { + onTimeout(Duration.seconds(1)) { expectUnreached() "FAIL" } @@ -52,11 +52,11 @@ class SelectTimeoutDurationTest : TestBase() { fun testNegativeTimeout() = runTest { expect(1) val result = select { - onTimeout(1.seconds) { + onTimeout(Duration.seconds(1)) { expectUnreached() "FAIL" } - onTimeout(-10.milliseconds) { + onTimeout(-Duration.milliseconds(10)) { expect(2) "OK" } @@ -71,13 +71,13 @@ class SelectTimeoutDurationTest : TestBase() { val iterations =10_000 for (i in 0..iterations) { val result = selectUnbiased { - onTimeout(-1.seconds) { + onTimeout(-Duration.seconds(1)) { 0 } onTimeout(Duration.ZERO) { 1 } - onTimeout(1.seconds) { + onTimeout(Duration.seconds(1)) { expectUnreached() 2 } diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index e08345a1d2..a98ea9732d 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -4,8 +4,8 @@ package kotlinx.coroutines +import kotlinx.browser.* import kotlinx.coroutines.internal.* -import kotlin.browser.* import kotlin.coroutines.* private external val navigator: dynamic @@ -13,12 +13,6 @@ private const val UNDEFINED = "undefined" internal external val process: dynamic internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { - // Check if we are running under ReactNative. We have to use NodeDispatcher under it. - // The problem is that ReactNative has a `window` object with `addEventListener`, but it does not really work. - // For details see https://github.com/Kotlin/kotlinx.coroutines/issues/236 - // The check for ReactNative is based on https://github.com/facebook/react-native/commit/3c65e62183ce05893be0822da217cb803b121c61 - jsTypeOf(navigator) != UNDEFINED && navigator != null && navigator.product == "ReactNative" -> - NodeDispatcher // 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." @@ -27,7 +21,7 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED -> window.asCoroutineDispatcher() // If process is undefined (e.g. in NativeScript, #1404), use SetTimeout-based dispatcher - jsTypeOf(process) == UNDEFINED -> SetTimeoutDispatcher + jsTypeOf(process) == UNDEFINED || jsTypeOf(process.nextTick) == UNDEFINED -> SetTimeoutDispatcher // Fallback to NodeDispatcher when browser environment is not detected else -> NodeDispatcher } diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index f2711f50af..147b31dc3e 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -32,7 +32,18 @@ public open class LinkedListNode { this._prev = node } + /* + * Remove that is invoked as a virtual function with a + * potentially augmented behaviour. + * I.g. `LockFreeLinkedListHead` throws, while `SendElementWithUndeliveredHandler` + * invokes handler on remove + */ public open fun remove(): Boolean { + return removeImpl() + } + + @PublishedApi + internal fun removeImpl(): Boolean { if (_removed) return false val prev = this._prev val next = this._next @@ -76,7 +87,7 @@ public open class LinkedListNode { public fun removeFirstOrNull(): Node? { val next = _next if (next === this) return null - check(next.remove()) { "Should remove" } + check(next.removeImpl()) { "Should remove" } return next } @@ -85,7 +96,7 @@ public open class LinkedListNode { if (next === this) return null if (next !is T) return null if (predicate(next)) return next - check(next.remove()) { "Should remove" } + check(next.removeImpl()) { "Should remove" } return next } } diff --git a/kotlinx-coroutines-core/js/test/PromiseTest.kt b/kotlinx-coroutines-core/js/test/PromiseTest.kt index d0f6b2b714..cc1297cd78 100644 --- a/kotlinx-coroutines-core/js/test/PromiseTest.kt +++ b/kotlinx-coroutines-core/js/test/PromiseTest.kt @@ -74,4 +74,16 @@ class PromiseTest : TestBase() { assertSame(d2, deferred) assertEquals("OK", d2.await()) } -} \ No newline at end of file + + @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") + } + } +} diff --git a/kotlinx-coroutines-core/js/test/TestBase.kt b/kotlinx-coroutines-core/js/test/TestBase.kt index 8b3d69a7f5..cc7865ba07 100644 --- a/kotlinx-coroutines-core/js/test/TestBase.kt +++ b/kotlinx-coroutines-core/js/test/TestBase.kt @@ -9,10 +9,15 @@ import kotlin.js.* public actual val isStressTest: Boolean = false public actual val stressTestMultiplier: Int = 1 +@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") +public actual typealias TestResult = Promise + 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 @@ -70,16 +75,37 @@ public actual open class TestBase actual constructor() { finished = false } - // todo: The dynamic (promise) result is a work-around for missing suspend tests, see KT-22228 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun runTest( expected: ((Throwable) -> Boolean)? = null, unhandled: List<(Throwable) -> Boolean> = emptyList(), block: suspend CoroutineScope.() -> Unit - ): dynamic { + ): TestResult { var exCount = 0 var ex: Throwable? = null - return GlobalScope.promise(block = block, context = CoroutineExceptionHandler { context, e -> + /* + * 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 { @@ -102,6 +128,8 @@ public actual open class TestBase actual constructor() { error?.let { throw it } check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" } } + lastTestPromise = result + return result } } diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index 96cda7b1af..4657bc7d1e 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -163,7 +163,7 @@ private class LazyActorCoroutine( return super.send(element) } - @Suppress("DEPRECATION_ERROR") + @Suppress("DEPRECATION") override fun offer(element: E): Boolean { start() return super.offer(element) diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index caad1e3323..9bbc6dc9eb 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -259,7 +259,7 @@ public actual open class LockFreeLinkedListNode { // Helps with removal of this node public actual fun helpRemove() { // Note: this node must be already removed - (next as Removed).ref.correctPrev(null) + (next as Removed).ref.helpRemovePrev() } // Helps with removal of nodes that are previous to this diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index 174c57b762..6153862e2a 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -216,6 +216,7 @@ internal actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.C @Suppress("ACTUAL_WITHOUT_EXPECT") internal actual typealias StackTraceElement = java.lang.StackTraceElement +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") internal actual fun Throwable.initCause(cause: Throwable) { // Resolved to member, verified by test initCause(cause) diff --git a/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt index 2b4e91c02a..89bbbfd7ee 100644 --- a/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt @@ -15,7 +15,7 @@ abstract class AbstractLincheckTest : VerifierState() { open fun StressOptions.customize(isStressTest: Boolean): StressOptions = this @Test - open fun modelCheckingTest() = ModelCheckingOptions() + fun modelCheckingTest() = ModelCheckingOptions() .iterations(if (isStressTest) 100 else 20) .invocationsPerIteration(if (isStressTest) 10_000 else 1_000) .commonConfiguration() diff --git a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt index 026752086c..59ff76a538 100644 --- a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt @@ -8,7 +8,10 @@ import kotlin.test.* class AsyncJvmTest : TestBase() { - // This must be a common test but it fails on JS because of KT-21961 + // We have the same test in common module, but the maintainer uses this particular file + // and semi-automatically types cmd+N + AsyncJvm in order to duck-tape any JVM samples/repros, + // please do not remove this test + @Test fun testAsyncWithFinally() = runTest { expect(1) diff --git a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt index c4232d6e60..179b2e5e6e 100644 --- a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt +++ b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt @@ -11,7 +11,6 @@ import java.util.* import java.util.Collections.* import java.util.concurrent.atomic.* import java.util.concurrent.locks.* -import kotlin.collections.ArrayList import kotlin.test.* object FieldWalker { @@ -90,6 +89,7 @@ object FieldWalker { cur = ref.parent path += "[${ref.index}]" } + else -> error("Should not be reached") } } path.reverse() @@ -154,8 +154,9 @@ object FieldWalker { while (true) { val fields = type.declaredFields.filter { !it.type.isPrimitive - && (statics || !Modifier.isStatic(it.modifiers)) - && !(it.type.isArray && it.type.componentType.isPrimitive) + && (statics || !Modifier.isStatic(it.modifiers)) + && !(it.type.isArray && it.type.componentType.isPrimitive) + && it.name != "previousOut" // System.out from TestBase that we store in a field to restore later } fields.forEach { it.isAccessible = true } // make them all accessible result.addAll(fields) diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index 17238e873c..61a2c8b8b7 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -7,11 +7,10 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* import kotlinx.coroutines.scheduling.* import org.junit.* -import java.lang.Math.* +import java.io.* import java.util.* import java.util.concurrent.atomic.* import kotlin.coroutines.* -import kotlin.math.* import kotlin.test.* private val VERBOSE = systemProp("test.verbose", false) @@ -23,12 +22,15 @@ public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ? public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1 +private const val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread + /** * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test. */ public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt -public val stressTestMultiplierCbrt = cbrt(stressTestMultiplier.toDouble()).roundToInt() +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias TestResult = Unit /** * Base class for tests, so that tests for predictable scheduling of actions in multiple coroutines sharing a single @@ -49,7 +51,11 @@ public val stressTestMultiplierCbrt = cbrt(stressTestMultiplier.toDouble()).roun * } * ``` */ -public actual open class TestBase actual constructor() { +public actual open class TestBase(private var disableOutCheck: Boolean) { + + actual constructor(): this(false) + + public actual val isBoundByJsTestTimeout = false private var actionIndex = AtomicInteger() private var finished = AtomicBoolean() private var error = AtomicReference() @@ -58,9 +64,15 @@ public actual open class TestBase actual constructor() { private lateinit var threadsBefore: Set private val uncaughtExceptions = Collections.synchronizedList(ArrayList()) private var originalUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null - private val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread + /* + * System.out that we redefine in order to catch any debugging/diagnostics + * 'println' from main source set. + * NB: We do rely on the name 'previousOut' in the FieldWalker in order to skip its + * processing + */ + 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. */ @@ -113,7 +125,7 @@ public actual open class TestBase actual constructor() { } /** - * Asserts that this it the last action in the test. It must be invoked by any test that used [expect]. + * Asserts that this is the last action in the test. It must be invoked by any test that used [expect]. */ public actual fun finish(index: Int) { expect(index) @@ -133,6 +145,16 @@ public actual open class TestBase actual constructor() { finished.set(false) } + private object TestOutputStream : PrintStream(object : OutputStream() { + override fun write(b: Int) { + error("Detected unexpected call to 'println' from source code") + } + }) + + fun println(message: Any?) { + previousOut.println(message) + } + @Before fun before() { initPoolsBeforeTest() @@ -143,6 +165,10 @@ public actual open class TestBase actual constructor() { e.printStackTrace() uncaughtExceptions.add(e) } + if (!disableOutCheck) { + previousOut = System.out + System.setOut(TestOutputStream) + } } @After @@ -154,7 +180,7 @@ public actual open class TestBase actual constructor() { } // Shutdown all thread pools shutdownPoolsAfterTest() - // Check that that are now leftover threads + // Check that are now leftover threads runCatching { checkTestThreads(threadsBefore) }.onFailure { @@ -162,6 +188,9 @@ public actual open class TestBase actual constructor() { } // Restore original uncaught exception handler Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler) + if (!disableOutCheck) { + System.setOut(previousOut) + } if (uncaughtExceptions.isNotEmpty()) { makeError("Expected no uncaught exceptions, but got $uncaughtExceptions") } @@ -187,7 +216,7 @@ public actual open class TestBase actual constructor() { expected: ((Throwable) -> Boolean)? = null, unhandled: List<(Throwable) -> Boolean> = emptyList(), block: suspend CoroutineScope.() -> Unit - ) { + ): TestResult { var exCount = 0 var ex: Throwable? = null try { diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt deleted file mode 100644 index 256ef62132..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicLongArray -import kotlin.math.* -import kotlin.test.* - -/** - * Tests lock-freedom of send and receive operations on rendezvous and conflated channels. - * There is a single channel with two sender and two receiver threads. - * When one sender or receiver gets suspended at most one other operation is allowed to cease having progress - * (`allowSuspendedThreads = 1`). - * - * **Note**: In the current implementation buffered channels are not lock-free, so this test would fail - * if channel is created with a buffer. - */ -class ChannelLFStressTest : TestBase() { - private val nSeconds = 5 * stressTestMultiplier - private val env = LockFreedomTestEnvironment("ChannelLFStressTest", allowSuspendedThreads = 1) - private lateinit var channel: Channel - - private val sendIndex = AtomicLong() - private val receiveCount = AtomicLong() - private val duplicateCount = AtomicLong() - - private val nCheckedSize = 10_000_000 - private val nChecked = (nCheckedSize * Long.SIZE_BITS).toLong() - private val receivedBits = AtomicLongArray(nCheckedSize) // bit set of received values - - @Test - fun testRendezvousLockFreedom() { - channel = Channel() - performLockFreedomTest() - // ensure that all sent were received - checkAllReceived() - } - - private fun performLockFreedomTest() { - env.onCompletion { - // We must cancel the channel to abort both senders & receivers - channel.cancel(TestCompleted()) - } - repeat(2) { env.testThread("sender-$it") { sender() } } - repeat(2) { env.testThread("receiver-$it") { receiver() } } - env.performTest(nSeconds) { - println("Sent: $sendIndex, Received: $receiveCount, dups: $duplicateCount") - } - // ensure no duplicates - assertEquals(0L, duplicateCount.get()) - } - - private fun checkAllReceived() { - for (i in 0 until min(sendIndex.get(), nChecked)) { - assertTrue(isReceived(i)) - } - } - - private suspend fun sender() { - val value = sendIndex.getAndIncrement() - try { - channel.send(value) - } catch (e: TestCompleted) { - check(env.isCompleted) // expected when test was completed - markReceived(value) // fake received (actually failed to send) - } - } - - private suspend fun receiver() { - val value = try { - channel.receive() - } catch (e: TestCompleted) { - check(env.isCompleted) // expected when test was completed - return - } - receiveCount.incrementAndGet() - markReceived(value) - } - - private fun markReceived(value: Long) { - if (value >= nChecked) return // too big - val index = (value / Long.SIZE_BITS).toInt() - val mask = 1L shl (value % Long.SIZE_BITS).toInt() - while (true) { - val bits = receivedBits.get(index) - if (bits and mask != 0L) { - duplicateCount.incrementAndGet() - break - } - if (receivedBits.compareAndSet(index, bits, bits or mask)) break - } - } - - private fun isReceived(value: Long): Boolean { - val index = (value / Long.SIZE_BITS).toInt() - val mask = 1L shl (value % Long.SIZE_BITS).toInt() - val bits = receivedBits.get(index) - return bits and mask != 0L - } - - private class TestCompleted : CancellationException() -} diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt deleted file mode 100644 index 225b848186..0000000000 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt +++ /dev/null @@ -1,169 +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.internal - -import kotlinx.atomicfu.LockFreedomTestEnvironment -import kotlinx.coroutines.stressTestMultiplier -import org.junit.Test -import java.util.* -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicReference -import kotlin.test.* - -/** - * This stress test has 4 threads adding randomly to the list and them immediately undoing - * this addition by remove, and 4 threads trying to remove nodes from two lists simultaneously (atomically). - */ -class LockFreeLinkedListAtomicLFStressTest { - private val env = LockFreedomTestEnvironment("LockFreeLinkedListAtomicLFStressTest") - - private data class Node(val i: Long) : LockFreeLinkedListNode() - - private val nSeconds = 5 * stressTestMultiplier - - private val nLists = 4 - private val nAdderThreads = 4 - private val nRemoverThreads = 4 - - private val lists = Array(nLists) { LockFreeLinkedListHead() } - - private val undone = AtomicLong() - private val missed = AtomicLong() - private val removed = AtomicLong() - private val error = AtomicReference() - private val index = AtomicLong() - - @Test - fun testStress() { - repeat(nAdderThreads) { threadId -> - val rnd = Random() - env.testThread(name = "adder-$threadId") { - when (rnd.nextInt(4)) { - 0 -> { - val list = lists[rnd.nextInt(nLists)] - val node = Node(index.incrementAndGet()) - addLastOp(list, node) - randomSpinWaitIntermission() - tryRemoveOp(node) - } - 1 -> { - // just to test conditional add - val list = lists[rnd.nextInt(nLists)] - val node = Node(index.incrementAndGet()) - addLastIfTrueOp(list, node) - randomSpinWaitIntermission() - tryRemoveOp(node) - } - 2 -> { - // just to test failed conditional add and burn some time - val list = lists[rnd.nextInt(nLists)] - val node = Node(index.incrementAndGet()) - addLastIfFalseOp(list, node) - } - 3 -> { - // add two atomically - val idx1 = rnd.nextInt(nLists - 1) - val idx2 = idx1 + 1 + rnd.nextInt(nLists - idx1 - 1) - check(idx1 < idx2) // that is our global order - val list1 = lists[idx1] - val list2 = lists[idx2] - val node1 = Node(index.incrementAndGet()) - val node2 = Node(index.incrementAndGet()) - addTwoOp(list1, node1, list2, node2) - randomSpinWaitIntermission() - tryRemoveOp(node1) - randomSpinWaitIntermission() - tryRemoveOp(node2) - } - else -> error("Cannot happen") - } - } - } - repeat(nRemoverThreads) { threadId -> - val rnd = Random() - env.testThread(name = "remover-$threadId") { - val idx1 = rnd.nextInt(nLists - 1) - val idx2 = idx1 + 1 + rnd.nextInt(nLists - idx1 - 1) - check(idx1 < idx2) // that is our global order - val list1 = lists[idx1] - val list2 = lists[idx2] - removeTwoOp(list1, list2) - } - } - env.performTest(nSeconds) { - val undone = undone.get() - val missed = missed.get() - val removed = removed.get() - println(" Adders undone $undone node additions") - println(" Adders missed $missed nodes") - println("Remover removed $removed nodes") - } - error.get()?.let { throw it } - assertEquals(missed.get(), removed.get()) - assertTrue(undone.get() > 0) - assertTrue(missed.get() > 0) - lists.forEach { it.validate() } - } - - private fun addLastOp(list: LockFreeLinkedListHead, node: Node) { - list.addLast(node) - } - - private fun addLastIfTrueOp(list: LockFreeLinkedListHead, node: Node) { - assertTrue(list.addLastIf(node) { true }) - } - - private fun addLastIfFalseOp(list: LockFreeLinkedListHead, node: Node) { - assertFalse(list.addLastIf(node) { false }) - } - - private fun addTwoOp(list1: LockFreeLinkedListHead, node1: Node, list2: LockFreeLinkedListHead, node2: Node) { - val add1 = list1.describeAddLast(node1) - val add2 = list2.describeAddLast(node2) - val op = object : AtomicOp() { - init { - add1.atomicOp = this - add2.atomicOp = this - } - override fun prepare(affected: Any?): Any? = - add1.prepare(this) ?: - add2.prepare(this) - - override fun complete(affected: Any?, failure: Any?) { - add1.complete(this, failure) - add2.complete(this, failure) - } - } - assertTrue(op.perform(null) == null) - } - - private fun tryRemoveOp(node: Node) { - if (node.remove()) - undone.incrementAndGet() - else - missed.incrementAndGet() - } - - private fun removeTwoOp(list1: LockFreeLinkedListHead, list2: LockFreeLinkedListHead) { - val remove1 = list1.describeRemoveFirst() - val remove2 = list2.describeRemoveFirst() - val op = object : AtomicOp() { - init { - remove1.atomicOp = this - remove2.atomicOp = this - } - override fun prepare(affected: Any?): Any? = - remove1.prepare(this) ?: - remove2.prepare(this) - - override fun complete(affected: Any?, failure: Any?) { - remove1.complete(this, failure) - remove2.complete(this, failure) - } - } - val success = op.perform(null) == null - if (success) removed.addAndGet(2) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt index f7f59eef5e..a278985fdd 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt @@ -9,15 +9,10 @@ import kotlinx.coroutines.sync.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.junit.* class MutexLincheckTest : AbstractLincheckTest() { private val mutex = Mutex() - override fun modelCheckingTest() { - // Ignored via empty body as the only way - } - @Operation fun tryLock() = mutex.tryLock() diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt deleted file mode 100644 index 4497bec5b9..0000000000 --- a/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.selects - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import org.junit.* -import org.junit.Ignore -import org.junit.Test -import kotlin.math.* -import kotlin.test.* - -/** - * A stress-test on lock-freedom of select sending/receiving into opposite channels. - */ -class SelectDeadlockLFStressTest : TestBase() { - private val env = LockFreedomTestEnvironment("SelectDeadlockLFStressTest", allowSuspendedThreads = 1) - private val nSeconds = 5 * stressTestMultiplier - - private val c1 = Channel() - private val c2 = Channel() - - @Test - fun testLockFreedom() = testScenarios( - "s1r2", - "s2r1", - "r1s2", - "r2s1" - ) - - private fun testScenarios(vararg scenarios: String) { - env.onCompletion { - c1.cancel(TestCompleted()) - c2.cancel(TestCompleted()) - } - val t = scenarios.mapIndexed { i, scenario -> - val idx = i + 1L - TestDef(idx, "$idx [$scenario]", scenario) - } - t.forEach { it.test() } - env.performTest(nSeconds) { - t.forEach { println(it) } - } - } - - private inner class TestDef( - var sendIndex: Long = 0L, - val name: String, - scenario: String - ) { - var receiveIndex = 0L - - val clauses: List.() -> Unit> = ArrayList.() -> Unit>().apply { - require(scenario.length % 2 == 0) - for (i in scenario.indices step 2) { - val ch = when (val c = scenario[i + 1]) { - '1' -> c1 - '2' -> c2 - else -> error("Channel '$c'") - } - val clause = when (val op = scenario[i]) { - 's' -> fun SelectBuilder.() { sendClause(ch) } - 'r' -> fun SelectBuilder.() { receiveClause(ch) } - else -> error("Operation '$op'") - } - add(clause) - } - } - - fun test() = env.testThread(name) { - doSendReceive() - } - - private suspend fun doSendReceive() { - try { - select { - for (clause in clauses) clause() - } - } catch (e: TestCompleted) { - assertTrue(env.isCompleted) - } - } - - private fun SelectBuilder.sendClause(c: Channel) = - c.onSend(sendIndex) { - sendIndex += 4L - } - - private fun SelectBuilder.receiveClause(c: Channel) = - c.onReceive { i -> - receiveIndex = max(i, receiveIndex) - } - - override fun toString(): String = "$name: send=$sendIndex, received=$receiveIndex" - } - - private class TestCompleted : CancellationException() -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt index 890f029ca2..4ffa6c0b11 100644 --- a/kotlinx-coroutines-core/native/test/TestBase.kt +++ b/kotlinx-coroutines-core/native/test/TestBase.kt @@ -7,7 +7,11 @@ package kotlinx.coroutines public actual val isStressTest: Boolean = false public actual val stressTestMultiplier: Int = 1 +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias TestResult = Unit + public actual open class TestBase actual constructor() { + public actual val isBoundByJsTestTimeout = false private var actionIndex = 0 private var finished = false private var error: Throwable? = null @@ -70,7 +74,7 @@ public actual open class TestBase actual constructor() { expected: ((Throwable) -> Boolean)? = null, unhandled: List<(Throwable) -> Boolean> = emptyList(), block: suspend CoroutineScope.() -> Unit - ) { + ): TestResult { var exCount = 0 var ex: Throwable? = null try { diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index f7b8602236..cd71f580f0 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.5.1.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.5.2.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/test/junit4/CoroutinesTimeoutDisabledTracesTest.kt b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutDisabledTracesTest.kt index 886007c3e8..2063090c82 100644 --- a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutDisabledTracesTest.kt +++ b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutDisabledTracesTest.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.* import org.junit.* import org.junit.runners.model.* -class CoroutinesTimeoutDisabledTracesTest : TestBase() { +class CoroutinesTimeoutDisabledTracesTest : TestBase(disableOutCheck = true) { @Rule @JvmField diff --git a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutEagerTest.kt b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutEagerTest.kt index 0845f5bcb3..7a686ff2a7 100644 --- a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutEagerTest.kt +++ b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutEagerTest.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.* import org.junit.* import org.junit.runners.model.* -class CoroutinesTimeoutEagerTest : TestBase() { +class CoroutinesTimeoutEagerTest : TestBase(disableOutCheck = true) { @Rule @JvmField diff --git a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt index ac3408e20a..53447ac5f9 100644 --- a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt +++ b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.* import org.junit.* import org.junit.runners.model.* -class CoroutinesTimeoutTest : TestBase() { +class CoroutinesTimeoutTest : TestBase(disableOutCheck = true) { @Rule @JvmField diff --git a/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt index 34ba679adb..6d25a6da7d 100644 --- a/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt +++ b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt @@ -104,6 +104,6 @@ internal class TestFailureValidation(private val testsSpec: Map = listOf(), val notExpectedOutParts: - List = listOf(), val error: Class? = null + val testName: String, val expectedOutParts: List = listOf(), + val notExpectedOutParts: List = listOf(), val error: Class? = null ) diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 622e81d50b..43ae18f532 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 408a43d1e1..71b2d69c5c 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your