aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Android.bp98
-rw-r--r--CHANGES.md172
-rw-r--r--CONTRIBUTING.md5
-rw-r--r--METADATA8
-rw-r--r--README.md167
-rw-r--r--RELEASE.md93
-rwxr-xr-xandroid-annotation-stubs/gen_annotations.sh15
-rw-r--r--android-annotation-stubs/src/org/codehaus/mojo/animal_sniffer/IgnoreJRERequirement.java39
-rw-r--r--android-annotation-stubs/tmpl.java39
-rw-r--r--benchmarks/build.gradle.kts30
-rw-r--r--benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt3
-rw-r--r--benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt11
-rw-r--r--benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt2
-rw-r--r--benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt6
-rw-r--r--build.gradle136
-rw-r--r--buildSrc/build.gradle.kts24
-rw-r--r--buildSrc/settings.gradle.kts1
-rw-r--r--buildSrc/src/main/kotlin/OptInPreset.kt14
-rw-r--r--buildSrc/src/main/kotlin/Projects.kt27
-rw-r--r--buildSrc/src/main/kotlin/Publishing.kt6
-rw-r--r--buildSrc/src/main/kotlin/SourceSets.kt19
-rw-r--r--buildSrc/src/main/kotlin/UnpackAar.kt33
-rw-r--r--buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts29
-rw-r--r--buildSrc/src/main/kotlin/bom-conventions.gradle.kts19
-rw-r--r--buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts4
-rw-r--r--buildSrc/src/main/kotlin/kover-conventions.gradle.kts54
-rwxr-xr-xbump-version.sh1
-rw-r--r--coroutines-guide.md13
-rw-r--r--docs/images/coroutine-breakpoint.pngbin106968 -> 116316 bytes
-rw-r--r--docs/images/coroutine-idea-debugging-1.pngbin248706 -> 329126 bytes
-rw-r--r--docs/images/flow-breakpoint.pngbin111598 -> 114426 bytes
-rw-r--r--docs/images/flow-build-project.pngbin44472 -> 70187 bytes
-rw-r--r--docs/images/flow-debug-project.pngbin44199 -> 80132 bytes
-rw-r--r--docs/topics/cancellation-and-timeouts.md86
-rw-r--r--docs/topics/channels.md33
-rw-r--r--docs/topics/compatibility.md12
-rw-r--r--docs/topics/composing-suspending-functions.md20
-rw-r--r--docs/topics/coroutine-context-and-dispatchers.md54
-rw-r--r--docs/topics/coroutines-basics.md33
-rw-r--r--docs/topics/coroutines-guide.md2
-rw-r--r--docs/topics/debug-coroutines-with-idea.md12
-rw-r--r--docs/topics/debug-flow-with-idea.md30
-rw-r--r--docs/topics/debugging.md19
-rw-r--r--docs/topics/exception-handling.md49
-rw-r--r--docs/topics/flow.md96
-rw-r--r--docs/topics/select-expression.md18
-rw-r--r--docs/topics/shared-mutable-state-and-concurrency.md22
-rw-r--r--gradle.properties24
-rw-r--r--gradle/compile-js-multiplatform.gradle2
-rw-r--r--gradle/compile-jvm-multiplatform.gradle8
-rw-r--r--gradle/dokka.gradle.kts10
-rw-r--r--gradle/node-js.gradle6
-rw-r--r--gradle/opt-in.gradle13
-rw-r--r--gradle/publish-npm-js.gradle22
-rw-r--r--gradle/publish.gradle13
-rw-r--r--gradle/test-mocha-js.gradle35
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--integration-testing/README.md15
-rw-r--r--integration-testing/build.gradle101
-rw-r--r--integration-testing/gradle.properties4
-rw-r--r--integration-testing/gradle/wrapper/gradle-wrapper.jarbin0 -> 59536 bytes
-rw-r--r--integration-testing/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xintegration-testing/gradlew234
-rw-r--r--integration-testing/gradlew.bat89
-rw-r--r--integration-testing/settings.gradle19
-rw-r--r--integration-testing/smokeTest/build.gradle43
-rw-r--r--integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt9
-rw-r--r--integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt13
-rw-r--r--integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt13
-rw-r--r--integration-testing/src/debugDynamicAgentTest/kotlin/DynamicAttachDebugTest.kt29
-rw-r--r--integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt (renamed from integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt)2
-rw-r--r--integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt40
-rw-r--r--integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt71
-rw-r--r--integration-testing/src/withGuavaTest/kotlin/ListAllCoroutineThrowableSubclassesTest.kt43
-rw-r--r--integration/kotlinx-coroutines-guava/README.md10
-rw-r--r--integration/kotlinx-coroutines-guava/build.gradle.kts9
-rw-r--r--integration/kotlinx-coroutines-guava/src/ListenableFuture.kt55
-rw-r--r--integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt82
-rw-r--r--integration/kotlinx-coroutines-jdk8/README.md12
-rw-r--r--integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt1
-rw-r--r--integration/kotlinx-coroutines-play-services/README.md12
-rw-r--r--integration/kotlinx-coroutines-play-services/build.gradle.kts31
-rw-r--r--integration/kotlinx-coroutines-play-services/src/Tasks.kt20
-rw-r--r--integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt9
-rw-r--r--integration/kotlinx-coroutines-play-services/test/TaskTest.kt8
-rw-r--r--integration/kotlinx-coroutines-slf4j/README.md2
-rw-r--r--integration/kotlinx-coroutines-slf4j/build.gradle.kts8
-rw-r--r--integration/kotlinx-coroutines-slf4j/src/MDCContext.kt3
-rw-r--r--integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt3
-rw-r--r--js/example-frontend-js/README.md2
-rw-r--r--js/example-frontend-js/src/ExampleMain.kt2
-rw-r--r--kotlinx-coroutines-core/README.md91
-rw-r--r--kotlinx-coroutines-core/api/kotlinx-coroutines-core.api82
-rw-r--r--kotlinx-coroutines-core/build.gradle133
-rw-r--r--kotlinx-coroutines-core/common/README.md105
-rw-r--r--kotlinx-coroutines-core/common/src/Annotations.kt13
-rw-r--r--kotlinx-coroutines-core/common/src/Await.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/Builders.common.kt18
-rw-r--r--kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt28
-rw-r--r--kotlinx-coroutines-core/common/src/CoroutineContext.common.kt13
-rw-r--r--kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt73
-rw-r--r--kotlinx-coroutines-core/common/src/CoroutineScope.kt24
-rw-r--r--kotlinx-coroutines-core/common/src/Delay.kt19
-rw-r--r--kotlinx-coroutines-core/common/src/Dispatchers.common.kt12
-rw-r--r--kotlinx-coroutines-core/common/src/EventLoop.common.kt29
-rw-r--r--kotlinx-coroutines-core/common/src/Job.kt14
-rw-r--r--kotlinx-coroutines-core/common/src/JobSupport.kt1
-rw-r--r--kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt8
-rw-r--r--kotlinx-coroutines-core/common/src/Timeout.kt6
-rw-r--r--kotlinx-coroutines-core/common/src/Unconfined.kt9
-rw-r--r--kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt2
-rw-r--r--kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt6
-rw-r--r--kotlinx-coroutines-core/common/src/channels/Channel.kt13
-rw-r--r--kotlinx-coroutines-core/common/src/channels/Channels.common.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/channels/Produce.kt10
-rw-r--r--kotlinx-coroutines-core/common/src/flow/Builders.kt19
-rw-r--r--kotlinx-coroutines-core/common/src/flow/Channels.kt41
-rw-r--r--kotlinx-coroutines-core/common/src/flow/Flow.kt44
-rw-r--r--kotlinx-coroutines-core/common/src/flow/FlowCollector.kt19
-rw-r--r--kotlinx-coroutines-core/common/src/flow/Migration.kt13
-rw-r--r--kotlinx-coroutines-core/common/src/flow/SharedFlow.kt54
-rw-r--r--kotlinx-coroutines-core/common/src/flow/SharingStarted.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/flow/StateFlow.kt6
-rw-r--r--kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt49
-rw-r--r--kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt24
-rw-r--r--kotlinx-coroutines-core/common/src/flow/internal/Merge.kt6
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Context.kt60
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Delay.kt5
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt3
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Errors.kt78
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Limit.kt2
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Lint.kt31
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Merge.kt11
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Share.kt13
-rw-r--r--kotlinx-coroutines-core/common/src/flow/operators/Transform.kt9
-rw-r--r--kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt30
-rw-r--r--kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt10
-rw-r--r--kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt106
-rw-r--r--kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt2
-rw-r--r--kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt5
-rw-r--r--kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt1
-rw-r--r--kotlinx-coroutines-core/common/src/internal/Symbol.kt4
-rw-r--r--kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt12
-rw-r--r--kotlinx-coroutines-core/common/src/selects/Select.kt2
-rw-r--r--kotlinx-coroutines-core/common/src/sync/Mutex.kt13
-rw-r--r--kotlinx-coroutines-core/common/test/DelayDurationTest.kt3
-rw-r--r--kotlinx-coroutines-core/common/test/EmptyContext.kt8
-rw-r--r--kotlinx-coroutines-core/common/test/LimitedParallelismSharedTest.kt34
-rw-r--r--kotlinx-coroutines-core/common/test/TestBase.common.kt4
-rw-r--r--kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt31
-rw-r--r--kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt43
-rw-r--r--kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt4
-rw-r--r--kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt2
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt57
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt28
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt3
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt2
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt2
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt9
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt9
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt13
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt2
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt3
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt58
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt59
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt15
-rw-r--r--kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt3
-rw-r--r--kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt4
-rw-r--r--kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt26
-rw-r--r--kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt20
-rw-r--r--kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt45
-rw-r--r--kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt19
-rw-r--r--kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt14
-rw-r--r--kotlinx-coroutines-core/concurrent/src/CompletionHandler.kt (renamed from kotlinx-coroutines-core/jvm/src/CompletionHandler.kt)0
-rw-r--r--kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt11
-rw-r--r--kotlinx-coroutines-core/concurrent/src/channels/Channels.kt (renamed from kotlinx-coroutines-core/jvm/src/channels/Channels.kt)76
-rw-r--r--kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt (renamed from kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt)8
-rw-r--r--kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt54
-rw-r--r--kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.kt (renamed from kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt)4
-rw-r--r--kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt67
-rw-r--r--kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt23
-rw-r--r--kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt8
-rw-r--r--kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt)12
-rw-r--r--kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt61
-rw-r--r--kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt (renamed from kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt)100
-rw-r--r--kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt10
-rw-r--r--kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt60
-rw-r--r--kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt)16
-rw-r--r--kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt)33
-rw-r--r--kotlinx-coroutines-core/concurrent/test/channels/TrySendBlockingTest.kt (renamed from kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt)7
-rw-r--r--kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt)8
-rw-r--r--kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt (renamed from kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt)11
-rw-r--r--kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt47
-rw-r--r--kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt39
-rw-r--r--kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt (renamed from kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt)5
-rw-r--r--kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt)33
-rw-r--r--kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt)2
-rw-r--r--kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt)45
-rw-r--r--kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt (renamed from kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt)54
-rw-r--r--kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt9
-rw-r--r--kotlinx-coroutines-core/js/src/CoroutineContext.kt6
-rw-r--r--kotlinx-coroutines-core/js/src/Dispatchers.kt16
-rw-r--r--kotlinx-coroutines-core/js/src/EventLoop.kt2
-rw-r--r--kotlinx-coroutines-core/js/src/JSDispatcher.kt5
-rw-r--r--kotlinx-coroutines-core/js/src/internal/Concurrent.kt1
-rw-r--r--kotlinx-coroutines-core/js/src/internal/LinkedList.kt2
-rw-r--r--kotlinx-coroutines-core/js/test/PromiseTest.kt2
-rw-r--r--kotlinx-coroutines-core/js/test/TestBase.kt3
-rw-r--r--kotlinx-coroutines-core/jvm/resources/DebugProbesKt.binbin1714 -> 1738 bytes
-rw-r--r--kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro31
-rw-r--r--kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro27
-rw-r--r--kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro11
-rw-r--r--kotlinx-coroutines-core/jvm/src/Builders.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/src/CommonPool.kt143
-rw-r--r--kotlinx-coroutines-core/jvm/src/CoroutineContext.kt158
-rw-r--r--kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt22
-rw-r--r--kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt52
-rw-r--r--kotlinx-coroutines-core/jvm/src/Dispatchers.kt66
-rw-r--r--kotlinx-coroutines-core/jvm/src/EventLoop.kt5
-rw-r--r--kotlinx-coroutines-core/jvm/src/Exceptions.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/src/Executors.kt3
-rw-r--r--kotlinx-coroutines-core/jvm/src/Interruptible.kt7
-rw-r--r--kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt113
-rw-r--r--kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt30
-rw-r--r--kotlinx-coroutines-core/jvm/src/channels/Actor.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt14
-rw-r--r--kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt16
-rw-r--r--kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt5
-rw-r--r--kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt89
-rw-r--r--kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt43
-rw-r--r--kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt111
-rw-r--r--kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt81
-rw-r--r--kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt8
-rw-r--r--kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt7
-rw-r--r--kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt38
-rw-r--r--kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt14
-rw-r--r--kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt212
-rw-r--r--kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt269
-rw-r--r--kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt37
-rw-r--r--kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt296
-rw-r--r--kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt56
-rw-r--r--kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt29
-rw-r--r--kotlinx-coroutines-core/jvm/test/ContinuationSerializationTest.kt94
-rw-r--r--kotlinx-coroutines-core/jvm/test/DispatcherKeyTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/FieldWalker.kt23
-rw-r--r--kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt42
-rw-r--r--kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt97
-rw-r--r--kotlinx-coroutines-core/jvm/test/LimitedParallelismUnhandledExceptionTest.kt32
-rw-r--r--kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt18
-rw-r--r--kotlinx-coroutines-core/jvm/test/TestBase.kt16
-rw-r--r--kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt10
-rw-r--r--kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt133
-rw-r--r--kotlinx-coroutines-core/jvm/test/ThreadContextMutableCopiesTest.kt134
-rw-r--r--kotlinx-coroutines-core/jvm/test/ThreadLocalStressTest.kt165
-rw-r--r--kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt4
-rw-r--r--kotlinx-coroutines-core/jvm/test/WithTimeoutOrNullJvmTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt70
-rw-r--r--kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt5
-rw-r--r--kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt29
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt1
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt1
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt1
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt12
-rw-r--r--kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt74
-rw-r--r--kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt18
-rw-r--r--kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt81
-rw-r--r--kotlinx-coroutines-core/jvm/test/flow/SafeCollectorMemoryLeakTest.kt48
-rw-r--r--kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt5
-rw-r--r--kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt8
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt16
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt16
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt6
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt20
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt4
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt26
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt13
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt36
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/example-context-06.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt24
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt4
-rw-r--r--kotlinx-coroutines-core/jvm/test/guide/test/GuideTest.kt0
-rw-r--r--kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt10
-rw-r--r--kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListLongStressTest.kt1
-rw-r--r--kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapStressTest.kt66
-rw-r--r--kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt7
-rw-r--r--kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt27
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt65
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt26
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt2
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt12
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt75
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt5
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt21
-rw-r--r--kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt6
-rw-r--r--kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt404
-rw-r--r--kotlinx-coroutines-core/native/src/Builders.kt39
-rw-r--r--kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt9
-rw-r--r--kotlinx-coroutines-core/native/src/CompletionHandler.kt22
-rw-r--r--kotlinx-coroutines-core/native/src/CoroutineContext.kt54
-rw-r--r--kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt4
-rw-r--r--kotlinx-coroutines-core/native/src/Dispatchers.kt45
-rw-r--r--kotlinx-coroutines-core/native/src/EventLoop.kt32
-rw-r--r--kotlinx-coroutines-core/native/src/Exceptions.kt7
-rw-r--r--kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt93
-rw-r--r--kotlinx-coroutines-core/native/src/internal/Concurrent.kt32
-rw-r--r--kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt83
-rw-r--r--kotlinx-coroutines-core/native/src/internal/LinkedList.kt170
-rw-r--r--kotlinx-coroutines-core/native/src/internal/Synchronized.kt6
-rw-r--r--kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt37
-rw-r--r--kotlinx-coroutines-core/native/test/DelayExceptionTest.kt16
-rw-r--r--kotlinx-coroutines-core/native/test/RunBlockingTest.kt18
-rw-r--r--kotlinx-coroutines-core/native/test/TestBase.kt29
-rw-r--r--kotlinx-coroutines-core/native/test/TestBaseExtension.kt19
-rw-r--r--kotlinx-coroutines-core/native/test/WorkerTest.kt4
-rw-r--r--kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt6
-rw-r--r--kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt108
-rw-r--r--kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt130
-rw-r--r--kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt35
-rw-r--r--kotlinx-coroutines-core/npm/README.md2
-rw-r--r--kotlinx-coroutines-debug/README.md27
-rw-r--r--kotlinx-coroutines-debug/build.gradle54
-rw-r--r--kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt19
-rw-r--r--kotlinx-coroutines-debug/test/BlockHoundTest.kt27
-rw-r--r--kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt96
-rw-r--r--kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt49
-rw-r--r--kotlinx-coroutines-debug/test/StacktraceUtils.kt7
-rw-r--r--kotlinx-coroutines-debug/test/ToStringTest.kt11
-rw-r--r--kotlinx-coroutines-test/MIGRATION.md447
-rw-r--r--kotlinx-coroutines-test/README.md542
-rw-r--r--kotlinx-coroutines-test/api/kotlinx-coroutines-test.api79
-rw-r--r--kotlinx-coroutines-test/build.gradle.kts10
-rw-r--r--kotlinx-coroutines-test/common/src/TestBuilders.kt321
-rw-r--r--kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt158
-rw-r--r--kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt262
-rw-r--r--kotlinx-coroutines-test/common/src/TestDispatcher.kt54
-rw-r--r--kotlinx-coroutines-test/common/src/TestDispatchers.kt (renamed from kotlinx-coroutines-test/src/TestDispatchers.kt)20
-rw-r--r--kotlinx-coroutines-test/common/src/TestScope.kt298
-rw-r--r--kotlinx-coroutines-test/common/src/internal/ReportingSupervisorJob.kt24
-rw-r--r--kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt96
-rw-r--r--kotlinx-coroutines-test/common/test/Helpers.kt73
-rw-r--r--kotlinx-coroutines-test/common/test/RunTestTest.kt372
-rw-r--r--kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt79
-rw-r--r--kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt335
-rw-r--r--kotlinx-coroutines-test/common/test/TestDispatchersTest.kt98
-rw-r--r--kotlinx-coroutines-test/common/test/TestScopeTest.kt487
-rw-r--r--kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt168
-rw-r--r--kotlinx-coroutines-test/js/src/TestBuilders.kt15
-rw-r--r--kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt13
-rw-r--r--kotlinx-coroutines-test/js/test/Helpers.kt20
-rw-r--r--kotlinx-coroutines-test/js/test/PromiseTest.kt21
-rw-r--r--kotlinx-coroutines-test/jvm/resources/META-INF/proguard/coroutines.pro (renamed from kotlinx-coroutines-test/resources/META-INF/proguard/coroutines.pro)0
-rw-r--r--kotlinx-coroutines-test/jvm/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory (renamed from kotlinx-coroutines-test/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory)0
-rw-r--r--kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt15
-rw-r--r--kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt31
-rw-r--r--kotlinx-coroutines-test/jvm/src/migration/DelayController.kt209
-rw-r--r--kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt218
-rw-r--r--kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt83
-rw-r--r--kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt (renamed from kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt)40
-rw-r--r--kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt345
-rw-r--r--kotlinx-coroutines-test/jvm/test/HelpersJvm.kt10
-rw-r--r--kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt112
-rw-r--r--kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt26
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt373
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt288
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt (renamed from kotlinx-coroutines-test/test/TestBuildersTest.kt)6
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt (renamed from kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt)12
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt77
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt (renamed from kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt)6
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt217
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt (renamed from kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt)11
-rw-r--r--kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt (renamed from kotlinx-coroutines-test/test/TestRunBlockingTest.kt)249
-rw-r--r--kotlinx-coroutines-test/native/src/TestBuilders.kt15
-rw-r--r--kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt13
-rw-r--r--kotlinx-coroutines-test/native/test/FailingTests.kt25
-rw-r--r--kotlinx-coroutines-test/native/test/Helpers.kt14
-rw-r--r--kotlinx-coroutines-test/npm/README.md4
-rw-r--r--kotlinx-coroutines-test/npm/package.json23
-rw-r--r--kotlinx-coroutines-test/src/DelayController.kt129
-rw-r--r--kotlinx-coroutines-test/src/TestBuilders.kt98
-rw-r--r--kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt215
-rw-r--r--kotlinx-coroutines-test/src/TestCoroutineScope.kt72
-rw-r--r--kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt76
-rw-r--r--kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt142
-rw-r--r--kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt26
-rw-r--r--kotlinx-coroutines-test/test/TestDispatchersTest.kt89
-rw-r--r--kotlinx-coroutines-test/test/TestModuleHelpers.kt22
-rw-r--r--license/NOTICE.txt2
-rw-r--r--reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt4
-rw-r--r--reactive/kotlinx-coroutines-reactive/README.md20
-rw-r--r--reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api5
-rw-r--r--reactive/kotlinx-coroutines-reactive/build.gradle.kts18
-rw-r--r--reactive/kotlinx-coroutines-reactive/src/Await.kt36
-rw-r--r--reactive/kotlinx-coroutines-reactive/src/Channel.kt32
-rw-r--r--reactive/kotlinx-coroutines-reactive/src/Convert.kt10
-rw-r--r--reactive/kotlinx-coroutines-reactive/src/Publish.kt14
-rw-r--r--reactive/kotlinx-coroutines-reactive/test/AwaitCancellationStressTest.kt43
-rw-r--r--reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt12
-rw-r--r--reactive/kotlinx-coroutines-reactive/test/PublishTest.kt40
-rw-r--r--reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt25
-rw-r--r--reactive/kotlinx-coroutines-reactor/README.md20
-rw-r--r--reactive/kotlinx-coroutines-reactor/build.gradle.kts17
-rw-r--r--reactive/kotlinx-coroutines-reactor/src/Mono.kt10
-rw-r--r--reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt7
-rw-r--r--reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt66
-rw-r--r--reactive/kotlinx-coroutines-reactor/test/FluxTest.kt4
-rw-r--r--reactive/kotlinx-coroutines-reactor/test/MonoTest.kt23
-rw-r--r--reactive/kotlinx-coroutines-rx2/README.md48
-rw-r--r--reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api8
-rw-r--r--reactive/kotlinx-coroutines-rx2/src/RxAwait.kt4
-rw-r--r--reactive/kotlinx-coroutines-rx2/src/RxChannel.kt46
-rw-r--r--reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt2
-rw-r--r--reactive/kotlinx-coroutines-rx2/src/RxObservable.kt15
-rw-r--r--reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt139
-rw-r--r--reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt4
-rw-r--r--reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt26
-rw-r--r--reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt87
-rw-r--r--reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt470
-rw-r--r--reactive/kotlinx-coroutines-rx3/README.md48
-rw-r--r--reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api4
-rw-r--r--reactive/kotlinx-coroutines-rx3/src/RxAwait.kt4
-rw-r--r--reactive/kotlinx-coroutines-rx3/src/RxObservable.kt15
-rw-r--r--reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt138
-rw-r--r--reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt26
-rw-r--r--reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt87
-rw-r--r--reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt468
-rw-r--r--settings.gradle5
-rwxr-xr-xsite/deploy.sh64
-rw-r--r--ui/coroutines-guide-ui.md36
-rw-r--r--ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts9
-rw-r--r--ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt3
-rw-r--r--ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt1
-rw-r--r--ui/kotlinx-coroutines-android/build.gradle.kts19
-rw-r--r--ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro3
-rw-r--r--ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro4
-rw-r--r--ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro9
-rw-r--r--ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro10
-rw-r--r--ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro3
-rw-r--r--ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt25
-rw-r--r--ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt8
-rw-r--r--ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt36
-rw-r--r--ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt1
-rw-r--r--ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt146
-rw-r--r--ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt201
-rw-r--r--ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt1
-rw-r--r--ui/kotlinx-coroutines-javafx/build.gradle.kts19
-rw-r--r--ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt13
-rw-r--r--ui/kotlinx-coroutines-swing/build.gradle.kts2
-rw-r--r--ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt10
460 files changed, 15013 insertions, 5721 deletions
diff --git a/.gitignore b/.gitignore
index 52843ca5..36de0e50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ build
out
target
local.properties
+/kotlin-js-store
diff --git a/Android.bp b/Android.bp
index 5225d8c3..db94babe 100644
--- a/Android.bp
+++ b/Android.bp
@@ -38,12 +38,13 @@ license {
java_library {
name: "kotlinx_coroutines",
host_supported: true,
- // This should be "core_current", but that causes nullability issues
- // for returned platform types that are explicitly marked @Nullable in
- // SDK 29 and current.
- sdk_version: "28",
+ sdk_version: "current",
+ min_sdk_version: "28",
srcs: ["kotlinx-coroutines-core/jvm/src/**/*.kt"],
- common_srcs: ["kotlinx-coroutines-core/common/src/**/*.kt"],
+ common_srcs: [
+ "kotlinx-coroutines-core/common/src/**/*.kt",
+ "kotlinx-coroutines-core/concurrent/src/**/*.kt",
+ ],
exclude_srcs: [
"kotlinx-coroutines-core/jvm/src/debug/**/*.kt",
"kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt",
@@ -52,33 +53,43 @@ java_library {
static_libs: [
"kotlinx_atomicfu",
],
+ libs: ["kotlinx-coroutines-android-annotation-stubs"],
kotlincflags: [
"-Xmulti-platform",
- "-Xuse-experimental=kotlin.ExperimentalMultiplatform",
- "-Xuse-experimental=kotlin.Experimental",
- "-Xuse-experimental=kotlin.experimental.ExperimentalTypeInference",
- "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
- "-Xuse-experimental=kotlinx.coroutines.FlowPreview",
- "-Xuse-experimental=kotlinx.coroutines.ObsoleteCoroutinesApi",
- "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
+ "-opt-in=kotlin.RequiresOptIn",
+ "-opt-in=kotlin.experimental.ExperimentalTypeInference",
+ "-opt-in=kotlin.ExperimentalMultiplatform",
+ "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.ObsoleteCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.FlowPreview",
],
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex"
],
+ target: {
+ host: {
+ libs: ["annotations"], // for android.annotation.SuppressLint
+ },
+ },
}
java_library {
name: "kotlinx_coroutines_android",
- sdk_version: "28",
+ sdk_version: "current",
+ min_sdk_version: "28",
srcs: ["ui/kotlinx-coroutines-android/src/**/*.kt"],
java_resource_dirs: ["ui/kotlinx-coroutines-android/resources"],
kotlincflags: [
- "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
- "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
],
- libs: [
+ static_libs: [
"kotlinx_coroutines",
+ ],
+ libs: [
"androidx.annotation_annotation",
],
apex_available: [
@@ -90,11 +101,14 @@ java_library {
java_library {
name: "kotlinx_coroutines_test",
host_supported: true,
- srcs: ["kotlinx-coroutines-test/src/**/*.kt"],
- java_resource_dirs: ["kotlinx-coroutines-test/resources"],
+ srcs: ["kotlinx-coroutines-test/jvm/src/**/*.kt"],
+ common_srcs: ["kotlinx-coroutines-test/common/src/**/*.kt"],
+ java_resource_dirs: ["kotlinx-coroutines-test/jvm/resources"],
kotlincflags: [
- "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
- "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-Xmulti-platform",
+ "-opt-in=kotlin.ExperimentalMultiplatform",
+ "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
],
libs: [
"kotlinx_coroutines",
@@ -105,12 +119,23 @@ java_library {
],
}
-// Temporary aliases for kotlinx-coroutines-core, kotlinx-coroutines-core-jvm and kotlinx-coroutines-android
+// Compile stub implementations of annotations used by kotlinx-coroutines but not present in the
+// Android tree.
+java_library {
+ name: "kotlinx-coroutines-android-annotation-stubs",
+ host_supported: true,
+ sdk_version: "core_current",
+ srcs: ["android-annotation-stubs/src/**/*.java"],
+}
+
+// Temporary aliases for kotlinx-coroutines-core, kotlinx-coroutines-core-jvm and
+// kotlinx-coroutines-android
java_library {
name: "kotlinx-coroutines-core",
host_supported: true,
- sdk_version: "28",
+ sdk_version: "current",
+ min_sdk_version: "28",
static_libs: ["kotlinx_coroutines"],
apex_available: [
"//apex_available:platform",
@@ -121,7 +146,8 @@ java_library {
java_library {
name: "kotlinx-coroutines-core-jvm",
host_supported: true,
- sdk_version: "28",
+ sdk_version: "current",
+ min_sdk_version: "28",
static_libs: ["kotlinx_coroutines"],
apex_available: [
"//apex_available:platform",
@@ -131,7 +157,8 @@ java_library {
java_library {
name: "kotlinx-coroutines-android",
- sdk_version: "28",
+ sdk_version: "current",
+ min_sdk_version: "28",
static_libs: [
"kotlinx_coroutines_android",
"kotlinx_coroutines",
@@ -145,4 +172,27 @@ java_library {
filegroup {
name: "kotlinx-coroutines-play-services",
srcs: ["integration/kotlinx-coroutines-play-services/src/**/*.kt"],
+ visibility: ["//visibility:public"],
}
+
+// Maven coordinate: org.jetbrains.kotlinx:kotlinx-coroutines-guava
+java_library {
+ name: "kotlinx_coroutines_guava",
+ host_supported: true,
+ srcs: ["integration/kotlinx-coroutines-guava/src/**/*.kt"],
+ kotlincflags: [
+ "-opt-in=kotlin.RequiresOptIn",
+ "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ ],
+ libs: [
+ "kotlinx_coroutines",
+ "kotlin-stdlib-jdk8",
+ "guava",
+ ],
+ apex_available: [
+ "//apex_available:platform",
+ "//apex_available:anyapex",
+ ],
+}
+
diff --git a/CHANGES.md b/CHANGES.md
index 611e9c9c..adadf234 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,171 @@
# Change log for kotlinx.coroutines
+## Version 1.6.4
+
+* Added `TestScope.backgroundScope` for launching coroutines that perform work in the background and need to be cancelled at the end of the test (#3287).
+* Fixed the POM of `kotlinx-coroutines-debug` having an incorrect reference to `kotlinx-coroutines-bom`, which cause the builds of Maven projects using the debug module to break (#3334).
+* Fixed the `Publisher.await` functions in `kotlinx-coroutines-reactive` not ensuring that the `Subscriber` methods are invoked serially (#3360). Thank you, @EgorKulbachka!
+* Fixed a memory leak in `withTimeout` on K/N with the new memory model (#3351).
+* Added the guarantee that all `Throwable` implementations in the core library are serializable (#3328).
+* Moved the documentation to <https://kotlinlang.org/api/kotlinx.coroutines/> (#3342).
+* Various documentation improvements.
+
+## Version 1.6.3
+
+* Updated atomicfu version to 0.17.3 (#3321), fixing the projects using this library with JS IR failing to build (#3305).
+
+## Version 1.6.2
+
+* Fixed a bug with `ThreadLocalElement` not being correctly updated when the most outer `suspend` function was called directly without `kotlinx.coroutines` (#2930).
+* Fixed multiple data races: one that might have been affecting `runBlocking` event loop, and a benign data race in `Mutex` (#3250, #3251).
+* Obsolete `TestCoroutineContext` is removed, which fixes the `kotlinx-coroutines-test` JPMS package being split between `kotlinx-coroutines-core` and `kotlinx-coroutines-test` (#3218).
+* Updated the ProGuard rules to further shrink the size of the resulting DEX file with coroutines (#3111, #3263). Thanks, @agrieve!
+* Atomicfu is updated to `0.17.2`, which includes a more efficient and robust JS IR transformer (#3255).
+* Kotlin is updated to `1.6.21`, Gradle version is updated to `7.4.2` (#3281). Thanks, @wojtek-kalicinski!
+* Various documentation improvements.
+
+## Version 1.6.1
+
+* Rollback of time-related functions dispatching on `Dispatchers.Main`.
+ This behavior was introduced in 1.6.0 and then found inconvenient and erroneous (#3106, #3113).
+* Reworked the newly-introduced `CopyableThreadContextElement` to solve issues uncovered after the initial release (#3227).
+* Fixed a bug with `ThreadLocalElement` not being properly updated in racy scenarios (#2930).
+* Reverted eager loading of default `CoroutineExceptionHandler` that triggered ANR on some devices (#3180).
+* New API to convert a `CoroutineDispatcher` to a Rx scheduler (#968, #548). Thanks @recheej!
+* Fixed a memory leak with the very last element emitted from `flow` builder being retained in memory (#3197).
+* Fixed a bug with `limitedParallelism` on K/N with new memory model throwing `ClassCastException` (#3223).
+* `CoroutineContext` is added to the exception printed to the default `CoroutineExceptionHandler` to improve debuggability (#3153).
+* Static memory consumption of `Dispatchers.Default` was significantly reduced (#3137).
+* Updated slf4j version in `kotlinx-coroutines-slf4j` from 1.7.25 to 1.7.32.
+
+## Version 1.6.0
+
+Note that this is a full changelog relative to the 1.5.2 version. Changelog relative to 1.6.0-RC3 can be found at the end.
+
+### kotlinx-coroutines-test rework
+
+* `kotlinx-coroutines-test` became a multiplatform library usable from K/JVM, K/JS, and K/N.
+* Its API was completely reworked to address long-standing issues with consistency, structured concurrency and correctness (#1203, #1609, #2379, #1749, #1204, #1390, #1222, #1395, #1881, #1910, #1772, #1626, #1742, #2082, #2102, #2405, #2462
+ ).
+* The old API is deprecated for removal, but the new API is based on the similar concepts ([README](kotlinx-coroutines-test/README.md)), and the migration path is designed to be graceful: [migration guide](kotlinx-coroutines-test/MIGRATION.md).
+
+### Dispatchers
+
+* Introduced `CoroutineDispatcher.limitedParallelism` that allows obtaining a view of the original dispatcher with limited parallelism (#2919).
+* `Dispatchers.IO.limitedParallelism` usages ignore the bound on the parallelism level of `Dispatchers.IO` itself to avoid starvation (#2943).
+* Introduced new `Dispatchers.shutdown` method for containerized environments (#2558).
+* `newSingleThreadContext` and `newFixedThreadPoolContext` are promoted to delicate API (#2919).
+
+### Breaking changes
+
+* When racing with cancellation, the `future` builder no longer reports unhandled exceptions into the global `CoroutineExceptionHandler`. Thanks @vadimsemenov! (#2774, #2791).
+* `Mutex.onLock` is deprecated for removal (#2794).
+* `Dispatchers.Main` is now used as the default source of time for `delay` and `withTimeout` when present(#2972).
+ * To opt-out from this behaviour, `kotlinx.coroutines.main.delay` system property can be set to `false`.
+* Java target of coroutines build is now 8 instead of 6 (#1589).
+* **Source-breaking change**: extension `collect` no longer resolves when used with a non-in-place argument of a functional type. This is a candidate for a fix, uncovered after 1.6.0, see #3107 for the additional details.
+
+### Bug fixes and improvements
+
+* Kotlin is updated to 1.6.0.
+* Kotlin/Native [new memory model](https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/) is now supported in regular builds of coroutines conditionally depending on whether `kotlin.native.binary.memoryModel` is enabled (#2914).
+* Introduced `CopyableThreadContextElement` for mutable context elements shared among multiple coroutines. Thanks @yorickhenning! (#2893).
+* `transformWhile`, `awaitClose`, `ProducerScope`, `merge`, `runningFold`, `runingReduce`, and `scan` are promoted to stable API (#2971).
+* `SharedFlow.subscriptionCount` no longer conflates incoming updates and gives all subscribers a chance to observe a short-lived subscription (#2488, #2863, #2871).
+* `Flow` exception transparency mechanism is improved to be more exception-friendly (#3017, #2860).
+* Cancellation from `flat*` operators that leverage multiple coroutines is no longer propagated upstream (#2964).
+* `SharedFlow.collect` now returns `Nothing` (#2789, #2502).
+* `DisposableHandle` is now `fun interface`, and corresponding inline extension is removed (#2790).
+* `FlowCollector` is now `fun interface`, and corresponding inline extension is removed (#3047).
+* Deprecation level of all previously deprecated signatures is raised (#3024).
+* The version file is shipped with each JAR as a resource (#2941).
+* Unhandled exceptions on K/N are passed to the standard library function `processUnhandledException` (#2981).
+* A direct executor is used for `Task` callbacks in `kotlinx-coroutines-play-services` (#2990).
+* Metadata of coroutines artifacts leverages Gradle platform to have all versions of dependencies aligned (#2865).
+* Default `CoroutineExceptionHandler` is loaded eagerly and does not invoke `ServiceLoader` on its exception-handling path (#2552).
+* Fixed the R8 rules for `ServiceLoader` optimization (#2880).
+* Fixed BlockHound integration false-positives (#2894, #2866, #2937).
+* Fixed the exception handler being invoked several times on Android, thanks to @1zaman (#3056).
+* `SendChannel.trySendBlocking` is now available on Kotlin/Native (#3064).
+* The exception recovery mechanism now uses `ClassValue` when available (#2997).
+* JNA is updated to 5.9.0 to support Apple M1 (#3001).
+* Obsolete method on internal `Delay` interface is deprecated (#2979).
+* Support of deprecated `CommonPool` is removed.
+* `@ExperimentalTime` is no longer needed for methods that use `Duration` (#3041).
+* JDK 1.6 is no longer required for building the project (#3043).
+* New version of Dokka is used, fixing the memory leak when building the coroutines and providing brand new reference visuals (https://kotlinlang.org/api/kotlinx.coroutines/) (#3051, #3054).
+
+### Changelog relative to version 1.6.0-RC3
+
+* Restored MPP binary compatibility on K/JS and K/N (#3104).
+* Fixed Dispatchers.Main not being fully initialized on Android and Swing (#3101).
+
+## Version 1.6.0-RC3
+
+* Fixed the error in 1.6.0-RC2 because of which `Flow.collect` couldn't be called due to the `@InternalCoroutinesApi` annotation (#3082)
+* Fixed some R8 warnings introduced in 1.6.0-RC (#3090)
+* `TestCoroutineScheduler` now provides a `TimeSource` with its virtual time via the `timeSource` property. Thanks @hfhbd! (#3087)
+
+## Version 1.6.0-RC2
+
+* `@ExperimentalTime` is no longer needed for methods that use `Duration` (#3041).
+* `FlowCollector` is now `fun interface`, and corresponding inline extension is removed (#3047).
+* Fixed the exception handler being invoked several times on Android, thanks to @1zaman (#3056).
+* The deprecated `TestCoroutineScope` is no longer sealed, to simplify migration from it (#3072).
+* `runTest` gives more informative errors when it times out waiting for external completion (#3071).
+* `SendChannel.trySendBlocking` is now available on Kotlin/Native (#3064).
+* Fixed the bug due to which `Dispatchers.Main` was not used for `delay` and `withTimeout` (#3046).
+* JDK 1.6 is no longer required for building the project (#3043).
+* New version of Dokka is used, fixing the memory leak when building the coroutines and providing brand new reference visuals (https://kotlinlang.org/api/kotlinx.coroutines/) (#3051, #3054).
+
+## Version 1.6.0-RC
+
+### kotlinx-coroutines-test rework
+
+* `kotlinx-coroutines-test` became a multiplatform library usable from K/JVM, K/JS, and K/N.
+* Its API was completely reworked to address long-standing issues with consistency, structured concurrency and correctness (#1203, #1609, #2379, #1749, #1204, #1390, #1222, #1395, #1881, #1910, #1772, #1626, #1742, #2082, #2102, #2405, #2462
+ ).
+* The old API is deprecated for removal, but the new API is based on the similar concepts ([README](kotlinx-coroutines-test/README.md)), and the migration path is designed to be graceful: [migration guide](kotlinx-coroutines-test/MIGRATION.md)
+
+### Dispatchers
+
+* Introduced `CoroutineDispatcher.limitedParallelism` that allows obtaining a view of the original dispatcher with limited parallelism (#2919).
+* `Dispatchers.IO.limitedParallelism` usages ignore the bound on the parallelism level of `Dispatchers.IO` itself to avoid starvation (#2943).
+* Introduced new `Dispatchers.shutdown` method for containerized environments (#2558).
+* `newSingleThreadContext` and `newFixedThreadPoolContext` are promoted to delicate API (#2919).
+
+### Breaking changes
+
+* When racing with cancellation, the `future` builder no longer reports unhandled exceptions into the global `CoroutineExceptionHandler`. Thanks @vadimsemenov! (#2774, #2791).
+* `Mutex.onLock` is deprecated for removal (#2794).
+* `Dispatchers.Main` is now used as the default source of time for `delay` and `withTimeout` when present(#2972).
+ * To opt-out from this behaviour, `kotlinx.coroutines.main.delay` system property can be set to `false`.
+* Java target of coroutines build is now 8 instead of 6 (#1589).
+
+### Bug fixes and improvements
+
+* Kotlin is updated to 1.6.0.
+* Kotlin/Native [new memory model](https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/) is now supported in regular builds of coroutines conditionally depending on whether `kotlin.native.binary.memoryModel` is enabled (#2914).
+* Introduced `CopyableThreadContextElement` for mutable context elements shared among multiple coroutines. Thanks @yorickhenning! (#2893).
+* `transformWhile`, `awaitClose`, `ProducerScope`, `merge`, `runningFold`, `runingReduce`, and `scan` are promoted to stable API (#2971).
+* `SharedFlow.subscriptionCount` no longer conflates incoming updates and gives all subscribers a chance to observe a short-lived subscription (#2488, #2863, #2871).
+* `Flow` exception transparency mechanism is improved to be more exception-friendly (#3017, #2860).
+* Cancellation from `flat*` operators that leverage multiple coroutines is no longer propagated upstream (#2964).
+* `SharedFlow.collect` now returns `Nothing` (#2789, #2502).
+* `DisposableHandle` is now `fun interface`, and corresponding inline extension is removed (#2790).
+* Deprecation level of all previously deprecated signatures is raised (#3024).
+* The version file is shipped with each JAR as a resource (#2941).
+* Unhandled exceptions on K/N are passed to the standard library function `processUnhandledException` (#2981).
+* A direct executor is used for `Task` callbacks in `kotlinx-coroutines-play-services` (#2990).
+* Metadata of coroutines artifacts leverages Gradle platform to have all versions of dependencies aligned (#2865).
+* Default `CoroutineExceptionHandler` is loaded eagerly and does not invoke `ServiceLoader` on its exception-handling path (#2552).
+* Fixed the R8 rules for `ServiceLoader` optimization (#2880).
+* Fixed BlockHound integration false-positives (#2894, #2866, #2937).
+* The exception recovery mechanism now uses `ClassValue` when available (#2997).
+* JNA is updated to 5.9.0 to support Apple M1 (#3001).
+* Obsolete method on internal `Delay` interface is deprecated (#2979).
+* Support of deprecated `CommonPool` is removed.
+
## Version 1.5.2
* Kotlin is updated to 1.5.30.
@@ -363,7 +529,7 @@ Gradle version 5.3 or later to use this version of kotlinx.coroutines in your Ko
### Flow
-This version is the first stable release with [`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) API.
+This version is the first stable release with [`Flow`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) API.
All `Flow` API not marked with `@FlowPreview` or `@ExperimentalCoroutinesApi` annotations are stable and here to stay.
Flow declarations marked with `@ExperimentalCoroutinesApi` have [the same guarantees](/docs/topics/compatibility.md#experimental-api) as regular experimental API.
@@ -692,9 +858,9 @@ Visible consequences of include more robust exception handling for large corouti
* All coroutine builders are now extensions on `CoroutineScope` and inherit its `coroutineContext`. Standalone builders are deprecated.
* As a consequence, all nested coroutines launched via builders now automatically establish parent-child relationship and inherit `CoroutineDispatcher`.
* All coroutine builders use `Dispatchers.Default` by default if `CoroutineInterceptor` is not present in their context.
- * [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) became the first-class citizen in `kolinx.coroutines`.
+ * [CoroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) became the first-class citizen in `kolinx.coroutines`.
* `withContext` `block` argument has `CoroutineScope` as a receiver.
- * [GlobalScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/) is introduced to simplify migration to new API and to launch global-level coroutines.
+ * [GlobalScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) is introduced to simplify migration to new API and to launch global-level coroutines.
* `currentScope` and `coroutineScope` builders are introduced to extract and provide `CoroutineScope`.
* Factory methods to create `CoroutineScope` from `CoroutineContext` are introduced.
* `CoroutineScope.isActive` became an extension property.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7d6e32d8..77b727b4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -80,15 +80,12 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad
### Environment requirements
* JDK >= 11 referred to by the `JAVA_HOME` environment variable.
-* JDK 1.6 referred to by the `JDK_16` environment variable.
- It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions.
* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests.
It is OK to have `JDK_18` to a non 1.8 JDK (e.g. `JAVA_HOME`) for external contributions.
-
+
For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`):
```shell
# This assumes JAVA_HOME is set already to a JDK >= 11 version
-export JDK_16="$JAVA_HOME"
export JDK_18="$JAVA_HOME"
```
diff --git a/METADATA b/METADATA
index 798558c5..9826606b 100644
--- a/METADATA
+++ b/METADATA
@@ -5,11 +5,11 @@ third_party {
type: GIT
value: "https://github.com/Kotlin/kotlinx.coroutines"
}
- version: "1.5.2"
+ version: "1.6.4"
license_type: NOTICE
last_upgrade_date {
- year: 2021
- month: 11
- day: 01
+ year: 2022
+ month: 7
+ day: 20
}
}
diff --git a/README.md b/README.md
index 6a13f07a..d9019dc3 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
# kotlinx.coroutines
-[![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
+[![Kotlin Stable](https://kotl.in/badges/stable.svg)](https://kotlinlang.org/docs/components-stability.html)
+[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)
-[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.5.2/pom)
-[![Kotlin](https://img.shields.io/badge/kotlin-1.5.30-blue.svg?logo=kotlin)](http://kotlinlang.org)
+[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.6.4)](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.6.4/pom)
+[![Kotlin](https://img.shields.io/badge/kotlin-1.6.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.5.30` release.
+This is a companion version for the Kotlin `1.6.21` release.
```kotlin
suspend fun main() = coroutineScope {
@@ -61,9 +62,9 @@ suspend fun main() = coroutineScope {
## Documentation
* Presentations and videos:
- * [Introduction to Coroutines](https://www.youtube.com/watch?v=_hfBv0a09Jc) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/introduction-to-coroutines-kotlinconf-2017))
- * [Deep dive into Coroutines](https://www.youtube.com/watch?v=YrrUCSi72E8) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/deep-dive-into-coroutines-on-jvm-kotlinconf-2017))
* [Kotlin Coroutines in Practice](https://www.youtube.com/watch?v=a3agLJQ6vt8) (Roman Elizarov at KotlinConf 2018, [slides](https://www.slideshare.net/elizarov/kotlin-coroutines-in-practice-kotlinconf-2018))
+ * [Deep Dive into Coroutines](https://www.youtube.com/watch?v=YrrUCSi72E8) (Roman Elizarov at KotlinConf 2017, [slides](https://www.slideshare.net/elizarov/deep-dive-into-coroutines-on-jvm-kotlinconf-2017))
+ * [History of Structured Concurrency in Coroutines](https://www.youtube.com/watch?v=Mj5P47F6nJg) (Roman Elizarov at Hydra 2019, [slides](https://speakerdeck.com/elizarov/structured-concurrency))
* Guides and manuals:
* [Guide to kotlinx.coroutines by example](https://kotlinlang.org/docs/coroutines-guide.html) (**read it first**)
* [Guide to UI programming with coroutines](ui/coroutines-guide-ui.md)
@@ -71,7 +72,7 @@ suspend fun main() = coroutineScope {
* [Compatibility policy and experimental annotations](docs/topics/compatibility.md)
* [Change log for kotlinx.coroutines](CHANGES.md)
* [Coroutines design document (KEEP)](https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md)
-* [Full kotlinx.coroutines API reference](https://kotlin.github.io/kotlinx.coroutines)
+* [Full kotlinx.coroutines API reference](https://kotlinlang.org/api/kotlinx.coroutines/)
## Using in your projects
@@ -83,7 +84,7 @@ Add dependencies (you can also add other modules that you need):
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
- <version>1.5.2</version>
+ <version>1.6.4</version>
</dependency>
```
@@ -91,7 +92,7 @@ And make sure that you use the latest Kotlin version:
```xml
<properties>
- <kotlin.version>1.5.30</kotlin.version>
+ <kotlin.version>1.6.21</kotlin.version>
</properties>
```
@@ -99,55 +100,39 @@ And make sure that you use the latest Kotlin version:
Add dependencies (you can also add other modules that you need):
-```groovy
+```kotlin
dependencies {
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
```
And make sure that you use the latest Kotlin version:
-```groovy
-buildscript {
- ext.kotlin_version = '1.5.30'
+```kotlin
+plugins {
+ // For build.gradle.kts (Kotlin DSL)
+ kotlin("jvm") version "1.6.21"
+
+ // For build.gradle (Groovy DSL)
+ id "org.jetbrains.kotlin.jvm" version "1.6.21"
}
```
Make sure that you have `mavenCentral()` in the list of repositories:
-```
-repository {
+```kotlin
+repositories {
mavenCentral()
}
```
-### Gradle Kotlin DSL
-
-Add dependencies (you can also add other modules that you need):
-
-```groovy
-dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
-}
-```
-
-And make sure that you use the latest Kotlin version:
-
-```groovy
-plugins {
- kotlin("jvm") version "1.5.20"
-}
-```
-
-Make sure that you have `mavenCentral()` in the list of repositories.
-
### Android
Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android)
module as a dependency when using `kotlinx.coroutines` on Android:
-```groovy
-implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
+```kotlin
+implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
```
This gives you access to the Android [Dispatchers.Main]
@@ -165,7 +150,8 @@ For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-
The `kotlinx-coroutines-core` artifact contains a resource file that is not required for the coroutines to operate
normally and is only used by the debugger. To exclude it at no loss of functionality, add the following snippet to the
`android` block in your Gradle file for the application subproject:
-```groovy
+
+```kotlin
packagingOptions {
resources.excludes += "DebugProbesKt.bin"
}
@@ -177,10 +163,11 @@ Core modules of `kotlinx.coroutines` are also available for
[Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) and [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html).
In common code that should get compiled for different platforms, you can add a dependency to `kotlinx-coroutines-core` right to the `commonMain` source set:
-```groovy
+
+```kotlin
commonMain {
dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
}
```
@@ -192,7 +179,7 @@ Platform-specific dependencies are recommended to be used only for non-multiplat
#### JS
Kotlin/JS version of `kotlinx.coroutines` is published as
-[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.5.2/jar)
+[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.6.4/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
@@ -201,14 +188,6 @@ Kotlin/Native version of `kotlinx.coroutines` is published as
[`kotlinx-coroutines-core-$platform`](https://mvnrepository.com/search?q=kotlinx-coroutines-core-) where `$platform` is
the target Kotlin/Native platform. [List of currently supported targets](https://github.com/Kotlin/kotlinx.coroutines/blob/master/gradle/compile-native-multiplatform.gradle#L16).
-
-Only single-threaded code (JS-style) on Kotlin/Native is supported in stable versions.
-Additionally, a special `-native-mt` version is released on a regular basis, for the state of multi-threaded coroutines support
-please follow the [corresponding issue](https://github.com/Kotlin/kotlinx.coroutines/issues/462) for the additional details.
-
-Since Kotlin/Native does not generally provide binary compatibility between versions,
-you should use the same version of the Kotlin/Native compiler as was used to build `kotlinx.coroutines`.
-
## Building and Contributing
See [Contributing Guidelines](CONTRIBUTING.md).
@@ -216,102 +195,102 @@ See [Contributing Guidelines](CONTRIBUTING.md).
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
-[Dispatchers]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/index.html
-[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
-[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
-[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
-[SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
-[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
-[Dispatchers.IO]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html
-[asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
-[Promise.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await.html
-[promise]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html
-[Window.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[Dispatchers]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/index.html
+[Dispatchers.Main]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
+[_coroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[_supervisorScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[withTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
+[MainScope()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
+[SupervisorJob()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
+[CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[Dispatchers.IO]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-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
+[Window.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
-[_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
-[filter]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/filter.html
-[map]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[_flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
+[filter]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/filter.html
+[map]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html
<!--- INDEX kotlinx.coroutines.channels -->
-[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
+[Channel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
<!--- INDEX kotlinx.coroutines.selects -->
-[select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
+[select]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
<!--- INDEX kotlinx.coroutines.sync -->
-[Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
-[Semaphore]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html
+[Mutex]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
+[Semaphore]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html
<!--- MODULE kotlinx-coroutines-test -->
<!--- INDEX kotlinx.coroutines.test -->
-[Dispatchers.setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
-[TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html
+[Dispatchers.setMain]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
+[TestCoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html
<!--- MODULE kotlinx-coroutines-debug -->
<!--- INDEX kotlinx.coroutines.debug -->
-[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
+[DebugProbes]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
<!--- INDEX kotlinx.coroutines.debug.junit4 -->
-[CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html
+[CoroutinesTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html
<!--- MODULE kotlinx-coroutines-slf4j -->
<!--- INDEX kotlinx.coroutines.slf4j -->
-[MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html
+[MDCContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html
<!--- MODULE kotlinx-coroutines-jdk8 -->
<!--- INDEX kotlinx.coroutines.future -->
-[CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/await.html
+[CompletionStage.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/await.html
<!--- MODULE kotlinx-coroutines-guava -->
<!--- INDEX kotlinx.coroutines.guava -->
-[ListenableFuture.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/await.html
+[ListenableFuture.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/await.html
<!--- MODULE kotlinx-coroutines-play-services -->
<!--- INDEX kotlinx.coroutines.tasks -->
-[Task.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/await.html
+[Task.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/await.html
<!--- MODULE kotlinx-coroutines-reactive -->
<!--- INDEX kotlinx.coroutines.reactive -->
-[Publisher.collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/collect.html
-[Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-single.html
-[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html
+[Publisher.collect]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/collect.html
+[Publisher.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-single.html
+[kotlinx.coroutines.reactive.publish]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html
<!--- MODULE kotlinx-coroutines-rx2 -->
<!--- INDEX kotlinx.coroutines.rx2 -->
-[rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html
-[rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html
+[rxFlowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html
+[rxSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html
<!--- MODULE kotlinx-coroutines-rx2 -->
<!--- INDEX kotlinx.coroutines.rx2 -->
<!--- MODULE kotlinx-coroutines-reactor -->
<!--- INDEX kotlinx.coroutines.reactor -->
-[flux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html
-[mono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html
+[flux]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html
+[mono]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html
<!--- END -->
diff --git a/RELEASE.md b/RELEASE.md
index edc7726a..ef7cc233 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,15 +1,15 @@
# kotlinx.coroutines release checklist
-To release new `<version>` of `kotlinx-coroutines`:
+To release a new `<version>` of `kotlinx-coroutines`:
-1. Checkout `develop` branch: <br>
+1. Checkout the `develop` branch: <br>
`git checkout develop`
-2. Retrieve the most recent `develop`: <br>
+2. Retrieve the most recent `develop`: <br>
`git pull`
-
+
3. Make sure the `master` branch is fully merged into `develop`:
- `git merge origin/master`
+ `git merge origin/master`
4. Search & replace `<old-version>` with `<version>` across the project files. Should replace in:
* Docs
@@ -17,62 +17,65 @@ To release new `<version>` of `kotlinx-coroutines`:
* [`kotlinx-coroutines-debug/README.md`](kotlinx-coroutines-debug/README.md)
* [`kotlinx-coroutines-test/README.md`](kotlinx-coroutines-test/README.md)
* [`coroutines-guide-ui.md`](ui/coroutines-guide-ui.md)
- * Properties
- * [`gradle.properties`](gradle.properties)
+ * Properties
+ * [`gradle.properties`](gradle.properties)
* Make sure to **exclude** `CHANGES.md` from replacements.
-
- As an alternative approach you can use `./bump-version.sh old_version new_version`
-
+
+ As an alternative approach, you can use `./bump-version.sh old_version new_version`
+
5. Write release notes in [`CHANGES.md`](CHANGES.md):
- * Use old releases as example of style.
+ * Use the old releases for style guidance.
* Write each change on a single line (don't wrap with CR).
- * Study commit message from previous release.
+ * Look through the commit messages since the previous release.
6. Create the branch for this release:
`git checkout -b version-<version>`
-7. Commit updated files to a new version branch:<br>
+7. Commit the updated files to the new version branch:<br>
`git commit -a -m "Version <version>"`
-
-8. Push the new version into the branch:<br>
+
+8. Push the new version to GitHub:<br>
`git push -u origin version-<version>`
-
-9. Create Pull-Request on GitHub from `version-<version>` branch into `master`:
+
+9. Create a Pull-Request on GitHub from the `version-<version>` branch into `master`:
* Review it.
- * Make sure it build on CI.
+ * Make sure it builds on CI.
* Get approval for it.
-
-0. Merge new version branch into `master`:<br>
- `git checkout master`<br>
- `git merge version-<version>`<br>
- `git push`
-1. On [TeamCity integration server](https://teamcity.jetbrains.com/project.html?projectId=KotlinTools_KotlinxCoroutines):
+0. On [TeamCity integration server](https://teamcity.jetbrains.com/project.html?projectId=KotlinTools_KotlinxCoroutines):
* Wait until "Build" configuration for committed `master` branch passes tests.
- * Run "Deploy (Configure, RUN THIS ONE)" configuration with the corresponding new version.
-
-2. In [GitHub](https://github.com/kotlin/kotlinx.coroutines) interface:
- * Create a release named `<version>`.
- * Cut & paste lines from [`CHANGES.md`](CHANGES.md) into description.
+ * Run "Deploy (Configure, RUN THIS ONE)" configuration with the corresponding new version:
+ - Use the `version-<version>` branch
+ - Set the `DeployVersion` build parameter to `<version>`
+ * Wait until all four "Deploy" configurations finish.
-3. Build and publish documentation for web-site
- (make sure you have [Docker](https://www.docker.com/) installed first): <br>
- `site/deploy.sh <version> push`
-
-4. In [Nexus](https://oss.sonatype.org/#stagingRepositories) admin interface:
+1. In [Nexus](https://oss.sonatype.org/#stagingRepositories) admin interface:
* Close the repository and wait for it to verify.
* Release the repository.
-
-5. Announce new release in [Slack](https://kotlinlang.slack.com)
-6. Switch into `develop` branch:<br>
+2. Merge the new version branch into `master`:<br>
+ `git checkout master`<br>
+ `git merge version-<version>`<br>
+ `git push`
+
+3. In [GitHub](https://github.com/kotlin/kotlinx.coroutines) interface:
+ * Create a release named `<version>`, creating the `<version>` tag.
+ * Cut & paste lines from [`CHANGES.md`](CHANGES.md) into description.
+
+4. Announce the new release in [Slack](https://kotlinlang.slack.com)
+
+5. Switch onto the `develop` branch:<br>
`git checkout develop`
-
-7. Fetch the latest `master`:<br>
- `git fetch`
-
-8. Merge release from `master`:<br>
+
+6. Fetch the latest `master`:<br>
+ `git fetch`
+
+7. Merge the release from `master`:<br>
`git merge origin/master`
-
-9. Push updates to `develop`:<br>
- `git push`
+
+8. Push the updates to GitHub:<br>
+ `git push`
+
+9. Build and publish the documentation for the web-site: <br>
+ * Set new value for [`kotlinx.coroutines.release.tag`](https://buildserver.labs.intellij.net/admin/editProject.html?projectId=Kotlin_KotlinSites_Builds_KotlinlangOrg_LibrariesAPIs&tab=projectParams)
+ * And run deploy [configuration](https://buildserver.labs.intellij.net/buildConfiguration/Kotlin_KotlinSites_Builds_KotlinlangOrg_KotlinCoroutinesApi?branch=%3Cdefault%3E&buildTypeTab=overview&mode=builds)
diff --git a/android-annotation-stubs/gen_annotations.sh b/android-annotation-stubs/gen_annotations.sh
new file mode 100755
index 00000000..97d4597a
--- /dev/null
+++ b/android-annotation-stubs/gen_annotations.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+ANNOTATIONS=(
+ org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+)
+
+for a in ${ANNOTATIONS[@]}; do
+ package=${a%.*}
+ class=${a##*.}
+ dir=$(dirname $0)/src/${package//.//}
+ file=${class}.java
+
+ mkdir -p ${dir}
+ sed -e"s/__PACKAGE__/${package}/" -e"s/__CLASS__/${class}/" tmpl.java > ${dir}/${file}
+done
diff --git a/android-annotation-stubs/src/org/codehaus/mojo/animal_sniffer/IgnoreJRERequirement.java b/android-annotation-stubs/src/org/codehaus/mojo/animal_sniffer/IgnoreJRERequirement.java
new file mode 100644
index 00000000..898bcbad
--- /dev/null
+++ b/android-annotation-stubs/src/org/codehaus/mojo/animal_sniffer/IgnoreJRERequirement.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.codehaus.mojo.animal_sniffer;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/* This is an annotation stub to avoid dependencies on annotations that aren't
+ * in the Android platform source tree. */
+
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface IgnoreJRERequirement {}
diff --git a/android-annotation-stubs/tmpl.java b/android-annotation-stubs/tmpl.java
new file mode 100644
index 00000000..91868f2c
--- /dev/null
+++ b/android-annotation-stubs/tmpl.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package __PACKAGE__;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/* This is an annotation stub to avoid dependencies on annotations that aren't
+ * in the Android platform source tree. */
+
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface __CLASS__ {}
diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts
index ce0bff1c..f64c4aaa 100644
--- a/benchmarks/build.gradle.kts
+++ b/benchmarks/build.gradle.kts
@@ -8,7 +8,6 @@ import me.champeau.gradle.*
import org.jetbrains.kotlin.gradle.tasks.*
plugins {
- id("net.ltgt.apt")
id("com.github.johnrengelman.shadow")
id("me.champeau.gradle.jmh") apply false
}
@@ -31,8 +30,6 @@ tasks.named<KotlinCompile>("compileJmhKotlin") {
}
}
-
-
// It is better to use the following to run benchmarks, otherwise you may get unexpected errors:
// ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark"
extensions.configure<JMHPluginExtension>("jmh") {
@@ -46,21 +43,34 @@ extensions.configure<JMHPluginExtension>("jmh") {
// includeTests = false
}
-tasks.named<Jar>("jmhJar") {
+val jmhJarTask = tasks.named<Jar>("jmhJar") {
archiveBaseName by "benchmarks"
archiveClassifier by null
archiveVersion by null
destinationDirectory.file("$rootDir")
}
+tasks {
+ // For some reason the DuplicatesStrategy from jmh is not enough
+ // and errors with duplicates appear unless I force it to WARN only:
+ withType<Copy> {
+ duplicatesStrategy = DuplicatesStrategy.WARN
+ }
+
+ build {
+ dependsOn(jmhJarTask)
+ }
+}
+
dependencies {
- compile("org.openjdk.jmh:jmh-core:1.26")
- compile("io.projectreactor:reactor-core:${version("reactor")}")
- compile("io.reactivex.rxjava2:rxjava:2.1.9")
- compile("com.github.akarnokd:rxjava2-extensions:0.20.8")
+ implementation("org.openjdk.jmh:jmh-core:1.26")
+ implementation("io.projectreactor:reactor-core:${version("reactor")}")
+ implementation("io.reactivex.rxjava2:rxjava:2.1.9")
+ implementation("com.github.akarnokd:rxjava2-extensions:0.20.8")
- compile("com.typesafe.akka:akka-actor_2.12:2.5.0")
- compile(project(":kotlinx-coroutines-core"))
+ implementation("com.typesafe.akka:akka-actor_2.12:2.5.0")
+ implementation(project(":kotlinx-coroutines-core"))
+ implementation(project(":kotlinx-coroutines-reactive"))
// add jmh dependency on main
"jmhImplementation"(sourceSets.main.get().runtimeClasspath)
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt
index 9948a371..ce64c6a4 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt
+++ b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt
@@ -25,12 +25,11 @@ abstract class ParametrizedDispatcherBase : CoroutineScope {
private var closeable: Closeable? = null
@Setup
- @UseExperimental(InternalCoroutinesApi::class)
open fun setup() {
coroutineContext = when {
dispatcher == "fjp" -> ForkJoinPool.commonPool().asCoroutineDispatcher()
dispatcher == "scheduler" -> {
- ExperimentalCoroutineDispatcher(CORES_COUNT).also { closeable = it }
+ Dispatchers.Default
}
dispatcher.startsWith("ftp") -> {
newFixedThreadPoolContext(dispatcher.substring(4).toInt(), dispatcher).also { closeable = it }
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt
index 40ddc8ec..9e1bfc43 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt
+++ b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt
@@ -6,13 +6,10 @@ package benchmarks
import benchmarks.common.*
import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher
-import kotlinx.coroutines.sync.Semaphore
-import kotlinx.coroutines.sync.withPermit
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.sync.*
import org.openjdk.jmh.annotations.*
-import java.util.concurrent.ForkJoinPool
-import java.util.concurrent.TimeUnit
+import java.util.concurrent.*
@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MICROSECONDS)
@@ -84,7 +81,7 @@ open class SemaphoreBenchmark {
enum class SemaphoreBenchDispatcherCreator(val create: (parallelism: Int) -> CoroutineDispatcher) {
FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }),
- EXPERIMENTAL({ parallelism -> ExperimentalCoroutineDispatcher(corePoolSize = parallelism, maxPoolSize = parallelism) })
+ EXPERIMENTAL({ parallelism -> Dispatchers.Default }) // TODO doesn't take parallelism into account
}
private const val WORK_INSIDE = 80
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt
index e7bd1f5f..acfb3f3c 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt
+++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt
@@ -30,7 +30,7 @@ open class SequencePlaysScrabble : ShakespearePlaysScrabble() {
val bonusForDoubleLetter: (String) -> Int = { word: String ->
toBeMaxed(word)
.map { letterScores[it - 'a'.toInt()] }
- .max()!!
+ .maxOrNull()!!
}
val score3: (String) -> Int = { word: String ->
diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt
index a6f0a473..d874f3bb 100644
--- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt
+++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt
@@ -27,10 +27,8 @@ import kotlin.coroutines.*
@State(Scope.Benchmark)
open class PingPongWithBlockingContext {
- @UseExperimental(InternalCoroutinesApi::class)
- private val experimental = ExperimentalCoroutineDispatcher(8)
- @UseExperimental(InternalCoroutinesApi::class)
- private val blocking = experimental.blocking(8)
+ private val experimental = Dispatchers.Default
+ private val blocking = Dispatchers.IO.limitedParallelism(8)
private val threadPool = newFixedThreadPoolContext(8, "PongCtx")
@TearDown
diff --git a/build.gradle b/build.gradle
index e4b12ff3..4d6af165 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,21 +2,16 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+
+import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.konan.target.HostManager
-import org.gradle.util.VersionNumber
import org.jetbrains.dokka.gradle.DokkaTaskPartial
-import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
-apply plugin: 'jdk-convention'
-apply from: rootProject.file("gradle/opt-in.gradle")
+import static Projects.*
-def coreModule = "kotlinx-coroutines-core"
-// Not applicable for Kotlin plugin
-def sourceless = ['kotlinx.coroutines', 'kotlinx-coroutines-bom', 'integration-testing']
-def internal = ['kotlinx.coroutines', 'benchmarks', 'integration-testing']
-// Not published
-def unpublished = internal + ['example-frontend-js', 'android-unit-tests']
+apply plugin: 'jdk-convention'
buildscript {
/*
@@ -47,12 +42,6 @@ buildscript {
}
}
- if (using_snapshot_version) {
- repositories {
- mavenLocal()
- }
- }
-
repositories {
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
@@ -65,11 +54,13 @@ buildscript {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version"
classpath "org.jetbrains.kotlinx:kotlinx-knit:$knit_version"
- classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version"
+ classpath "com.github.node-gradle:gradle-node-plugin:$gradle_node_version"
classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$binary_compatibility_validator_version"
+ classpath "ru.vyarus:gradle-animalsniffer-plugin:1.5.4" // Android API check
+ classpath "org.jetbrains.kotlinx:kover:$kover_version"
// JMH plugins
- classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0"
+ classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
}
CacheRedirector.configureBuildScript(buildscript, rootProject)
@@ -102,13 +93,6 @@ allprojects {
kotlin_version = rootProject.properties['kotlin_snapshot_version']
}
- if (using_snapshot_version) {
- repositories {
- mavenLocal()
- maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
- }
- }
-
ext.unpublished = unpublished
// This project property is set during nightly stress test
@@ -121,11 +105,14 @@ allprojects {
}
apply plugin: "binary-compatibility-validator"
+apply plugin: "base"
+apply plugin: "kover-conventions"
+
apiValidation {
ignoredProjects += unpublished + ["kotlinx-coroutines-bom"]
if (build_snapshot_train) {
ignoredProjects.remove("example-frontend-js")
- ignoredProjects.add("kotlinx-coroutines-core")
+ ignoredProjects.add(coreModule)
}
ignoredPackages += "kotlinx.coroutines.internal"
}
@@ -139,30 +126,54 @@ allprojects {
*/
google()
mavenCentral()
+ maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" }
}
}
+// needs to be before evaluationDependsOn due to weird Gradle ordering
+apply plugin: "animalsniffer-conventions"
+
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
evaluationDependsOn(":$coreModule")
- def platform = PlatformKt.platformOf(it)
- apply plugin: "kotlin-${platform}-conventions"
- dependencies {
- // See comment below for rationale, it will be replaced with "project" dependency
- api project(":$coreModule")
- // the only way IDEA can resolve test classes
- testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
+ if (isMultiplatform(it)) {
+ apply plugin: "kotlin-multiplatform"
+ apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle")
+ apply from: rootProject.file("gradle/compile-common.gradle")
+
+ if (rootProject.ext["native_targets_enabled"] as Boolean) {
+ apply from: rootProject.file("gradle/compile-native-multiplatform.gradle")
+ }
+
+ apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
+ apply from: rootProject.file("gradle/publish-npm-js.gradle")
+ kotlin.sourceSets.commonMain.dependencies {
+ api project(":$coreModule")
+ }
+ kotlin.sourceSets.jvmTest.dependencies {
+ implementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
+ }
+ } else {
+ def platform = PlatformKt.platformOf(it)
+ apply plugin: "kotlin-${platform}-conventions"
+ dependencies {
+ api project(":$coreModule")
+ // the only way IDEA can resolve test classes
+ testImplementation project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
+ }
}
}
+apply plugin: "bom-conventions"
+
// Configure subprojects with Kotlin sources
configure(subprojects.findAll { !sourceless.contains(it.name) }) {
// Use atomicfu plugin, it also adds all the necessary dependencies
apply plugin: 'kotlinx-atomicfu'
// Configure options for all Kotlin compilation tasks
- tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
- kotlinOptions.freeCompilerArgs += optInAnnotations.collect { "-Xopt-in=" + it }
+ tasks.withType(AbstractKotlinCompile).all {
+ kotlinOptions.freeCompilerArgs += OptInPreset.optInAnnotations.collect { "-Xopt-in=" + it }
kotlinOptions.freeCompilerArgs += "-progressive"
// Disable KT-36770 for RxJava2 integration
kotlinOptions.freeCompilerArgs += "-XXLanguage:-ProhibitUsingNullableTypeParameterAgainstNotNullAnnotated"
@@ -189,7 +200,7 @@ if (build_snapshot_train) {
}
println "Manifest of kotlin-compiler-embeddable.jar for coroutines"
- configure(subprojects.findAll { it.name == "kotlinx-coroutines-core" }) {
+ configure(subprojects.findAll { it.name == coreModule }) {
configurations.matching { it.name == "kotlinCompilerClasspath" }.all {
resolvedConfiguration.getFiles().findAll { it.name.contains("kotlin-compiler-embeddable") }.each {
def manifest = zipTree(it).matching {
@@ -206,9 +217,8 @@ if (build_snapshot_train) {
// Redefine source sets because we are not using 'kotlin/main/fqn' folder convention
configure(subprojects.findAll {
- !sourceless.contains(it.name) &&
+ !sourceless.contains(it.name) && !isMultiplatform(it) &&
it.name != "benchmarks" &&
- it.name != coreModule &&
it.name != "example-frontend-js"
}) {
// Pure JS and pure MPP doesn't have this notion and are configured separately
@@ -221,11 +231,11 @@ configure(subprojects.findAll {
}
}
-def core_docs_url = "https://kotlin.github.io/kotlinx.coroutines/$coreModule/"
+def core_docs_url = "https://kotlinlang.org/api/kotlinx.coroutines/$coreModule/"
def core_docs_file = "$projectDir/kotlinx-coroutines-core/build/dokka/htmlPartial/package-list"
apply plugin: "org.jetbrains.dokka"
-configure(subprojects.findAll { !unpublished.contains(it.name) }) {
+configure(subprojects.findAll { !unpublished.contains(it.name) && it.name != coreModule }) {
if (it.name != 'kotlinx-coroutines-bom') {
apply from: rootProject.file('gradle/dokka.gradle.kts')
}
@@ -245,10 +255,44 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) {
}
}
}
+
+ def thisProject = it
+ if (thisProject.name in sourceless) {
+ return
+ }
+
+ def versionFileTask = thisProject.tasks.register("versionFileTask") {
+ def name = thisProject.name.replace("-", "_")
+ def versionFile = thisProject.layout.buildDirectory.file("${name}.version")
+ it.outputs.file(versionFile)
+
+ it.doLast {
+ versionFile.get().asFile.text = version.toString()
+ }
+ }
+
+ List<String> jarTasks
+ if (isMultiplatform(it)) {
+ jarTasks = ["jvmJar", "metadataJar"]
+ } else if (it.name == "kotlinx-coroutines-debug") {
+ // We shadow debug module instead of just packaging it
+ jarTasks = ["shadowJar"]
+ } else {
+ jarTasks = ["jar"]
+ }
+
+ for (name in jarTasks) {
+ thisProject.tasks.named(name, Jar) {
+ it.dependsOn versionFileTask
+ it.from(versionFileTask) {
+ into("META-INF")
+ }
+ }
+ }
}
// Report Kotlin compiler version when building project
-println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION")
+println("Using Kotlin compiler version: $KotlinCompilerVersion.VERSION")
// --------------- Cache redirector ---------------
@@ -262,8 +306,6 @@ def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm"
task deploy(dependsOn: publishTasks)
-apply plugin: 'base'
-
clean.dependsOn gradle.includedBuilds.collect { it.task(':clean') }
// --------------- Knit configuration ---------------
@@ -271,7 +313,7 @@ clean.dependsOn gradle.includedBuilds.collect { it.task(':clean') }
apply plugin: 'kotlinx-knit'
knit {
- siteRoot = "https://kotlin.github.io/kotlinx.coroutines"
+ siteRoot = "https://kotlinlang.org/api/kotlinx.coroutines"
moduleRoots = [".", "integration", "reactive", "ui"]
moduleDocs = "build/dokka/htmlPartial"
dokkaMultiModuleRoot = "build/dokka/htmlMultiModule/"
@@ -302,12 +344,12 @@ allprojects { subProject ->
.matching {
// Excluding substituted project itself because of circular dependencies, but still do it
// for "*Test*" configurations
- subProject.name != "kotlinx-coroutines-core" || it.name.contains("Test")
+ subProject.name != coreModule || it.name.contains("Test")
}
.configureEach { conf ->
conf.resolutionStrategy.dependencySubstitution {
- substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core"))
- .using(project(":kotlinx-coroutines-core"))
+ substitute(module("org.jetbrains.kotlinx:$coreModule"))
+ .using(project(":$coreModule"))
.because("Because Kotlin compiler embeddable leaks coroutines into the runtime classpath, " +
"triggering all sort of incompatible class changes errors")
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index c54e226a..eaa03f2f 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -19,7 +19,6 @@ repositories {
maven("https://plugins.gradle.org/m2")
}
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
-
if (buildSnapshotTrain) {
mavenLocal()
}
@@ -44,6 +43,25 @@ fun version(target: String): String {
dependencies {
implementation(kotlin("gradle-plugin", version("kotlin")))
- implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}")
- implementation("org.jetbrains.dokka:dokka-core:${version("dokka")}")
+ /*
+ * Dokka is compiled with language level = 1.4, but depends on Kotlin 1.6.0, while
+ * our version of Gradle bundles Kotlin 1.4.x and can read metadata only up to 1.5.x,
+ * thus we're excluding stdlib compiled with 1.6.0 from dependencies.
+ */
+ implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") {
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
+ }
+ implementation("org.jetbrains.dokka:dokka-core:${version("dokka")}") {
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
+ }
+ implementation("ru.vyarus:gradle-animalsniffer-plugin:1.5.3") // Android API check
+ implementation("org.jetbrains.kotlinx:kover:${version("kover")}") {
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7")
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
+ }
}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
index e30c3ee5..c2e859f6 100644
--- a/buildSrc/settings.gradle.kts
+++ b/buildSrc/settings.gradle.kts
@@ -4,7 +4,6 @@
pluginManagement {
val build_snapshot_train: String? by settings
repositories {
- maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/")
val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true
if (cacheRedirectorEnabled) {
println("Redirecting repositories for buildSrc buildscript")
diff --git a/buildSrc/src/main/kotlin/OptInPreset.kt b/buildSrc/src/main/kotlin/OptInPreset.kt
new file mode 100644
index 00000000..fdcdb8ec
--- /dev/null
+++ b/buildSrc/src/main/kotlin/OptInPreset.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:JvmName("OptInPreset")
+
+val optInAnnotations = listOf(
+ "kotlin.RequiresOptIn",
+ "kotlin.experimental.ExperimentalTypeInference",
+ "kotlin.ExperimentalMultiplatform",
+ "kotlinx.coroutines.DelicateCoroutinesApi",
+ "kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "kotlinx.coroutines.ObsoleteCoroutinesApi",
+ "kotlinx.coroutines.InternalCoroutinesApi",
+ "kotlinx.coroutines.FlowPreview")
diff --git a/buildSrc/src/main/kotlin/Projects.kt b/buildSrc/src/main/kotlin/Projects.kt
index dd284b61..af709893 100644
--- a/buildSrc/src/main/kotlin/Projects.kt
+++ b/buildSrc/src/main/kotlin/Projects.kt
@@ -1,8 +1,31 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-
-import org.gradle.api.Project
+@file:JvmName("Projects")
+import org.gradle.api.*
fun Project.version(target: String): String =
property("${target}_version") as String
+
+val coreModule = "kotlinx-coroutines-core"
+val testModule = "kotlinx-coroutines-test"
+
+val multiplatform = setOf(coreModule, testModule)
+// Not applicable for Kotlin plugin
+val sourceless = setOf("kotlinx.coroutines", "kotlinx-coroutines-bom")
+val internal = setOf("kotlinx.coroutines", "benchmarks")
+// Not published
+val unpublished = internal + setOf("example-frontend-js", "android-unit-tests")
+
+val Project.isMultiplatform: Boolean get() = name in multiplatform
+
+// Projects that we do not check for Android API level 14 check due to various limitations
+val androidNonCompatibleProjects = setOf(
+ "kotlinx-coroutines-debug",
+ "kotlinx-coroutines-swing",
+ "kotlinx-coroutines-javafx",
+ "kotlinx-coroutines-jdk8",
+ "kotlinx-coroutines-jdk9",
+ "kotlinx-coroutines-reactor",
+ "kotlinx-coroutines-test"
+)
diff --git a/buildSrc/src/main/kotlin/Publishing.kt b/buildSrc/src/main/kotlin/Publishing.kt
index 8c6dd5de..cb612c50 100644
--- a/buildSrc/src/main/kotlin/Publishing.kt
+++ b/buildSrc/src/main/kotlin/Publishing.kt
@@ -7,6 +7,7 @@
import org.gradle.api.Project
import org.gradle.api.artifacts.dsl.*
import org.gradle.api.publish.maven.*
+import org.gradle.kotlin.dsl.*
import org.gradle.plugins.signing.*
import java.net.*
@@ -56,6 +57,11 @@ fun configureMavenPublication(rh: RepositoryHandler, project: Project) {
password = project.getSensitiveProperty("libs.sonatype.password")
}
}
+
+ // Something that's easy to "clean" for development, not mavenLocal
+ rh.maven("${project.rootProject.buildDir}/repo") {
+ name = "buildRepo"
+ }
}
fun signPublicationIfKeyPresent(project: Project, publication: MavenPublication) {
diff --git a/buildSrc/src/main/kotlin/SourceSets.kt b/buildSrc/src/main/kotlin/SourceSets.kt
new file mode 100644
index 00000000..3ad1dd4d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/SourceSets.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+import org.jetbrains.kotlin.gradle.plugin.*
+
+fun KotlinSourceSet.configureMultiplatform() {
+ val srcDir = if (name.endsWith("Main")) "src" else "test"
+ val platform = name.dropLast(4)
+ kotlin.srcDir("$platform/$srcDir")
+ if (name == "jvmMain") {
+ resources.srcDir("$platform/resources")
+ } else if (name == "jvmTest") {
+ resources.srcDir("$platform/test-resources")
+ }
+ languageSettings {
+ optInAnnotations.forEach { optIn(it) }
+ progressiveMode = true
+ }
+}
diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt
index b3152d7a..afe2627a 100644
--- a/buildSrc/src/main/kotlin/UnpackAar.kt
+++ b/buildSrc/src/main/kotlin/UnpackAar.kt
@@ -2,18 +2,49 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+import org.gradle.api.*
import org.gradle.api.artifacts.transform.InputArtifact
import org.gradle.api.artifacts.transform.TransformAction
import org.gradle.api.artifacts.transform.TransformOutputs
import org.gradle.api.artifacts.transform.TransformParameters
+import org.gradle.api.attributes.*
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
+import org.gradle.kotlin.dsl.*
import java.io.File
import java.nio.file.Files
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
-// TODO move back to kotlinx-coroutines-play-services when it's migrated to the kts
+// Attributes used by aar dependencies
+val artifactType = Attribute.of("artifactType", String::class.java)
+val unpackedAar = Attribute.of("unpackedAar", Boolean::class.javaObjectType)
+
+fun Project.configureAar() = configurations.configureEach {
+ afterEvaluate {
+ if (isCanBeResolved && !isCanBeConsumed) {
+ attributes.attribute(unpackedAar, true) // request all AARs to be unpacked
+ }
+ }
+}
+
+fun DependencyHandlerScope.configureAarUnpacking() {
+ attributesSchema {
+ attribute(unpackedAar)
+ }
+
+ artifactTypes {
+ create("aar") {
+ attributes.attribute(unpackedAar, false)
+ }
+ }
+
+ registerTransform(UnpackAar::class.java) {
+ from.attribute(unpackedAar, false).attribute(artifactType, "aar")
+ to.attribute(unpackedAar, true).attribute(artifactType, "jar")
+ }
+}
+
@Suppress("UnstableApiUsage")
abstract class UnpackAar : TransformAction<TransformParameters.None> {
@get:InputArtifact
diff --git a/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts b/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts
new file mode 100644
index 00000000..f00a0b31
--- /dev/null
+++ b/buildSrc/src/main/kotlin/animalsniffer-conventions.gradle.kts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+import ru.vyarus.gradle.plugin.animalsniffer.*
+
+configure(subprojects) {
+ // Skip JDK 8 projects or unpublished ones
+ if (!shouldSniff()) return@configure
+ apply(plugin = "ru.vyarus.animalsniffer")
+ project.plugins.withType(JavaPlugin::class.java) {
+ configure<AnimalSnifferExtension> {
+ sourceSets = listOf((project.extensions.getByName("sourceSets") as SourceSetContainer).getByName("main"))
+ }
+ val signature: Configuration by configurations
+ dependencies {
+ signature("net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature")
+ signature("org.codehaus.mojo.signature:java17:1.0@signature")
+ }
+ }
+}
+
+fun Project.shouldSniff(): Boolean {
+ // Skip all non-JVM projects
+ if (platformOf(project) != "jvm") return false
+ val name = project.name
+ if (name in unpublished || name in sourceless || name in androidNonCompatibleProjects) return false
+ return true
+}
diff --git a/buildSrc/src/main/kotlin/bom-conventions.gradle.kts b/buildSrc/src/main/kotlin/bom-conventions.gradle.kts
new file mode 100644
index 00000000..45f30edf
--- /dev/null
+++ b/buildSrc/src/main/kotlin/bom-conventions.gradle.kts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+import org.gradle.kotlin.dsl.*
+import org.jetbrains.kotlin.gradle.dsl.*
+
+
+configure(subprojects.filter { it.name !in unpublished }) {
+ if (name == "kotlinx-coroutines-bom" || name == "kotlinx.coroutines") return@configure
+ if (isMultiplatform) {
+ kotlinExtension.sourceSets.getByName("jvmMain").dependencies {
+ api(project.dependencies.platform(project(":kotlinx-coroutines-bom")))
+ }
+ } else {
+ dependencies {
+ "api"(platform(project(":kotlinx-coroutines-bom")))
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts
index c7744f87..90847f45 100644
--- a/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/kotlin-jvm-conventions.gradle.kts
@@ -11,8 +11,8 @@ plugins {
}
java {
- sourceCompatibility = JavaVersion.VERSION_1_6
- targetCompatibility = JavaVersion.VERSION_1_6
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
diff --git a/buildSrc/src/main/kotlin/kover-conventions.gradle.kts b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts
new file mode 100644
index 00000000..052e2bb6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/kover-conventions.gradle.kts
@@ -0,0 +1,54 @@
+import kotlinx.kover.api.*
+import kotlinx.kover.tasks.*
+
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+apply(plugin = "kover")
+
+val notCovered = sourceless + internal + unpublished
+
+val expectedCoverage = mutableMapOf(
+ // These have lower coverage in general, it can be eventually fixed
+ "kotlinx-coroutines-swing" to 70, // awaitFrame is not tested
+ "kotlinx-coroutines-javafx" to 39, // JavaFx is not tested on TC because its graphic subsystem cannot be initialized in headless mode
+
+ // Reactor has lower coverage in general due to various fatal error handling features
+ "kotlinx-coroutines-reactor" to 75)
+
+extensions.configure<KoverExtension> {
+ disabledProjects = notCovered
+ /*
+ * Is explicitly enabled on TC in a separate build step.
+ * Examples:
+ * ./gradlew :p:check -- doesn't verify coverage
+ * ./gradlew :p:check -Pkover.enabled=true -- verifies coverage
+ * ./gradlew :p:koverReport -Pkover.enabled=true -- generates report
+ */
+ isDisabled = !(properties["kover.enabled"]?.toString()?.toBoolean() ?: false)
+ // TODO remove when updating Kover to version 0.5.x
+ intellijEngineVersion.set("1.0.657")
+}
+
+subprojects {
+ val projectName = name
+ if (projectName in notCovered) return@subprojects
+ tasks.withType<KoverVerificationTask> {
+ rule {
+ bound {
+ /*
+ * 85 is our baseline that we aim to raise to 90+.
+ * Missing coverage is typically due to bugs in the agent
+ * (e.g. signatures deprecated with an error are counted),
+ * sometimes it's various diagnostic `toString` or `catch` for OOMs/VerificationErrors,
+ * but some places are definitely worth visiting.
+ */
+ minValue = expectedCoverage[projectName] ?: 85 // COVERED_LINES_PERCENTAGE
+ }
+ }
+ }
+
+ tasks.withType<KoverHtmlReportTask> {
+ htmlReportDir.set(file(rootProject.buildDir.toString() + "/kover/" + project.name + "/html"))
+ }
+}
diff --git a/bump-version.sh b/bump-version.sh
index ae0fc0b0..e49910f4 100755
--- a/bump-version.sh
+++ b/bump-version.sh
@@ -21,6 +21,7 @@ update_version "kotlinx-coroutines-debug/README.md"
update_version "kotlinx-coroutines-test/README.md"
update_version "ui/coroutines-guide-ui.md"
update_version "gradle.properties"
+update_version "integration-test/gradle.properties"
# Escape dots, e.g. 1.0.0 -> 1\.0\.0
escaped_old_version=$(echo $old_version | sed s/[.]/\\\\./g)
diff --git a/coroutines-guide.md b/coroutines-guide.md
index 3b4707cf..3cc035ae 100644
--- a/coroutines-guide.md
+++ b/coroutines-guide.md
@@ -1,14 +1,3 @@
The main coroutines guide has moved to the [docs folder](docs/topics/coroutines-guide.md) and split up into smaller documents.
-## Table of contents
-
-<!--- TOC_REF docs/topics/coroutines-basics.md -->
-<!--- TOC_REF docs/topics/cancellation-and-timeouts.md -->
-<!--- TOC_REF docs/topics/composing-suspending-functions.md -->
-<!--- TOC_REF docs/topics/coroutine-context-and-dispatchers.md -->
-<!--- TOC_REF docs/topics/flow.md -->
-<!--- TOC_REF docs/topics/channels.md -->
-<!--- TOC_REF docs/topics/exception-handling.md -->
-<!--- TOC_REF docs/topics/shared-mutable-state-and-concurrency.md -->
-<!--- TOC_REF docs/topics/select-expression.md -->
-<!--- END -->
+It is recommended to read the guide on the [kotlinlang website](https://kotlinlang.org/docs/coroutines-guide.html), with proper HTML formatting and runnable samples.
diff --git a/docs/images/coroutine-breakpoint.png b/docs/images/coroutine-breakpoint.png
index b547e77d..d0e34e89 100644
--- a/docs/images/coroutine-breakpoint.png
+++ b/docs/images/coroutine-breakpoint.png
Binary files differ
diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png
index 0afe9925..c8243072 100644
--- a/docs/images/coroutine-idea-debugging-1.png
+++ b/docs/images/coroutine-idea-debugging-1.png
Binary files differ
diff --git a/docs/images/flow-breakpoint.png b/docs/images/flow-breakpoint.png
index aa98e18e..a7a38cce 100644
--- a/docs/images/flow-breakpoint.png
+++ b/docs/images/flow-breakpoint.png
Binary files differ
diff --git a/docs/images/flow-build-project.png b/docs/images/flow-build-project.png
index 22186213..12221c77 100644
--- a/docs/images/flow-build-project.png
+++ b/docs/images/flow-build-project.png
Binary files differ
diff --git a/docs/images/flow-debug-project.png b/docs/images/flow-debug-project.png
index 98d392e2..f5b20bd9 100644
--- a/docs/images/flow-debug-project.png
+++ b/docs/images/flow-debug-project.png
Binary files differ
diff --git a/docs/topics/cancellation-and-timeouts.md b/docs/topics/cancellation-and-timeouts.md
index 5221db92..0cbafb59 100644
--- a/docs/topics/cancellation-and-timeouts.md
+++ b/docs/topics/cancellation-and-timeouts.md
@@ -103,6 +103,42 @@ job: I'm sleeping 4 ...
main: Now I can quit.
-->
+The same problem can be observed by catching a [CancellationException] and not rethrowing it:
+
+```kotlin
+import kotlinx.coroutines.*
+
+fun main() = runBlocking {
+//sampleStart
+ val job = launch(Dispatchers.Default) {
+ repeat(5) { i ->
+ try {
+ // print a message twice a second
+ println("job: I'm sleeping $i ...")
+ delay(500)
+ } catch (e: Exception) {
+ // log the exception
+ println(e)
+ }
+ }
+ }
+ delay(1300L) // delay a bit
+ println("main: I'm tired of waiting!")
+ job.cancelAndJoin() // cancels the job and waits for its completion
+ println("main: Now I can quit.")
+//sampleEnd
+}
+```
+{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
+
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt).
+>
+{type="note"}
+
+While catching `Exception` is an anti-pattern, this issue may surface in more subtle ways, like when using the
+[`runCatching`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html) function,
+which does not rethrow [CancellationException].
+
## Making computation code cancellable
There are two approaches to making computation code cancellable. The first one is to periodically
@@ -137,7 +173,7 @@ fun main() = runBlocking {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt).
>
{type="note"}
@@ -154,8 +190,8 @@ main: Now I can quit.
## Closing resources with `finally`
-Cancellable suspending functions throw [CancellationException] on cancellation which can be handled in
-the usual way. For example, `try {...} finally {...}` expression and Kotlin `use` function execute their
+Cancellable suspending functions throw [CancellationException] on cancellation, which can be handled in
+the usual way. For example, the `try {...} finally {...}` expression and Kotlin's `use` function execute their
finalization actions normally when a coroutine is cancelled:
```kotlin
@@ -182,7 +218,7 @@ fun main() = runBlocking {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt).
>
{type="note"}
@@ -237,7 +273,7 @@ fun main() = runBlocking {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt).
>
{type="note"}
@@ -275,7 +311,7 @@ fun main() = runBlocking {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt).
>
{type="note"}
@@ -318,7 +354,7 @@ fun main() = runBlocking {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt).
>
{type="note"}
@@ -378,7 +414,7 @@ fun main() {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt).
>
{type="note"}
@@ -387,13 +423,13 @@ fun main() {
If you run the above code you'll see that it does not always print zero, though it may depend on the timings
of your machine you may need to tweak timeouts in this example to actually see non-zero values.
-> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe,
-> since it always happens from the same main thread. More on that will be explained in the next chapter
+> Note that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe,
+> since it always happens from the same main thread. More on that will be explained in the chapter
> on coroutine context.
>
{type="note"}
-To workaround this problem you can store a reference to the resource in the variable as opposed to returning it
+To work around this problem you can store a reference to the resource in the variable as opposed to returning it
from the `withTimeout` block.
```kotlin
@@ -431,7 +467,7 @@ fun main() {
```
{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
-> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt).
+> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt).
>
{type="note"}
@@ -444,18 +480,18 @@ This example always prints zero. Resources do not leak.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[cancelAndJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html
-[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
-[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
-[CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
-[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[isActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
-[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
-[withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[cancelAndJoin]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html
+[Job.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
+[Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[CancellationException]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
+[yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
+[isActive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[NonCancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
+[withTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
+[withTimeoutOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
<!--- END -->
diff --git a/docs/topics/channels.md b/docs/topics/channels.md
index 7f41eaec..3b8b11fb 100644
--- a/docs/topics/channels.md
+++ b/docs/topics/channels.md
@@ -573,6 +573,7 @@ Now let's see how it works in practice:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
+//sampleStart
fun main() = runBlocking<Unit> {
val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // create ticker channel
var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
@@ -596,7 +597,9 @@ fun main() = runBlocking<Unit> {
tickerChannel.cancel() // indicate that no more elements are needed
}
+//sampleEnd
```
+{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt).
>
@@ -624,26 +627,26 @@ delay between elements.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[kotlin.coroutines.CoroutineContext.cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-children.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[kotlin.coroutines.CoroutineContext.cancelChildren]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-children.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
<!--- INDEX kotlinx.coroutines.channels -->
-[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
-[SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
-[ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
-[SendChannel.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html
-[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
-[consumeEach]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/consume-each.html
-[Channel()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel.html
-[ticker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/ticker.html
-[ReceiveChannel.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/cancel.html
-[TickerMode.FIXED_DELAY]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-ticker-mode/-f-i-x-e-d_-d-e-l-a-y/index.html
+[Channel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
+[SendChannel.send]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
+[ReceiveChannel.receive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
+[SendChannel.close]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html
+[produce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
+[consumeEach]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/consume-each.html
+[Channel()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel.html
+[ticker]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/ticker.html
+[ReceiveChannel.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/cancel.html
+[TickerMode.FIXED_DELAY]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-ticker-mode/-f-i-x-e-d_-d-e-l-a-y/index.html
<!--- INDEX kotlinx.coroutines.selects -->
-[select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
+[select]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
<!--- END -->
diff --git a/docs/topics/compatibility.md b/docs/topics/compatibility.md
index 97448116..9570f198 100644
--- a/docs/topics/compatibility.md
+++ b/docs/topics/compatibility.md
@@ -75,7 +75,7 @@ When some API is deprecated, it goes through multiple stages and there is at lea
this option in case of unforeseen problems such as security holes.
## Using annotated API
-All API annotations are [kotlin.Experimental](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-experimental/index.html).
+All API annotations are [kotlin.Experimental](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-experimental/).
It is done in order to produce compilation warning about using experimental or obsolete API.
Warnings can be disabled either programmatically for a specific call site or globally for the whole module.
@@ -117,13 +117,13 @@ For the Maven project, a warning can be disabled by passing a compiler flag in y
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
<!--- INDEX kotlinx.coroutines -->
-[ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
-[FlowPreview]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-flow-preview/index.html
-[ObsoleteCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-obsolete-coroutines-api/index.html
-[InternalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-internal-coroutines-api/index.html
+[ExperimentalCoroutinesApi]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
+[FlowPreview]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-flow-preview/index.html
+[ObsoleteCoroutinesApi]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-obsolete-coroutines-api/index.html
+[InternalCoroutinesApi]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-internal-coroutines-api/index.html
<!--- END -->
diff --git a/docs/topics/composing-suspending-functions.md b/docs/topics/composing-suspending-functions.md
index e244d8c2..8ed73361 100644
--- a/docs/topics/composing-suspending-functions.md
+++ b/docs/topics/composing-suspending-functions.md
@@ -402,15 +402,15 @@ Computation failed with ArithmeticException
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
-[CoroutineStart.LAZY]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-l-a-z-y/index.html
-[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
-[Job.start]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html
-[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[CoroutineStart.LAZY]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-l-a-z-y/index.html
+[Deferred.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
+[Job.start]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html
+[GlobalScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[_coroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
<!--- END -->
diff --git a/docs/topics/coroutine-context-and-dispatchers.md b/docs/topics/coroutine-context-and-dispatchers.md
index 96482148..32d18919 100644
--- a/docs/topics/coroutine-context-and-dispatchers.md
+++ b/docs/topics/coroutine-context-and-dispatchers.md
@@ -148,7 +148,7 @@ The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in I
The **Debug** tool window contains the **Coroutines** tab. In this tab, you can find information about both currently running and suspended coroutines.
The coroutines are grouped by the dispatcher they are running on.
-![Debugging coroutines](coroutine-idea-debugging-1.png)
+![Debugging coroutines](coroutine-idea-debugging-1.png){width=700}
With the coroutine debugger, you can:
* Check the state of each coroutine.
@@ -306,7 +306,7 @@ However, this parent-child relation can be explicitly overriden in one of two wa
1. When a different scope is explicitly specified when launching a coroutine (for example, `GlobalScope.launch`),
then it does not inherit a `Job` from the parent scope.
-2. When a different `Job` object is passed as the context for the new coroutine (as show in the example below),
+2. When a different `Job` object is passed as the context for the new coroutine (as shown in the example below),
then it overrides the `Job` of the parent scope.
In both cases, the launched coroutine is not tied to the scope it was launched from and operates independently.
@@ -334,8 +334,8 @@ fun main() = runBlocking<Unit> {
}
delay(500)
request.cancel() // cancel processing of the request
- delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")
+ delay(1000) // delay the main thread for a second to see what happens
//sampleEnd
}
```
@@ -350,8 +350,8 @@ The output of this code is:
```text
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
-job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?
+job1: I am not affected by cancellation of the request
```
<!--- TEST -->
@@ -658,28 +658,28 @@ that should be implemented.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
-[ExecutorCoroutineDispatcher.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-executor-coroutine-dispatcher/close.html
-[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[isActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html
-[CoroutineScope.coroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/coroutine-context.html
-[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
-[CoroutineName]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html
-[CoroutineScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html
-[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
-[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
-[asContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
-[ensurePresent]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-present.html
-[ThreadContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/index.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[Dispatchers.Unconfined]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[newSingleThreadContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
+[ExecutorCoroutineDispatcher.close]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-executor-coroutine-dispatcher/close.html
+[runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[DEBUG_PROPERTY_NAME]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[isActive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html
+[CoroutineScope.coroutineContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/coroutine-context.html
+[Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[CoroutineName]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html
+[CoroutineScope()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html
+[MainScope()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
+[Dispatchers.Main]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
+[asContextElement]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
+[ensurePresent]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-present.html
+[ThreadContextElement]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/index.html
<!--- END -->
diff --git a/docs/topics/coroutines-basics.md b/docs/topics/coroutines-basics.md
index 5d9d0e6d..ad33dcfb 100644
--- a/docs/topics/coroutines-basics.md
+++ b/docs/topics/coroutines-basics.md
@@ -75,7 +75,7 @@ Coroutines follow a principle of
which delimits the lifetime of the coroutine. The above example shows that [runBlocking] establishes the corresponding
scope and that is why the previous example waits until `World!` is printed after a second's delay and only then exits.
-In the real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not
+In a real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not
lost and do not leak. An outer scope cannot complete until all its children coroutines complete.
Structured concurrency also ensures that any errors in the code are properly reported and are never lost.
@@ -245,14 +245,17 @@ Done
<!--- TEST -->
-## Coroutines ARE light-weight
+## Coroutines are light-weight
-Run the following code:
+Coroutines are less resource-intensive than JVM threads. Code that exhausts the
+JVM's available memory when using threads can be expressed using coroutines
+without hitting resource limits. For example, the following code launches
+100000 distinct coroutines that each wait 5 seconds and then print a period
+('.') while consuming very little memory:
```kotlin
import kotlinx.coroutines.*
-//sampleStart
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
@@ -261,8 +264,9 @@ fun main() = runBlocking {
}
}
}
-//sampleEnd
```
+<!-- While coroutines do have a smaller memory footprint than threads, this
+example will exhaust the playground's heap memory; don't make it runnable. -->
> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-basic-06.kt).
>
@@ -270,19 +274,18 @@ fun main() = runBlocking {
<!--- TEST lines.size == 1 && lines[0] == ".".repeat(100_000) -->
-It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot.
-
-Now, try that with threads (remove `runBlocking`, replace `launch` with `thread`, and replace `delay` with `Thread.sleep`).
-What would happen? (Most likely your code will produce some sort of out-of-memory error)
+If you write the same program using threads (remove `runBlocking`, replace
+`launch` with `thread`, and replace `delay` with `Thread.sleep`), it will
+likely consume too much memory and throw an out-of-memory error.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[_coroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
<!--- END -->
diff --git a/docs/topics/coroutines-guide.md b/docs/topics/coroutines-guide.md
index 0c6432f6..73c4c1ac 100644
--- a/docs/topics/coroutines-guide.md
+++ b/docs/topics/coroutines-guide.md
@@ -33,6 +33,6 @@ In order to use coroutines as well as follow the examples in this guide, you nee
* [Guide to UI programming with coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md)
* [Coroutines design document (KEEP)](https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md)
-* [Full kotlinx.coroutines API reference](https://kotlin.github.io/kotlinx.coroutines)
+* [Full kotlinx.coroutines API reference](https://kotlinlang.org/api/kotlinx.coroutines/)
* [Best practices for coroutines in Android](https://developer.android.com/kotlin/coroutines/coroutines-best-practices)
* [Additional Android resources for Kotlin coroutines and flow](https://developer.android.com/kotlin/coroutines/additional-resources)
diff --git a/docs/topics/debug-coroutines-with-idea.md b/docs/topics/debug-coroutines-with-idea.md
index e59075e0..2541c924 100644
--- a/docs/topics/debug-coroutines-with-idea.md
+++ b/docs/topics/debug-coroutines-with-idea.md
@@ -16,11 +16,11 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui
The `src` directory contains Kotlin source files and resources. The `main.kt` file contains sample code that will print `Hello World!`.
-2. Change code in the `main()` function:
+3. Change code in the `main()` function:
- * Use the [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine.
- * Use the [`async()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) function to create coroutines that compute deferred values `a` and `b`.
- * Use the [`await()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) function to await the computation result.
+ * Use the [`runBlocking()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine.
+ * Use the [`async()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) function to create coroutines that compute deferred values `a` and `b`.
+ * Use the [`await()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) function to await the computation result.
* Use the [`println()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/println.html) function to print computing status and the result of multiplication to the output.
```kotlin
@@ -61,7 +61,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui
![Debug the coroutine](coroutine-debug-1.png)
-3. Resume the debugger session by clicking **Resume program** in the **Debug** tool window:
+3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window:
![Debug the coroutine](coroutine-debug-2.png)
@@ -70,7 +70,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui
* The second coroutine is calculating the `a` value – it has the **RUNNING** status.
* The third coroutine has the **CREATED** status and isn’t calculating the value of `b`.
-4. Resume the debugger session by clicking **Resume program** in the **Debug** tool window:
+4. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window:
![Build a console application](coroutine-debug-3.png)
diff --git a/docs/topics/debug-flow-with-idea.md b/docs/topics/debug-flow-with-idea.md
index 745dcb17..b769e795 100644
--- a/docs/topics/debug-flow-with-idea.md
+++ b/docs/topics/debug-flow-with-idea.md
@@ -10,7 +10,7 @@ The tutorial assumes you have prior knowledge of the [coroutines](coroutines-gui
## Create a Kotlin flow
-Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html) with a slow emitter and a slow collector:
+Create a Kotlin [flow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html) with a slow emitter and a slow collector:
1. Open a Kotlin project in IntelliJ IDEA. If you don't have a project, [create one](jvm-get-started.md#create-an-application).
@@ -18,10 +18,10 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
The `src` directory contains Kotlin source files and resources. The `main.kt` file contains sample code that will print `Hello World!`.
-2. Create the `simple()` function that returns a flow of three numbers:
+3. Create the `simple()` function that returns a flow of three numbers:
- * Use the [`delay()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) function to imitate CPU-consuming blocking code. It suspends the coroutine for 100 ms without blocking the thread.
- * Produce the values in the `for` loop using the [`emit()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html) function.
+ * Use the [`delay()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) function to imitate CPU-consuming blocking code. It suspends the coroutine for 100 ms without blocking the thread.
+ * Produce the values in the `for` loop using the [`emit()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html) function.
```kotlin
import kotlinx.coroutines.*
@@ -36,11 +36,11 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
}
```
-3. Change the code in the `main()` function:
+4. Change the code in the `main()` function:
- * Use the [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine.
- * Collect the emitted values using the [`collect()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html) function.
- * Use the [`delay()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) function to imitate CPU-consuming code. It suspends the coroutine for 300 ms without blocking the thread.
+ * Use the [`runBlocking()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) block to wrap a coroutine.
+ * Collect the emitted values using the [`collect()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html) function.
+ * Use the [`delay()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) function to imitate CPU-consuming code. It suspends the coroutine for 300 ms without blocking the thread.
* Print the collected value from the flow using the [`println()`](https://kotlinlang.org/api/latest/jvm/stdlib/stdlib/kotlin.io/println.html) function.
```kotlin
@@ -53,13 +53,13 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
}
```
-4. Build the code by clicking **Build Project**.
+5. Build the code by clicking **Build Project**.
![Build an application](flow-build-project.png)
## Debug the coroutine
-1. Set a breakpoint at the at the line where the `emit()` function is called:
+1. Set a breakpoint at the line where the `emit()` function is called:
![Build a console application](flow-breakpoint.png)
@@ -74,7 +74,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
![Debug the coroutine](flow-debug-1.png)
-3. Resume the debugger session by clicking **Resume program** in the **Debug** tool window. The program stops at the same breakpoint.
+3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window. The program stops at the same breakpoint.
![Debug the coroutine](flow-resume-debug.png)
@@ -88,7 +88,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
2. Enhance the code to run the emitter and collector concurrently:
- * Add a call to the [`buffer()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html) function to run the emitter and collector concurrently. `buffer()` stores emitted values and runs the flow collector in a separate coroutine.
+ * Add a call to the [`buffer()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html) function to run the emitter and collector concurrently. `buffer()` stores emitted values and runs the flow collector in a separate coroutine.
```kotlin
fun main() = runBlocking<Unit> {
@@ -101,7 +101,7 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
}
```
-4. Build the code by clicking **Build Project**.
+3. Build the code by clicking **Build Project**.
## Debug a Kotlin flow with two coroutines
@@ -117,10 +117,10 @@ Create a Kotlin [flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-corou
The `buffer()` function buffers emitted values from the flow.
The emitter coroutine has the **RUNNING** status, and the collector coroutine has the **SUSPENDED** status.
-2. Resume the debugger session by clicking **Resume program** in the **Debug** tool window.
+3. Resume the debugger session by clicking **Resume Program** in the **Debug** tool window.
![Debugging coroutines](flow-debug-4.png)
Now the collector coroutine has the **RUNNING** status, while the emitter coroutine has the **SUSPENDED** status.
- You can dig deeper into each coroutine to debug your code. \ No newline at end of file
+ You can dig deeper into each coroutine to debug your code.
diff --git a/docs/topics/debugging.md b/docs/topics/debugging.md
index 5ff4d549..a2c32e0b 100644
--- a/docs/topics/debugging.md
+++ b/docs/topics/debugging.md
@@ -7,7 +7,6 @@
* [Stacktrace recovery](#stacktrace-recovery)
* [Stacktrace recovery machinery](#stacktrace-recovery-machinery)
* [Debug agent](#debug-agent)
- * [Debug agent and Android](#debug-agent-and-android)
* [Android optimization](#android-optimization)
<!--- END -->
@@ -77,12 +76,6 @@ additionally enhancing stacktraces with information where coroutine was created.
The full tutorial of how to use debug agent can be found in the corresponding [readme](../../kotlinx-coroutines-debug/README.md).
-### Debug agent and Android
-
-Unfortunately, Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`.
-
-Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support android-gradle 3.3
-
<!---
Make an exception googlable
java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;
@@ -98,18 +91,18 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana
## Android optimization
In optimized (release) builds with R8 version 1.6.0 or later both
-[Debugging mode](../../docs/debugging.md#debug-mode) and
-[Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery)
+[Debugging mode](debugging.md#debug-mode) and
+[Stacktrace recovery](debugging.md#stacktrace-recovery)
are permanently turned off.
For more details see ["Optimization" section for Android](../../ui/kotlinx-coroutines-android/README.md#optimization).
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
-[CoroutineName]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html
-[CopyableThrowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/index.html
-[CopyableThrowable.createCopy]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/create-copy.html
+[DEBUG_PROPERTY_NAME]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
+[CoroutineName]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html
+[CopyableThrowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/index.html
+[CopyableThrowable.createCopy]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/create-copy.html
<!--- MODULE kotlinx-coroutines-debug -->
<!--- END -->
diff --git a/docs/topics/exception-handling.md b/docs/topics/exception-handling.md
index 35e645f4..8bf8c106 100644
--- a/docs/topics/exception-handling.md
+++ b/docs/topics/exception-handling.md
@@ -15,7 +15,7 @@ When these builders are used to create a _root_ coroutine, that is not a _child_
the former builders treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`,
while the latter are relying on the user to consume the final
exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive]
-([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section).
+([produce] and [receive][ReceiveChannel.receive] are covered in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section).
It can be demonstrated by a simple example that creates root coroutines using the [GlobalScope]:
@@ -28,6 +28,7 @@ It can be demonstrated by a simple example that creates root coroutines using th
```kotlin
import kotlinx.coroutines.*
+//sampleStart
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
@@ -47,7 +48,9 @@ fun main() = runBlocking {
println("Caught ArithmeticException")
}
}
+//sampleEnd
```
+{kotlin-runnable="true" kotlin-min-compiler-version="1.3"}
> You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt).
>
@@ -68,19 +71,13 @@ Caught ArithmeticException
## CoroutineExceptionHandler
It is possible to customize the default behavior of printing **uncaught** exceptions to the console.
-[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as generic `catch` block for
+[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as a generic `catch` block for
this root coroutine and all its children where custom exception handling may take place.
It is similar to [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
with the corresponding exception when the handler is called. Normally, the handler is used to
log the exception, show some kind of error message, terminate, and/or restart the application.
-On JVM it is possible to redefine global exception handler for all coroutines by registering [CoroutineExceptionHandler] via
-[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
-Global exception handler is similar to
-[`Thread.defaultUncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDefaultUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler))
-which is used when no more specific handlers are registered.
-On Android, `uncaughtExceptionPreHandler` is installed as a global coroutine exception handler.
`CoroutineExceptionHandler` is invoked only on **uncaught** exceptions &mdash; exceptions that were not handled in any other way.
In particular, all _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of
@@ -349,7 +346,7 @@ hierarchy of coroutines. Let us take a look at the case when unidirectional canc
A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI's child tasks
have failed, it is not always necessary to cancel (effectively kill) the whole UI component,
-but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer needed.
+but if the UI component is destroyed (and its job is cancelled), then it is necessary to cancel all child jobs as their results are no longer needed.
Another example is a server process that spawns multiple child jobs and needs to _supervise_
their execution, tracking their failures and only restarting the failed ones.
@@ -509,25 +506,25 @@ The scope is completed
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
-[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
-[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
-[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
-[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
-[Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html
-[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
-[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
+[CancellationException]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[Deferred.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
+[GlobalScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
+[CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[Job.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
+[runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[SupervisorJob()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
+[Job()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html
+[_coroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[_supervisorScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
<!--- INDEX kotlinx.coroutines.channels -->
-[actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
-[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
-[ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
+[actor]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
+[produce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
+[ReceiveChannel.receive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
<!--- END -->
diff --git a/docs/topics/flow.md b/docs/topics/flow.md
index 311411f8..538a9d67 100644
--- a/docs/topics/flow.md
+++ b/docs/topics/flow.md
@@ -1832,9 +1832,9 @@ Integration modules include conversions from and to `Flow`, integration with Rea
<!-- stdlib references -->
[collections]: https://kotlinlang.org/docs/reference/collections-overview.html
-[List]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html
+[List]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/
[forEach]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/for-each.html
-[Sequence]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/index.html
+[Sequence]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/
[Sequence.zip]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/zip.html
[Sequence.flatten]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/flatten.html
[Sequence.flatMap]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/flat-map.html
@@ -1843,54 +1843,54 @@ Integration modules include conversions from and to `Flow`, integration with Rea
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
-[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
-[ensureActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html
-[CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[withTimeoutOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[Dispatchers.Main]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Job.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
+[Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[ensureActive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html
+[CancellationException]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
-[_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
-[FlowCollector.emit]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html
-[collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html
-[flowOf]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-of.html
-[map]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html
-[filter]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/filter.html
-[transform]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
-[take]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/take.html
-[toList]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-list.html
-[toSet]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-set.html
-[first]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/first.html
-[single]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/single.html
-[reduce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/reduce.html
-[fold]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/fold.html
-[flowOn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html
-[buffer]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html
-[conflate]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.html
-[collectLatest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect-latest.html
-[zip]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/zip.html
-[combine]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine.html
-[onEach]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-each.html
-[flatMapConcat]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-concat.html
-[flattenConcat]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flatten-concat.html
-[flatMapMerge]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-merge.html
-[flattenMerge]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flatten-merge.html
-[DEFAULT_CONCURRENCY]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-d-e-f-a-u-l-t_-c-o-n-c-u-r-r-e-n-c-y.html
-[flatMapLatest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html
-[catch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html
-[onCompletion]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-completion.html
-[launchIn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html
-[IntRange.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/as-flow.html
-[cancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/cancellable.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[_flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
+[FlowCollector.emit]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html
+[collect]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html
+[flowOf]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-of.html
+[map]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html
+[filter]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/filter.html
+[transform]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
+[take]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/take.html
+[toList]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-list.html
+[toSet]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-set.html
+[first]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/first.html
+[single]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/single.html
+[reduce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/reduce.html
+[fold]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/fold.html
+[flowOn]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html
+[buffer]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html
+[conflate]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.html
+[collectLatest]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect-latest.html
+[zip]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/zip.html
+[combine]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine.html
+[onEach]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-each.html
+[flatMapConcat]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-concat.html
+[flattenConcat]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flatten-concat.html
+[flatMapMerge]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-merge.html
+[flattenMerge]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flatten-merge.html
+[DEFAULT_CONCURRENCY]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-d-e-f-a-u-l-t_-c-o-n-c-u-r-r-e-n-c-y.html
+[flatMapLatest]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html
+[catch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html
+[onCompletion]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-completion.html
+[launchIn]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html
+[IntRange.asFlow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/as-flow.html
+[cancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/cancellable.html
<!--- END -->
diff --git a/docs/topics/select-expression.md b/docs/topics/select-expression.md
index 3d20ff39..f3055737 100644
--- a/docs/topics/select-expression.md
+++ b/docs/topics/select-expression.md
@@ -213,7 +213,7 @@ Channel 'a' is closed
<!--- TEST -->
-There are couple of observations to make out of it.
+There are a couple of observations to make out of it.
First of all, `select` is _biased_ to the first clause. When several clauses are selectable at the same time,
the first one among them gets selected. Here, both channels are constantly producing strings, so `a` channel,
@@ -228,7 +228,7 @@ channel is already closed.
Select expression has [onSend][SendChannel.onSend] clause that can be used for a great good in combination
with a biased nature of selection.
-Let us write an example of producer of integers that sends its values to a `side` channel when
+Let us write an example of a producer of integers that sends its values to a `side` channel when
the consumers on its primary channel cannot keep up with it:
```kotlin
@@ -490,18 +490,18 @@ Channel was closed
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
+[Deferred.onAwait]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
<!--- INDEX kotlinx.coroutines.channels -->
-[ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
-[ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
-[ReceiveChannel.onReceiveCatching]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
-[SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
-[SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
+[ReceiveChannel.receive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
+[ReceiveChannel.onReceive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
+[ReceiveChannel.onReceiveCatching]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
+[SendChannel.send]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
+[SendChannel.onSend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
<!--- INDEX kotlinx.coroutines.selects -->
-[select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
+[select]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
<!--- END -->
diff --git a/docs/topics/shared-mutable-state-and-concurrency.md b/docs/topics/shared-mutable-state-and-concurrency.md
index 40b0134a..99cc42bc 100644
--- a/docs/topics/shared-mutable-state-and-concurrency.md
+++ b/docs/topics/shared-mutable-state-and-concurrency.md
@@ -9,7 +9,7 @@ but others are unique.
## The problem
-Let us launch a hundred coroutines all doing the same action thousand times.
+Let us launch a hundred coroutines all doing the same action a thousand times.
We'll also measure their completion time for further comparisons:
```kotlin
@@ -384,7 +384,7 @@ single reference to the actor can be carried around as its handle.
The first step of using an actor is to define a class of messages that an actor is going to process.
Kotlin's [sealed classes](https://kotlinlang.org/docs/reference/sealed-classes.html) are well suited for that purpose.
We define `CounterMsg` sealed class with `IncCounter` message to increment a counter and `GetCounter` message
-to get its value. The later needs to send a response. A [CompletableDeferred] communication
+to get its value. The latter needs to send a response. A [CompletableDeferred] communication
primitive, that represents a single value that will be known (communicated) in the future,
is used here for that purpose.
@@ -494,20 +494,20 @@ have to switch to a different context at all.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[CompletableDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-deferred/index.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[CompletableDeferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-deferred/index.html
<!--- INDEX kotlinx.coroutines.sync -->
-[Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
-[Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
-[Mutex.unlock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/unlock.html
-[withLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/with-lock.html
+[Mutex]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
+[Mutex.lock]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
+[Mutex.unlock]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/unlock.html
+[withLock]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/with-lock.html
<!--- INDEX kotlinx.coroutines.channels -->
-[actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
-[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
+[actor]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
+[produce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
<!--- END -->
diff --git a/gradle.properties b/gradle.properties
index 26e5147c..e452a07e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,18 +3,18 @@
#
# Kotlin
-version=1.5.2-SNAPSHOT
+version=1.6.4-SNAPSHOT
group=org.jetbrains.kotlinx
-kotlin_version=1.5.30
+kotlin_version=1.6.21
# Dependencies
junit_version=4.12
junit5_version=5.7.0
-atomicfu_version=0.16.3
-knit_version=0.3.0
+atomicfu_version=0.17.3
+knit_version=0.4.0
html_version=0.7.2
lincheck_version=2.14
-dokka_version=1.5.0
+dokka_version=1.6.21
byte_buddy_version=1.10.9
reactor_version=3.4.1
reactive_streams_version=1.0.3
@@ -22,20 +22,21 @@ rxjava2_version=2.2.8
rxjava3_version=3.0.2
javafx_version=11.0.2
javafx_plugin_version=0.0.8
-binary_compatibility_validator_version=0.7.0
+binary_compatibility_validator_version=0.11.0
+kover_version=0.5.0
blockhound_version=1.0.2.RELEASE
-jna_version=5.5.0
+jna_version=5.9.0
# Android versions
android_version=4.1.1.4
androidx_annotation_version=1.1.0
-robolectric_version=4.0.2
+robolectric_version=4.4
baksmali_version=2.2.7
# JS
kotlin.js.compiler=both
-gradle_node_version=1.2.0
-node_version=8.9.3
+gradle_node_version=3.1.1
+node_version=10.0.0
npm_version=5.7.1
mocha_version=6.2.2
mocha_headless_chrome_version=1.8.2
@@ -53,7 +54,8 @@ jekyll_version=4.0
# JS IR backend sometimes crashes with out-of-memory
# TODO: Remove once KT-37187 is fixed
-org.gradle.jvmargs=-Xmx4g
+org.gradle.jvmargs=-Xmx3g
kotlin.mpp.enableCompatibilityMetadataVariant=true
kotlin.mpp.stability.nowarn=true
+kotlinx.atomicfu.enableIrTransformation=true
diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle
index d6df7e40..c6fc757c 100644
--- a/gradle/compile-js-multiplatform.gradle
+++ b/gradle/compile-js-multiplatform.gradle
@@ -60,7 +60,7 @@ compileTestJsLegacy.configure {
task populateNodeModules(type: Copy, dependsOn: compileTestJsLegacy) {
// we must copy output that is transformed by atomicfu
from(kotlin.js().compilations.main.output.allOutputs)
- into "$node.nodeModulesDir/node_modules"
+ into node.nodeProjectDir.dir("node_modules")
def configuration = configurations.hasProperty("jsLegacyTestRuntimeClasspath")
? configurations.jsLegacyTestRuntimeClasspath
diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle
index 5e650427..88b71797 100644
--- a/gradle/compile-jvm-multiplatform.gradle
+++ b/gradle/compile-jvm-multiplatform.gradle
@@ -2,12 +2,16 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-sourceCompatibility = 1.6
-targetCompatibility = 1.6
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
kotlin {
jvm {}
sourceSets {
+ jvmMain.dependencies {
+ compileOnly "org.codehaus.mojo:animal-sniffer-annotations:1.20"
+ }
+
jvmTest.dependencies {
api "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Workaround to make addSuppressed work in tests
diff --git a/gradle/dokka.gradle.kts b/gradle/dokka.gradle.kts
index 659890a3..2470ded3 100644
--- a/gradle/dokka.gradle.kts
+++ b/gradle/dokka.gradle.kts
@@ -37,12 +37,8 @@ tasks.withType(DokkaTaskPartial::class).configureEach {
packageListUrl.set(rootProject.projectDir.toPath().resolve("site/stdlib.package.list").toUri().toURL())
}
- if (project.name != "kotlinx-coroutines-core") {
+ if (!project.isMultiplatform) {
dependsOn(project.configurations["compileClasspath"])
- doFirst {
- // resolve classpath only during execution
- classpath.from(project.configurations["compileClasspath"].files)// + project.sourceSets.main.output.files)
- }
}
}
}
@@ -66,10 +62,6 @@ if (project.name == "kotlinx-coroutines-core") {
val jvmMain by getting {
makeLinkMapping(project.file("jvm"))
}
-
- configureEach {
- classpath.from(project.configurations["jvmCompileClasspath"].files)
- }
}
}
}
diff --git a/gradle/node-js.gradle b/gradle/node-js.gradle
index 42f101c5..5eddc5fa 100644
--- a/gradle/node-js.gradle
+++ b/gradle/node-js.gradle
@@ -2,13 +2,13 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-apply plugin: 'com.moowork.node'
+apply plugin: 'com.github.node-gradle.node'
node {
version = "$node_version"
npmVersion = "$npm_version"
download = true
- nodeModulesDir = file(buildDir)
+ nodeProjectDir = file(buildDir)
}
// Configures testing for JS modules
@@ -25,7 +25,7 @@ task prepareNodePackage(type: Copy) {
from("npm") {
exclude 'package.json'
}
- into "$node.nodeModulesDir"
+ into node.nodeProjectDir
}
npmInstall.dependsOn prepareNodePackage
diff --git a/gradle/opt-in.gradle b/gradle/opt-in.gradle
deleted file mode 100644
index 22f022db..00000000
--- a/gradle/opt-in.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-ext.optInAnnotations = [
- "kotlin.RequiresOptIn",
- "kotlin.experimental.ExperimentalTypeInference",
- "kotlin.ExperimentalMultiplatform",
- "kotlinx.coroutines.DelicateCoroutinesApi",
- "kotlinx.coroutines.ExperimentalCoroutinesApi",
- "kotlinx.coroutines.ObsoleteCoroutinesApi",
- "kotlinx.coroutines.InternalCoroutinesApi",
- "kotlinx.coroutines.FlowPreview"]
diff --git a/gradle/publish-npm-js.gradle b/gradle/publish-npm-js.gradle
index 382c6749..9d415277 100644
--- a/gradle/publish-npm-js.gradle
+++ b/gradle/publish-npm-js.gradle
@@ -40,17 +40,15 @@ task preparePublishNpm(type: Copy) {
task publishNpm(type: NpmTask, dependsOn: [preparePublishNpm]) {
workingDir = npmDeployDir
-
- doFirst {
- def npmDeployTag = distTag(version)
- def deployArgs = ['publish',
- "--//registry.npmjs.org/:_authToken=$authToken",
- "--tag=$npmDeployTag"]
- if (dryRun == "true") {
- println("$npmDeployDir \$ npm arguments: $deployArgs")
- args = ['pack']
- } else {
- args = deployArgs
- }
+
+ def npmDeployTag = distTag(version)
+ def deployArgs = ['publish',
+ "--//registry.npmjs.org/:_authToken=$authToken",
+ "--tag=$npmDeployTag"]
+ if (dryRun == "true") {
+ println("$npmDeployDir \$ npm arguments: $deployArgs")
+ args = ['pack']
+ } else {
+ args = deployArgs
}
}
diff --git a/gradle/publish.gradle b/gradle/publish.gradle
index 3a0a4224..f3b1561d 100644
--- a/gradle/publish.gradle
+++ b/gradle/publish.gradle
@@ -6,17 +6,18 @@ import org.gradle.util.VersionNumber
// Configures publishing of Maven artifacts to Maven Central
-apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'signing'
// ------------- tasks
-def isMultiplatform = project.name == "kotlinx-coroutines-core"
+def isMultiplatform = project.name == "kotlinx-coroutines-core" || project.name == "kotlinx-coroutines-test"
def isBom = project.name == "kotlinx-coroutines-bom"
if (!isBom) {
- apply plugin: "com.github.johnrengelman.shadow"
+ if (project.name == "kotlinx-coroutines-debug") {
+ apply plugin: "com.github.johnrengelman.shadow"
+ }
// empty xxx-javadoc.jar
task javadocJar(type: Jar) {
@@ -44,11 +45,7 @@ publishing {
// Configure java publications for regular non-MPP modules
publications {
maven(MavenPublication) {
- if (project.name == "kotlinx-coroutines-debug") {
- project.shadow.component(it)
- } else {
- from components.java
- }
+ from components.java
artifact sourcesJar
}
}
diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle
index d011eeaa..27d2e5b3 100644
--- a/gradle/test-mocha-js.gradle
+++ b/gradle/test-mocha-js.gradle
@@ -9,7 +9,7 @@ task installDependenciesMochaNode(type: NpmTask, dependsOn: [npmInstall]) {
"mocha@$mocha_version",
"source-map-support@$source_map_support_version",
'--no-save']
- if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]
+ if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
}
def compileJsLegacy = tasks.hasProperty("compileKotlinJsLegacy")
@@ -22,9 +22,9 @@ def compileTestJsLegacy = tasks.hasProperty("compileTestKotlinJsLegacy")
// todo: use atomicfu-transformed test files here (not critical)
task testMochaNode(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaNode]) {
- script = file("$node.nodeModulesDir/node_modules/mocha/bin/mocha")
- args = [compileTestJsLegacy.outputFile, '--require', 'source-map-support/register']
- if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter']
+ script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha")
+ args = [compileTestJsLegacy.outputFile.path, '--require', 'source-map-support/register']
+ if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
}
def jsLegacyTestTask = project.tasks.findByName('jsLegacyTest') ? jsLegacyTest : jsTest
@@ -40,8 +40,8 @@ task installDependenciesMochaChrome(type: NpmTask, dependsOn: [npmInstall]) {
"kotlin@$kotlin_version",
"kotlin-test@$kotlin_version",
'--no-save']
- if (project.hasProperty("teamcity")) args += [
- "mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]
+ if (project.hasProperty("teamcity")) args.addAll([
+ "mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
}
def mochaChromeTestPage = file("$buildDir/test-page.html")
@@ -51,19 +51,20 @@ task prepareMochaChrome(dependsOn: [compileTestJsLegacy, installDependenciesMoch
}
prepareMochaChrome.doLast {
+ def nodeProjDir = node.nodeProjectDir.getAsFile().get()
mochaChromeTestPage.text = """<!DOCTYPE html>
<html>
<head>
<title>Mocha Tests</title>
<meta charset="utf-8">
- <link rel="stylesheet" href="$node.nodeModulesDir/node_modules/mocha/mocha.css">
+ <link rel="stylesheet" href="$nodeProjDir/node_modules/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
- <script src="$node.nodeModulesDir/node_modules/mocha/mocha.js"></script>
+ <script src="$nodeProjDir/node_modules/mocha/mocha.js"></script>
<script>mocha.setup('bdd');</script>
- <script src="$node.nodeModulesDir/node_modules/kotlin/kotlin.js"></script>
- <script src="$node.nodeModulesDir/node_modules/kotlin-test/kotlin-test.js"></script>
+ <script src="$nodeProjDir/node_modules/kotlin/kotlin.js"></script>
+ <script src="$nodeProjDir/node_modules/kotlin-test/kotlin-test.js"></script>
<script src="$compileJsLegacy.outputFile"></script>
<script src="$compileTestJsLegacy.outputFile"></script>
<script>mocha.run();</script>
@@ -73,9 +74,9 @@ prepareMochaChrome.doLast {
}
task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) {
- script = file("$node.nodeModulesDir/node_modules/mocha-headless-chrome/bin/start")
- args = [compileTestJsLegacy.outputFile, '--file', mochaChromeTestPage]
- if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter']
+ script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha-headless-chrome/bin/start")
+ args = [compileTestJsLegacy.outputFile.path, '--file', mochaChromeTestPage]
+ if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
}
// todo: Commented out because mocha-headless-chrome does not work on TeamCity
@@ -90,13 +91,13 @@ task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) {
"jsdom-global@$jsdom_global_version",
"source-map-support@$source_map_support_version",
'--no-save']
- if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]
+ if (project.hasProperty("teamcity")) args.addAll(["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"])
}
task testMochaJsdom(type: NodeTask, dependsOn: [compileTestJsLegacy, installDependenciesMochaJsdom]) {
- script = file("$node.nodeModulesDir/node_modules/mocha/bin/mocha")
- args = [compileTestJsLegacy.outputFile, '--require', 'source-map-support/register', '--require', 'jsdom-global/register']
- if (project.hasProperty("teamcity")) args += ['--reporter', 'mocha-teamcity-reporter']
+ script = file("${node.nodeProjectDir.getAsFile().get()}/node_modules/mocha/bin/mocha")
+ args = [compileTestJsLegacy.outputFile.path, '--require', 'source-map-support/register', '--require', 'jsdom-global/register']
+ if (project.hasProperty("teamcity")) args.addAll(['--reporter', 'mocha-teamcity-reporter'])
}
jsLegacyTestTask.dependsOn testMochaJsdom
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d7ae858e..f57489cf 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,6 +4,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/integration-testing/README.md b/integration-testing/README.md
index 4754081a..0ede9b25 100644
--- a/integration-testing/README.md
+++ b/integration-testing/README.md
@@ -1,14 +1,13 @@
# Integration tests
-This is a supplementary subproject of kotlinx.coroutines that provides
-integration tests.
+This is a supplementary project that provides integration tests.
The tests are the following:
-* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu
- In order for the test to work, one needs to run gradle with `-PdryRun=true`.
- `-PdryRun` affects `npmPublish` so that it only provides a packed publication
- and does not in fact attempt to send the build for publication.
-* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath
+* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath.
+* `CoreAgentTest` checks that `kotlinx-coroutines-core` can be run as a Java agent.
* `DebugAgentTest` checks that the coroutine debugger can be run as a Java agent.
+* `smokeTest` builds the test project that depends on coroutines.
-All the available tests can be run with `integration-testing:test`.
+The `integration-testing` project is expected to be in a subdirectory of the main `kotlinx.coroutines` project.
+
+To run all the available tests: `cd integration-testing` + `./gradlew check`.
diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle
index 6efa3a14..985a40ed 100644
--- a/integration-testing/build.gradle
+++ b/integration-testing/build.gradle
@@ -5,7 +5,7 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
plugins {
- id("kotlin-jvm-conventions")
+ id "org.jetbrains.kotlin.jvm"
}
repositories {
@@ -13,27 +13,71 @@ repositories {
mavenCentral()
}
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+ testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
+}
+
sourceSets {
- npmTest {
+ // Test that relies on Guava to reflectively check all Throwable subclasses in coroutines
+ withGuavaTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
+
+ dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation 'com.google.guava:guava:31.1-jre'
+ }
}
+ // Checks correctness of Maven publication (JAR resources) and absence of atomicfu symbols
mavenTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
+
+ dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+ }
}
+ // Checks that kotlinx-coroutines-debug can be used as -javaagent parameter
debugAgentTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
+
+ dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutines_version"
+ }
}
+ // Checks that kotlinx-coroutines-debug agent can self-attach dynamically to JVM as standalone dependency
+ debugDynamicAgentTest {
+ kotlin
+ compileClasspath += sourceSets.test.runtimeClasspath
+ runtimeClasspath += sourceSets.test.runtimeClasspath
+
+ dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutines_version"
+ }
+ }
+
+ // Checks that kotlinx-coroutines-core can be used as -javaagent parameter
coreAgentTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
+
+ dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ }
}
}
@@ -43,62 +87,41 @@ compileDebugAgentTestKotlin {
}
}
-task npmTest(type: Test) {
- def sourceSet = sourceSets.npmTest
- environment "projectRoot", project.rootDir
- environment "deployVersion", version
- def dryRunNpm = project.properties['dryRun']
- def doRun = dryRunNpm == "true" // so that we don't accidentally publish anything, especially before the test
- onlyIf { doRun }
- if (doRun) { // `onlyIf` only affects execution of the task, not the dependency subtree
- dependsOn(project(':').getTasksByName("publishNpm", true))
- }
+task withGuavaTest(type: Test) {
+ environment "version", coroutines_version
+ def sourceSet = sourceSets.withGuavaTest
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
}
task mavenTest(type: Test) {
+ environment "version", coroutines_version
def sourceSet = sourceSets.mavenTest
- dependsOn(project(':').getTasksByName("publishToMavenLocal", true))
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
- // we can't depend on the subprojects because we need to test the classfiles that are published in the end.
- // also, we can't put this in the `dependencies` block because the resolution would happen before publication.
- def mavenTestClasspathConfiguration = project.configurations.detachedConfiguration(
- project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"),
- project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"))
-
- mavenTestClasspathConfiguration.attributes {
- attribute(KotlinPlatformType.attribute, KotlinPlatformType.jvm)
- }
-
- classpath += mavenTestClasspathConfiguration
}
task debugAgentTest(type: Test) {
def sourceSet = sourceSets.debugAgentTest
- dependsOn(project(':kotlinx-coroutines-debug').shadowJar)
- jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0])
+ def coroutinesDebugJar = sourceSet.runtimeClasspath.filter {it.name == "kotlinx-coroutines-debug-${coroutines_version}.jar" }.singleFile
+ jvmArgs ('-javaagent:' + coroutinesDebugJar)
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
+ systemProperties project.properties.subMap(["overwrite.probes"])
}
-task coreAgentTest(type: Test) {
- def sourceSet = sourceSets.coreAgentTest
- dependsOn(project(':kotlinx-coroutines-core').jvmJar)
- jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-core').jvmJar.outputs.files.getFiles()[0])
+task debugDynamicAgentTest(type: Test) {
+ def sourceSet = sourceSets.debugDynamicAgentTest
testClassesDirs = sourceSet.output.classesDirs
classpath = sourceSet.runtimeClasspath
}
-dependencies {
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
- testImplementation 'junit:junit:4.12'
- npmTestImplementation 'org.apache.commons:commons-compress:1.18'
- npmTestImplementation 'com.google.code.gson:gson:2.8.5'
- debugAgentTestCompile project(':kotlinx-coroutines-core')
- debugAgentTestCompile project(':kotlinx-coroutines-debug')
- coreAgentTestCompile project(':kotlinx-coroutines-core')
+task coreAgentTest(type: Test) {
+ def sourceSet = sourceSets.coreAgentTest
+ def coroutinesCoreJar = sourceSet.runtimeClasspath.filter {it.name == "kotlinx-coroutines-core-jvm-${coroutines_version}.jar" }.singleFile
+ jvmArgs ('-javaagent:' + coroutinesCoreJar)
+ testClassesDirs = sourceSet.output.classesDirs
+ classpath = sourceSet.runtimeClasspath
}
compileTestKotlin {
@@ -106,5 +129,5 @@ compileTestKotlin {
}
check {
- dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest])
+ dependsOn([withGuavaTest, debugDynamicAgentTest, mavenTest, debugAgentTest, coreAgentTest, 'smokeTest:build'])
}
diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties
new file mode 100644
index 00000000..1038d817
--- /dev/null
+++ b/integration-testing/gradle.properties
@@ -0,0 +1,4 @@
+kotlin_version=1.6.21
+coroutines_version=1.6.4-SNAPSHOT
+
+kotlin.code.style=official
diff --git a/integration-testing/gradle/wrapper/gradle-wrapper.jar b/integration-testing/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7454180f
--- /dev/null
+++ b/integration-testing/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/integration-testing/gradle/wrapper/gradle-wrapper.properties b/integration-testing/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..92f06b50
--- /dev/null
+++ b/integration-testing/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/integration-testing/gradlew b/integration-testing/gradlew
new file mode 100755
index 00000000..1b6c7873
--- /dev/null
+++ b/integration-testing/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/integration-testing/gradlew.bat b/integration-testing/gradlew.bat
new file mode 100644
index 00000000..107acd32
--- /dev/null
+++ b/integration-testing/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/integration-testing/settings.gradle b/integration-testing/settings.gradle
new file mode 100644
index 00000000..67336c98
--- /dev/null
+++ b/integration-testing/settings.gradle
@@ -0,0 +1,19 @@
+pluginManagement {
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id == "org.jetbrains.kotlin.multiplatform" || requested.id.id == "org.jetbrains.kotlin.jvm") {
+ useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
+ }
+ }
+ }
+
+ repositories {
+ mavenCentral()
+ maven { url "https://plugins.gradle.org/m2/" }
+ mavenLocal()
+ }
+}
+
+include 'smokeTest'
+
+rootProject.name = "kotlinx-coroutines-integration-testing"
diff --git a/integration-testing/smokeTest/build.gradle b/integration-testing/smokeTest/build.gradle
new file mode 100644
index 00000000..b200bb2f
--- /dev/null
+++ b/integration-testing/smokeTest/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'org.jetbrains.kotlin.multiplatform'
+}
+
+repositories {
+ // Coroutines from the outer project are published by previous CI buils step
+ mavenLocal()
+ mavenCentral()
+}
+
+kotlin {
+ jvm()
+ js(IR) {
+ nodejs()
+ }
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation kotlin('stdlib-common')
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation kotlin('test-common')
+ implementation kotlin('test-annotations-common')
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+ }
+ }
+ jsTest {
+ dependencies {
+ implementation kotlin('test-js')
+ }
+ }
+ jvmTest {
+ dependencies {
+ implementation kotlin('test')
+ implementation kotlin('test-junit')
+ }
+ }
+ }
+}
diff --git a/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt b/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt
new file mode 100644
index 00000000..c5da677b
--- /dev/null
+++ b/integration-testing/smokeTest/src/commonMain/kotlin/Sample.kt
@@ -0,0 +1,9 @@
+import kotlinx.coroutines.*
+
+suspend fun doWorld() = coroutineScope {
+ launch {
+ delay(1000L)
+ println("World!")
+ }
+ println("Hello")
+}
diff --git a/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt b/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt
new file mode 100644
index 00000000..a8c6598e
--- /dev/null
+++ b/integration-testing/smokeTest/src/commonTest/kotlin/SampleTest.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+import kotlinx.coroutines.test.*
+import kotlin.test.*
+
+class SampleTest {
+ @Test
+ fun test() = runTest {
+ doWorld()
+ }
+}
diff --git a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt
index ce82e577..84886a18 100644
--- a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt
+++ b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt
@@ -20,20 +20,19 @@ class PrecompiledDebugProbesTest {
val classFileResourcePath = className.replace(".", "/") + ".class"
val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!!
val array = stream.readBytes()
- val binFile = clz.classLoader.getResourceAsStream("DebugProbesKt.bin")!!
- val binContent = binFile.readBytes()
+ // we expect the integration testing project to be in a subdirectory of the main kotlinx.coroutines project
+ val base = File("").absoluteFile.parentFile
+ val probes = File(base, "kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin")
+ val binContent = probes.readBytes()
if (overwrite) {
- val url = clz.classLoader.getResource("DebugProbesKt.bin")!!
- val base = url.toExternalForm().toString().removePrefix("jar:file:").substringBefore("/build")
- val probes = File(base, "jvm/resources/DebugProbesKt.bin")
FileOutputStream(probes).use { it.write(array) }
println("Content was successfully overwritten!")
} else {
assertTrue(
array.contentEquals(binContent),
"Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " +
- "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true and " +
- "ensure that classfile has major version equal to 50 (Java 6 compliance)")
+ "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true."
+ )
}
}
}
diff --git a/integration-testing/src/debugDynamicAgentTest/kotlin/DynamicAttachDebugTest.kt b/integration-testing/src/debugDynamicAgentTest/kotlin/DynamicAttachDebugTest.kt
new file mode 100644
index 00000000..ff9cac84
--- /dev/null
+++ b/integration-testing/src/debugDynamicAgentTest/kotlin/DynamicAttachDebugTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+import org.junit.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.debug.*
+import org.junit.Test
+import java.io.*
+import java.lang.IllegalStateException
+
+class DynamicAttachDebugTest {
+
+ @Test
+ fun testAgentDumpsCoroutines() =
+ DebugProbes.withDebugProbes {
+ runBlocking {
+ val baos = ByteArrayOutputStream()
+ DebugProbes.dumpCoroutines(PrintStream(baos))
+ // if the agent works, then dumps should contain something,
+ // at least the fact that this test is running.
+ Assert.assertTrue(baos.toString().contains("testAgentDumpsCoroutines"))
+ }
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun testAgentIsNotInstalled() {
+ DebugProbes.dumpCoroutines(PrintStream(ByteArrayOutputStream()))
+ }
+}
diff --git a/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt
index 39d6598b..dbb1921d 100644
--- a/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt
+++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationAtomicfuValidator.kt
@@ -8,7 +8,7 @@ import org.junit.*
import org.junit.Assert.assertTrue
import java.util.jar.*
-class MavenPublicationValidator {
+class MavenPublicationAtomicfuValidator {
private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray()
@Test
diff --git a/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt
new file mode 100644
index 00000000..da87d4cc
--- /dev/null
+++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationVersionValidator.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.validator
+
+import org.junit.*
+import org.junit.Test
+import java.util.jar.*
+import kotlin.test.*
+
+class MavenPublicationVersionValidator {
+
+ @Test
+ fun testMppJar() {
+ val clazz = Class.forName("kotlinx.coroutines.Job")
+ JarFile(clazz.protectionDomain.codeSource.location.file).checkForVersion("kotlinx_coroutines_core.version")
+ }
+
+ @Test
+ fun testAndroidJar() {
+ val clazz = Class.forName("kotlinx.coroutines.android.HandlerDispatcher")
+ JarFile(clazz.protectionDomain.codeSource.location.file).checkForVersion("kotlinx_coroutines_android.version")
+ }
+
+ private fun JarFile.checkForVersion(file: String) {
+ val actualFile = "META-INF/$file"
+ val version = System.getenv("version")
+ use {
+ for (e in entries()) {
+ if (e.name == actualFile) {
+ val string = getInputStream(e).readAllBytes().decodeToString()
+ assertEquals(version, string)
+ return
+ }
+ }
+ error("File $file not found")
+ }
+ }
+}
diff --git a/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt b/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt
deleted file mode 100644
index 8e1b9f99..00000000
--- a/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.validator
-
-import com.google.gson.*
-import org.apache.commons.compress.archivers.tar.*
-import org.junit.*
-import java.io.*
-import java.util.zip.*
-import org.junit.Assert.*
-
-class NpmPublicationValidator {
- private val VERSION = System.getenv("deployVersion")!!
- private val BUILD_DIR = System.getenv("projectRoot")!!
- private val NPM_ARTIFACT = "$BUILD_DIR/kotlinx-coroutines-core/build/npm/kotlinx-coroutines-core-$VERSION.tgz"
-
- @Test
- fun testPackageJson() {
- println("Checking dependencies of $NPM_ARTIFACT")
- val visited = visit("package.json") {
- val json = JsonParser().parse(content()).asJsonObject
- assertEquals(VERSION, json["version"].asString)
- assertNull(json["dependencies"])
- val peerDependencies = json["peerDependencies"].asJsonObject
- assertEquals(1, peerDependencies.size())
- assertNotNull(peerDependencies["kotlin"])
- }
- assertEquals(1, visited)
- }
-
- @Test
- fun testAtomicfuDependencies() {
- println("Checking contents of $NPM_ARTIFACT")
- val visited = visit(".js") {
- val content = content()
- assertFalse(content, content.contains("atomicfu", true))
- assertFalse(content, content.contains("atomicint", true))
- assertFalse(content, content.contains("atomicboolean", true))
- }
- assertEquals(2, visited)
- }
-
- private fun InputStream.content(): String {
- val bais = ByteArrayOutputStream()
- val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
- var read = read(buffer, 0, DEFAULT_BUFFER_SIZE)
- while (read >= 0) {
- bais.write(buffer, 0, read)
- read = read(buffer, 0, DEFAULT_BUFFER_SIZE)
- }
- return bais.toString()
- }
-
- private inline fun visit(fileSuffix: String, block: InputStream.(entry: TarArchiveEntry) -> Unit): Int {
- var visited = 0
- TarArchiveInputStream(GZIPInputStream(FileInputStream(NPM_ARTIFACT))).use { tais ->
- var entry: TarArchiveEntry? = tais.nextTarEntry ?: return 0
- do {
- if (entry!!.name.endsWith(fileSuffix)) {
- ++visited
- tais.block(entry)
- }
- entry = tais.nextTarEntry
- } while (entry != null)
-
- return visited
- }
- }
-}
diff --git a/integration-testing/src/withGuavaTest/kotlin/ListAllCoroutineThrowableSubclassesTest.kt b/integration-testing/src/withGuavaTest/kotlin/ListAllCoroutineThrowableSubclassesTest.kt
new file mode 100644
index 00000000..fefcc005
--- /dev/null
+++ b/integration-testing/src/withGuavaTest/kotlin/ListAllCoroutineThrowableSubclassesTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import com.google.common.reflect.*
+import kotlinx.coroutines.*
+import org.junit.Test
+import kotlin.test.*
+
+class ListAllCoroutineThrowableSubclassesTest {
+
+ /*
+ * These are all the known throwables in kotlinx.coroutines.
+ * If you add one, this test will fail to make
+ * you ensure your exception type is java.io.Serializable.
+ *
+ * We do not have means to check it automatically, so checks are delegated to humans.
+ *
+ * See #3328 for serialization rationale.
+ */
+ private val knownThrowables = setOf(
+ "kotlinx.coroutines.TimeoutCancellationException",
+ "kotlinx.coroutines.JobCancellationException",
+ "kotlinx.coroutines.internal.UndeliveredElementException",
+ "kotlinx.coroutines.CompletionHandlerException",
+ "kotlinx.coroutines.DiagnosticCoroutineContextException",
+ "kotlinx.coroutines.CoroutinesInternalError",
+ "kotlinx.coroutines.channels.ClosedSendChannelException",
+ "kotlinx.coroutines.channels.ClosedReceiveChannelException",
+ "kotlinx.coroutines.flow.internal.ChildCancelledException",
+ "kotlinx.coroutines.flow.internal.AbortFlowException",
+ )
+
+ @Test
+ fun testThrowableSubclassesAreSerializable() {
+ val classes = ClassPath.from(this.javaClass.classLoader)
+ .getTopLevelClassesRecursive("kotlinx.coroutines");
+ val throwables = classes.filter { Throwable::class.java.isAssignableFrom(it.load()) }.map { it.toString() }
+ assertEquals(knownThrowables.sorted(), throwables.sorted())
+ }
+}
diff --git a/integration/kotlinx-coroutines-guava/README.md b/integration/kotlinx-coroutines-guava/README.md
index 34b8e581..c84fb861 100644
--- a/integration/kotlinx-coroutines-guava/README.md
+++ b/integration/kotlinx-coroutines-guava/README.md
@@ -51,17 +51,17 @@ Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/L
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
<!--- MODULE kotlinx-coroutines-guava -->
<!--- INDEX kotlinx.coroutines.guava -->
-[future]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/future.html
-[com.google.common.util.concurrent.ListenableFuture.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/await.html
-[kotlinx.coroutines.Deferred.asListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/as-listenable-future.html
+[future]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/future.html
+[com.google.common.util.concurrent.ListenableFuture.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/await.html
+[kotlinx.coroutines.Deferred.asListenableFuture]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/as-listenable-future.html
<!--- INDEX com.google.common.util.concurrent -->
-[com.google.common.util.concurrent.ListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/https://google.github.io/guava/releases/28.0-jre/api/docs/com/google/common/util/concurrent/ListenableFuture.html
+[com.google.common.util.concurrent.ListenableFuture]: https://kotlinlang.org/api/kotlinx.coroutines/https://google.github.io/guava/releases/31.0.1-jre/api/docs/com/google/common/util/concurrent/ListenableFuture.html
<!--- END -->
diff --git a/integration/kotlinx-coroutines-guava/build.gradle.kts b/integration/kotlinx-coroutines-guava/build.gradle.kts
index 12a6ca70..2a84ca93 100644
--- a/integration/kotlinx-coroutines-guava/build.gradle.kts
+++ b/integration/kotlinx-coroutines-guava/build.gradle.kts
@@ -2,10 +2,15 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-val guavaVersion = "28.0-jre"
+val guavaVersion = "31.0.1-jre"
dependencies {
- compile("com.google.guava:guava:$guavaVersion")
+ api("com.google.guava:guava:$guavaVersion")
+}
+
+java {
+ targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_1_8
}
externalDocumentationLink(
diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt
index 8f11e0a9..0820f1f1 100644
--- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt
+++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt
@@ -14,7 +14,7 @@ import kotlin.coroutines.*
/**
* Starts [block] in a new coroutine and returns a [ListenableFuture] pointing to its result.
*
- * The coroutine is immediately started. Passing [CoroutineStart.LAZY] to [start] throws
+ * The coroutine is started immediately. Passing [CoroutineStart.LAZY] to [start] throws
* [IllegalArgumentException], because Futures don't have a way to start lazily.
*
* When the created coroutine [isCompleted][Job.isCompleted], it will try to
@@ -35,10 +35,12 @@ import kotlin.coroutines.*
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging
* facilities.
*
- * Note that the error and cancellation semantics of [future] are _subtly different_ than [asListenableFuture]'s.
- * In particular, any exception that happens in the coroutine after returned future is
- * successfully cancelled will be passed to the [CoroutineExceptionHandler] from the [context].
- * See [ListenableFutureCoroutine] for details.
+ * Note that the error and cancellation semantics of [future] are _different_ than [async]'s.
+ * In contrast to [Deferred], [Future] doesn't have an intermediate `Cancelling` state. If
+ * the returned `Future` is successfully cancelled, and `block` throws afterward, the thrown
+ * error is dropped, and getting the `Future`'s value will throw a `CancellationException` with
+ * no cause. This is to match the specification and behavior of
+ * `java.util.concurrent.FutureTask`.
*
* @param context added overlaying [CoroutineScope.coroutineContext] to form the new context.
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
@@ -133,10 +135,8 @@ public fun <T> ListenableFuture<T>.asDeferred(): Deferred<T> {
// Finally, if this isn't done yet, attach a Listener that will complete the Deferred.
val deferred = CompletableDeferred<T>()
Futures.addCallback(this, object : FutureCallback<T> {
- override fun onSuccess(result: T?) {
- // Here we work with flexible types, so we unchecked cast to trick the type system
- @Suppress("UNCHECKED_CAST")
- runCatching { deferred.complete(result as T) }
+ override fun onSuccess(result: T) {
+ runCatching { deferred.complete(result) }
.onFailure { handleCoroutineException(EmptyCoroutineContext, it) }
}
@@ -241,8 +241,8 @@ public suspend fun <T> ListenableFuture<T>.await(): T {
return suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
addListener(
- ToContinuation(this, cont),
- MoreExecutors.directExecutor())
+ ToContinuation(this, cont),
+ MoreExecutors.directExecutor())
cont.invokeOnCancellation {
cancel(false)
}
@@ -284,16 +284,13 @@ private class ToContinuation<T>(
* By documented contract, a [Future] has been cancelled if
* and only if its `isCancelled()` method returns true.
*
- * Any error that occurs after successfully cancelling a [ListenableFuture] will be passed
- * to the [CoroutineExceptionHandler] from the context. The contract of [Future] does not permit
- * it to return an error after it is successfully cancelled.
- *
- * By calling [asListenableFuture] on a [Deferred], any error that occurs after successfully
- * cancelling the [ListenableFuture] representation of the [Deferred] will _not_ be passed to
- * the [CoroutineExceptionHandler]. Cancelling a [Deferred] places that [Deferred] in the
- * cancelling/cancelled states defined by [Job], which _can_ show the error. It's assumed that
- * the [Deferred] pointing to the task will be used to observe any error outcome occurring after
- * cancellation.
+ * Any error that occurs after successfully cancelling a [ListenableFuture] is lost.
+ * The contract of [Future] does not permit it to return an error after it is successfully cancelled.
+ * On the other hand, we can't report an unhandled exception to [CoroutineExceptionHandler],
+ * otherwise [Future.cancel] can lead to an app crash which arguably is a contract violation.
+ * In contrast to [Future] which can't change its outcome after a successful cancellation,
+ * cancelling a [Deferred] places that [Deferred] in the cancelling/cancelled states defined by [Job],
+ * which _can_ show the error.
*
* This may be counterintuitive, but it maintains the error and cancellation contracts of both
* the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point
@@ -312,10 +309,14 @@ private class ListenableFutureCoroutine<T>(
}
override fun onCancelled(cause: Throwable, handled: Boolean) {
- if (!future.completeExceptionallyOrCancel(cause) && !handled) {
- // prevents loss of exception that was not handled by parent & could not be set to JobListenableFuture
- handleCoroutineException(context, cause)
- }
+ // Note: if future was cancelled in a race with a cancellation of this
+ // coroutine, and the future was successfully cancelled first, the cause of coroutine
+ // cancellation is dropped in this promise. A Future can only be completed once.
+ //
+ // This is consistent with FutureTask behaviour. A race between a Future.cancel() and
+ // a FutureTask.setException() for the same Future will similarly drop the
+ // cause of a failure-after-cancellation.
+ future.completeExceptionallyOrCancel(cause)
}
}
@@ -348,7 +349,7 @@ private class JobListenableFuture<T>(private val jobToCancel: Job): ListenableFu
*
* To preserve Coroutine's [CancellationException], this future points to either `T` or [Cancelled].
*/
- private val auxFuture = SettableFuture.create<Any>()
+ private val auxFuture = SettableFuture.create<Any?>()
/**
* `true` if [auxFuture.get][ListenableFuture.get] throws [ExecutionException].
@@ -433,7 +434,7 @@ private class JobListenableFuture<T>(private val jobToCancel: Job): ListenableFu
}
/** See [get()]. */
- private fun getInternal(result: Any): T = if (result is Cancelled) {
+ private fun getInternal(result: Any?): T = if (result is Cancelled) {
throw CancellationException().initCause(result.exception)
} else {
// We know that `auxFuture` can contain either `T` or `Cancelled`.
diff --git a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt
index 69ba1930..511b1b03 100644
--- a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt
+++ b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt
@@ -555,11 +555,7 @@ class ListenableFutureTest : TestBase() {
}
@Test
- fun testUnhandledExceptionOnExternalCancellation() = runTest(
- unhandled = listOf(
- { it -> it is TestException } // exception is unhandled because there is no parent
- )
- ) {
+ fun testUnhandledExceptionOnExternalCancellation() = runTest {
expect(1)
// No parent here (NonCancellable), so nowhere to propagate exception
val result = future(NonCancellable + Dispatchers.Unconfined) {
@@ -567,7 +563,7 @@ class ListenableFutureTest : TestBase() {
delay(Long.MAX_VALUE)
} finally {
expect(2)
- throw TestException() // this exception cannot be handled
+ throw TestException() // this exception cannot be handled and is set to be lost.
}
}
result.cancel(true)
@@ -708,23 +704,6 @@ class ListenableFutureTest : TestBase() {
assertEquals(testException, thrown.cause)
}
- @Test
- fun stressTestJobListenableFutureIsCancelledDoesNotThrow() = runTest {
- repeat(1000) {
- val deferred = CompletableDeferred<String>()
- val asListenableFuture = deferred.asListenableFuture()
- // We heed two threads to test a race condition.
- withContext(Dispatchers.Default) {
- val cancellationJob = launch {
- asListenableFuture.cancel(false)
- }
- while (!cancellationJob.isCompleted) {
- asListenableFuture.isCancelled // Shouldn't throw.
- }
- }
- }
- }
-
private inline fun <reified T: Throwable> ListenableFuture<*>.checkFutureException() {
val e = assertFailsWith<ExecutionException> { get() }
val cause = e.cause!!
@@ -775,4 +754,61 @@ class ListenableFutureTest : TestBase() {
assertEquals(count, completed.get())
}
}
+
+ @Test
+ fun testFuturePropagatesExceptionToParentAfterCancellation() = runTest {
+ val throwLatch = CompletableDeferred<Boolean>()
+ val cancelLatch = CompletableDeferred<Boolean>()
+ val parent = Job()
+ val scope = CoroutineScope(parent)
+ val exception = TestException("propagated to parent")
+ val future = scope.future {
+ cancelLatch.complete(true)
+ withContext(NonCancellable) {
+ throwLatch.await()
+ throw exception
+ }
+ }
+ cancelLatch.await()
+ future.cancel(true)
+ throwLatch.complete(true)
+ parent.join()
+ assertTrue(parent.isCancelled)
+ assertEquals(exception, parent.getCancellationException().cause)
+ }
+
+ // Stress tests.
+
+ @Test
+ fun testFutureDoesNotReportToCoroutineExceptionHandler() = runTest {
+ repeat(1000) {
+ supervisorScope { // Don't propagate failures in children to parent and other children.
+ val innerFuture = SettableFuture.create<Unit>()
+ val outerFuture = async { innerFuture.await() }
+
+ withContext(Dispatchers.Default) {
+ launch { innerFuture.setException(TestException("can be lost")) }
+ launch { outerFuture.cancel() }
+ // nothing should be reported to CoroutineExceptionHandler, otherwise `Future.cancel` contract violation.
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testJobListenableFutureIsCancelledDoesNotThrow() = runTest {
+ repeat(1000) {
+ val deferred = CompletableDeferred<String>()
+ val asListenableFuture = deferred.asListenableFuture()
+ // We heed two threads to test a race condition.
+ withContext(Dispatchers.Default) {
+ val cancellationJob = launch {
+ asListenableFuture.cancel(false)
+ }
+ while (!cancellationJob.isCompleted) {
+ asListenableFuture.isCancelled // Shouldn't throw.
+ }
+ }
+ }
+ }
}
diff --git a/integration/kotlinx-coroutines-jdk8/README.md b/integration/kotlinx-coroutines-jdk8/README.md
index 35808c6f..321e2934 100644
--- a/integration/kotlinx-coroutines-jdk8/README.md
+++ b/integration/kotlinx-coroutines-jdk8/README.md
@@ -54,15 +54,15 @@ Integration with JDK8 [CompletableFuture] (Android API level 24).
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
<!--- MODULE kotlinx-coroutines-jdk8 -->
<!--- INDEX kotlinx.coroutines.future -->
-[future]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/future.html
-[java.util.concurrent.CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/await.html
-[java.util.concurrent.CompletionStage.asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/as-deferred.html
-[kotlinx.coroutines.Deferred.asCompletableFuture]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/as-completable-future.html
+[future]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/future.html
+[java.util.concurrent.CompletionStage.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/await.html
+[java.util.concurrent.CompletionStage.asDeferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/as-deferred.html
+[kotlinx.coroutines.Deferred.asCompletableFuture]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/as-completable-future.html
<!--- END -->
diff --git a/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt
index 1d804e59..b0d72de8 100644
--- a/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt
+++ b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt
@@ -19,7 +19,6 @@ public fun <T> Stream<T>.consumeAsFlow(): Flow<T> = StreamFlow(this)
private class StreamFlow<T>(private val stream: Stream<T>) : Flow<T> {
private val consumed = atomic(false)
- @InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
if (!consumed.compareAndSet(false, true)) error("Stream.consumeAsFlow can be collected only once")
try {
diff --git a/integration/kotlinx-coroutines-play-services/README.md b/integration/kotlinx-coroutines-play-services/README.md
index e5e0e613..17b6500a 100644
--- a/integration/kotlinx-coroutines-play-services/README.md
+++ b/integration/kotlinx-coroutines-play-services/README.md
@@ -34,6 +34,12 @@ val currentLocationTask = fusedLocationProviderClient.getCurrentLocation(PRIORIT
val currentLocation = currentLocationTask.await(cancellationTokenSource) // cancelling `await` also cancels `currentLocationTask`, and vice versa
```
-[asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/as-deferred.html
-[await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/await.html
-[asTask]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/kotlinx.coroutines.-deferred/as-task.html
+
+<!--- MODULE kotlinx-coroutines-play-services -->
+<!--- INDEX kotlinx.coroutines.tasks -->
+
+[asDeferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/as-deferred.html
+[await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/await.html
+[asTask]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/as-task.html
+
+<!--- END -->
diff --git a/integration/kotlinx-coroutines-play-services/build.gradle.kts b/integration/kotlinx-coroutines-play-services/build.gradle.kts
index 59f3b0bd..9f8a1287 100644
--- a/integration/kotlinx-coroutines-play-services/build.gradle.kts
+++ b/integration/kotlinx-coroutines-play-services/build.gradle.kts
@@ -4,36 +4,17 @@
val tasksVersion = "16.0.1"
-val artifactType = Attribute.of("artifactType", String::class.java)
-val unpackedAar = Attribute.of("unpackedAar", Boolean::class.javaObjectType)
-
-configurations.configureEach {
- afterEvaluate {
- if (isCanBeResolved) {
- attributes.attribute(unpackedAar, true) // request all AARs to be unpacked
- }
- }
-}
+project.configureAar()
dependencies {
- attributesSchema {
- attribute(unpackedAar)
- }
-
- artifactTypes {
- create("aar") {
- attributes.attribute(unpackedAar, false)
- }
- }
-
- registerTransform(UnpackAar::class.java) {
- from.attribute(unpackedAar, false).attribute(artifactType, "aar")
- to.attribute(unpackedAar, true).attribute(artifactType, "jar")
- }
-
+ configureAarUnpacking()
api("com.google.android.gms:play-services-tasks:$tasksVersion") {
exclude(group="com.android.support")
}
+
+ // Required by robolectric
+ testImplementation("androidx.test:core:1.2.0")
+ testImplementation("androidx.test:monitor:1.2.0")
}
externalDocumentationLink(
diff --git a/integration/kotlinx-coroutines-play-services/src/Tasks.kt b/integration/kotlinx-coroutines-play-services/src/Tasks.kt
index c37ac7a0..0451d7be 100644
--- a/integration/kotlinx-coroutines-play-services/src/Tasks.kt
+++ b/integration/kotlinx-coroutines-play-services/src/Tasks.kt
@@ -8,6 +8,8 @@ package kotlinx.coroutines.tasks
import com.google.android.gms.tasks.*
import kotlinx.coroutines.*
+import java.lang.Runnable
+import java.util.concurrent.Executor
import kotlin.coroutines.*
/**
@@ -71,7 +73,8 @@ private fun <T> Task<T>.asDeferredImpl(cancellationTokenSource: CancellationToke
deferred.completeExceptionally(e)
}
} else {
- addOnCompleteListener {
+ // Run the callback directly to avoid unnecessarily scheduling on the main thread.
+ addOnCompleteListener(DirectExecutor) {
val e = it.exception
if (e == null) {
@Suppress("UNCHECKED_CAST")
@@ -114,7 +117,8 @@ public suspend fun <T> Task<T>.await(): T = awaitImpl(null)
* leads to an unspecified behaviour.
*/
@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0
-public suspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource)
+public suspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource): T =
+ awaitImpl(cancellationTokenSource)
private suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T {
// fast path
@@ -133,7 +137,8 @@ private suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationT
}
return suspendCancellableCoroutine { cont ->
- addOnCompleteListener {
+ // Run the callback directly to avoid unnecessarily scheduling on the main thread.
+ addOnCompleteListener(DirectExecutor) {
val e = it.exception
if (e == null) {
@Suppress("UNCHECKED_CAST")
@@ -150,3 +155,12 @@ private suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationT
}
}
}
+
+/**
+ * An [Executor] that just directly executes the [Runnable].
+ */
+private object DirectExecutor : Executor {
+ override fun execute(r: Runnable) {
+ r.run()
+ }
+}
diff --git a/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt b/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt
index 6026ffd7..e286ee19 100644
--- a/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt
+++ b/integration/kotlinx-coroutines-play-services/test/FakeAndroid.kt
@@ -2,10 +2,17 @@ package android.os
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import java.util.concurrent.*
class Handler(val looper: Looper) {
fun post(r: Runnable): Boolean {
- GlobalScope.launch { r.run() }
+ try {
+ GlobalScope.launch { r.run() }
+ } catch (e: RejectedExecutionException) {
+ // Execute leftover callbacks in place for tests
+ r.run()
+ }
+
return true
}
}
diff --git a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt
index b125192e..34fbe23b 100644
--- a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt
+++ b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt
@@ -45,8 +45,8 @@ class TaskTest : TestBase() {
}
@Test
- fun testCancelledAsTask() {
- val deferred = GlobalScope.async {
+ fun testCancelledAsTask() = runTest {
+ val deferred = async(Dispatchers.Default) {
delay(100)
}.apply { cancel() }
@@ -60,8 +60,8 @@ class TaskTest : TestBase() {
}
@Test
- fun testThrowingAsTask() {
- val deferred = GlobalScope.async<Int> {
+ fun testThrowingAsTask() = runTest({ e -> e is TestException }) {
+ val deferred = async<Int>(Dispatchers.Default) {
throw TestException("Fail")
}
diff --git a/integration/kotlinx-coroutines-slf4j/README.md b/integration/kotlinx-coroutines-slf4j/README.md
index e23d3907..37f87008 100644
--- a/integration/kotlinx-coroutines-slf4j/README.md
+++ b/integration/kotlinx-coroutines-slf4j/README.md
@@ -21,6 +21,6 @@ Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
<!--- MODULE kotlinx-coroutines-slf4j -->
<!--- INDEX kotlinx.coroutines.slf4j -->
-[MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html
+[MDCContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html
<!--- END -->
diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle.kts b/integration/kotlinx-coroutines-slf4j/build.gradle.kts
index a341eefe..35523333 100644
--- a/integration/kotlinx-coroutines-slf4j/build.gradle.kts
+++ b/integration/kotlinx-coroutines-slf4j/build.gradle.kts
@@ -3,10 +3,10 @@
*/
dependencies {
- compile("org.slf4j:slf4j-api:1.7.25")
- testCompile("io.github.microutils:kotlin-logging:1.5.4")
- testRuntime("ch.qos.logback:logback-classic:1.2.3")
- testRuntime("ch.qos.logback:logback-core:1.2.3")
+ implementation("org.slf4j:slf4j-api:1.7.32")
+ testImplementation("io.github.microutils:kotlin-logging:2.1.0")
+ testRuntimeOnly("ch.qos.logback:logback-classic:1.2.7")
+ testRuntimeOnly("ch.qos.logback:logback-core:1.2.7")
}
externalDocumentationLink(
diff --git a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
index 9528f2b2..0fbfece6 100644
--- a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
+++ b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
@@ -28,7 +28,7 @@ public typealias MDCContextMap = Map<String, String>?
* }
* ```
*
- * Note that you cannot update MDC context from inside of the coroutine simply
+ * Note that you cannot update MDC context from inside the coroutine simply
* using [MDC.put]. These updates are going to be lost on the next suspension and
* reinstalled to the MDC context that was captured or explicitly specified in
* [contextMap] when this object was created on the next resumption.
@@ -43,6 +43,7 @@ public class MDCContext(
/**
* The value of [MDC] context map.
*/
+ @Suppress("MemberVisibilityCanBePrivate")
public val contextMap: MDCContextMap = MDC.getCopyOfContextMap()
) : ThreadContextElement<MDCContextMap>, AbstractCoroutineContextElement(Key) {
/**
diff --git a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
index 7d18359c..532c47e9 100644
--- a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
+++ b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
@@ -102,9 +102,10 @@ class MDCContextTest : TestBase() {
val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!!
withContext(Dispatchers.Default + MDCContext()) {
assertEquals("myValue", MDC.get("myKey"))
+ assertEquals("myValue", coroutineContext[MDCContext]?.contextMap?.get("myKey"))
withContext(mainDispatcher) {
assertEquals("myValue", MDC.get("myKey"))
}
}
}
-} \ No newline at end of file
+}
diff --git a/js/example-frontend-js/README.md b/js/example-frontend-js/README.md
index ad61372d..55e70886 100644
--- a/js/example-frontend-js/README.md
+++ b/js/example-frontend-js/README.md
@@ -15,4 +15,4 @@ gradlew :example-frontend-js:run
```
Built and deployed application is available at the library documentation site
-[here](https://kotlin.github.io/kotlinx.coroutines/example-frontend-js/index.html).
+[here](https://kotlinlang.org/api/kotlinx.coroutines/example-frontend-js/index.html).
diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt
index d4e530b0..67c6ef04 100644
--- a/js/example-frontend-js/src/ExampleMain.kt
+++ b/js/example-frontend-js/src/ExampleMain.kt
@@ -8,7 +8,7 @@ import kotlinx.html.div
import kotlinx.html.dom.*
import kotlinx.html.js.onClickFunction
import org.w3c.dom.*
-import kotlin.browser.*
+import kotlinx.browser.*
import kotlin.coroutines.*
import kotlin.math.*
import kotlin.random.Random
diff --git a/kotlinx-coroutines-core/README.md b/kotlinx-coroutines-core/README.md
index c21e5048..6f59b682 100644
--- a/kotlinx-coroutines-core/README.md
+++ b/kotlinx-coroutines-core/README.md
@@ -57,7 +57,6 @@ helper function. [NonCancellable] job object is provided to suppress cancellatio
| [SendChannel][kotlinx.coroutines.channels.SendChannel] | [send][kotlinx.coroutines.channels.SendChannel.send] | [onSend][kotlinx.coroutines.channels.SendChannel.onSend] | [trySend][kotlinx.coroutines.channels.SendChannel.trySend]
| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.channels.ReceiveChannel.onReceive] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive]
| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receiveCatching][kotlinx.coroutines.channels.receiveCatching] | [onReceiveCatching][kotlinx.coroutines.channels.onReceiveCatching] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive]
-| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | [onLock][kotlinx.coroutines.sync.Mutex.onLock] | [tryLock][kotlinx.coroutines.sync.Mutex.tryLock]
| none | [delay][kotlinx.coroutines.delay] | [onTimeout][kotlinx.coroutines.selects.SelectBuilder.onTimeout] | none
# Package kotlinx.coroutines
@@ -84,66 +83,60 @@ Select expression to perform multiple suspending operations simultaneously until
Low-level primitives for finer-grained control of coroutines.
-# Package kotlinx.coroutines.test
-
-Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-coroutines-test` module.
-
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[kotlinx.coroutines.launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[kotlinx.coroutines.Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[kotlinx.coroutines.CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[kotlinx.coroutines.async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[kotlinx.coroutines.Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
-[kotlinx.coroutines.runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[kotlinx.coroutines.Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[kotlinx.coroutines.Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
-[kotlinx.coroutines.NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
-[kotlinx.coroutines.CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
-[kotlinx.coroutines.delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[kotlinx.coroutines.yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[kotlinx.coroutines.withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[kotlinx.coroutines.withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
-[kotlinx.coroutines.withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
-[kotlinx.coroutines.awaitAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html
-[kotlinx.coroutines.joinAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html
-[suspendCancellableCoroutine]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
-[NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
-[kotlinx.coroutines.Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
-[kotlinx.coroutines.Job.onJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/on-join.html
-[kotlinx.coroutines.Job.isCompleted]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html
-[kotlinx.coroutines.Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
-[kotlinx.coroutines.Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
+[kotlinx.coroutines.launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[kotlinx.coroutines.Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[kotlinx.coroutines.CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[kotlinx.coroutines.async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[kotlinx.coroutines.Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[kotlinx.coroutines.runBlocking]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[kotlinx.coroutines.Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[kotlinx.coroutines.Dispatchers.Unconfined]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
+[kotlinx.coroutines.NonCancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
+[kotlinx.coroutines.CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[kotlinx.coroutines.delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[kotlinx.coroutines.yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
+[kotlinx.coroutines.withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[kotlinx.coroutines.withTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
+[kotlinx.coroutines.withTimeoutOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
+[kotlinx.coroutines.awaitAll]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html
+[kotlinx.coroutines.joinAll]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html
+[suspendCancellableCoroutine]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
+[NonCancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
+[kotlinx.coroutines.Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[kotlinx.coroutines.Job.onJoin]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/on-join.html
+[kotlinx.coroutines.Job.isCompleted]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html
+[kotlinx.coroutines.Deferred.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
+[kotlinx.coroutines.Deferred.onAwait]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
<!--- INDEX kotlinx.coroutines.sync -->
-[kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
-[kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
-[kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html
-[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html
+[kotlinx.coroutines.sync.Mutex]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
+[kotlinx.coroutines.sync.Mutex.lock]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
<!--- INDEX kotlinx.coroutines.channels -->
-[kotlinx.coroutines.channels.produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
-[kotlinx.coroutines.channels.ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html
-[kotlinx.coroutines.channels.ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
-[kotlinx.coroutines.channels.Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
-[kotlinx.coroutines.channels.SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
-[kotlinx.coroutines.channels.ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
-[kotlinx.coroutines.channels.SendChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
-[kotlinx.coroutines.channels.SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
-[kotlinx.coroutines.channels.SendChannel.trySend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
-[kotlinx.coroutines.channels.ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
-[kotlinx.coroutines.channels.ReceiveChannel.tryReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/try-receive.html
-[kotlinx.coroutines.channels.receiveCatching]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive-catching.html
-[kotlinx.coroutines.channels.onReceiveCatching]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
+[kotlinx.coroutines.channels.produce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
+[kotlinx.coroutines.channels.ReceiveChannel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html
+[kotlinx.coroutines.channels.ProducerScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
+[kotlinx.coroutines.channels.Channel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
+[kotlinx.coroutines.channels.SendChannel.send]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
+[kotlinx.coroutines.channels.ReceiveChannel.receive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
+[kotlinx.coroutines.channels.SendChannel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
+[kotlinx.coroutines.channels.SendChannel.onSend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
+[kotlinx.coroutines.channels.SendChannel.trySend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
+[kotlinx.coroutines.channels.ReceiveChannel.onReceive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
+[kotlinx.coroutines.channels.ReceiveChannel.tryReceive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/try-receive.html
+[kotlinx.coroutines.channels.receiveCatching]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive-catching.html
+[kotlinx.coroutines.channels.onReceiveCatching]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
<!--- INDEX kotlinx.coroutines.selects -->
-[kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
-[kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/on-timeout.html
+[kotlinx.coroutines.selects.select]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
+[kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/on-timeout.html
<!--- INDEX kotlinx.coroutines.test -->
<!--- END -->
diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
index 50bfb60d..dd7f889e 100644
--- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
+++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
@@ -140,11 +140,24 @@ public final class kotlinx/coroutines/CompletionHandlerException : java/lang/Run
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
}
+public abstract interface class kotlinx/coroutines/CopyableThreadContextElement : kotlinx/coroutines/ThreadContextElement {
+ public abstract fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement;
+ public abstract fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext;
+}
+
+public final class kotlinx/coroutines/CopyableThreadContextElement$DefaultImpls {
+ public static fun fold (Lkotlinx/coroutines/CopyableThreadContextElement;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+ public static fun get (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
+ public static fun minusKey (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
+ public static fun plus (Lkotlinx/coroutines/CopyableThreadContextElement;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
+}
+
public abstract interface class kotlinx/coroutines/CopyableThrowable {
public abstract fun createCopy ()Ljava/lang/Throwable;
}
public final class kotlinx/coroutines/CoroutineContextKt {
+ public static final fun newCoroutineContext (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
public static final fun newCoroutineContext (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
}
@@ -156,6 +169,7 @@ public abstract class kotlinx/coroutines/CoroutineDispatcher : kotlin/coroutines
public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
public final fun interceptContinuation (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
public fun isDispatchNeeded (Lkotlin/coroutines/CoroutineContext;)Z
+ public fun limitedParallelism (I)Lkotlinx/coroutines/CoroutineDispatcher;
public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
public final fun plus (Lkotlinx/coroutines/CoroutineDispatcher;)Lkotlinx/coroutines/CoroutineDispatcher;
public final fun releaseInterceptedContinuation (Lkotlin/coroutines/Continuation;)V
@@ -273,12 +287,17 @@ public final class kotlinx/coroutines/DelayKt {
public abstract interface annotation class kotlinx/coroutines/DelicateCoroutinesApi : java/lang/annotation/Annotation {
}
+public final class kotlinx/coroutines/DispatchedTaskKt {
+ public static final field MODE_CANCELLABLE I
+}
+
public final class kotlinx/coroutines/Dispatchers {
public static final field INSTANCE Lkotlinx/coroutines/Dispatchers;
public static final fun getDefault ()Lkotlinx/coroutines/CoroutineDispatcher;
public static final fun getIO ()Lkotlinx/coroutines/CoroutineDispatcher;
public static final fun getMain ()Lkotlinx/coroutines/MainCoroutineDispatcher;
public static final fun getUnconfined ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public final fun shutdown ()V
}
public final class kotlinx/coroutines/DispatchersKt {
@@ -366,8 +385,13 @@ public final class kotlinx/coroutines/Job$DefaultImpls {
public final class kotlinx/coroutines/Job$Key : kotlin/coroutines/CoroutineContext$Key {
}
+public class kotlinx/coroutines/JobImpl : kotlinx/coroutines/JobSupport, kotlinx/coroutines/CompletableJob {
+ public fun <init> (Lkotlinx/coroutines/Job;)V
+ public fun complete ()Z
+ public fun completeExceptionally (Ljava/lang/Throwable;)Z
+}
+
public final class kotlinx/coroutines/JobKt {
- public static final fun DisposableHandle (Lkotlin/jvm/functions/Function0;)Lkotlinx/coroutines/DisposableHandle;
public static final fun Job (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/CompletableJob;
public static final synthetic fun Job (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job;
public static synthetic fun Job$default (Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableJob;
@@ -447,6 +471,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin
public abstract class kotlinx/coroutines/MainCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher {
public fun <init> ()V
public abstract fun getImmediate ()Lkotlinx/coroutines/MainCoroutineDispatcher;
+ public fun limitedParallelism (I)Lkotlinx/coroutines/CoroutineDispatcher;
public fun toString ()Ljava/lang/String;
protected final fun toStringInternalImpl ()Ljava/lang/String;
}
@@ -543,6 +568,15 @@ public final class kotlinx/coroutines/TimeoutKt {
public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
+public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement {
+ public static final field Key Lkotlinx/coroutines/YieldContext$Key;
+ public field dispatcherWasUnconfined Z
+ public fun <init> ()V
+}
+
+public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key {
+}
+
public final class kotlinx/coroutines/YieldKt {
public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
@@ -887,8 +921,6 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun asSharedFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)Lkotlinx/coroutines/flow/SharedFlow;
public static final fun asStateFlow (Lkotlinx/coroutines/flow/MutableStateFlow;)Lkotlinx/coroutines/flow/StateFlow;
- public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel;
- public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel;
public static final synthetic fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow;
public static final fun buffer (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
@@ -899,7 +931,7 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun catch (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
public static final fun channelFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static final synthetic fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun collectIndexed (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun collectLatest (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun combine (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
@@ -958,10 +990,6 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun flowOf (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun flowOf ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun flowOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow;
- public static final fun flowViaChannel (ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
- public static synthetic fun flowViaChannel$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
- public static final fun flowWith (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
- public static synthetic fun flowWith$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun fold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun forEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V
public static final fun getDEFAULT_CONCURRENCY ()I
@@ -978,8 +1006,6 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
public static final fun onEmpty (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
- public static final fun onErrorCollect (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
- public static synthetic fun onErrorCollect$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun onErrorResume (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
public static final fun onErrorResumeNext (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
@@ -995,9 +1021,7 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun replay (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
public static final fun replay (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow;
- public static final synthetic fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
public static final fun retry (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
- public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun retryWhen (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow;
public static final fun runningFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
@@ -1061,6 +1085,7 @@ public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotli
}
public abstract interface class kotlinx/coroutines/flow/SharedFlow : kotlinx/coroutines/flow/Flow {
+ public abstract fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getReplayCache ()Ljava/util/List;
}
@@ -1284,36 +1309,3 @@ public final class kotlinx/coroutines/sync/SemaphoreKt {
public static final fun withPermit (Lkotlinx/coroutines/sync/Semaphore;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
-public final class kotlinx/coroutines/test/TestCoroutineContext : kotlin/coroutines/CoroutineContext {
- public fun <init> ()V
- public fun <init> (Ljava/lang/String;)V
- public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun advanceTimeBy (JLjava/util/concurrent/TimeUnit;)J
- public static synthetic fun advanceTimeBy$default (Lkotlinx/coroutines/test/TestCoroutineContext;JLjava/util/concurrent/TimeUnit;ILjava/lang/Object;)J
- public final fun advanceTimeTo (JLjava/util/concurrent/TimeUnit;)V
- public static synthetic fun advanceTimeTo$default (Lkotlinx/coroutines/test/TestCoroutineContext;JLjava/util/concurrent/TimeUnit;ILjava/lang/Object;)V
- public final fun assertAllUnhandledExceptions (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun assertAllUnhandledExceptions$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
- public final fun assertAnyUnhandledException (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun assertAnyUnhandledException$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
- public final fun assertExceptions (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun assertExceptions$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
- public final fun assertUnhandledException (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun assertUnhandledException$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
- public final fun cancelAllActions ()V
- public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
- public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
- public final fun getExceptions ()Ljava/util/List;
- public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
- public final fun now (Ljava/util/concurrent/TimeUnit;)J
- public static synthetic fun now$default (Lkotlinx/coroutines/test/TestCoroutineContext;Ljava/util/concurrent/TimeUnit;ILjava/lang/Object;)J
- public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
- public fun toString ()Ljava/lang/String;
- public final fun triggerActions ()V
-}
-
-public final class kotlinx/coroutines/test/TestCoroutineContextKt {
- public static final fun withTestContext (Lkotlinx/coroutines/test/TestCoroutineContext;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun withTestContext$default (Lkotlinx/coroutines/test/TestCoroutineContext;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-}
-
diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle
index c45ca08c..9791b445 100644
--- a/kotlinx-coroutines-core/build.gradle
+++ b/kotlinx-coroutines-core/build.gradle
@@ -14,6 +14,8 @@ if (rootProject.ext.native_targets_enabled) {
apply from: rootProject.file("gradle/compile-js-multiplatform.gradle")
apply from: rootProject.file('gradle/publish-npm-js.gradle')
+apply from: rootProject.file('gradle/dokka.gradle.kts')
+apply from: rootProject.file('gradle/publish.gradle')
/* ==========================================================================
Configure source sets structure for kotlinx-coroutines-core:
@@ -70,39 +72,65 @@ if (rootProject.ext.native_targets_enabled) {
* because JMV-only projects depend on core, thus core should always be initialized before configuration.
*/
kotlin {
- configure(sourceSets) {
- def srcDir = name.endsWith('Main') ? 'src' : 'test'
- def platform = name[0..-5]
- kotlin.srcDirs = ["$platform/$srcDir"]
- if (name == "jvmMain") {
- resources.srcDirs = ["$platform/resources"]
- } else if (name == "jvmTest") {
- resources.srcDirs = ["$platform/test-resources"]
+ sourceSets.forEach {
+ SourceSetsKt.configureMultiplatform(it)
+ }
+
+ /*
+ * Configure four test runs:
+ * 1) Old memory model, Main thread
+ * 2) New memory model, Main thread
+ * 3) Old memory model, BG thread
+ * 4) New memory model, BG thread (required for Dispatchers.Main tests on Darwin)
+ *
+ * All new MM targets are build with optimize = true to have stress tests properly run.
+ */
+ targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithTests.class).configureEach {
+ binaries {
+ // Test for memory leaks using a special entry point that does not exit but returns from main
+ binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"]
}
- languageSettings {
- progressiveMode = true
- optInAnnotations.each { useExperimentalAnnotation(it) }
+
+ binaries.test("newMM", [DEBUG]) {
+ def thisTest = it
+ freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"]
+ optimized = true
+ binaryOptions["memoryModel"] = "experimental"
+ testRuns.create("newMM") {
+ setExecutionSourceFrom(thisTest)
+ // A hack to get different suffixes in the aggregated report.
+ executionTask.configure { targetName = "$targetName new MM" }
+ }
}
- }
- configure(targets) {
- // Configure additional binaries and test runs -- one for each OS
- if (["macos", "linux", "mingw"].any { name.startsWith(it) }) {
- binaries {
- // Test for memory leaks using a special entry point that does not exit but returns from main
- binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"]
- // Configure a separate test where code runs in background
- test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) {
- freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"]
- }
+ binaries.test("worker", [DEBUG]) {
+ def thisTest = it
+ freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"]
+ testRuns.create("worker") {
+ setExecutionSourceFrom(thisTest)
+ executionTask.configure { targetName = "$targetName worker" }
}
- testRuns {
- background { setExecutionSourceFrom(binaries.backgroundDebugTest) }
+ }
+
+ binaries.test("workerWithNewMM", [DEBUG]) {
+ def thisTest = it
+ optimized = true
+ freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"]
+ binaryOptions["memoryModel"] = "experimental"
+ testRuns.create("workerWithNewMM") {
+ setExecutionSourceFrom(thisTest)
+ executionTask.configure { targetName = "$targetName worker with new MM" }
}
}
}
+
+ jvm {
+ // For animal sniffer
+ withJava()
+ }
}
+
configurations {
configureKotlinJvmPlatform(kotlinCompilerPluginClasspath)
}
@@ -159,28 +187,10 @@ kotlin.sourceSets {
jvmTest.dependencies {
api "org.jetbrains.kotlinx:lincheck:$lincheck_version"
api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version"
- api "com.esotericsoftware:kryo:4.0.0"
implementation project(":android-unit-tests")
}
}
-task checkJdk16() {
- // only fail w/o JDK_16 when actually trying to compile, not during project setup phase
- doLast {
- if (!System.env.JDK_16) {
- throw new GradleException("JDK_16 environment variable is not defined. " +
- "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " +
- "Please ensure JDK 1.6 is installed and that JDK_16 points to it.")
- }
- }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
- kotlinOptions.jdkHome = System.env.JDK_16
- // only fail when actually trying to compile, not during project setup phase
- dependsOn(checkJdk16)
-}
-
jvmTest {
minHeapSize = '1g'
maxHeapSize = '1g'
@@ -246,30 +256,37 @@ task jvmLincheckTest(type: Test, dependsOn: compileTestKotlinJvm) {
static void configureJvmForLincheck(task) {
task.minHeapSize = '1g'
- task.maxHeapSize = '6g' // we may need more space for building an interleaving tree in the model checking mode
+ task.maxHeapSize = '4g' // we may need more space for building an interleaving tree in the model checking mode
task.jvmArgs = ['--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED', // required for transformation
- '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode
+ '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode
task.systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2'
task.systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '1' // better for the model checking mode
}
-task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) {
- classpath = files { jvmTest.classpath }
- testClassesDirs = files { jvmTest.testClassesDirs }
- executable = "$System.env.JDK_16/bin/java"
- exclude '**/*LFStressTest.*' // lock-freedom tests use LockFreedomTestEnvironment which needs JDK8
- exclude '**/*LincheckTest.*' // Lincheck tests use LinChecker which needs JDK8
- exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8
- exclude '**/ExceptionsGuideTest.*'
- exclude '**/RunInterruptibleStressTest.*' // fails on JDK 1.6 due to JDK bug
+// Always check additional test sets
+task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest])
+check.dependsOn moreTest
+
+tasks.jvmLincheckTest {
+ kover {
+ enabled = false // Always disabled, lincheck doesn't really support coverage
+ }
}
-// Run jdk16Test test only during nightly stress test
-jdk16Test.onlyIf { project.properties['stressTest'] != null }
+def commonKoverExcludes =
+ ["kotlinx.coroutines.debug.*", // Tested by debug module
+ "kotlinx.coroutines.channels.ChannelsKt__DeprecatedKt.*", // Deprecated
+ "kotlinx.coroutines.scheduling.LimitingDispatcher", // Deprecated
+ "kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher" // Deprecated
+ ]
-// Always check additional test sets
-task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest, jdk16Test])
-check.dependsOn moreTest
+tasks.koverHtmlReport {
+ excludes = commonKoverExcludes
+}
+
+tasks.koverVerify {
+ excludes = commonKoverExcludes
+}
task testsJar(type: Jar, dependsOn: jvmTestClasses) {
classifier = 'tests'
diff --git a/kotlinx-coroutines-core/common/README.md b/kotlinx-coroutines-core/common/README.md
index fcfe334c..041cd3ec 100644
--- a/kotlinx-coroutines-core/common/README.md
+++ b/kotlinx-coroutines-core/common/README.md
@@ -60,17 +60,12 @@ helper function. [NonCancellable] job object is provided to suppress cancellatio
| [SendChannel][kotlinx.coroutines.channels.SendChannel] | [send][kotlinx.coroutines.channels.SendChannel.send] | [onSend][kotlinx.coroutines.channels.SendChannel.onSend] | [trySend][kotlinx.coroutines.channels.SendChannel.trySend]
| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.channels.ReceiveChannel.onReceive] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive]
| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receiveCatching][kotlinx.coroutines.channels.ReceiveChannel.receiveCatching] | [onReceiveCatching][kotlinx.coroutines.channels.ReceiveChannel.onReceiveCatching] | [tryReceive][kotlinx.coroutines.channels.ReceiveChannel.tryReceive]
-| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | [onLock][kotlinx.coroutines.sync.Mutex.onLock] | [tryLock][kotlinx.coroutines.sync.Mutex.tryLock]
| none | [delay] | [onTimeout][kotlinx.coroutines.selects.SelectBuilder.onTimeout] | none
This module provides debugging facilities for coroutines (run JVM with `-ea` or `-Dkotlinx.coroutines.debug` options)
and [newCoroutineContext] function to write user-defined coroutine builders that work with these
debugging facilities. See [DEBUG_PROPERTY_NAME] for more details.
-This module provides a special CoroutineContext type [TestCoroutineCoroutineContext][kotlinx.coroutines.test.TestCoroutineContext] that
-allows the writer of code that contains Coroutines with delays and timeouts to write non-flaky unit-tests for that code allowing these tests to
-terminate in near zero time. See the documentation for this class for more information.
-
# Package kotlinx.coroutines
General-purpose coroutine builders, contexts, and helper functions.
@@ -98,67 +93,61 @@ Low-level primitives for finer-grained control of coroutines.
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
-[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
-[newFixedThreadPoolContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-fixed-thread-pool-context.html
-[asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
-[NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
-[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
-[withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
-[awaitAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html
-[joinAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html
-[suspendCancellableCoroutine]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
-[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
-[Job.onJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/on-join.html
-[Job.isCompleted]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html
-[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
-[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
-[newCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-coroutine-context.html
-[DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[Deferred]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[Dispatchers.Unconfined]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
+[newSingleThreadContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
+[newFixedThreadPoolContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-fixed-thread-pool-context.html
+[asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
+[NonCancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html
+[CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[withTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
+[withTimeoutOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html
+[awaitAll]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html
+[joinAll]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html
+[suspendCancellableCoroutine]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
+[Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[Job.onJoin]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/on-join.html
+[Job.isCompleted]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html
+[Deferred.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
+[Deferred.onAwait]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
+[newCoroutineContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-coroutine-context.html
+[DEBUG_PROPERTY_NAME]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
<!--- INDEX kotlinx.coroutines.sync -->
-[kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
-[kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
-[kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html
-[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html
+[kotlinx.coroutines.sync.Mutex]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
+[kotlinx.coroutines.sync.Mutex.lock]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
<!--- INDEX kotlinx.coroutines.channels -->
-[kotlinx.coroutines.channels.produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
-[kotlinx.coroutines.channels.ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html
-[kotlinx.coroutines.channels.ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
-[kotlinx.coroutines.channels.actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
-[kotlinx.coroutines.channels.SendChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
-[kotlinx.coroutines.channels.ActorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-actor-scope/index.html
-[kotlinx.coroutines.channels.Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
-[kotlinx.coroutines.channels.SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
-[kotlinx.coroutines.channels.ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
-[kotlinx.coroutines.channels.SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
-[kotlinx.coroutines.channels.SendChannel.trySend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
-[kotlinx.coroutines.channels.ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
-[kotlinx.coroutines.channels.ReceiveChannel.tryReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/try-receive.html
-[kotlinx.coroutines.channels.ReceiveChannel.receiveCatching]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive-catching.html
-[kotlinx.coroutines.channels.ReceiveChannel.onReceiveCatching]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
+[kotlinx.coroutines.channels.produce]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
+[kotlinx.coroutines.channels.ReceiveChannel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html
+[kotlinx.coroutines.channels.ProducerScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
+[kotlinx.coroutines.channels.actor]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
+[kotlinx.coroutines.channels.SendChannel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
+[kotlinx.coroutines.channels.ActorScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-actor-scope/index.html
+[kotlinx.coroutines.channels.Channel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
+[kotlinx.coroutines.channels.SendChannel.send]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
+[kotlinx.coroutines.channels.ReceiveChannel.receive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
+[kotlinx.coroutines.channels.SendChannel.onSend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
+[kotlinx.coroutines.channels.SendChannel.trySend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
+[kotlinx.coroutines.channels.ReceiveChannel.onReceive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
+[kotlinx.coroutines.channels.ReceiveChannel.tryReceive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/try-receive.html
+[kotlinx.coroutines.channels.ReceiveChannel.receiveCatching]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive-catching.html
+[kotlinx.coroutines.channels.ReceiveChannel.onReceiveCatching]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-catching.html
<!--- INDEX kotlinx.coroutines.selects -->
-[kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
-[kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/on-timeout.html
-
-<!--- INDEX kotlinx.coroutines.test -->
-
-[kotlinx.coroutines.test.TestCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.test/-test-coroutine-context/index.html
+[kotlinx.coroutines.selects.select]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
+[kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/on-timeout.html
<!--- END -->
diff --git a/kotlinx-coroutines-core/common/src/Annotations.kt b/kotlinx-coroutines-core/common/src/Annotations.kt
index 724cc8cb..bacce394 100644
--- a/kotlinx-coroutines-core/common/src/Annotations.kt
+++ b/kotlinx-coroutines-core/common/src/Annotations.kt
@@ -30,6 +30,19 @@ public annotation class DelicateCoroutinesApi
*/
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
+@Target(
+ AnnotationTarget.CLASS,
+ AnnotationTarget.ANNOTATION_CLASS,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.LOCAL_VARIABLE,
+ AnnotationTarget.VALUE_PARAMETER,
+ AnnotationTarget.CONSTRUCTOR,
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER,
+ AnnotationTarget.TYPEALIAS
+)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
public annotation class ExperimentalCoroutinesApi
diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt
index e06ed330..c1669e25 100644
--- a/kotlinx-coroutines-core/common/src/Await.kt
+++ b/kotlinx-coroutines-core/common/src/Await.kt
@@ -29,8 +29,8 @@ public suspend fun <T> awaitAll(vararg deferreds: Deferred<T>): List<T> =
* when all deferred computations are complete or resumes with the first thrown exception if any of computations
* complete exceptionally including cancellation.
*
- * This function is **not** equivalent to `this.map { it.await() }` which fails only when when it sequentially
- * gets to wait the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
+ * This function is **not** equivalent to `this.map { it.await() }` which fails only when it sequentially
+ * gets to wait for the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt
index a11ffe9e..3dea68cf 100644
--- a/kotlinx-coroutines-core/common/src/Builders.common.kt
+++ b/kotlinx-coroutines-core/common/src/Builders.common.kt
@@ -126,12 +126,15 @@ private class LazyDeferredCoroutine<T>(
* This suspending function is cancellable. It immediately checks for cancellation of
* the resulting context and throws [CancellationException] if it is not [active][CoroutineContext.isActive].
*
- * This function uses dispatcher from the new context, shifting execution of the [block] into the
- * different thread if a new dispatcher is specified, and back to the original dispatcher
- * when it completes. Note that the result of `withContext` invocation is
- * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**,
- * which means that if the original [coroutineContext], in which `withContext` was invoked,
- * is cancelled by the time its dispatcher starts to execute the code,
+ * Calls to [withContext] whose [context] argument provides a [CoroutineDispatcher] that is
+ * different from the current one, by necessity, perform additional dispatches: the [block]
+ * can not be executed immediately and needs to be dispatched for execution on
+ * the passed [CoroutineDispatcher], and then when the [block] completes, the execution
+ * has to shift back to the original dispatcher.
+ *
+ * Note that the result of `withContext` invocation is dispatched into the original context in a cancellable way
+ * with a **prompt cancellation guarantee**, which means that if the original [coroutineContext]
+ * in which `withContext` was invoked is cancelled by the time its dispatcher starts to execute the code,
* it discards the result of `withContext` and throws [CancellationException].
*
* The cancellation behaviour described above is enabled if and only if the dispatcher is being changed.
@@ -148,7 +151,8 @@ public suspend fun <T> withContext(
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
- val newContext = oldContext + context
+ // Copy CopyableThreadContextElement if necessary
+ val newContext = oldContext.newCoroutineContext(context)
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- new context is the same as the old one
diff --git a/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt
new file mode 100644
index 00000000..9c670329
--- /dev/null
+++ b/kotlinx-coroutines-core/common/src/CloseableCoroutineDispatcher.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+/**
+ * [CoroutineDispatcher] that provides a method to close it,
+ * causing the rejection of any new tasks and cleanup of all underlying resources
+ * associated with the current dispatcher.
+ * Examples of closeable dispatchers are dispatchers backed by `java.lang.Executor` and
+ * by `kotlin.native.Worker`.
+ *
+ * **The `CloseableCoroutineDispatcher` class is not stable for inheritance in 3rd party libraries**, as new methods
+ * might be added to this interface in the future, but is stable for use.
+ */
+@ExperimentalCoroutinesApi
+public expect abstract class CloseableCoroutineDispatcher() : CoroutineDispatcher {
+
+ /**
+ * Initiate the closing sequence of the coroutine dispatcher.
+ * After a successful call to [close], no new tasks will
+ * be accepted to be [dispatched][dispatch], but the previously dispatched tasks will be run.
+ *
+ * Invocations of `close` are idempotent and thread-safe.
+ */
+ public abstract fun close()
+}
diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
index 68b4b1a3..9153f398 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
@@ -7,13 +7,20 @@ package kotlinx.coroutines
import kotlin.coroutines.*
/**
- * Creates a context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
- * [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on).
+ * Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
+ * [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on)
+ * and copyable-thread-local facilities on JVM.
*/
public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
-internal expect fun createDefaultDispatcher(): CoroutineDispatcher
+/**
+ * Creates a context for coroutine builder functions that do not launch a new coroutine, e.g. [withContext].
+ * @suppress
+ */
+@InternalCoroutinesApi
+public expect fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext
+@PublishedApi
@Suppress("PropertyName")
internal expect val DefaultDelay: Delay
diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
index d5613d41..568353bf 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
@@ -45,6 +45,9 @@ public abstract class CoroutineDispatcher :
* potentially forming an event-loop to prevent stack overflows.
* The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
*
+ * The [context] parameter represents the context of the coroutine that is being dispatched,
+ * or [EmptyCoroutineContext] if a non-coroutine-specific [Runnable] is dispatched instead.
+ *
* A dispatcher can override this method to provide a performance optimization and avoid paying a cost of an unnecessary dispatch.
* E.g. [MainCoroutineDispatcher.immediate] checks whether we are already in the required UI thread in this method and avoids
* an additional dispatch when it is not required.
@@ -58,22 +61,78 @@ public abstract class CoroutineDispatcher :
*
* This method should generally be exception-safe. An exception thrown from this method
* may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state.
+ *
+ * @see dispatch
+ * @see Dispatchers.Unconfined
*/
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
/**
- * Dispatches execution of a runnable [block] onto another thread in the given [context].
+ * Creates a view of the current dispatcher that limits the parallelism to the given [value][parallelism].
+ * The resulting view uses the original dispatcher for execution, but with the guarantee that
+ * no more than [parallelism] coroutines are executed at the same time.
+ *
+ * This method does not impose restrictions on the number of views or the total sum of parallelism values,
+ * each view controls its own parallelism independently with the guarantee that the effective parallelism
+ * of all views cannot exceed the actual parallelism of the original dispatcher.
+ *
+ * ### Limitations
+ *
+ * The default implementation of `limitedParallelism` does not support direct dispatchers,
+ * such as executing the given runnable in place during [dispatch] calls.
+ * Any dispatcher that may return `false` from [isDispatchNeeded] is considered direct.
+ * For direct dispatchers, it is recommended to override this method
+ * and provide a domain-specific implementation or to throw an [UnsupportedOperationException].
+ *
+ * ### Example of usage
+ * ```
+ * private val backgroundDispatcher = newFixedThreadPoolContext(4, "App Background")
+ * // At most 2 threads will be processing images as it is really slow and CPU-intensive
+ * private val imageProcessingDispatcher = backgroundDispatcher.limitedParallelism(2)
+ * // At most 3 threads will be processing JSON to avoid image processing starvation
+ * private val jsonProcessingDispatcher = backgroundDispatcher.limitedParallelism(3)
+ * // At most 1 thread will be doing IO
+ * private val fileWriterDispatcher = backgroundDispatcher.limitedParallelism(1)
+ * ```
+ * Note how in this example the application has an executor with 4 threads, but the total sum of all limits
+ * is 6. Still, at most 4 coroutines can be executed simultaneously as each view limits only its own parallelism.
+ *
+ * Note that this example was structured in such a way that it illustrates the parallelism guarantees.
+ * In practice, it is usually better to use [Dispatchers.IO] or [Dispatchers.Default] instead of creating a
+ * `backgroundDispatcher`. It is both possible and advised to call `limitedParallelism` on them.
+ */
+ @ExperimentalCoroutinesApi
+ public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ return LimitedDispatcher(this, parallelism)
+ }
+
+ /**
+ * Requests execution of a runnable [block].
+ * The dispatcher guarantees that [block] will eventually execute, typically by dispatching it to a thread pool,
+ * using a dedicated thread, or just executing the block in place.
+ * The [context] parameter represents the context of the coroutine that is being dispatched,
+ * or [EmptyCoroutineContext] if a non-coroutine-specific [Runnable] is dispatched instead.
+ * Implementations may use [context] for additional context-specific information,
+ * such as priority, whether the dispatched coroutine can be invoked in place,
+ * coroutine name, and additional diagnostic elements.
+ *
* This method should guarantee that the given [block] will be eventually invoked,
* otherwise the system may reach a deadlock state and never leave it.
- * Cancellation mechanism is transparent for [CoroutineDispatcher] and is managed by [block] internals.
+ * The cancellation mechanism is transparent for [CoroutineDispatcher] and is managed by [block] internals.
*
* This method should generally be exception-safe. An exception thrown from this method
- * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state.
+ * may leave the coroutines that use this dispatcher in an inconsistent and hard-to-debug state.
+ *
+ * This method must not immediately call [block]. Doing so may result in `StackOverflowError`
+ * when `dispatch` is invoked repeatedly, for example when [yield] is called in a loop.
+ * In order to execute a block in place, it is required to return `false` from [isDispatchNeeded]
+ * and delegate the `dispatch` implementation to `Dispatchers.Unconfined.dispatch` in such cases.
+ * To support this, the coroutines machinery ensures in-place execution and forms an event-loop to
+ * avoid unbound recursion.
*
- * This method must not immediately call [block]. Doing so would result in [StackOverflowError]
- * when [yield] is repeatedly called from a loop. However, an implementation that returns `false` from
- * [isDispatchNeeded] can delegate this function to `dispatch` method of [Dispatchers.Unconfined], which is
- * integrated with [yield] to avoid this problem.
+ * @see isDispatchNeeded
+ * @see Dispatchers.Unconfined
*/
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt
index 3ed233bf..b0928d5c 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt
@@ -12,7 +12,7 @@ import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
/**
- * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc)
+ * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc.)
* is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
* to automatically propagate all its elements and cancellation.
*
@@ -28,8 +28,8 @@ import kotlin.coroutines.intrinsics.*
* By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a
* [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation.
*
- * Every coroutine builder (like [launch], [async], etc)
- * and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope
+ * Every coroutine builder (like [launch], [async], and others)
+ * and every scoping function (like [coroutineScope] and [withContext]) provides _its own_ scope
* with its own [Job] instance into the inner block of code it runs.
* By convention, they all wait for all the coroutines inside their block to complete before completing themselves,
* thus enforcing the structured concurrency. See [Job] documentation for more details.
@@ -42,14 +42,14 @@ import kotlin.coroutines.intrinsics.*
* ### Custom usage
*
* `CoroutineScope` should be declared as a property on entities with a well-defined lifecycle that are
- * responsible for launching children coroutines. The corresponding instance of `CoroutineScope` shall be created
- * with either `CoroutineScope()` or `MainScope()` functions. The difference between them is only in the
- * [CoroutineDispatcher]:
+ * responsible for launching child coroutines. The corresponding instance of `CoroutineScope` shall be created
+ * with either `CoroutineScope()` or `MainScope()`:
*
- * * `CoroutineScope()` uses [Dispatchers.Default] for its coroutines.
- * * `MainScope()` uses [Dispatchers.Main] for its coroutines.
+ * * `CoroutineScope()` uses the [context][CoroutineContext] provided to it as a parameter for its coroutines
+ * and adds a [Job] if one is not provided as part of the context.
+ * * `MainScope()` uses [Dispatchers.Main] for its coroutines and has a [SupervisorJob].
*
- * **The key part of custom usage of `CustomScope` is cancelling it at the end of the lifecycle.**
+ * **The key part of custom usage of `CoroutineScope` is cancelling it at the end of the lifecycle.**
* The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines
* is no longer needed. It cancels all the coroutines that might still be running on behalf of it.
*
@@ -178,7 +178,7 @@ public val CoroutineScope.isActive: Boolean
* ```
* // concurrently load configuration and data
* suspend fun loadConfigurationAndData() {
- * coroutinesScope {
+ * coroutineScope {
* launch { loadConfiguration() }
* launch { loadData() }
* }
@@ -269,8 +269,8 @@ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
* Creates a [CoroutineScope] that wraps the given coroutine [context].
*
* If the given [context] does not contain a [Job] element, then a default `Job()` is created.
- * This way, cancellation or failure of any child coroutine in this scope cancels all the other children,
- * just like inside [coroutineScope] block.
+ * This way, failure of any child coroutine in this scope or [cancellation][CoroutineScope.cancel] of the scope itself
+ * cancels all the scope's children, just like inside [coroutineScope] block.
*/
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt
index 4543c5dd..301ed2d3 100644
--- a/kotlinx-coroutines-core/common/src/Delay.kt
+++ b/kotlinx-coroutines-core/common/src/Delay.kt
@@ -19,15 +19,12 @@ import kotlin.time.*
*/
@InternalCoroutinesApi
public interface Delay {
- /**
- * Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
- *
- * This suspending function is cancellable.
- * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
- * immediately resumes with [CancellationException].
- * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
- * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
- */
+
+ /** @suppress **/
+ @Deprecated(
+ message = "Deprecated without replacement as an internal method never intended for public use",
+ level = DeprecationLevel.ERROR
+ ) // Error since 1.6.0
public suspend fun delay(time: Long) {
if (time <= 0) return // don't delay
return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) }
@@ -54,8 +51,6 @@ public interface Delay {
* Schedules invocation of a specified [block] after a specified delay [timeMillis].
* The resulting [DisposableHandle] can be used to [dispose][DisposableHandle.dispose] of this invocation
* request if it is not needed anymore.
- *
- * This implementation uses a built-in single-threaded scheduled executor service.
*/
public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
DefaultDelay.invokeOnTimeout(timeMillis, block, context)
@@ -138,7 +133,6 @@ public suspend fun delay(timeMillis: Long) {
*
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
*/
-@ExperimentalTime
public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMillis())
/** Returns [Delay] implementation of the given context */
@@ -148,6 +142,5 @@ internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor)
* Convert this duration to its millisecond value.
* Positive durations are coerced at least `1`.
*/
-@ExperimentalTime
internal fun Duration.toDelayMillis(): Long =
if (this > Duration.ZERO) inWholeMilliseconds.coerceAtLeast(1) else 0
diff --git a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt
index 8681b182..28e67a42 100644
--- a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt
+++ b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt
@@ -26,9 +26,9 @@ public expect object Dispatchers {
*
* Access to this property may throw an [IllegalStateException] if no main dispatchers are present in the classpath.
*
- * Depending on platform and classpath it can be mapped to different dispatchers:
+ * Depending on platform and classpath, it can be mapped to different dispatchers:
* - On JS and Native it is equivalent to the [Default] dispatcher.
- * - On JVM it either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the
+ * - On JVM it is either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the
* [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
*
* In order to work with the `Main` dispatcher, the following artifact should be added to the project runtime dependencies:
@@ -48,7 +48,7 @@ public expect object Dispatchers {
* stack overflows.
*
* ### Event loop
- * Event loop semantics is a purely internal concept and have no guarantees on the order of execution
+ * Event loop semantics is a purely internal concept and has no guarantees on the order of execution
* except that all queued coroutines will be executed on the current thread in the lexical scope of the outermost
* unconfined coroutine.
*
@@ -63,11 +63,11 @@ public expect object Dispatchers {
* }
* println("Done")
* ```
- * Can print both "1 2 3" and "1 3 2", this is an implementation detail that can be changed.
- * But it is guaranteed that "Done" will be printed only when both `withContext` calls are completed.
+ * Can print both "1 2 3" and "1 3 2". This is an implementation detail that can be changed.
+ * However, it is guaranteed that "Done" will be printed only when both `withContext` calls are completed.
*
* If you need your coroutine to be confined to a particular thread or a thread-pool after resumption,
- * but still want to execute it in the current call-frame until its first suspension, then you can use
+ * but still want to execute it in the current call-frame until its first suspension, you can use
* an optional [CoroutineStart] parameter in coroutine builders like
* [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to
* the value of [CoroutineStart.UNDISPATCHED].
diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
index e6a57c92..12940c54 100644
--- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt
+++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
@@ -115,7 +115,12 @@ internal abstract class EventLoop : CoroutineDispatcher() {
}
}
- protected open fun shutdown() {}
+ final override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ return this
+ }
+
+ open fun shutdown() {}
}
@ThreadLocal
@@ -231,8 +236,13 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
if (timeNanos < MAX_DELAY_NS) {
val now = nanoTime()
DelayedResumeTask(now + timeNanos, continuation).also { task ->
- continuation.disposeOnCancellation(task)
+ /*
+ * Order is important here: first we schedule the heap and only then
+ * publish it to continuation. Otherwise, `DelayedResumeTask` would
+ * have to know how to be disposed of even when it wasn't scheduled yet.
+ */
schedule(now, task)
+ continuation.disposeOnCancellation(task)
}
}
}
@@ -271,7 +281,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
// then process one event from queue
val task = dequeue()
if (task != null) {
- task.run()
+ platformAutoreleasePool { task.run() }
return 0
}
return nextTime
@@ -279,7 +289,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)
- public fun enqueue(task: Runnable) {
+ open fun enqueue(task: Runnable) {
if (enqueueImpl(task)) {
// todo: we should unpark only when this delayed task became first in the queue
unpark()
@@ -405,6 +415,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
*/
@JvmField var nanoTime: Long
) : Runnable, Comparable<DelayedTask>, DisposableHandle, ThreadSafeHeapNode {
+ @Volatile
private var _heap: Any? = null // null | ThreadSafeHeap | DISPOSED_TASK
override var heap: ThreadSafeHeap<*>?
@@ -526,3 +537,13 @@ internal expect object DefaultExecutor {
public fun enqueue(task: Runnable)
}
+/**
+ * Used by Darwin targets to wrap a [Runnable.run] call in an Objective-C Autorelease Pool. It is a no-op on JVM, JS and
+ * non-Darwin native targets.
+ *
+ * Coroutines on Darwin targets can call into the Objective-C world, where a callee may push a to-be-returned object to
+ * the Autorelease Pool, so as to avoid a premature ARC release before it reaches the caller. This means the pool must
+ * be eventually drained to avoid leaks. Since Kotlin Coroutines does not use [NSRunLoop], which provides automatic
+ * pool management, it must manage the pool creation and pool drainage manually.
+ */
+internal expect inline fun platformAutoreleasePool(crossinline block: () -> Unit)
diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt
index 9552153a..31d90eee 100644
--- a/kotlinx-coroutines-core/common/src/Job.kt
+++ b/kotlinx-coroutines-core/common/src/Job.kt
@@ -387,7 +387,7 @@ public fun Job0(parent: Job? = null): Job = Job(parent)
/**
* A handle to an allocated object that can be disposed to make it eligible for garbage collection.
*/
-public interface DisposableHandle {
+public fun interface DisposableHandle {
/**
* Disposes the corresponding object, making it eligible for garbage collection.
* Repeated invocation of this function has no effect.
@@ -395,18 +395,6 @@ public interface DisposableHandle {
public fun dispose()
}
-/**
- * @suppress **This an internal API and should not be used from general code.**
- */
-@Suppress("FunctionName")
-@InternalCoroutinesApi
-public inline fun DisposableHandle(crossinline block: () -> Unit): DisposableHandle =
- object : DisposableHandle {
- override fun dispose() {
- block()
- }
- }
-
// -------------------- Parent-child communication --------------------
/**
diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt
index 0a3dd234..1b5975c8 100644
--- a/kotlinx-coroutines-core/common/src/JobSupport.kt
+++ b/kotlinx-coroutines-core/common/src/JobSupport.kt
@@ -1312,6 +1312,7 @@ private class Empty(override val isActive: Boolean) : Incomplete {
override fun toString(): String = "Empty{${if (isActive) "Active" else "New" }}"
}
+@PublishedApi // for a custom job in the test module
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
init { initParentJob(parent) }
override val onCancelComplete get() = true
diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
index 602da6e0..a7065ccd 100644
--- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
+++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
@@ -4,6 +4,8 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.*
+
/**
* Base class for special [CoroutineDispatcher] which is confined to application "Main" or "UI" thread
* and used for any UI-based activities. Instance of `MainDispatcher` can be obtained by [Dispatchers.Main].
@@ -51,6 +53,12 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {
*/
override fun toString(): String = toStringInternalImpl() ?: "$classSimpleName@$hexAddress"
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ // MainCoroutineDispatcher is single-threaded -- short-circuit any attempts to limit it
+ return this
+ }
+
/**
* Internal method for more specific [toString] implementations. It returns non-null
* string if this dispatcher is set in the platform as the main one.
diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt
index 264a2b9d..6a6829df 100644
--- a/kotlinx-coroutines-core/common/src/Timeout.kt
+++ b/kotlinx-coroutines-core/common/src/Timeout.kt
@@ -64,7 +64,6 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
*
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
-@ExperimentalTime
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
@@ -131,7 +130,6 @@ public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend Corout
*
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
-@ExperimentalTime
public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
withTimeoutOrNull(timeout.toDelayMillis(), block)
@@ -165,7 +163,7 @@ private class TimeoutCoroutine<U, in T: U>(
*/
public class TimeoutCancellationException internal constructor(
message: String,
- @JvmField internal val coroutine: Job?
+ @JvmField @Transient internal val coroutine: Job?
) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> {
/**
* Creates a timeout exception with the given message.
@@ -175,7 +173,7 @@ public class TimeoutCancellationException internal constructor(
internal constructor(message: String) : this(message, null)
// message is never null in fact
- override fun createCopy(): TimeoutCancellationException? =
+ override fun createCopy(): TimeoutCancellationException =
TimeoutCancellationException(message ?: "", coroutine).also { it.initCause(this) }
}
diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt
index 4f486458..5837ae83 100644
--- a/kotlinx-coroutines-core/common/src/Unconfined.kt
+++ b/kotlinx-coroutines-core/common/src/Unconfined.kt
@@ -11,10 +11,16 @@ import kotlin.jvm.*
* A coroutine dispatcher that is not confined to any specific thread.
*/
internal object Unconfined : CoroutineDispatcher() {
+
+ @ExperimentalCoroutinesApi
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ throw UnsupportedOperationException("limitedParallelism is not supported for Dispatchers.Unconfined")
+ }
+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
override fun dispatch(context: CoroutineContext, block: Runnable) {
- // It can only be called by the "yield" function. See also code of "yield" function.
+ /** It can only be called by the [yield] function. See also code of [yield] function. */
val yieldContext = context[YieldContext]
if (yieldContext != null) {
// report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
@@ -32,6 +38,7 @@ internal object Unconfined : CoroutineDispatcher() {
/**
* Used to detect calls to [Unconfined.dispatch] from [yield] function.
*/
+@PublishedApi
internal class YieldContext : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<YieldContext>
diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
index 4751296c..b92ced6a 100644
--- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
@@ -136,7 +136,7 @@ internal abstract class AbstractSendChannel<E>(
return sendSuspend(element)
}
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION", "DEPRECATION_ERROR")
override fun offer(element: E): Boolean {
// Temporary migration for offer users who rely on onUndeliveredElement
try {
diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt
index 600eb6a9..0a96f753 100644
--- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt
@@ -33,6 +33,11 @@ internal class ArrayBroadcastChannel<E>(
require(capacity >= 1) { "ArrayBroadcastChannel capacity must be at least 1, but $capacity was specified" }
}
+ /**
+ * NB: prior to changing any logic of ArrayBroadcastChannel internals, please ensure that
+ * you do not break internal invariants of the SubscriberList implementation on K/N and KJS
+ */
+
/*
* Writes to buffer are guarded by bufferLock, but reads from buffer are concurrent with writes
* - Write element to buffer then write "tail" (volatile)
@@ -60,6 +65,7 @@ internal class ArrayBroadcastChannel<E>(
get() = _size.value
set(value) { _size.value = value }
+ @Suppress("DEPRECATION")
private val subscribers = subscriberList<Subscriber<E>>()
override val isBufferAlwaysFull: Boolean get() = false
diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt
index b15c4262..5ad79fdc 100644
--- a/kotlinx-coroutines-core/common/src/channels/Channel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt
@@ -64,7 +64,6 @@ public interface SendChannel<in E> {
*/
public val onSend: SelectClause2<E, SendChannel<E>>
-
/**
* Immediately adds the specified [element] to this channel, if this doesn't violate its capacity restrictions,
* and returns the successful result. Otherwise, returns failed or closed result.
@@ -158,10 +157,10 @@ public interface SendChannel<in E> {
* @suppress **Deprecated**.
*/
@Deprecated(
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
message = "Deprecated in the favour of 'trySend' method",
replaceWith = ReplaceWith("trySend(element).isSuccess")
- ) // Warning since 1.5.0
+ ) // Warning since 1.5.0, error since 1.6.0
public fun offer(element: E): Boolean {
val result = trySend(element)
if (result.isSuccess) return true
@@ -314,12 +313,12 @@ public interface ReceiveChannel<out E> {
* @suppress **Deprecated**.
*/
@Deprecated(
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
message = "Deprecated in the favour of 'tryReceive'. " +
"Please note that the provided replacement does not rethrow channel's close cause as 'poll' did, " +
"for the precise replacement please refer to the 'poll' documentation",
replaceWith = ReplaceWith("tryReceive().getOrNull()")
- ) // Warning since 1.5.0
+ ) // Warning since 1.5.0, error since 1.6.0
public fun poll(): E? {
val result = tryReceive()
if (result.isSuccess) return result.getOrThrow()
@@ -365,7 +364,7 @@ public interface ReceiveChannel<out E> {
message = "Deprecated in favor of onReceiveCatching extension",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("onReceiveCatching")
- ) // Warning since 1.3.0, error in 1.5.0, will be hidden or removed in 1.6.0
+ ) // Warning since 1.3.0, error in 1.5.0, will be hidden or removed in 1.7.0
public val onReceiveOrNull: SelectClause1<E?>
get() {
return object : SelectClause1<E?> {
@@ -685,7 +684,7 @@ public interface ChannelIterator<out E> {
* exception which is either rethrown from the caller method or handed off to the exception handler in the current context
* (see [CoroutineExceptionHandler]) when one is available.
*
- * A typical usage for `onDeliveredElement` is to close a resource that is being transferred via the channel. The
+ * A typical usage for `onUndeliveredElement` is to close a resource that is being transferred via the channel. The
* following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel
* are cancelled. Resources are never lost.
*
diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt
index e0b4f9d2..a78e2f18 100644
--- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt
+++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt
@@ -50,7 +50,7 @@ public inline fun <E, R> BroadcastChannel<E>.consume(block: ReceiveChannel<E>.()
@Deprecated(
"Deprecated in the favour of 'receiveCatching'",
ReplaceWith("receiveCatching().getOrNull()"),
- DeprecationLevel.WARNING
+ DeprecationLevel.ERROR
) // Warning since 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
public suspend fun <E : Any> ReceiveChannel<E>.receiveOrNull(): E? {
@@ -63,7 +63,7 @@ public suspend fun <E : Any> ReceiveChannel<E>.receiveOrNull(): E? {
*/
@Deprecated(
"Deprecated in the favour of 'onReceiveCatching'",
- level = DeprecationLevel.WARNING
+ level = DeprecationLevel.ERROR
) // Warning since 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0
public fun <E : Any> ReceiveChannel<E>.onReceiveOrNull(): SelectClause1<E?> {
@Suppress("DEPRECATION", "UNCHECKED_CAST")
diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt
index f7f60cf9..177e80cb 100644
--- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt
@@ -19,7 +19,7 @@ import kotlinx.coroutines.selects.*
*/
internal open class ConflatedChannel<E>(onUndeliveredElement: OnUndeliveredElement<E>?) : AbstractChannel<E>(onUndeliveredElement) {
protected final override val isBufferAlwaysEmpty: Boolean get() = false
- protected final override val isBufferEmpty: Boolean get() = value === EMPTY
+ protected final override val isBufferEmpty: Boolean get() = lock.withLock { value === EMPTY }
protected final override val isBufferAlwaysFull: Boolean get() = false
protected final override val isBufferFull: Boolean get() = false
@@ -139,5 +139,5 @@ internal open class ConflatedChannel<E>(onUndeliveredElement: OnUndeliveredEleme
// ------ debug ------
override val bufferDebugString: String
- get() = "(value=$value)"
+ get() = lock.withLock { "(value=$value)" }
}
diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt
index 3342fb6e..da8f884b 100644
--- a/kotlinx-coroutines-core/common/src/channels/Produce.kt
+++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt
@@ -6,14 +6,11 @@ package kotlinx.coroutines.channels
import kotlinx.coroutines.*
import kotlin.coroutines.*
+import kotlinx.coroutines.flow.*
/**
- * Scope for the [produce][CoroutineScope.produce] coroutine builder.
- *
- * **Note: This is an experimental api.** Behavior of producers that work as children in a parent scope with respect
- * to cancellation and error handling may change in the future.
+ * Scope for the [produce][CoroutineScope.produce], [callbackFlow] and [channelFlow] builders.
*/
-@ExperimentalCoroutinesApi
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
/**
* A reference to the channel this coroutine [sends][send] elements to.
@@ -45,7 +42,6 @@ public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
* }
* ```
*/
-@ExperimentalCoroutinesApi
public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) {
check(kotlin.coroutines.coroutineContext[Job] === this) { "awaitClose() can only be invoked from the producer context" }
try {
@@ -137,7 +133,7 @@ internal fun <E> CoroutineScope.produce(
return coroutine
}
-internal open class ProducerCoroutine<E>(
+private class ProducerCoroutine<E>(
parentContext: CoroutineContext, channel: Channel<E>
) : ChannelCoroutine<E>(parentContext, channel, true, active = true), ProducerScope<E> {
override val isActive: Boolean
diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt
index 66b55a90..c4b55e10 100644
--- a/kotlinx-coroutines-core/common/src/flow/Builders.kt
+++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt
@@ -199,25 +199,6 @@ public fun LongRange.asFlow(): Flow<Long> = flow {
}
/**
- * @suppress
- */
-@FlowPreview
-@Deprecated(
- message = "Use channelFlow with awaitClose { } instead of flowViaChannel and invokeOnClose { }.",
- level = DeprecationLevel.ERROR
-) // To be removed in 1.4.x
-@Suppress("DeprecatedCallableAddReplaceWith")
-public fun <T> flowViaChannel(
- bufferSize: Int = BUFFERED,
- @BuilderInference block: CoroutineScope.(channel: SendChannel<T>) -> Unit
-): Flow<T> {
- return channelFlow<T> {
- block(channel)
- awaitClose()
- }.buffer(bufferSize)
-}
-
-/**
* Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel]
* provided to the builder's [block] of code via [ProducerScope]. It allows elements to be
* produced by code that is running in a different context or concurrently.
diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt
index 382953ef..51ed4270 100644
--- a/kotlinx-coroutines-core/common/src/flow/Channels.kt
+++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt
@@ -179,45 +179,14 @@ public fun <T> BroadcastChannel<T>.asFlow(): Flow<T> = flow {
}
/**
- * ### Deprecated
- *
- * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows.
- * [SharedFlow] is an easier-to-use and more flow-centric API for the same purposes, so using
- * [shareIn] operator is preferred. It is not a direct replacement, so please
- * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb:
- *
- * * Replace `broadcastIn(scope)` and `broadcastIn(scope, CoroutineStart.LAZY)` with `shareIn(scope, 0, SharingStarted.Lazily)`.
- * * Replace `broadcastIn(scope, CoroutineStart.DEFAULT)` with `shareIn(scope, 0, SharingStarted.Eagerly)`.
- */
-@Deprecated(
- message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel",
- replaceWith = ReplaceWith("this.shareIn(scope, SharingStarted.Lazily, 0)"),
- level = DeprecationLevel.ERROR
-) // WARNING in 1.4.0, error in 1.5.0, removed in 1.6.0 (was @FlowPreview)
-public fun <T> Flow<T>.broadcastIn(
- scope: CoroutineScope,
- start: CoroutineStart = CoroutineStart.LAZY
-): BroadcastChannel<T> {
- // Backwards compatibility with operator fusing
- val channelFlow = asChannelFlow()
- val capacity = when (channelFlow.onBufferOverflow) {
- BufferOverflow.SUSPEND -> channelFlow.produceCapacity
- BufferOverflow.DROP_OLDEST -> Channel.CONFLATED
- BufferOverflow.DROP_LATEST ->
- throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST")
- }
- return scope.broadcast(channelFlow.context, capacity = capacity, start = start) {
- collect { value ->
- send(value)
- }
- }
-}
-
-/**
* Creates a [produce] coroutine that collects the given flow.
*
* This transformation is **stateful**, it launches a [produce] coroutine
- * that collects the given flow and thus resulting channel should be properly closed or cancelled.
+ * that collects the given flow, and has the same behavior:
+ *
+ * * if collecting the flow throws, the channel will be closed with that exception
+ * * if the [ReceiveChannel] is cancelled, the collection of the flow will be cancelled
+ * * if collecting the flow completes normally, the [ReceiveChannel] will be closed normally
*
* A channel with [default][Channel.Factory.BUFFERED] buffer size is created.
* Use [buffer] operator on the flow before calling `produceIn` to specify a value other than
diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt
index 0ccd343e..3520c48b 100644
--- a/kotlinx-coroutines-core/common/src/flow/Flow.kt
+++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt
@@ -108,7 +108,7 @@ import kotlin.coroutines.*
* val myFlow = flow {
* // GlobalScope.launch { // is prohibited
* // launch(Dispatchers.IO) { // is prohibited
- * // withContext(CoroutineName("myFlow")) // is prohibited
+ * // withContext(CoroutineName("myFlow")) { // is prohibited
* emit(1) // OK
* coroutineScope {
* emit(2) // OK -- still the same coroutine
@@ -131,10 +131,12 @@ import kotlin.coroutines.*
*
* ### Exception transparency
*
- * Flow implementations never catch or handle exceptions that occur in downstream flows. From the implementation standpoint
- * it means that calls to [emit][FlowCollector.emit] and [emitAll] shall never be wrapped into
- * `try { ... } catch { ... }` blocks. Exception handling in flows shall be performed with
- * [catch][Flow.catch] operator and it is designed to only catch exceptions coming from upstream flows while passing
+ * When `emit` or `emitAll` throws, the Flow implementations must immediately stop emitting new values and finish with an exception.
+ * For diagnostics or application-specific purposes, the exception may be different from the one thrown by the emit operation,
+ * suppressing the original exception as discussed below.
+ * If there is a need to emit values after the downstream failed, please use the [catch][Flow.catch] operator.
+ *
+ * The [catch][Flow.catch] operator only catches upstream exceptions, but passes
* all downstream exceptions. Similarly, terminal operators like [collect][Flow.collect]
* throw any unhandled exceptions that occur in their code or in upstream flows, for example:
*
@@ -147,6 +149,13 @@ import kotlin.coroutines.*
* ```
* The same reasoning can be applied to the [onCompletion] operator that is a declarative replacement for the `finally` block.
*
+ * All exception-handling Flow operators follow the principle of exception suppression:
+ *
+ * If the upstream flow throws an exception during its completion when the downstream exception has been thrown,
+ * the downstream exception becomes superseded and suppressed by the upstream exception, being a semantic
+ * equivalent of throwing from `finally` block. However, this doesn't affect the operation of the exception-handling operators,
+ * which consider the downstream exception to be the root cause and behave as if the upstream didn't throw anything.
+ *
* Failure to adhere to the exception transparency requirement can lead to strange behaviors which make
* it hard to reason about the code because an exception in the `collect { ... }` could be somehow "caught"
* by an upstream flow, limiting the ability of local reasoning about the code.
@@ -163,19 +172,29 @@ import kotlin.coroutines.*
*
* **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods
* might be added to this interface in the future, but is stable for use.
- * Use the `flow { ... }` builder function to create an implementation.
+ *
+ * Use the `flow { ... }` builder function to create an implementation, or extend [AbstractFlow].
+ * These implementations ensure that the context preservation property is not violated, and prevent most
+ * of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation.
*/
public interface Flow<out T> {
+
/**
* Accepts the given [collector] and [emits][FlowCollector.emit] values into it.
- * This method should never be implemented or used directly.
*
- * The only way to implement the `Flow` interface directly is to extend [AbstractFlow].
- * To collect it into a specific collector, either `collector.emitAll(flow)` or `collect { ... }` extension
- * should be used. Such limitation ensures that the context preservation property is not violated and prevents most
- * of the developer mistakes related to concurrency, inconsistent flow dispatchers and cancellation.
+ * This method can be used along with SAM-conversion of [FlowCollector]:
+ * ```
+ * myFlow.collect { value -> println("Collected $value") }
+ * ```
+ *
+ * ### Method inheritance
+ *
+ * To ensure the context preservation property, it is not recommended implementing this method directly.
+ * Instead, [AbstractFlow] can be used as the base type to properly ensure flow's properties.
+ *
+ * All default flow implementations ensure context preservation and exception transparency properties on a best-effort basis
+ * and throw [IllegalStateException] if a violation was detected.
*/
- @InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
}
@@ -205,7 +224,6 @@ public interface Flow<out T> {
@FlowPreview
public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> {
- @InternalCoroutinesApi
public final override suspend fun collect(collector: FlowCollector<T>) {
val safeCollector = SafeCollector(collector, coroutineContext)
try {
diff --git a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt
index d1c1565c..2877fe55 100644
--- a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt
+++ b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt
@@ -8,10 +8,25 @@ package kotlinx.coroutines.flow
* [FlowCollector] is used as an intermediate or a terminal collector of the flow and represents
* an entity that accepts values emitted by the [Flow].
*
- * This interface should usually not be implemented directly, but rather used as a receiver in a [flow] builder when implementing a custom operator.
+ * This interface should usually not be implemented directly, but rather used as a receiver in a [flow] builder when implementing a custom operator,
+ * or with SAM-conversion.
* Implementations of this interface are not thread-safe.
+ *
+ * Example of usage:
+ *
+ * ```
+ * val flow = getMyEvents()
+ * try {
+ * flow.collect { value ->
+ * println("Received $value")
+ * }
+ * println("My events are consumed successfully")
+ * } catch (e: Throwable) {
+ * println("Exception from the flow: $e")
+ * }
+ * ```
*/
-public interface FlowCollector<in T> {
+public fun interface FlowCollector<in T> {
/**
* Collects the value emitted by the upstream.
diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt
index 6278081a..e398740b 100644
--- a/kotlinx-coroutines-core/common/src/flow/Migration.kt
+++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt
@@ -260,7 +260,7 @@ public fun <T> Flow<T>.skip(count: Int): Flow<T> = noImpl()
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'forEach' is 'collect'",
- replaceWith = ReplaceWith("collect(block)")
+ replaceWith = ReplaceWith("collect(action)")
)
public fun <T> Flow<T>.forEach(action: suspend (value: T) -> Unit): Unit = noImpl()
@@ -354,6 +354,7 @@ public fun <T> Flow<T>.concatWith(value: T): Flow<T> = noImpl()
)
public fun <T> Flow<T>.concatWith(other: Flow<T>): Flow<T> = noImpl()
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'combineLatest' is 'combine'",
@@ -362,6 +363,7 @@ public fun <T> Flow<T>.concatWith(other: Flow<T>): Flow<T> = noImpl()
public fun <T1, T2, R> Flow<T1>.combineLatest(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R> =
combine(this, other, transform)
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'combineLatest' is 'combine'",
@@ -373,6 +375,7 @@ public fun <T1, T2, T3, R> Flow<T1>.combineLatest(
transform: suspend (T1, T2, T3) -> R
) = combine(this, other, other2, transform)
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'combineLatest' is 'combine'",
@@ -385,6 +388,7 @@ public fun <T1, T2, T3, T4, R> Flow<T1>.combineLatest(
transform: suspend (T1, T2, T3, T4) -> R
) = combine(this, other, other2, other3, transform)
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'combineLatest' is 'combine'",
@@ -422,6 +426,7 @@ public fun <T> Flow<T>.delayFlow(timeMillis: Long): Flow<T> = onStart { delay(ti
)
public fun <T> Flow<T>.delayEach(timeMillis: Long): Flow<T> = onEach { delay(timeMillis) }
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogues of 'switchMap' are 'transformLatest', 'flatMapLatest' and 'mapLatest'",
@@ -429,6 +434,7 @@ public fun <T> Flow<T>.delayEach(timeMillis: Long): Flow<T> = onEach { delay(tim
)
public fun <T, R> Flow<T>.switchMap(transform: suspend (value: T) -> Flow<R>): Flow<R> = flatMapLatest(transform)
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR, // Warning since 1.3.8, was experimental when deprecated, ERROR since 1.5.0
message = "'scanReduce' was renamed to 'runningReduce' to be consistent with Kotlin standard library",
@@ -436,6 +442,7 @@ public fun <T, R> Flow<T>.switchMap(transform: suspend (value: T) -> Flow<R>): F
)
public fun <T> Flow<T>.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow<T> = runningReduce(operation)
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'publish()' is 'shareIn'. \n" +
@@ -446,6 +453,7 @@ public fun <T> Flow<T>.scanReduce(operation: suspend (accumulator: T, value: T)
)
public fun <T> Flow<T>.publish(): Flow<T> = noImpl()
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'publish(bufferSize)' is 'buffer' followed by 'shareIn'. \n" +
@@ -456,6 +464,7 @@ public fun <T> Flow<T>.publish(): Flow<T> = noImpl()
)
public fun <T> Flow<T>.publish(bufferSize: Int): Flow<T> = noImpl()
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'replay()' is 'shareIn' with unlimited replay. \n" +
@@ -466,6 +475,7 @@ public fun <T> Flow<T>.publish(bufferSize: Int): Flow<T> = noImpl()
)
public fun <T> Flow<T>.replay(): Flow<T> = noImpl()
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'replay(bufferSize)' is 'shareIn' with the specified replay parameter. \n" +
@@ -476,6 +486,7 @@ public fun <T> Flow<T>.replay(): Flow<T> = noImpl()
)
public fun <T> Flow<T>.replay(bufferSize: Int): Flow<T> = noImpl()
+/** @suppress */
@Deprecated(
level = DeprecationLevel.ERROR,
message = "Flow analogue of 'cache()' is 'shareIn' with unlimited replay and 'started = SharingStared.Lazily' argument'",
diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
index d79e2034..0a291f25 100644
--- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
+++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt
@@ -115,7 +115,7 @@ import kotlin.native.concurrent.*
* ### Implementation notes
*
* Shared flow implementation uses a lock to ensure thread-safety, but suspending collector and emitter coroutines are
- * resumed outside of this lock to avoid dead-locks when using unconfined coroutines. Adding new subscribers
+ * resumed outside of this lock to avoid deadlocks when using unconfined coroutines. Adding new subscribers
* has `O(1)` amortized cost, but emitting has `O(N)` cost, where `N` is the number of subscribers.
*
* ### Not stable for inheritance
@@ -129,6 +129,18 @@ public interface SharedFlow<out T> : Flow<T> {
* A snapshot of the replay cache.
*/
public val replayCache: List<T>
+
+ /**
+ * Accepts the given [collector] and [emits][FlowCollector.emit] values into it.
+ * To emit values from a shared flow into a specific collector, either `collector.emitAll(flow)` or `collect { ... }`
+ * SAM-conversion can be used.
+ *
+ * **A shared flow never completes**. A call to [Flow.collect] or any other terminal operator
+ * on a shared flow never completes normally.
+ *
+ * @see [Flow.collect] for implementation and inheritance details.
+ */
+ override suspend fun collect(collector: FlowCollector<T>): Nothing
}
/**
@@ -155,8 +167,15 @@ public interface SharedFlow<out T> : Flow<T> {
*/
public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
/**
- * Emits a [value] to this shared flow, suspending on buffer overflow if the shared flow was created
- * with the default [BufferOverflow.SUSPEND] strategy.
+ * Emits a [value] to this shared flow, suspending on buffer overflow.
+ *
+ * This call can suspend only when the [BufferOverflow] strategy is
+ * [SUSPEND][BufferOverflow.SUSPEND] **and** there are subscribers collecting this shared flow.
+ *
+ * If there are no subscribers, the buffer is not used.
+ * Instead, the most recently emitted value is simply stored into
+ * the replay cache if one was configured, displacing the older elements there,
+ * or dropped if no replay cache was configured.
*
* See [tryEmit] for a non-suspending variant of this function.
*
@@ -167,12 +186,16 @@ public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
/**
* Tries to emit a [value] to this shared flow without suspending. It returns `true` if the value was
- * emitted successfully. When this function returns `false`, it means that the call to a plain [emit]
- * function will suspend until there is a buffer space available.
+ * emitted successfully (see below). When this function returns `false`, it means that a call to a plain [emit]
+ * function would suspend until there is buffer space available.
*
- * A shared flow configured with a [BufferOverflow] strategy other than [SUSPEND][BufferOverflow.SUSPEND]
- * (either [DROP_OLDEST][BufferOverflow.DROP_OLDEST] or [DROP_LATEST][BufferOverflow.DROP_LATEST]) never
- * suspends on [emit], and thus `tryEmit` to such a shared flow always returns `true`.
+ * This call can return `false` only when the [BufferOverflow] strategy is
+ * [SUSPEND][BufferOverflow.SUSPEND] **and** there are subscribers collecting this shared flow.
+ *
+ * If there are no subscribers, the buffer is not used.
+ * Instead, the most recently emitted value is simply stored into
+ * the replay cache if one was configured, displacing the older elements there,
+ * or dropped if no replay cache was configured. In any case, `tryEmit` returns `true`.
*
* This method is **thread-safe** and can be safely invoked from concurrent coroutines without
* external synchronization.
@@ -198,6 +221,8 @@ public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
* }
* .launchIn(scope) // launch it
* ```
+ *
+ * Implementation note: the resulting flow **does not** conflate subscription count.
*/
public val subscriptionCount: StateFlow<Int>
@@ -253,7 +278,7 @@ public fun <T> MutableSharedFlow(
// ------------------------------------ Implementation ------------------------------------
-private class SharedFlowSlot : AbstractSharedFlowSlot<SharedFlowImpl<*>>() {
+internal class SharedFlowSlot : AbstractSharedFlowSlot<SharedFlowImpl<*>>() {
@JvmField
var index = -1L // current "to-be-emitted" index, -1 means the slot is free now
@@ -275,7 +300,7 @@ private class SharedFlowSlot : AbstractSharedFlowSlot<SharedFlowImpl<*>>() {
}
}
-private class SharedFlowImpl<T>(
+internal open class SharedFlowImpl<T>(
private val replay: Int,
private val bufferCapacity: Int,
private val onBufferOverflow: BufferOverflow
@@ -334,8 +359,15 @@ private class SharedFlowImpl<T>(
result
}
+ /*
+ * A tweak for SubscriptionCountStateFlow to get the latest value.
+ */
+ @Suppress("UNCHECKED_CAST")
+ protected val lastReplayedLocked: T
+ get() = buffer!!.getBufferAt(replayIndex + replaySize - 1) as T
+
@Suppress("UNCHECKED_CAST")
- override suspend fun collect(collector: FlowCollector<T>) {
+ override suspend fun collect(collector: FlowCollector<T>): Nothing {
val slot = allocateSlot()
try {
if (collector is SubscribedFlowCollector) collector.onSubscription()
diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt
index f4c6f2ee..05541424 100644
--- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt
+++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt
@@ -5,7 +5,7 @@
package kotlinx.coroutines.flow
import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.internal.*
+import kotlinx.coroutines.internal.IgnoreJreRequirement
import kotlin.time.*
/**
@@ -135,7 +135,6 @@ public fun interface SharingStarted {
* are negative.
*/
@Suppress("FunctionName")
-@ExperimentalTime
public fun SharingStarted.Companion.WhileSubscribed(
stopTimeout: Duration = Duration.ZERO,
replayExpiration: Duration = Duration.INFINITE
@@ -204,5 +203,6 @@ private class StartedWhileSubscribed(
stopTimeout == other.stopTimeout &&
replayExpiration == other.replayExpiration
+ @IgnoreJreRequirement // desugared hashcode implementation
override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode()
}
diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt
index 9e82e787..be6cbd6b 100644
--- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt
+++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt
@@ -380,7 +380,7 @@ private class StateFlowImpl<T>(
throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported")
}
- override suspend fun collect(collector: FlowCollector<T>) {
+ override suspend fun collect(collector: FlowCollector<T>): Nothing {
val slot = allocateSlot()
try {
if (collector is SubscribedFlowCollector) collector.onSubscription()
@@ -415,10 +415,6 @@ private class StateFlowImpl<T>(
fuseStateFlow(context, capacity, onBufferOverflow)
}
-internal fun MutableStateFlow<Int>.increment(delta: Int) {
- update { it + delta }
-}
-
internal fun <T> StateFlow<T>.fuseStateFlow(
context: CoroutineContext,
capacity: Int,
diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt
index 7114cc08..39ca9839 100644
--- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt
+++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt
@@ -4,6 +4,7 @@
package kotlinx.coroutines.flow.internal
+import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
@@ -26,12 +27,12 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
protected var nCollectors = 0 // number of allocated (!free) slots
private set
private var nextIndex = 0 // oracle for the next free slot index
- private var _subscriptionCount: MutableStateFlow<Int>? = null // init on first need
+ private var _subscriptionCount: SubscriptionCountStateFlow? = null // init on first need
val subscriptionCount: StateFlow<Int>
get() = synchronized(this) {
// allocate under lock in sync with nCollectors variable
- _subscriptionCount ?: MutableStateFlow(nCollectors).also {
+ _subscriptionCount ?: SubscriptionCountStateFlow(nCollectors).also {
_subscriptionCount = it
}
}
@@ -43,7 +44,7 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
@Suppress("UNCHECKED_CAST")
protected fun allocateSlot(): S {
// Actually create slot under lock
- var subscriptionCount: MutableStateFlow<Int>? = null
+ var subscriptionCount: SubscriptionCountStateFlow? = null
val slot = synchronized(this) {
val slots = when (val curSlots = slots) {
null -> createSlotArray(2).also { slots = it }
@@ -74,7 +75,7 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
@Suppress("UNCHECKED_CAST")
protected fun freeSlot(slot: S) {
// Release slot under lock
- var subscriptionCount: MutableStateFlow<Int>? = null
+ var subscriptionCount: SubscriptionCountStateFlow? = null
val resumes = synchronized(this) {
nCollectors--
subscriptionCount = _subscriptionCount // retrieve under lock if initialized
@@ -83,10 +84,10 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
(slot as AbstractSharedFlowSlot<Any>).freeLocked(this)
}
/*
- Resume suspended coroutines.
- This can happens when the subscriber that was freed was a slow one and was holding up buffer.
- When this subscriber was freed, previously queued emitted can now wake up and are resumed here.
- */
+ * Resume suspended coroutines.
+ * This can happen when the subscriber that was freed was a slow one and was holding up buffer.
+ * When this subscriber was freed, previously queued emitted can now wake up and are resumed here.
+ */
for (cont in resumes) cont?.resume(Unit)
// decrement subscription count
subscriptionCount?.increment(-1)
@@ -99,3 +100,35 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
}
}
}
+
+/**
+ * [StateFlow] that represents the number of subscriptions.
+ *
+ * It is exposed as a regular [StateFlow] in our public API, but it is implemented as [SharedFlow] undercover to
+ * avoid conflations of consecutive updates because the subscription count is very sensitive to it.
+ *
+ * The importance of non-conflating can be demonstrated with the following example:
+ * ```
+ * val shared = flowOf(239).stateIn(this, SharingStarted.Lazily, 42) // stateIn for the sake of the initial value
+ * println(shared.first())
+ * yield()
+ * println(shared.first())
+ * ```
+ * If the flow is shared within the same dispatcher (e.g. Main) or with a slow/throttled one,
+ * the `SharingStarted.Lazily` will never be able to start the source: `first` sees the initial value and immediately
+ * unsubscribes, leaving the asynchronous `SharingStarted` with conflated zero.
+ *
+ * To avoid that (especially in a more complex scenarios), we do not conflate subscription updates.
+ */
+private class SubscriptionCountStateFlow(initialValue: Int) : StateFlow<Int>,
+ SharedFlowImpl<Int>(1, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST)
+{
+ init { tryEmit(initialValue) }
+
+ override val value: Int
+ get() = synchronized(this) { lastReplayedLocked }
+
+ fun increment(delta: Int) = synchronized(this) {
+ tryEmit(lastReplayedLocked + delta)
+ }
+}
diff --git a/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt
index b3955256..9a81eefa 100644
--- a/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt
+++ b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt
@@ -51,33 +51,11 @@ internal fun <R> scopedFlow(@BuilderInference block: suspend CoroutineScope.(Flo
flowScope { block(this@flow) }
}
-internal fun <T> CoroutineScope.flowProduce(
- context: CoroutineContext,
- capacity: Int = 0,
- @BuilderInference block: suspend ProducerScope<T>.() -> Unit
-): ReceiveChannel<T> {
- val channel = Channel<T>(capacity)
- val newContext = newCoroutineContext(context)
- val coroutine = FlowProduceCoroutine(newContext, channel)
- coroutine.start(CoroutineStart.ATOMIC, coroutine, block)
- return coroutine
-}
-
private class FlowCoroutine<T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
- public override fun childCancelled(cause: Throwable): Boolean {
- if (cause is ChildCancelledException) return true
- return cancelImpl(cause)
- }
-}
-
-private class FlowProduceCoroutine<T>(
- parentContext: CoroutineContext,
- channel: Channel<T>
-) : ProducerCoroutine<T>(parentContext, channel) {
- public override fun childCancelled(cause: Throwable): Boolean {
+ override fun childCancelled(cause: Throwable): Boolean {
if (cause is ChildCancelledException) return true
return cancelImpl(cause)
}
diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt
index 9eca8aa0..c18adba3 100644
--- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt
+++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt
@@ -22,7 +22,7 @@ internal class ChannelFlowTransformLatest<T, R>(
override suspend fun flowCollect(collector: FlowCollector<R>) {
assert { collector is SendingCollector } // So cancellation behaviour is not leaking into the downstream
- flowScope {
+ coroutineScope {
var previousFlow: Job? = null
flow.collect { value ->
previousFlow?.apply {
@@ -49,7 +49,7 @@ internal class ChannelFlowMerge<T>(
ChannelFlowMerge(flow, concurrency, context, capacity, onBufferOverflow)
override fun produceImpl(scope: CoroutineScope): ReceiveChannel<T> {
- return scope.flowProduce(context, capacity, block = collectToFun)
+ return scope.produce(context, capacity, block = collectToFun)
}
override suspend fun collectTo(scope: ProducerScope<T>) {
@@ -87,7 +87,7 @@ internal class ChannelLimitedFlowMerge<T>(
ChannelLimitedFlowMerge(flows, context, capacity, onBufferOverflow)
override fun produceImpl(scope: CoroutineScope): ReceiveChannel<T> {
- return scope.flowProduce(context, capacity, block = collectToFun)
+ return scope.produce(context, capacity, block = collectToFun)
}
override suspend fun collectTo(scope: ProducerScope<T>) {
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt
index 04342ed0..8ed5606b 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt
@@ -171,7 +171,7 @@ public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED): Flow<T> = buffer(capaci
* ```
*
* Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED],
- * with is, in turn, a shortcut to a buffer that only keeps the latest element as
+ * which is, in turn, a shortcut to a buffer that only keeps the latest element as
* created by `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`.
*
* ### Operator fusion
@@ -277,64 +277,6 @@ private class CancellableFlowImpl<T>(private val flow: Flow<T>) : CancellableFlo
}
}
-/**
- * The operator that changes the context where all transformations applied to the given flow within a [builder] are executed.
- * This operator is context preserving and does not affect the context of the preceding and subsequent operations.
- *
- * Example:
- *
- * ```
- * flow // not affected
- * .map { ... } // Not affected
- * .flowWith(Dispatchers.IO) {
- * map { ... } // in IO
- * .filter { ... } // in IO
- * }
- * .map { ... } // Not affected
- * ```
- *
- * For more explanation of context preservation please refer to [Flow] documentation.
- *
- * This operator is deprecated without replacement because it was discovered that it doesn't play well with coroutines
- * and flow semantics:
- *
- * 1) It doesn't prevent context elements from the downstream to leak into its body
- * ```
- * flowOf(1).flowWith(EmptyCoroutineContext) {
- * onEach { println(kotlin.coroutines.coroutineContext[CoroutineName]) } // Will print 42
- * }.flowOn(CoroutineName(42))
- * ```
- * 2) To avoid such leaks, new primitive should be introduced to `kotlinx.coroutines` -- the subtraction of contexts.
- * And this will become a new concept to learn, maintain and explain.
- * 3) It defers the execution of declarative [builder] until the moment of [collection][Flow.collect] similarly
- * to `Observable.defer`. But it is unexpected because nothing in the name `flowWith` reflects this fact.
- * 4) It can be confused with [flowOn] operator, though [flowWith] is much rarer.
- *
- * @suppress
- */
-@FlowPreview
-@Deprecated(message = "flowWith is deprecated without replacement, please refer to its KDoc for an explanation", level = DeprecationLevel.ERROR) // Error in beta release, removal in 1.4
-public fun <T, R> Flow<T>.flowWith(
- flowContext: CoroutineContext,
- bufferSize: Int = BUFFERED,
- builder: Flow<T>.() -> Flow<R>
-): Flow<R> {
- checkFlowContext(flowContext)
- val source = this
- return unsafeFlow {
- /**
- * Here we should remove a Job instance from the context.
- * All builders are written using scoping and no global coroutines are launched, so it is safe not to provide explicit Job.
- * It is also necessary not to mess with cancellation if multiple flowWith are used.
- */
- val originalContext = currentCoroutineContext().minusKey(Job)
- val prepared = source.flowOn(originalContext).buffer(bufferSize)
- builder(prepared).flowOn(flowContext).buffer(bufferSize).collect { value ->
- return@collect emit(value)
- }
- }
-}
-
private fun checkFlowContext(context: CoroutineContext) {
require(context[Job] == null) {
"Flow context cannot contain job in it. Had $context"
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt
index fed5962b..258dc3ee 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt
@@ -17,12 +17,12 @@ import kotlin.time.*
/* Scaffolding for Knit code examples
<!--- TEST_NAME FlowDelayTest -->
<!--- PREFIX .*-duration-.*
-@file:OptIn(ExperimentalTime::class)
----- INCLUDE .*-duration-.*
import kotlin.time.*
----- INCLUDE .*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
----- SUFFIX .*
@@ -149,7 +149,6 @@ public fun <T> Flow<T>.debounce(timeoutMillis: (T) -> Long): Flow<T> =
* Note that the resulting flow does not emit anything as long as the original flow emits
* items faster than every [timeout] milliseconds.
*/
-@ExperimentalTime
@FlowPreview
public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> =
debounce(timeout.toDelayMillis())
@@ -196,7 +195,6 @@ public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> =
*
* @param timeout [T] is the emitted value and the return value is timeout in [Duration].
*/
-@ExperimentalTime
@FlowPreview
@JvmName("debounceDuration")
@OptIn(kotlin.experimental.ExperimentalTypeInference::class)
@@ -345,6 +343,5 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil
*
* Note that the latest element is not emitted if it does not fit into the sampling window.
*/
-@ExperimentalTime
@FlowPreview
public fun <T> Flow<T>.sample(period: Duration): Flow<T> = sample(period.toDelayMillis())
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt
index 0d67f889..f211a1b2 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt
@@ -15,7 +15,7 @@ import kotlin.native.concurrent.*
/**
* Returns flow where all subsequent repetitions of the same value are filtered out.
*
- * Note that any instance of [StateFlow] already behaves as if `distinctUtilChanged` operator is
+ * Note that any instance of [StateFlow] already behaves as if `distinctUntilChanged` operator is
* applied to it, so applying `distinctUntilChanged` to a `StateFlow` has no effect.
* See [StateFlow] documentation on Operator Fusion.
* Also, repeated application of `distinctUntilChanged` operator on any flow has no effect.
@@ -71,7 +71,6 @@ private class DistinctFlowImpl<T>(
@JvmField val keySelector: (T) -> Any?,
@JvmField val areEquivalent: (old: Any?, new: Any?) -> Boolean
): Flow<T> {
- @InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
var previousKey: Any? = NULL
upstream.collect { value ->
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt
index 608221e0..30512f40 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt
@@ -61,35 +61,6 @@ public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable)
}
/**
- * @suppress **Deprecated**: Use `(Throwable) -> Boolean` functional type
- */
-@Deprecated(
- level = DeprecationLevel.ERROR,
- message = "Use (Throwable) -> Boolean functional type",
- replaceWith = ReplaceWith("(Throwable) -> Boolean")
-)
-public typealias ExceptionPredicate = (Throwable) -> Boolean
-
-/**
- * Switches to the [fallback] flow if the original flow throws an exception that matches the [predicate].
- * Cancellation exceptions that were caused by the direct [cancel] call are not handled by this operator.
- *
- * @suppress **Deprecated**: Use `catch { e -> if (predicate(e)) emitAll(fallback) else throw e }`
- */
-@Deprecated(
- level = DeprecationLevel.ERROR,
- message = "Use catch { e -> if (predicate(e)) emitAll(fallback) else throw e }",
- replaceWith = ReplaceWith("catch { e -> if (predicate(e)) emitAll(fallback) else throw e }")
-)
-public fun <T> Flow<T>.onErrorCollect(
- fallback: Flow<T>,
- predicate: (Throwable) -> Boolean = { true }
-): Flow<T> = catch { e ->
- if (!predicate(e)) throw e
- emitAll(fallback)
-}
-
-/**
* Retries collection of the given flow up to [retries] times when an exception that matches the
* given [predicate] occurs in the upstream flow. This operator is *transparent* to exceptions that occur
* in downstream flow and does not retry on exceptions that are thrown to cancel the flow.
@@ -124,16 +95,6 @@ public fun <T> Flow<T>.retry(
return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}
-@FlowPreview
-@Deprecated(level = DeprecationLevel.HIDDEN, message = "binary compatibility with retries: Int preview version")
-public fun <T> Flow<T>.retry(
- retries: Int = Int.MAX_VALUE,
- predicate: (Throwable) -> Boolean = { true }
-): Flow<T> {
- require(retries > 0) { "Expected positive amount of retries, but had $retries" }
- return retryWhen { cause, attempt -> predicate(cause) && attempt < retries }
-}
-
/**
* Retries collection of the given flow when an exception occurs in the upstream flow and the
* [predicate] returns true. The predicate also receives an `attempt` number as parameter,
@@ -186,6 +147,7 @@ public fun <T> Flow<T>.retryWhen(predicate: suspend FlowCollector<T>.(cause: Thr
}
// Return exception from upstream or null
+@Suppress("NAME_SHADOWING")
internal suspend fun <T> Flow<T>.catchImpl(
collector: FlowCollector<T>
): Throwable? {
@@ -200,6 +162,8 @@ internal suspend fun <T> Flow<T>.catchImpl(
}
}
} catch (e: Throwable) {
+ // Otherwise, smartcast is impossible
+ val fromDownstream = fromDownstream
/*
* First check ensures that we catch an original exception, not one rethrown by an operator.
* Seconds check ignores cancellation causes, they cannot be caught.
@@ -207,7 +171,41 @@ internal suspend fun <T> Flow<T>.catchImpl(
if (e.isSameExceptionAs(fromDownstream) || e.isCancellationCause(coroutineContext)) {
throw e // Rethrow exceptions from downstream and cancellation causes
} else {
- return e // not from downstream
+ /*
+ * The exception came from the upstream [semi-] independently.
+ * For pure failures, when the downstream functions normally, we handle the exception as intended.
+ * But if the downstream has failed prior to or concurrently
+ * with the upstream, we forcefully rethrow it, preserving the contextual information and ensuring that it's not lost.
+ */
+ if (fromDownstream == null) {
+ return e
+ }
+ /*
+ * We consider the upstream exception as the superseding one when both upstream and downstream
+ * fail, suppressing the downstream exception, and operating similarly to `finally` block with
+ * the useful addition of adding the original downstream exception to suppressed ones.
+ *
+ * That's important for the following scenarios:
+ * ```
+ * flow {
+ * val resource = ...
+ * try {
+ * ... emit as well ...
+ * } finally {
+ * resource.close() // Throws in the shutdown sequence when 'collect' already has thrown an exception
+ * }
+ * }.catch { } // or retry
+ * .collect { ... }
+ * ```
+ * when *the downstream* throws.
+ */
+ if (e is CancellationException) {
+ fromDownstream.addSuppressed(e)
+ throw fromDownstream
+ } else {
+ e.addSuppressed(fromDownstream)
+ throw e
+ }
}
}
return null
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt
index 8fbf1a2b..1b8abad0 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt
@@ -109,10 +109,8 @@ public fun <T> Flow<T>.takeWhile(predicate: suspend (T) -> Boolean): Flow<T> = f
* emit(progress) // always emit progress
* !progress.isDone() // continue while download is not done
* }
- * }
* ```
*/
-@ExperimentalCoroutinesApi
public fun <T, R> Flow<T>.transformWhile(
@BuilderInference transform: suspend FlowCollector<R>.(value: T) -> Boolean
): Flow<R> =
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt
index 83f83e1e..b140e628 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt
@@ -13,6 +13,7 @@ import kotlin.internal.InlineOnly
/**
* Applying [cancellable][Flow.cancellable] to a [SharedFlow] has no effect.
* See the [SharedFlow] documentation on Operator Fusion.
+ * @suppress
*/
@Deprecated(
level = DeprecationLevel.ERROR,
@@ -24,6 +25,7 @@ public fun <T> SharedFlow<T>.cancellable(): Flow<T> = noImpl()
/**
* Applying [flowOn][Flow.flowOn] to [SharedFlow] has no effect.
* See the [SharedFlow] documentation on Operator Fusion.
+ * @suppress
*/
@Deprecated(
level = DeprecationLevel.ERROR,
@@ -35,6 +37,7 @@ public fun <T> SharedFlow<T>.flowOn(context: CoroutineContext): Flow<T> = noImpl
/**
* Applying [conflate][Flow.conflate] to [StateFlow] has no effect.
* See the [StateFlow] documentation on Operator Fusion.
+ * @suppress
*/
@Deprecated(
level = DeprecationLevel.ERROR,
@@ -46,6 +49,7 @@ public fun <T> StateFlow<T>.conflate(): Flow<T> = noImpl()
/**
* Applying [distinctUntilChanged][Flow.distinctUntilChanged] to [StateFlow] has no effect.
* See the [StateFlow] documentation on Operator Fusion.
+ * @suppress
*/
@Deprecated(
level = DeprecationLevel.ERROR,
@@ -54,6 +58,9 @@ public fun <T> StateFlow<T>.conflate(): Flow<T> = noImpl()
)
public fun <T> StateFlow<T>.distinctUntilChanged(): Flow<T> = noImpl()
+/**
+ * @suppress
+ */
@Deprecated(
message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." +
"Use currentCoroutineContext().isActive or cancellable() operator instead " +
@@ -65,6 +72,9 @@ public fun <T> StateFlow<T>.distinctUntilChanged(): Flow<T> = noImpl()
public val FlowCollector<*>.isActive: Boolean
get() = noImpl()
+/**
+ * @suppress
+ */
@Deprecated(
message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." +
"Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly",
@@ -73,6 +83,9 @@ public val FlowCollector<*>.isActive: Boolean
)
public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl()
+/**
+ * @suppress
+ */
@Deprecated(
message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." +
"Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly",
@@ -82,6 +95,9 @@ public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit =
public val FlowCollector<*>.coroutineContext: CoroutineContext
get() = noImpl()
+/**
+ * @suppress
+ */
@Deprecated(
message = "SharedFlow never completes, so this operator typically has not effect, it can only " +
"catch exceptions from 'onSubscribe' operator",
@@ -92,6 +108,9 @@ public val FlowCollector<*>.coroutineContext: CoroutineContext
public inline fun <T> SharedFlow<T>.catch(noinline action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
(this as Flow<T>).catch(action)
+/**
+ * @suppress
+ */
@Deprecated(
message = "SharedFlow never completes, so this operator has no effect.",
level = DeprecationLevel.WARNING,
@@ -104,6 +123,9 @@ public inline fun <T> SharedFlow<T>.retry(
): Flow<T> =
(this as Flow<T>).retry(retries, predicate)
+/**
+ * @suppress
+ */
@Deprecated(
message = "SharedFlow never completes, so this operator has no effect.",
level = DeprecationLevel.WARNING,
@@ -113,6 +135,9 @@ public inline fun <T> SharedFlow<T>.retry(
public inline fun <T> SharedFlow<T>.retryWhen(noinline predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean): Flow<T> =
(this as Flow<T>).retryWhen(predicate)
+/**
+ * @suppress
+ */
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
message = "SharedFlow never completes, so this terminal operation never completes.",
@@ -122,6 +147,9 @@ public inline fun <T> SharedFlow<T>.retryWhen(noinline predicate: suspend FlowCo
public suspend inline fun <T> SharedFlow<T>.toList(): List<T> =
(this as Flow<T>).toList()
+/**
+ * @suppress
+ */
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
message = "SharedFlow never completes, so this terminal operation never completes.",
@@ -131,6 +159,9 @@ public suspend inline fun <T> SharedFlow<T>.toList(): List<T> =
public suspend inline fun <T> SharedFlow<T>.toSet(): Set<T> =
(this as Flow<T>).toSet()
+/**
+ * @suppress
+ */
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
message = "SharedFlow never completes, so this terminal operation never completes.",
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt
index 432160f3..35c44d08 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt
@@ -61,7 +61,7 @@ public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>
* its concurrent merging so that only one properly configured channel is used for execution of merging logic.
*
* @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected
- * at the same time. By default it is equal to [DEFAULT_CONCURRENCY].
+ * at the same time. By default, it is equal to [DEFAULT_CONCURRENCY].
*/
@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(
@@ -71,8 +71,7 @@ public fun <T, R> Flow<T>.flatMapMerge(
map(transform).flattenMerge(concurrency)
/**
- * Flattens the given flow of flows into a single flow in a sequentially manner, without interleaving nested flows.
- * This method is conceptually identical to `flattenMerge(concurrency = 1)` but has faster implementation.
+ * Flattens the given flow of flows into a single flow in a sequential manner, without interleaving nested flows.
*
* Inner flows are collected by this operator *sequentially*.
*/
@@ -90,7 +89,6 @@ public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
* Applications of [flowOn], [buffer], and [produceIn] _after_ this operator are fused with
* its concurrent merging so that only one properly configured channel is used for execution of merging logic.
*/
-@ExperimentalCoroutinesApi
public fun <T> Iterable<Flow<T>>.merge(): Flow<T> {
/*
* This is a fuseable implementation of the following operator:
@@ -114,14 +112,13 @@ public fun <T> Iterable<Flow<T>>.merge(): Flow<T> {
* Applications of [flowOn], [buffer], and [produceIn] _after_ this operator are fused with
* its concurrent merging so that only one properly configured channel is used for execution of merging logic.
*/
-@ExperimentalCoroutinesApi
public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()
/**
* Flattens the given flow of flows into a single flow with a [concurrency] limit on the number of
* concurrently collected flows.
*
- * If [concurrency] is more than 1, then inner flows are be collected by this operator *concurrently*.
+ * If [concurrency] is more than 1, then inner flows are collected by this operator *concurrently*.
* With `concurrency == 1` this operator is identical to [flattenConcat].
*
* ### Operator fusion
@@ -133,7 +130,7 @@ public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge(
* and size of its output buffer can be changed by applying subsequent [buffer] operator.
*
* @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected
- * at the same time. By default it is equal to [DEFAULT_CONCURRENCY].
+ * at the same time. By default, it is equal to [DEFAULT_CONCURRENCY].
*/
@FlowPreview
public fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> {
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt
index 4fa74d8e..2b690e3c 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt
@@ -197,8 +197,16 @@ private fun <T> CoroutineScope.launchSharing(
shared: MutableSharedFlow<T>,
started: SharingStarted,
initialValue: T
-): Job =
- launch(context) { // the single coroutine to rule the sharing
+): Job {
+ /*
+ * Conditional start: in the case when sharing and subscribing happens in the same dispatcher, we want to
+ * have the following invariants preserved:
+ * * Delayed sharing strategies have a chance to immediately observe consecutive subscriptions.
+ * E.g. in the cases like `flow.shareIn(...); flow.take(1)` we want sharing strategy to see the initial subscription
+ * * Eager sharing does not start immediately, so the subscribers have actual chance to subscribe _prior_ to sharing.
+ */
+ val start = if (started == SharingStarted.Eagerly) CoroutineStart.DEFAULT else CoroutineStart.UNDISPATCHED
+ return launch(context, start = start) { // the single coroutine to rule the sharing
// Optimize common built-in started strategies
when {
started === SharingStarted.Eagerly -> {
@@ -230,6 +238,7 @@ private fun <T> CoroutineScope.launchSharing(
}
}
}
+}
// -------------------------------- stateIn --------------------------------
diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt
index a47ae776..0f9e3959 100644
--- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt
+++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt
@@ -45,7 +45,7 @@ public fun <T: Any> Flow<T?>.filterNotNull(): Flow<T> = transform<T?, T> { value
* Returns a flow containing the results of applying the given [transform] function to each value of the original flow.
*/
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
- return@transform emit(transform(value))
+ return@transform emit(transform(value))
}
/**
@@ -81,11 +81,10 @@ public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T> = transform
* ```
* flowOf(1, 2, 3).scan(emptyList<Int>()) { acc, value -> acc + value }.toList()
* ```
- * will produce `[], [1], [1, 2], [1, 2, 3]]`.
+ * will produce `[], [1], [1, 2], [1, 2, 3]`.
*
* This function is an alias to [runningFold] operator.
*/
-@ExperimentalCoroutinesApi
public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R> = runningFold(initial, operation)
/**
@@ -95,9 +94,8 @@ public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend
* ```
* flowOf(1, 2, 3).runningFold(emptyList<Int>()) { acc, value -> acc + value }.toList()
* ```
- * will produce `[], [1], [1, 2], [1, 2, 3]]`.
+ * will produce `[], [1], [1, 2], [1, 2, 3]`.
*/
-@ExperimentalCoroutinesApi
public fun <T, R> Flow<T>.runningFold(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R> = flow {
var accumulator: R = initial
emit(accumulator)
@@ -118,7 +116,6 @@ public fun <T, R> Flow<T>.runningFold(initial: R, @BuilderInference operation: s
* ```
* will produce `[1, 3, 6, 10]`
*/
-@ExperimentalCoroutinesApi
public fun <T> Flow<T>.runningReduce(operation: suspend (accumulator: T, value: T) -> T): Flow<T> = flow {
var accumulator: Any? = NULL
collect { value ->
diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt
index 771f8332..98852c58 100644
--- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt
+++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt
@@ -51,29 +51,6 @@ public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
}
/**
- * Terminal flow operator that collects the given flow with a provided [action].
- * If any exception occurs during collect or in the provided flow, this exception is rethrown from this method.
- *
- * Example of use:
- *
- * ```
- * val flow = getMyEvents()
- * try {
- * flow.collect { value ->
- * println("Received $value")
- * }
- * println("My events are consumed successfully")
- * } catch (e: Throwable) {
- * println("Exception from the flow: $e")
- * }
- * ```
- */
-public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
- collect(object : FlowCollector<T> {
- override suspend fun emit(value: T) = action(value)
- })
-
-/**
* Terminal flow operator that collects the given flow with a provided [action] that takes the index of an element (zero-based) and the element.
* If any exception occurs during collect or in the provided flow, this exception is rethrown from this method.
*
@@ -131,3 +108,10 @@ public suspend fun <T> FlowCollector<T>.emitAll(flow: Flow<T>) {
ensureActive()
flow.collect(this)
}
+
+/** @suppress */
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Backwards compatibility with JS and K/N")
+public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
+ collect(object : FlowCollector<T> {
+ override suspend fun emit(value: T) = action(value)
+ })
diff --git a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
index 9f2699ae..fb254a0e 100644
--- a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
+++ b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt
@@ -8,10 +8,12 @@ package kotlinx.coroutines.internal
* Special kind of list intended to be used as collection of subscribers in `ArrayBroadcastChannel`
* On JVM it's CopyOnWriteList and on JS it's MutableList.
*
- * Note that this alias is intentionally not named as CopyOnWriteList to avoid accidental misusage outside of ArrayBroadcastChannel
+ * Note that this alias is intentionally not named as CopyOnWriteList to avoid accidental misusage outside of the ArrayBroadcastChannel
*/
internal typealias SubscribersList<E> = MutableList<E>
+@Deprecated(message = "Implementation of this primitive is tailored to specific ArrayBroadcastChannel usages on K/N " +
+ "and K/JS platforms and it is unsafe to use it anywhere else")
internal expect fun <E> subscriberList(): SubscribersList<E>
internal expect class ReentrantLock() {
diff --git a/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt b/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt
new file mode 100644
index 00000000..1df81c9c
--- /dev/null
+++ b/kotlinx-coroutines-core/common/src/internal/InternalAnnotations.common.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+// Ignore JRE requirements for animal-sniffer, compileOnly dependency
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
+@OptionalExpectation
+internal expect annotation class IgnoreJreRequirement()
diff --git a/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt
new file mode 100644
index 00000000..28f37ecf
--- /dev/null
+++ b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * The result of .limitedParallelism(x) call, a dispatcher
+ * that wraps the given dispatcher, but limits the parallelism level, while
+ * trying to emulate fairness.
+ */
+internal class LimitedDispatcher(
+ private val dispatcher: CoroutineDispatcher,
+ private val parallelism: Int
+) : CoroutineDispatcher(), Runnable, Delay by (dispatcher as? Delay ?: DefaultDelay) {
+
+ @Volatile
+ private var runningWorkers = 0
+
+ private val queue = LockFreeTaskQueue<Runnable>(singleConsumer = false)
+
+ // A separate object that we can synchronize on for K/N
+ private val workerAllocationLock = SynchronizedObject()
+
+ @ExperimentalCoroutinesApi
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ if (parallelism >= this.parallelism) return this
+ return super.limitedParallelism(parallelism)
+ }
+
+ override fun run() {
+ var fairnessCounter = 0
+ while (true) {
+ val task = queue.removeFirstOrNull()
+ if (task != null) {
+ try {
+ task.run()
+ } catch (e: Throwable) {
+ handleCoroutineException(EmptyCoroutineContext, e)
+ }
+ // 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well
+ if (++fairnessCounter >= 16 && dispatcher.isDispatchNeeded(this)) {
+ // Do "yield" to let other views to execute their runnable as well
+ // Note that we do not decrement 'runningWorkers' as we still committed to do our part of work
+ dispatcher.dispatch(this, this)
+ return
+ }
+ continue
+ }
+
+ synchronized(workerAllocationLock) {
+ --runningWorkers
+ if (queue.size == 0) return
+ ++runningWorkers
+ fairnessCounter = 0
+ }
+ }
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ dispatchInternal(block) {
+ dispatcher.dispatch(this, this)
+ }
+ }
+
+ @InternalCoroutinesApi
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ dispatchInternal(block) {
+ dispatcher.dispatchYield(this, this)
+ }
+ }
+
+ private inline fun dispatchInternal(block: Runnable, dispatch: () -> Unit) {
+ // Add task to queue so running workers will be able to see that
+ if (addAndTryDispatching(block)) return
+ /*
+ * Protect against the race when the number of workers is enough,
+ * but one (because of synchronized serialization) attempts to complete,
+ * and we just observed the number of running workers smaller than the actual
+ * number (hit right between `--runningWorkers` and `++runningWorkers` in `run()`)
+ */
+ if (!tryAllocateWorker()) return
+ dispatch()
+ }
+
+ private fun tryAllocateWorker(): Boolean {
+ synchronized(workerAllocationLock) {
+ if (runningWorkers >= parallelism) return false
+ ++runningWorkers
+ return true
+ }
+ }
+
+ private fun addAndTryDispatching(block: Runnable): Boolean {
+ queue.addLast(block)
+ return runningWorkers >= parallelism
+ }
+}
+
+// Save a few bytecode ops
+internal fun Int.checkParallelism() = require(this >= 1) { "Expected positive parallelism level, but got $this" }
diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt
index 0e1d1b47..8b20ade1 100644
--- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt
+++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt
@@ -43,7 +43,7 @@ public expect open class LockFreeLinkedListNode() {
public expect open class LockFreeLinkedListHead() : LockFreeLinkedListNode {
public val isEmpty: Boolean
public inline fun <reified T : LockFreeLinkedListNode> forEach(block: (T) -> Unit)
- public final override fun remove(): Boolean // Actual return type is Nothing, KT-27534
+ public final override fun remove(): Nothing
}
/** @suppress **This is unstable API and it is subject to change.** */
diff --git a/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt b/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt
index 0b863868..45872f17 100644
--- a/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt
+++ b/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt
@@ -14,6 +14,11 @@ public interface MainDispatcherFactory {
/**
* Creates the main dispatcher. [allFactories] parameter contains all factories found by service loader.
* This method is not guaranteed to be idempotent.
+ *
+ * It is required that this method fails with an exception instead of returning an instance that doesn't work
+ * correctly as a [Delay].
+ * The reason for this is that, on the JVM, [DefaultDelay] will use [Dispatchers.Main] for most delays by default
+ * if this method returns an instance without throwing.
*/
public fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher
diff --git a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt
index 2d00768d..2812b931 100644
--- a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt
+++ b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt
@@ -40,6 +40,7 @@ internal expect suspend inline fun recoverAndThrow(exception: Throwable): Nothin
* The opposite of [recoverStackTrace].
* It is guaranteed that `unwrap(recoverStackTrace(e)) === e`
*/
+@PublishedApi // published for the multiplatform implementation of kotlinx-coroutines-test
internal expect fun <E: Throwable> unwrap(exception: E): E
internal expect class StackTraceElement
diff --git a/kotlinx-coroutines-core/common/src/internal/Symbol.kt b/kotlinx-coroutines-core/common/src/internal/Symbol.kt
index 84db2ef6..b629951f 100644
--- a/kotlinx-coroutines-core/common/src/internal/Symbol.kt
+++ b/kotlinx-coroutines-core/common/src/internal/Symbol.kt
@@ -4,12 +4,14 @@
package kotlinx.coroutines.internal
+import kotlin.jvm.*
+
/**
* A symbol class that is used to define unique constants that are self-explanatory in debugger.
*
* @suppress **This is unstable API and it is subject to change.**
*/
-internal class Symbol(val symbol: String) {
+internal class Symbol(@JvmField val symbol: String) {
override fun toString(): String = "<$symbol>"
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
diff --git a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt
index 43b7e9de..ad881031 100644
--- a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt
+++ b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt
@@ -37,6 +37,16 @@ public open class ThreadSafeHeap<T> : SynchronizedObject() where T: ThreadSafeHe
_size.value = 0
}
+ public fun find(
+ predicate: (value: T) -> Boolean
+ ): T? = synchronized(this) block@{
+ for (i in 0 until size) {
+ val value = a?.get(i)!!
+ if (predicate(value)) return@block value
+ }
+ null
+ }
+
public fun peek(): T? = synchronized(this) { firstImpl() }
public fun removeFirstOrNull(): T? = synchronized(this) {
@@ -47,7 +57,6 @@ public open class ThreadSafeHeap<T> : SynchronizedObject() where T: ThreadSafeHe
}
}
- // @Synchronized // NOTE! NOTE! NOTE! inline fun cannot be @Synchronized
public inline fun removeFirstIf(predicate: (T) -> Boolean): T? = synchronized(this) {
val first = firstImpl() ?: return null
if (predicate(first)) {
@@ -59,7 +68,6 @@ public open class ThreadSafeHeap<T> : SynchronizedObject() where T: ThreadSafeHe
public fun addLast(node: T): Unit = synchronized(this) { addImpl(node) }
- // @Synchronized // NOTE! NOTE! NOTE! inline fun cannot be @Synchronized
// Condition also receives current first node in the heap
public inline fun addLastIf(node: T, cond: (T?) -> Boolean): Boolean = synchronized(this) {
if (cond(firstImpl())) {
diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt
index a7172707..92132224 100644
--- a/kotlinx-coroutines-core/common/src/selects/Select.kt
+++ b/kotlinx-coroutines-core/common/src/selects/Select.kt
@@ -62,7 +62,6 @@ public interface SelectBuilder<in R> {
* **Note: This is an experimental api.** It may be replaced with light-weight timer/timeout channels in the future.
*/
@ExperimentalCoroutinesApi
-@ExperimentalTime
public fun <R> SelectBuilder<R>.onTimeout(timeout: Duration, block: suspend () -> R): Unit =
onTimeout(timeout.toDelayMillis(), block)
@@ -186,7 +185,6 @@ public interface SelectInstance<in R> {
* | [SendChannel] | [send][SendChannel.send] | [onSend][SendChannel.onSend]
* | [ReceiveChannel] | [receive][ReceiveChannel.receive] | [onReceive][ReceiveChannel.onReceive]
* | [ReceiveChannel] | [receiveCatching][ReceiveChannel.receiveCatching] | [onReceiveCatching][ReceiveChannel.onReceiveCatching]
- * | [Mutex] | [lock][Mutex.lock] | [onLock][Mutex.onLock]
* | none | [delay] | [onTimeout][SelectBuilder.onTimeout]
*
* This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this
diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt
index 19584e09..681d5db6 100644
--- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt
+++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt
@@ -52,8 +52,7 @@ public interface Mutex {
* Note that this function does not check for cancellation when it is not suspended.
* Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed.
*
- * This function can be used in [select] invocation with [onLock] clause.
- * Use [tryLock] to try acquire lock without waiting.
+ * Use [tryLock] to try acquiring a lock without waiting.
*
* This function is fair; suspended callers are resumed in first-in-first-out order.
*
@@ -63,10 +62,10 @@ public interface Mutex {
public suspend fun lock(owner: Any? = null)
/**
- * Clause for [select] expression of [lock] suspending function that selects when the mutex is locked.
- * Additional parameter for the clause in the `owner` (see [lock]) and when the clause is selected
- * the reference to this mutex is passed into the corresponding block.
+ * Deprecated for removal without built-in replacement.
*/
+ @Deprecated(level = DeprecationLevel.WARNING, message = "Mutex.onLock deprecated without replacement. " +
+ "For additional details please refer to #2794") // WARNING since 1.6.0
public val onLock: SelectClause2<Any?, Mutex>
/**
@@ -362,7 +361,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
}
private class LockedQueue(
- @JvmField var owner: Any
+ @Volatile @JvmField var owner: Any
) : LockFreeLinkedListHead() {
override fun toString(): String = "LockedQueue[$owner]"
}
@@ -370,7 +369,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
private abstract inner class LockWaiter(
@JvmField val owner: Any?
) : LockFreeLinkedListNode(), DisposableHandle {
- private val isTaken = atomic<Boolean>(false)
+ private val isTaken = atomic(false)
fun take(): Boolean = isTaken.compareAndSet(false, true)
final override fun dispose() { remove() }
abstract fun tryResumeLockWaiter(): Boolean
diff --git a/kotlinx-coroutines-core/common/test/DelayDurationTest.kt b/kotlinx-coroutines-core/common/test/DelayDurationTest.kt
index 3dd55bde..f0544099 100644
--- a/kotlinx-coroutines-core/common/test/DelayDurationTest.kt
+++ b/kotlinx-coroutines-core/common/test/DelayDurationTest.kt
@@ -10,8 +10,9 @@ package kotlinx.coroutines
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.Duration.Companion.nanoseconds
-@ExperimentalTime
class DelayDurationTest : TestBase() {
@Test
diff --git a/kotlinx-coroutines-core/common/test/EmptyContext.kt b/kotlinx-coroutines-core/common/test/EmptyContext.kt
index ad78429d..97efec34 100644
--- a/kotlinx-coroutines-core/common/test/EmptyContext.kt
+++ b/kotlinx-coroutines-core/common/test/EmptyContext.kt
@@ -7,10 +7,6 @@ package kotlinx.coroutines
import kotlinx.coroutines.intrinsics.*
import kotlin.coroutines.*
-suspend fun <T> withEmptyContext(block: suspend () -> T): T {
- val baseline = Result.failure<T>(IllegalStateException("Block was suspended"))
- var result: Result<T> = baseline
- block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { result = it })
- while (result == baseline) yield()
- return result.getOrThrow()
+suspend fun <T> withEmptyContext(block: suspend () -> T): T = suspendCoroutine { cont ->
+ block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { cont.resumeWith(it) })
}
diff --git a/kotlinx-coroutines-core/common/test/LimitedParallelismSharedTest.kt b/kotlinx-coroutines-core/common/test/LimitedParallelismSharedTest.kt
new file mode 100644
index 00000000..d01e8571
--- /dev/null
+++ b/kotlinx-coroutines-core/common/test/LimitedParallelismSharedTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.test.*
+
+class LimitedParallelismSharedTest : TestBase() {
+
+ @Test
+ fun testLimitedDefault() = runTest {
+ // Test that evaluates the very basic completion of tasks in limited dispatcher
+ // for all supported platforms.
+ // For more specific and concurrent tests, see 'concurrent' package.
+ val view = Dispatchers.Default.limitedParallelism(1)
+ val view2 = Dispatchers.Default.limitedParallelism(1)
+ val j1 = launch(view) {
+ while (true) {
+ yield()
+ }
+ }
+ val j2 = launch(view2) { j1.cancel() }
+ joinAll(j1, j2)
+ }
+
+ @Test
+ fun testParallelismSpec() {
+ assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(0) }
+ assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(-1) }
+ assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(Int.MIN_VALUE) }
+ Dispatchers.Default.limitedParallelism(Int.MAX_VALUE)
+ }
+}
diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt
index 71c45769..8b7024a6 100644
--- a/kotlinx-coroutines-core/common/test/TestBase.common.kt
+++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt
@@ -7,11 +7,13 @@
package kotlinx.coroutines
import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.test.*
public expect val isStressTest: Boolean
public expect val stressTestMultiplier: Int
+public expect val stressTestMultiplierSqrt: Int
/**
* The result of a multiplatform asynchronous test.
@@ -20,6 +22,8 @@ public expect val stressTestMultiplier: Int
@Suppress("NO_ACTUAL_FOR_EXPECT")
public expect class TestResult
+public expect val isNative: Boolean
+
public expect open class TestBase constructor() {
/*
* In common tests we emulate parameterized tests
diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt
index efd55fe2..60e64f58 100644
--- a/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt
+++ b/kotlinx-coroutines-core/common/test/WithTimeoutDurationTest.kt
@@ -8,8 +8,9 @@ package kotlinx.coroutines
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
-@ExperimentalTime
class WithTimeoutDurationTest : TestBase() {
/**
* Tests a case of no timeout and no suspension inside.
@@ -17,7 +18,7 @@ class WithTimeoutDurationTest : TestBase() {
@Test
fun testBasicNoSuspend() = runTest {
expect(1)
- val result = withTimeout(Duration.seconds(10)) {
+ val result = withTimeout(10.seconds) {
expect(2)
"OK"
}
@@ -31,7 +32,7 @@ class WithTimeoutDurationTest : TestBase() {
@Test
fun testBasicSuspend() = runTest {
expect(1)
- val result = withTimeout(Duration.seconds(10)) {
+ val result = withTimeout(10.seconds) {
expect(2)
yield()
expect(3)
@@ -54,7 +55,7 @@ class WithTimeoutDurationTest : TestBase() {
}
expect(2)
// test that it does not yield to the above job when started
- val result = withTimeout(Duration.seconds(1)) {
+ val result = withTimeout(1.seconds) {
expect(3)
yield() // yield only now
expect(5)
@@ -74,7 +75,7 @@ class WithTimeoutDurationTest : TestBase() {
fun testYieldBlockingWithTimeout() = runTest(
expected = { it is CancellationException }
) {
- withTimeout(Duration.milliseconds(100)) {
+ withTimeout(100.milliseconds) {
while (true) {
yield()
}
@@ -87,7 +88,7 @@ class WithTimeoutDurationTest : TestBase() {
@Test
fun testWithTimeoutChildWait() = runTest {
expect(1)
- withTimeout(Duration.milliseconds(100)) {
+ withTimeout(100.milliseconds) {
expect(2)
// launch child with timeout
launch {
@@ -102,7 +103,7 @@ class WithTimeoutDurationTest : TestBase() {
@Test
fun testBadClass() = runTest {
val bad = BadClass()
- val result = withTimeout(Duration.milliseconds(100)) {
+ val result = withTimeout(100.milliseconds) {
bad
}
assertSame(bad, result)
@@ -118,9 +119,9 @@ class WithTimeoutDurationTest : TestBase() {
fun testExceptionOnTimeout() = runTest {
expect(1)
try {
- withTimeout(Duration.milliseconds(100)) {
+ withTimeout(100.milliseconds) {
expect(2)
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
expectUnreached()
"OK"
}
@@ -135,10 +136,10 @@ class WithTimeoutDurationTest : TestBase() {
expected = { it is CancellationException }
) {
expect(1)
- withTimeout(Duration.milliseconds(100)) {
+ withTimeout(100.milliseconds) {
expect(2)
try {
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
} catch (e: CancellationException) {
finish(3)
}
@@ -151,10 +152,10 @@ class WithTimeoutDurationTest : TestBase() {
fun testSuppressExceptionWithAnotherException() = runTest {
expect(1)
try {
- withTimeout(Duration.milliseconds(100)) {
+ withTimeout(100.milliseconds) {
expect(2)
try {
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
} catch (e: CancellationException) {
expect(3)
throw TestException()
@@ -172,7 +173,7 @@ class WithTimeoutDurationTest : TestBase() {
fun testNegativeTimeout() = runTest {
expect(1)
try {
- withTimeout(-Duration.milliseconds(1)) {
+ withTimeout(-1.milliseconds) {
expectUnreached()
"OK"
}
@@ -187,7 +188,7 @@ class WithTimeoutDurationTest : TestBase() {
expect(1)
try {
expect(2)
- withTimeout(Duration.seconds(1)) {
+ withTimeout(1.seconds) {
expect(3)
throw TestException()
}
diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt
index b5777753..92dba7b3 100644
--- a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt
+++ b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullDurationTest.kt
@@ -10,8 +10,9 @@ package kotlinx.coroutines
import kotlinx.coroutines.channels.*
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
-@ExperimentalTime
class WithTimeoutOrNullDurationTest : TestBase() {
/**
* Tests a case of no timeout and no suspension inside.
@@ -19,7 +20,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testBasicNoSuspend() = runTest {
expect(1)
- val result = withTimeoutOrNull(Duration.seconds(10)) {
+ val result = withTimeoutOrNull(10.seconds) {
expect(2)
"OK"
}
@@ -33,7 +34,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testBasicSuspend() = runTest {
expect(1)
- val result = withTimeoutOrNull(Duration.seconds(10)) {
+ val result = withTimeoutOrNull(10.seconds) {
expect(2)
yield()
expect(3)
@@ -56,7 +57,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
}
expect(2)
// test that it does not yield to the above job when started
- val result = withTimeoutOrNull(Duration.seconds(1)) {
+ val result = withTimeoutOrNull(1.seconds) {
expect(3)
yield() // yield only now
expect(5)
@@ -74,7 +75,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testYieldBlockingWithTimeout() = runTest {
expect(1)
- val result = withTimeoutOrNull(Duration.milliseconds(100)) {
+ val result = withTimeoutOrNull(100.milliseconds) {
while (true) {
yield()
}
@@ -86,7 +87,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testSmallTimeout() = runTest {
val channel = Channel<Int>(1)
- val value = withTimeoutOrNull(Duration.milliseconds(1)) {
+ val value = withTimeoutOrNull(1.milliseconds) {
channel.receive()
}
assertNull(value)
@@ -103,8 +104,8 @@ class WithTimeoutOrNullDurationTest : TestBase() {
fun testInnerTimeout() = runTest(
expected = { it is CancellationException }
) {
- withTimeoutOrNull(Duration.milliseconds(1000)) {
- withTimeout(Duration.milliseconds(10)) {
+ withTimeoutOrNull(1000.milliseconds) {
+ withTimeout(10.milliseconds) {
while (true) {
yield()
}
@@ -119,7 +120,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
fun testNestedTimeout() = runTest(expected = { it is TimeoutCancellationException }) {
withTimeoutOrNull(Duration.INFINITE) {
// Exception from this withTimeout is not suppressed by withTimeoutOrNull
- withTimeout(Duration.milliseconds(10)) {
+ withTimeout(10.milliseconds) {
delay(Duration.INFINITE)
1
}
@@ -131,9 +132,9 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testOuterTimeout() = runTest {
var counter = 0
- val result = withTimeoutOrNull(Duration.milliseconds(250)) {
+ val result = withTimeoutOrNull(320.milliseconds) {
while (true) {
- val inner = withTimeoutOrNull(Duration.milliseconds(100)) {
+ val inner = withTimeoutOrNull(150.milliseconds) {
while (true) {
yield()
}
@@ -149,7 +150,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testBadClass() = runTest {
val bad = BadClass()
- val result = withTimeoutOrNull(Duration.milliseconds(100)) {
+ val result = withTimeoutOrNull(100.milliseconds) {
bad
}
assertSame(bad, result)
@@ -164,9 +165,9 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testNullOnTimeout() = runTest {
expect(1)
- val result = withTimeoutOrNull(Duration.milliseconds(100)) {
+ val result = withTimeoutOrNull(100.milliseconds) {
expect(2)
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
expectUnreached()
"OK"
}
@@ -177,10 +178,10 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testSuppressExceptionWithResult() = runTest {
expect(1)
- val result = withTimeoutOrNull(Duration.milliseconds(100)) {
+ val result = withTimeoutOrNull(100.milliseconds) {
expect(2)
try {
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
} catch (e: CancellationException) {
expect(3)
}
@@ -194,10 +195,10 @@ class WithTimeoutOrNullDurationTest : TestBase() {
fun testSuppressExceptionWithAnotherException() = runTest {
expect(1)
try {
- withTimeoutOrNull(Duration.milliseconds(100)) {
+ withTimeoutOrNull(100.milliseconds) {
expect(2)
try {
- delay(Duration.milliseconds(1000))
+ delay(1000.milliseconds)
} catch (e: CancellationException) {
expect(3)
throw TestException()
@@ -216,11 +217,11 @@ class WithTimeoutOrNullDurationTest : TestBase() {
@Test
fun testNegativeTimeout() = runTest {
expect(1)
- var result = withTimeoutOrNull(-Duration.milliseconds(1)) {
+ var result = withTimeoutOrNull(-1.milliseconds) {
expectUnreached()
}
assertNull(result)
- result = withTimeoutOrNull(Duration.milliseconds(0)) {
+ result = withTimeoutOrNull(0.milliseconds) {
expectUnreached()
}
assertNull(result)
@@ -232,7 +233,7 @@ class WithTimeoutOrNullDurationTest : TestBase() {
expect(1)
try {
expect(2)
- withTimeoutOrNull<Unit>(Duration.milliseconds(1000)) {
+ withTimeoutOrNull<Unit>(1000.milliseconds) {
expect(3)
throw TestException()
}
diff --git a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt
index 90bcf2da..ee896c9b 100644
--- a/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt
+++ b/kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt
@@ -129,9 +129,9 @@ class WithTimeoutOrNullTest : TestBase() {
@Test
fun testOuterTimeout() = runTest {
var counter = 0
- val result = withTimeoutOrNull(250) {
+ val result = withTimeoutOrNull(320) {
while (true) {
- val inner = withTimeoutOrNull(100) {
+ val inner = withTimeoutOrNull(150) {
while (true) {
yield()
}
diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt
index b5f1bf7b..5f39d320 100644
--- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt
@@ -235,7 +235,7 @@ class FlowInvariantsTest : TestBase() {
}
expectUnreached()
} catch (e: IllegalStateException) {
- assertTrue(e.message!!.contains("Flow invariant is violated"))
+ assertTrue(e.message!!.contains("Flow invariant is violated"), "But had: ${e.message}")
finish(2)
}
}
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt
index 447eb73b..ad91e498 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt
@@ -144,4 +144,61 @@ class CatchTest : TestBase() {
.collect()
finish(9)
}
+
+ @Test
+ fun testUpstreamExceptionConcurrentWithDownstream() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw TestException()
+ }
+ }.catch { expectUnreached() }.onEach {
+ expect(2)
+ throw TestException2()
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
+
+ @Test
+ fun testUpstreamExceptionConcurrentWithDownstreamCancellation() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw TestException()
+ }
+ }.catch { expectUnreached() }.onEach {
+ expect(2)
+ throw CancellationException("")
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
+
+ @Test
+ fun testUpstreamCancellationIsIgnoredWhenDownstreamFails() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw CancellationException("")
+ }
+ }.catch { expectUnreached() }.onEach {
+ expect(2)
+ throw TestException("")
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt
index aa0893e8..0268a232 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt
@@ -7,7 +7,7 @@ package kotlinx.coroutines.flow
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.test.*
-import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
class DebounceTest : TestBase() {
@Test
@@ -198,31 +198,29 @@ class DebounceTest : TestBase() {
finish(4)
}
- @ExperimentalTime
@Test
fun testDurationBasic() = withVirtualTime {
expect(1)
val flow = flow {
expect(3)
emit("A")
- delay(Duration.milliseconds(1500))
+ delay(1500.milliseconds)
emit("B")
- delay(Duration.milliseconds(500))
+ delay(500.milliseconds)
emit("C")
- delay(Duration.milliseconds(250))
+ delay(250.milliseconds)
emit("D")
- delay(Duration.milliseconds(2000))
+ delay(2000.milliseconds)
emit("E")
expect(4)
}
expect(2)
- val result = flow.debounce(Duration.milliseconds(1000)).toList()
+ val result = flow.debounce(1000.milliseconds).toList()
assertEquals(listOf("A", "D", "E"), result)
finish(5)
}
- @ExperimentalTime
@Test
fun testDebounceSelectorBasic() = withVirtualTime {
expect(1)
@@ -271,7 +269,6 @@ class DebounceTest : TestBase() {
finish(5)
}
- @ExperimentalTime
@Test
fun testZeroDebounceTimeSelector() = withVirtualTime {
expect(1)
@@ -289,20 +286,19 @@ class DebounceTest : TestBase() {
finish(5)
}
- @ExperimentalTime
@Test
fun testDebounceDurationSelectorBasic() = withVirtualTime {
expect(1)
val flow = flow {
expect(3)
emit("A")
- delay(Duration.milliseconds(1500))
+ delay(1500.milliseconds)
emit("B")
- delay(Duration.milliseconds(500))
+ delay(500.milliseconds)
emit("C")
- delay(Duration.milliseconds(250))
+ delay(250.milliseconds)
emit("D")
- delay(Duration.milliseconds(2000))
+ delay(2000.milliseconds)
emit("E")
expect(4)
}
@@ -310,9 +306,9 @@ class DebounceTest : TestBase() {
expect(2)
val result = flow.debounce {
if (it == "C") {
- Duration.milliseconds(0)
+ 0.milliseconds
} else {
- Duration.milliseconds(1000)
+ 1000.milliseconds
}
}.toList()
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt
index 1c5a3053..dfa28274 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt
@@ -48,10 +48,9 @@ class DropTest : TestBase() {
expectUnreached()
}
}.drop(1)
- .map {
+ .map<Int, Int> {
expect(4)
throw TestException()
- 42
}.catch { emit(42) }
expect(1)
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt
index 3de5d54a..f52416d8 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt
@@ -38,7 +38,6 @@ class FilterTest : TestBase() {
}.filter {
latch.receive()
throw TestException()
- true
}.catch { emit(42) }
assertEquals(42, flow.single())
@@ -74,7 +73,6 @@ class FilterTest : TestBase() {
}.filterNot {
latch.receive()
throw TestException()
- true
}.catch { emit(42) }
assertEquals(42, flow.single())
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt
index 4095172d..f09db120 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeBaseTest.kt
@@ -72,7 +72,7 @@ abstract class FlatMapMergeBaseTest : FlatMapBaseTest() {
emit(2)
expectUnreached()
}.flatMap {
- if (it == 1) flow<Int> {
+ if (it == 1) flow {
expect(5)
latch.send(Unit)
hang { expect(7) }
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt
index a92189c4..f8102218 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeFastPathTest.kt
@@ -39,19 +39,14 @@ class FlatMapMergeFastPathTest : FlatMapMergeBaseTest() {
@Test
fun testCancellationExceptionDownstream() = runTest {
- val flow = flow {
- emit(1)
- hang { expect(2) }
- }.flatMapMerge {
+ val flow = flowOf(1, 2, 3).flatMapMerge {
flow {
emit(it)
- expect(1)
throw CancellationException("")
}
}.buffer(64)
- assertFailsWith<CancellationException>(flow)
- finish(3)
+ assertEquals(listOf(1, 2, 3), flow.toList())
}
@Test
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt
index 7470289e..c2ce346d 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt
@@ -69,19 +69,14 @@ class FlatMapMergeTest : FlatMapMergeBaseTest() {
@Test
fun testCancellationExceptionDownstream() = runTest {
- val flow = flow {
- emit(1)
- hang { expect(2) }
- }.flatMapMerge {
+ val flow = flowOf(1, 2, 3).flatMapMerge {
flow {
emit(it)
- expect(1)
throw CancellationException("")
}
}
- assertFailsWith<CancellationException>(flow)
- finish(3)
+ assertEquals(listOf(1, 2, 3), flow.toList())
}
@Test
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt
index 084af5b9..4ec7cc3c 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FlattenConcatTest.kt
@@ -36,4 +36,17 @@ class FlattenConcatTest : FlatMapBaseTest() {
consumer.cancelAndJoin()
finish(2)
}
+
+ @Test
+ fun testCancellation() = runTest {
+ val flow = flow {
+ repeat(5) {
+ emit(flow {
+ if (it == 2) throw CancellationException("")
+ emit(1)
+ })
+ }
+ }
+ assertFailsWith<CancellationException>(flow.flattenConcat())
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt
index 68653281..8fba8456 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt
@@ -83,7 +83,7 @@ class FlowOnTest : TestBase() {
}.map {
expect(2)
assertEquals("throwing", it)
- throw TestException(); it
+ throw TestException()
}.flowOn(NamedDispatchers("throwing"))
assertFailsWith<TestException>(flow)
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt
index 893811df..d8bb4800 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt
@@ -39,10 +39,9 @@ class MapNotNullTest : TestBase() {
}
emit(1)
}
- }.mapNotNull {
+ }.mapNotNull<Int, Int> {
latch.receive()
throw TestException()
- it + 1
}.catch { emit(42) }
assertEquals(42, flow.single())
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt
index 12481885..f0847984 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt
@@ -46,6 +46,64 @@ abstract class MergeTest : TestBase() {
}
@Test
+ fun testOneSourceCancelled() = runTest {
+ val flow = flow {
+ expect(1)
+ emit(1)
+ expect(2)
+ yield()
+ throw CancellationException("")
+ }
+
+ val otherFlow = flow {
+ repeat(5) {
+ emit(1)
+ yield()
+ }
+
+ expect(3)
+ }
+
+ val result = listOf(flow, otherFlow).merge().toList()
+ assertEquals(MutableList(6) { 1 }, result)
+ finish(4)
+ }
+
+ @Test
+ fun testOneSourceCancelledNonFused() = runTest {
+ val flow = flow {
+ expect(1)
+ emit(1)
+ expect(2)
+ yield()
+ throw CancellationException("")
+ }
+
+ val otherFlow = flow {
+ repeat(5) {
+ emit(1)
+ yield()
+ }
+
+ expect(3)
+ }
+
+ val result = listOf(flow, otherFlow).nonFuseableMerge().toList()
+ assertEquals(MutableList(6) { 1 }, result)
+ finish(4)
+ }
+
+ private fun <T> Iterable<Flow<T>>.nonFuseableMerge(): Flow<T> {
+ return channelFlow {
+ forEach { flow ->
+ launch {
+ flow.collect { send(it) }
+ }
+ }
+ }
+ }
+
+ @Test
fun testIsolatedContext() = runTest {
val flow = flow {
emit(NamedDispatchers.name())
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt
index b8a6b198..e5dde1b7 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/RetryTest.kt
@@ -104,4 +104,61 @@ class RetryTest : TestBase() {
job.cancelAndJoin()
finish(3)
}
-} \ No newline at end of file
+
+ @Test
+ fun testUpstreamExceptionConcurrentWithDownstream() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw TestException()
+ }
+ }.retry { expectUnreached(); true }.onEach {
+ expect(2)
+ throw TestException2()
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
+
+ @Test
+ fun testUpstreamExceptionConcurrentWithDownstreamCancellation() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw TestException()
+ }
+ }.retry { expectUnreached(); true }.onEach {
+ expect(2)
+ throw CancellationException("")
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
+
+ @Test
+ fun testUpstreamCancellationIsIgnoredWhenDownstreamFails() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw CancellationException("")
+ }
+ }.retry { expectUnreached(); true }.onEach {
+ expect(2)
+ throw TestException("")
+ }
+
+ assertFailsWith<TestException>(flow)
+ finish(4)
+ }
+}
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt
index 87bee56f..3c04abdd 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt
@@ -9,6 +9,7 @@ import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
class SampleTest : TestBase() {
@Test
@@ -250,7 +251,6 @@ class SampleTest : TestBase() {
expect(2)
yield()
throw TestException()
- it
}
assertFailsWith<TestException>(flow)
@@ -274,26 +274,25 @@ class SampleTest : TestBase() {
finish(4)
}
- @ExperimentalTime
@Test
- public fun testDurationBasic() = withVirtualTime {
+ fun testDurationBasic() = withVirtualTime {
expect(1)
val flow = flow {
expect(3)
emit("A")
- delay(Duration.milliseconds(1500))
+ delay(1500.milliseconds)
emit("B")
- delay(Duration.milliseconds(500))
+ delay(500.milliseconds)
emit("C")
- delay(Duration.milliseconds(250))
+ delay(250.milliseconds)
emit("D")
- delay(Duration.milliseconds(2000))
+ delay(2000.milliseconds)
emit("E")
expect(4)
}
expect(2)
- val result = flow.sample(Duration.milliseconds(1000)).toList()
+ val result = flow.sample(1000.milliseconds).toList()
assertEquals(listOf("A", "B", "D"), result)
finish(5)
}
diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt
index 62d2322c..ea8939fe 100644
--- a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt
@@ -88,9 +88,8 @@ class TakeTest : TestBase() {
emit(1)
}
}.take(2)
- .map {
+ .map<Int, Int> {
throw TestException()
- 42
}.catch { emit(42) }
assertEquals(42, flow.single())
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt
index 0528e97e..c19d5236 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt
@@ -21,7 +21,7 @@ class ShareInConflationTest : TestBase() {
op: suspend Flow<Int>.(CoroutineScope) -> Flow<Int>
) = runTest {
expect(1)
- // emit all and conflate, then should collect bufferCapacity latest ones
+ // emit all and conflate, then should collect bufferCapacity the latest ones
val done = Job()
flow {
repeat(n) { i ->
@@ -159,4 +159,4 @@ class ShareInConflationTest : TestBase() {
checkConflation(1, BufferOverflow.DROP_LATEST) {
buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0)
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
index db69e2bc..cf83a50b 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt
@@ -210,4 +210,30 @@ class ShareInTest : TestBase() {
stop()
}
}
+
+ @Test
+ fun testShouldStart() = runTest {
+ val flow = flow {
+ expect(2)
+ emit(1)
+ expect(3)
+ }.shareIn(this, SharingStarted.Lazily)
+
+ expect(1)
+ flow.onSubscription { throw CancellationException("") }
+ .catch { e -> assertTrue { e is CancellationException } }
+ .collect()
+ yield()
+ finish(4)
+ }
+
+ @Test
+ fun testShouldStartScalar() = runTest {
+ val j = Job()
+ val shared = flowOf(239).stateIn(this + j, SharingStarted.Lazily, 42)
+ assertEquals(42, shared.first())
+ yield()
+ assertEquals(239, shared.first())
+ j.cancel()
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
index 6e18b38f..98e04f00 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt
@@ -798,4 +798,24 @@ class SharedFlowTest : TestBase() {
job.join()
finish(5)
}
+
+ @Test
+ fun testSubscriptionCount() = runTest {
+ val flow = MutableSharedFlow<Int>()
+ fun startSubscriber() = launch(start = CoroutineStart.UNDISPATCHED) { flow.collect() }
+
+ assertEquals(0, flow.subscriptionCount.first())
+
+ val j1 = startSubscriber()
+ assertEquals(1, flow.subscriptionCount.first())
+
+ val j2 = startSubscriber()
+ assertEquals(2, flow.subscriptionCount.first())
+
+ j1.cancelAndJoin()
+ assertEquals(1, flow.subscriptionCount.first())
+
+ j2.cancelAndJoin()
+ assertEquals(0, flow.subscriptionCount.first())
+ }
}
diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt
index 516bb2e2..da2f3e5f 100644
--- a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt
+++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt
@@ -7,6 +7,8 @@ package kotlinx.coroutines.flow
import kotlinx.coroutines.*
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
class SharingStartedWhileSubscribedTest : TestBase() {
@Test // make sure equals works properly, or otherwise other tests don't make sense
@@ -26,19 +28,52 @@ class SharingStartedWhileSubscribedTest : TestBase() {
}
}
- @OptIn(ExperimentalTime::class)
@Test
fun testDurationParams() {
assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO))
- assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(Duration.milliseconds(10)))
+ assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds))
assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds))
assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE))
assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO))
assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed(
- replayExpiration = Duration.milliseconds(3)
+ replayExpiration = 3.milliseconds
))
- assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds))
+ assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000),
+ SharingStarted.WhileSubscribed(replayExpiration = 7.seconds))
assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE))
}
-}
+ @Test
+ fun testShouldRestart() = runTest {
+ var started = 0
+ val flow = flow {
+ expect(1 + ++started)
+ emit(1)
+ hang { }
+ }.shareIn(this, SharingStarted.WhileSubscribed(100 /* ms */))
+
+ expect(1)
+ flow.first()
+ delay(200)
+ flow.first()
+ finish(4)
+ coroutineContext.job.cancelChildren()
+ }
+
+ @Test
+ fun testImmediateUnsubscribe() = runTest {
+ val flow = flow {
+ expect(2)
+ emit(1)
+ hang { finish(4) }
+ }.shareIn(this, SharingStarted.WhileSubscribed(400, 0 /* ms */), 1)
+
+ expect(1)
+ repeat(5) {
+ flow.first()
+ delay(100)
+ }
+ expect(3)
+ coroutineContext.job.cancelChildren()
+ }
+}
diff --git a/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt
index 26d6f809..62e62e5f 100644
--- a/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt
+++ b/kotlinx-coroutines-core/common/test/selects/SelectTimeoutDurationTest.kt
@@ -7,22 +7,23 @@ package kotlinx.coroutines.selects
import kotlinx.coroutines.*
import kotlin.test.*
import kotlin.time.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
-@ExperimentalTime
class SelectTimeoutDurationTest : TestBase() {
@Test
fun testBasic() = runTest {
expect(1)
val result = select<String> {
- onTimeout(Duration.milliseconds(1000)) {
+ onTimeout(1000.milliseconds) {
expectUnreached()
"FAIL"
}
- onTimeout(Duration.milliseconds(100)) {
+ onTimeout(100.milliseconds) {
expect(2)
"OK"
}
- onTimeout(Duration.milliseconds(500)) {
+ onTimeout(500.milliseconds) {
expectUnreached()
"FAIL"
}
@@ -35,7 +36,7 @@ class SelectTimeoutDurationTest : TestBase() {
fun testZeroTimeout() = runTest {
expect(1)
val result = select<String> {
- onTimeout(Duration.seconds(1)) {
+ onTimeout(1.seconds) {
expectUnreached()
"FAIL"
}
@@ -52,11 +53,11 @@ class SelectTimeoutDurationTest : TestBase() {
fun testNegativeTimeout() = runTest {
expect(1)
val result = select<String> {
- onTimeout(Duration.seconds(1)) {
+ onTimeout(1.seconds) {
expectUnreached()
"FAIL"
}
- onTimeout(-Duration.milliseconds(10)) {
+ onTimeout(-10.milliseconds) {
expect(2)
"OK"
}
@@ -71,13 +72,13 @@ class SelectTimeoutDurationTest : TestBase() {
val iterations =10_000
for (i in 0..iterations) {
val result = selectUnbiased<Int> {
- onTimeout(-Duration.seconds(1)) {
+ onTimeout((-1).seconds) {
0
}
onTimeout(Duration.ZERO) {
1
}
- onTimeout(Duration.seconds(1)) {
+ onTimeout(1.seconds) {
expectUnreached()
2
}
diff --git a/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt
new file mode 100644
index 00000000..8a6c0923
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.coroutines.*
+
+/**
+ * Runs a new coroutine and **blocks** the current thread until its completion.
+ * This function should not be used from a coroutine. It is designed to bridge regular blocking code
+ * to libraries that are written in suspending style, to be used in `main` functions and in tests.
+ */
+public expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T \ No newline at end of file
diff --git a/kotlinx-coroutines-core/jvm/src/CompletionHandler.kt b/kotlinx-coroutines-core/concurrent/src/CompletionHandler.kt
index 4835f796..4835f796 100644
--- a/kotlinx-coroutines-core/jvm/src/CompletionHandler.kt
+++ b/kotlinx-coroutines-core/concurrent/src/CompletionHandler.kt
diff --git a/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt b/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt
new file mode 100644
index 00000000..a2b4241f
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+@ExperimentalCoroutinesApi
+public expect fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher
+
+@ExperimentalCoroutinesApi
+public expect fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher
diff --git a/kotlinx-coroutines-core/jvm/src/channels/Channels.kt b/kotlinx-coroutines-core/concurrent/src/channels/Channels.kt
index 0df8278b..24422b5a 100644
--- a/kotlinx-coroutines-core/jvm/src/channels/Channels.kt
+++ b/kotlinx-coroutines-core/concurrent/src/channels/Channels.kt
@@ -1,65 +1,13 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
@file:JvmMultifileClass
@file:JvmName("ChannelsKt")
package kotlinx.coroutines.channels
import kotlinx.coroutines.*
+import kotlin.jvm.*
/**
- * **Deprecated** blocking variant of send.
- * This method is deprecated in the favour of [trySendBlocking].
- *
- * `sendBlocking` is a dangerous primitive &mdash; it throws an exception
- * if the channel was closed or, more commonly, cancelled.
- * Cancellation exceptions in non-blocking code are unexpected and frequently
- * trigger internal failures.
- *
- * These bugs are hard-to-spot during code review and they forced users to write
- * their own wrappers around `sendBlocking`.
- * So this function is deprecated and replaced with a more explicit primitive.
- *
- * The real-world example of broken usage with Firebase:
- *
- * ```kotlin
- * callbackFlow {
- * val listener = object : ValueEventListener {
- * override fun onDataChange(snapshot: DataSnapshot) {
- * // This line may fail and crash the app when the downstream flow is cancelled
- * sendBlocking(DataSnapshot(snapshot))
- * }
- *
- * override fun onCancelled(error: DatabaseError) {
- * close(error.toException())
- * }
- * }
- *
- * firebaseQuery.addValueEventListener(listener)
- * awaitClose { firebaseQuery.removeEventListener(listener) }
- * }
- * ```
- */
-@Deprecated(
- level = DeprecationLevel.WARNING,
- message = "Deprecated in the favour of 'trySendBlocking'. " +
- "Consider handling the result of 'trySendBlocking' explicitly and rethrow exception if necessary",
- replaceWith = ReplaceWith("trySendBlocking(element)")
-)
-public fun <E> SendChannel<E>.sendBlocking(element: E) {
- // fast path
- if (trySend(element).isSuccess)
- return
- // slow path
- runBlocking {
- send(element)
- }
-}
-
-/**
- * Adds [element] into to this channel, **blocking** the caller while this channel is full,
+ * Adds [element] to this channel, **blocking** the caller while this channel is full,
* and returning either [successful][ChannelResult.isSuccess] result when the element was added, or
* failed result representing closed channel with a corresponding exception.
*
@@ -77,9 +25,8 @@ public fun <E> SendChannel<E>.sendBlocking(element: E) {
*
* For this operation it is guaranteed that [failure][ChannelResult.failed] always contains an exception in it.
*
- * @throws [InterruptedException] if the current thread is interrupted during the blocking send operation.
+ * @throws `InterruptedException` on JVM if the current thread is interrupted during the blocking send operation.
*/
-@Throws(InterruptedException::class)
public fun <E> SendChannel<E>.trySendBlocking(element: E): ChannelResult<Unit> {
/*
* Sent successfully -- bail out.
@@ -94,3 +41,20 @@ public fun <E> SendChannel<E>.trySendBlocking(element: E): ChannelResult<Unit> {
else ChannelResult.closed(r.exceptionOrNull())
}
}
+
+/** @suppress */
+@Deprecated(
+ level = DeprecationLevel.ERROR,
+ message = "Deprecated in the favour of 'trySendBlocking'. " +
+ "Consider handling the result of 'trySendBlocking' explicitly and rethrow exception if necessary",
+ replaceWith = ReplaceWith("trySendBlocking(element)")
+) // WARNING in 1.5.0, ERROR in 1.6.0, HIDDEN in 1.7.0
+public fun <E> SendChannel<E>.sendBlocking(element: E) {
+ // fast path
+ if (trySend(element).isSuccess)
+ return
+ // slow path
+ runBlocking {
+ send(element)
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt
index 9bbc6dc9..b4b36dad 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt
+++ b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt
@@ -7,6 +7,8 @@ package kotlinx.coroutines.internal
import kotlinx.atomicfu.*
import kotlinx.coroutines.*
+import kotlin.jvm.*
+import kotlin.native.concurrent.*
private typealias Node = LockFreeLinkedListNode
@@ -20,9 +22,11 @@ internal const val SUCCESS: Int = 1
internal const val FAILURE: Int = 2
@PublishedApi
+@SharedImmutable
internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE")
@PublishedApi
+@SharedImmutable
internal val LIST_EMPTY: Any = Symbol("LIST_EMPTY")
/** @suppress **This is unstable API and it is subject to change.** */
@@ -616,7 +620,7 @@ public actual open class LockFreeLinkedListNode {
assert { next === this._next.value }
}
- override fun toString(): String = "${this::class.java.simpleName}@${Integer.toHexString(System.identityHashCode(this))}"
+ override fun toString(): String = "${this::classSimpleName}@${this.hexAddress}"
}
private class Removed(@JvmField val ref: Node) {
@@ -646,7 +650,7 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
}
// just a defensive programming -- makes sure that list head sentinel is never removed
- public actual final override fun remove(): Boolean = error("head cannot be removed")
+ public actual final override fun remove(): Nothing = error("head cannot be removed")
// optimization: because head is never removed, we don't have to read _next.value to check these:
override val isRemoved: Boolean get() = false
diff --git a/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt b/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt
new file mode 100644
index 00000000..8fc4f4ef
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/AbstractDispatcherConcurrencyTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+import kotlinx.coroutines.channels.*
+import kotlin.test.*
+
+
+abstract class AbstractDispatcherConcurrencyTest : TestBase() {
+
+ public abstract val dispatcher: CoroutineDispatcher
+
+ @Test
+ fun testLaunchAndJoin() = runMtTest {
+ expect(1)
+ var capturedMutableState = 0
+ val job = GlobalScope.launch(dispatcher) {
+ ++capturedMutableState
+ expect(2)
+ }
+ runBlocking { job.join() }
+ assertEquals(1, capturedMutableState)
+ finish(3)
+ }
+
+ @Test
+ fun testDispatcherHasOwnThreads() = runMtTest {
+ val channel = Channel<Int>()
+ GlobalScope.launch(dispatcher) {
+ channel.send(42)
+ }
+
+ var result = ChannelResult.failure<Int>()
+ while (!result.isSuccess) {
+ result = channel.tryReceive()
+ // Block the thread, wait
+ }
+ // Delivery was successful, let's check it
+ assertEquals(42, result.getOrThrow())
+ }
+
+ @Test
+ fun testDelayInDispatcher() = runMtTest {
+ expect(1)
+ val job = GlobalScope.launch(dispatcher) {
+ expect(2)
+ delay(100)
+ expect(3)
+ }
+ runBlocking { job.join() }
+ finish(4)
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt b/kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.kt
index 2612b841..74751fcc 100644
--- a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/AtomicCancellationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines
@@ -142,4 +142,4 @@ class AtomicCancellationTest : TestBase() {
yield() // to jobToJoin & canceller
expect(6)
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt b/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt
new file mode 100644
index 00000000..d4252da3
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/ConcurrentExceptionsStressTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.coroutines.exceptions.*
+import kotlinx.coroutines.internal.*
+import kotlin.test.*
+
+class ConcurrentExceptionsStressTest : TestBase() {
+ private val nWorkers = 4
+ private val nRepeat = 1000 * stressTestMultiplier
+
+ private var workers: Array<CloseableCoroutineDispatcher> = emptyArray()
+
+ @AfterTest
+ fun tearDown() {
+ workers.forEach {
+ it.close()
+ }
+ }
+
+ @Test
+ fun testStress() = runMtTest {
+ workers = Array(nWorkers) { index ->
+ newSingleThreadContext("JobExceptionsStressTest-$index")
+ }
+
+ repeat(nRepeat) {
+ testOnce()
+ }
+ }
+
+ @Suppress("SuspendFunctionOnCoroutineScope") // workaround native inline fun stacktraces
+ private suspend fun CoroutineScope.testOnce() {
+ val deferred = async(NonCancellable) {
+ repeat(nWorkers) { index ->
+ // Always launch a coroutine even if parent job was already cancelled (atomic start)
+ launch(workers[index], start = CoroutineStart.ATOMIC) {
+ randomWait()
+ throw StressException(index)
+ }
+ }
+ }
+ deferred.join()
+ assertTrue(deferred.isCancelled)
+ val completionException = deferred.getCompletionExceptionOrNull()
+ val cause = completionException as? StressException
+ ?: unexpectedException("completion", completionException)
+ val suppressed = cause.suppressed
+ val indices = listOf(cause.index) + suppressed.mapIndexed { index, e ->
+ (e as? StressException)?.index ?: unexpectedException("suppressed $index", e)
+ }
+ repeat(nWorkers) { index ->
+ assertTrue(index in indices, "Exception $index is missing: $indices")
+ }
+ assertEquals(nWorkers, indices.size, "Duplicated exceptions in list: $indices")
+ }
+
+ private fun unexpectedException(msg: String, e: Throwable?): Nothing {
+ throw IllegalStateException("Unexpected $msg exception", e)
+ }
+
+ private class StressException(val index: Int) : SuppressSupportingThrowable()
+}
+
diff --git a/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt b/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt
new file mode 100644
index 00000000..a4d40fb2
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlinx.coroutines.*
+
+internal expect open class SuppressSupportingThrowable() : Throwable
+expect val Throwable.suppressed: Array<Throwable>
+expect fun Throwable.printStackTrace()
+
+expect fun randomWait()
+
+expect fun currentThreadName(): String
+
+inline fun CloseableCoroutineDispatcher.use(block: (CloseableCoroutineDispatcher) -> Unit) {
+ try {
+ block(this)
+ } finally {
+ close()
+ }
+}
diff --git a/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt b/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt
new file mode 100644
index 00000000..a12930cc
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/DefaultDispatcherConcurrencyTest.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+class DefaultDispatcherConcurrencyTest : AbstractDispatcherConcurrencyTest() {
+ override val dispatcher: CoroutineDispatcher = Dispatchers.Default
+}
diff --git a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt b/kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt
index 50d86f32..431bb697 100644
--- a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/JobStructuredJoinStressTest.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines
-import org.junit.*
import kotlin.coroutines.*
+import kotlin.test.*
/**
* Test a race between job failure and join.
@@ -16,12 +16,12 @@ class JobStructuredJoinStressTest : TestBase() {
private val nRepeats = 10_000 * stressTestMultiplier
@Test
- fun testStressRegularJoin() {
+ fun testStressRegularJoin() = runMtTest {
stress(Job::join)
}
@Test
- fun testStressSuspendCancellable() {
+ fun testStressSuspendCancellable() = runMtTest {
stress { job ->
suspendCancellableCoroutine { cont ->
job.invokeOnCompletion { cont.resume(Unit) }
@@ -30,7 +30,7 @@ class JobStructuredJoinStressTest : TestBase() {
}
@Test
- fun testStressSuspendCancellableReusable() {
+ fun testStressSuspendCancellableReusable() = runMtTest {
stress { job ->
suspendCancellableCoroutineReusable { cont ->
job.invokeOnCompletion { cont.resume(Unit) }
@@ -61,4 +61,4 @@ class JobStructuredJoinStressTest : TestBase() {
}
finish(2 + nRepeats)
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt b/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt
new file mode 100644
index 00000000..8d38f05b
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.exceptions.*
+import kotlin.test.*
+
+class LimitedParallelismConcurrentTest : TestBase() {
+
+ private val targetParallelism = 4
+ private val iterations = 100_000
+ private val parallelism = atomic(0)
+
+ private fun checkParallelism() {
+ val value = parallelism.incrementAndGet()
+ randomWait()
+ assertTrue { value <= targetParallelism }
+ parallelism.decrementAndGet()
+ }
+
+ @Test
+ fun testLimitedExecutor() = runMtTest {
+ val executor = newFixedThreadPoolContext(targetParallelism, "test")
+ val view = executor.limitedParallelism(targetParallelism)
+ doStress {
+ repeat(iterations) {
+ launch(view) {
+ checkParallelism()
+ }
+ }
+ }
+ executor.close()
+ }
+
+ private suspend inline fun doStress(crossinline block: suspend CoroutineScope.() -> Unit) {
+ repeat(stressTestMultiplier) {
+ coroutineScope {
+ block()
+ }
+ }
+ }
+
+ @Test
+ fun testTaskFairness() = runMtTest {
+ val executor = newSingleThreadContext("test")
+ val view = executor.limitedParallelism(1)
+ val view2 = executor.limitedParallelism(1)
+ val j1 = launch(view) {
+ while (true) {
+ yield()
+ }
+ }
+ val j2 = launch(view2) { j1.cancel() }
+ joinAll(j1, j2)
+ executor.close()
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt b/kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt
index de38df6b..70f6b8ba 100644
--- a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt
@@ -1,16 +1,16 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-
package kotlinx.coroutines
+import kotlinx.coroutines.exceptions.*
import kotlin.coroutines.*
import kotlin.test.*
class RunBlockingTest : TestBase() {
@Test
- fun testWithTimeoutBusyWait() = runBlocking {
+ fun testWithTimeoutBusyWait() = runMtTest {
val value = withTimeoutOrNull(10) {
while (isActive) {
// Busy wait
@@ -52,14 +52,14 @@ class RunBlockingTest : TestBase() {
}
@Test
- fun testOtherDispatcher() {
+ fun testOtherDispatcher() = runMtTest {
expect(1)
val name = "RunBlockingTest.testOtherDispatcher"
val thread = newSingleThreadContext(name)
runBlocking(thread) {
expect(2)
assertSame(coroutineContext[ContinuationInterceptor], thread)
- assertTrue(Thread.currentThread().name.contains(name))
+ assertTrue(currentThreadName().contains(name))
yield() // should work
expect(3)
}
@@ -67,19 +67,20 @@ class RunBlockingTest : TestBase() {
thread.close()
}
-
@Test
- fun testCancellation() = newFixedThreadPoolContext(2, "testCancellation").use {
- val job = GlobalScope.launch(it) {
- runBlocking(coroutineContext) {
- while (true) {
- yield()
+ fun testCancellation() = runMtTest {
+ newFixedThreadPoolContext(2, "testCancellation").use {
+ val job = GlobalScope.launch(it) {
+ runBlocking(coroutineContext) {
+ while (true) {
+ yield()
+ }
}
}
- }
- runBlocking {
- job.cancelAndJoin()
+ runBlocking {
+ job.cancelAndJoin()
+ }
}
}
@@ -104,40 +105,44 @@ class RunBlockingTest : TestBase() {
}
}
- @Test(expected = CancellationException::class)
- fun testDispatchOnShutdown() = runBlocking<Unit> {
- expect(1)
- val job = launch(NonCancellable) {
- try {
- expect(2)
- delay(Long.MAX_VALUE)
- } finally {
- finish(4)
+ @Test
+ fun testDispatchOnShutdown(): Unit = assertFailsWith<CancellationException> {
+ runBlocking {
+ expect(1)
+ val job = launch(NonCancellable) {
+ try {
+ expect(2)
+ delay(Long.MAX_VALUE)
+ } finally {
+ finish(4)
+ }
}
- }
- yield()
- expect(3)
- coroutineContext.cancel()
- job.cancel()
- }
+ yield()
+ expect(3)
+ coroutineContext.cancel()
+ job.cancel()
+ }
+ }.let { }
- @Test(expected = CancellationException::class)
- fun testDispatchOnShutdown2() = runBlocking<Unit> {
- coroutineContext.cancel()
- expect(1)
- val job = launch(NonCancellable, start = CoroutineStart.UNDISPATCHED) {
- try {
- expect(2)
- delay(Long.MAX_VALUE)
- } finally {
- finish(4)
+ @Test
+ fun testDispatchOnShutdown2(): Unit = assertFailsWith<CancellationException> {
+ runBlocking {
+ coroutineContext.cancel()
+ expect(1)
+ val job = launch(NonCancellable, start = CoroutineStart.UNDISPATCHED) {
+ try {
+ expect(2)
+ delay(Long.MAX_VALUE)
+ } finally {
+ finish(4)
+ }
}
- }
- expect(3)
- job.cancel()
- }
+ expect(3)
+ job.cancel()
+ }
+ }.let { }
@Test
fun testNestedRunBlocking() = runBlocking {
@@ -157,22 +162,13 @@ class RunBlockingTest : TestBase() {
fun testIncompleteState() {
val handle = runBlocking {
// See #835
- coroutineContext[Job]!!.invokeOnCompletion { }
+ coroutineContext[Job]!!.invokeOnCompletion { }
}
handle.dispose()
}
@Test
- fun testContract() {
- val rb: Int
- runBlocking {
- rb = 42
- }
- rb.hashCode() // unused
- }
-
- @Test
fun testCancelledParent() {
val job = Job()
job.cancel()
diff --git a/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt b/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt
new file mode 100644
index 00000000..b19bf50e
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/TestBaseExtension.common.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+expect fun TestBase.runMtTest(
+ expected: ((Throwable) -> Boolean)? = null,
+ unhandled: List<(Throwable) -> Boolean> = emptyList(),
+ block: suspend CoroutineScope.() -> Unit
+): TestResult
diff --git a/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt
new file mode 100644
index 00000000..30b1075c
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/channels/BroadcastChannelSubStressTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.channels
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+/**
+ * Creates a broadcast channel and repeatedly opens new subscription, receives event, closes it,
+ * to stress test the logic of opening the subscription
+ * to broadcast channel while events are being concurrently sent to it.
+ */
+class BroadcastChannelSubStressTest: TestBase() {
+
+ private val nSeconds = 5 * stressTestMultiplier
+ private val sentTotal = atomic(0L)
+ private val receivedTotal = atomic(0L)
+
+ @Test
+ fun testStress() = runMtTest {
+ TestBroadcastChannelKind.values().forEach { kind ->
+ println("--- BroadcastChannelSubStressTest $kind")
+ val broadcast = kind.create<Long>()
+ val sender =
+ launch(context = Dispatchers.Default + CoroutineName("Sender")) {
+ while (isActive) {
+ broadcast.send(sentTotal.incrementAndGet())
+ }
+ }
+ val receiver =
+ launch(context = Dispatchers.Default + CoroutineName("Receiver")) {
+ var last = -1L
+ while (isActive) {
+ val channel = broadcast.openSubscription()
+ val i = channel.receive()
+ check(i >= last) { "Last was $last, got $i" }
+ if (!kind.isConflated) check(i != last) { "Last was $last, got it again" }
+ receivedTotal.incrementAndGet()
+ last = i
+ channel.cancel()
+ }
+ }
+ var prevSent = -1L
+ repeat(nSeconds) { sec ->
+ delay(1000)
+ val curSent = sentTotal.value
+ println("${sec + 1}: Sent $curSent, received ${receivedTotal.value}")
+ check(curSent > prevSent) { "Send stalled at $curSent events" }
+ prevSent = curSent
+ }
+ withTimeout(5000) {
+ sender.cancelAndJoin()
+ receiver.cancelAndJoin()
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt
index 86adfee0..3e38eec3 100644
--- a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/channels/ChannelCancelUndeliveredElementStressTest.kt
@@ -4,14 +4,14 @@
package kotlinx.coroutines.channels
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.*
-import java.util.concurrent.atomic.*
import kotlin.random.*
import kotlin.test.*
class ChannelCancelUndeliveredElementStressTest : TestBase() {
- private val repeatTimes = 10_000 * stressTestMultiplier
+ private val repeatTimes = (if (isNative) 1_000 else 10_000) * stressTestMultiplier
// total counters
private var sendCnt = 0
@@ -25,10 +25,10 @@ class ChannelCancelUndeliveredElementStressTest : TestBase() {
private var dSendExceptionCnt = 0
private var dTrySendFailedCnt = 0
private var dReceivedCnt = 0
- private val dUndeliveredCnt = AtomicInteger()
+ private val dUndeliveredCnt = atomic(0)
@Test
- fun testStress() = runTest {
+ fun testStress() = runMtTest {
repeat(repeatTimes) {
val channel = Channel<Int>(1) { dUndeliveredCnt.incrementAndGet() }
val j1 = launch(Dispatchers.Default) {
@@ -43,23 +43,23 @@ class ChannelCancelUndeliveredElementStressTest : TestBase() {
joinAll(j1, j2)
// All elements must be either received or undelivered (IN every run)
- if (dSendCnt - dTrySendFailedCnt != dReceivedCnt + dUndeliveredCnt.get()) {
+ if (dSendCnt - dTrySendFailedCnt != dReceivedCnt + dUndeliveredCnt.value) {
println(" Send: $dSendCnt")
println("Send exception: $dSendExceptionCnt")
println("trySend failed: $dTrySendFailedCnt")
println(" Received: $dReceivedCnt")
- println(" Undelivered: ${dUndeliveredCnt.get()}")
+ println(" Undelivered: ${dUndeliveredCnt.value}")
error("Failed")
}
trySendFailedCnt += dTrySendFailedCnt
receivedCnt += dReceivedCnt
- undeliveredCnt += dUndeliveredCnt.get()
+ undeliveredCnt += dUndeliveredCnt.value
// clear for next run
dSendCnt = 0
dSendExceptionCnt = 0
dTrySendFailedCnt = 0
dReceivedCnt = 0
- dUndeliveredCnt.set(0)
+ dUndeliveredCnt.value = 0
}
// Stats
println(" Send: $sendCnt")
diff --git a/kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt
index 2b3c05bc..5da00d2a 100644
--- a/kotlinx-coroutines-core/jvm/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/channels/ConflatedBroadcastChannelNotifyStressTest.kt
@@ -1,29 +1,28 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.channels
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import org.junit.Test
-import java.util.concurrent.atomic.*
import kotlin.test.*
class ConflatedBroadcastChannelNotifyStressTest : TestBase() {
private val nSenders = 2
private val nReceivers = 3
- private val nEvents = 500_000 * stressTestMultiplier
+ private val nEvents = (if (isNative) 5_000 else 500_000) * stressTestMultiplier
private val timeLimit = 30_000L * stressTestMultiplier // 30 sec
private val broadcast = ConflatedBroadcastChannel<Int>()
- private val sendersCompleted = AtomicInteger()
- private val receiversCompleted = AtomicInteger()
- private val sentTotal = AtomicInteger()
- private val receivedTotal = AtomicInteger()
+ private val sendersCompleted = atomic(0)
+ private val receiversCompleted = atomic(0)
+ private val sentTotal = atomic(0)
+ private val receivedTotal = atomic(0)
@Test
- fun testStressNotify()= runBlocking {
+ fun testStressNotify()= runMtTest {
println("--- ConflatedBroadcastChannelNotifyStressTest")
val senders = List(nSenders) { senderId ->
launch(Dispatchers.Default + CoroutineName("Sender$senderId")) {
@@ -57,7 +56,7 @@ class ConflatedBroadcastChannelNotifyStressTest : TestBase() {
var seconds = 0
while (true) {
delay(1000)
- println("${++seconds}: Sent ${sentTotal.get()}, received ${receivedTotal.get()}")
+ println("${++seconds}: Sent ${sentTotal.value}, received ${receivedTotal.value}")
}
}
try {
@@ -71,13 +70,13 @@ class ConflatedBroadcastChannelNotifyStressTest : TestBase() {
}
progressJob.cancel()
println("Tested with nSenders=$nSenders, nReceivers=$nReceivers")
- println("Completed successfully ${sendersCompleted.get()} sender coroutines")
- println("Completed successfully ${receiversCompleted.get()} receiver coroutines")
- println(" Sent ${sentTotal.get()} events")
- println(" Received ${receivedTotal.get()} events")
- assertEquals(nSenders, sendersCompleted.get())
- assertEquals(nReceivers, receiversCompleted.get())
- assertEquals(nEvents, sentTotal.get())
+ println("Completed successfully ${sendersCompleted.value} sender coroutines")
+ println("Completed successfully ${receiversCompleted.value} receiver coroutines")
+ println(" Sent ${sentTotal.value} events")
+ println(" Received ${receivedTotal.value} events")
+ assertEquals(nSenders, sendersCompleted.value)
+ assertEquals(nReceivers, receiversCompleted.value)
+ assertEquals(nEvents, sentTotal.value)
}
private suspend fun waitForEvent(): Int =
diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt b/kotlinx-coroutines-core/concurrent/test/channels/TrySendBlockingTest.kt
index 8512aebc..77c65183 100644
--- a/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/channels/TrySendBlockingTest.kt
@@ -1,17 +1,16 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.channels
import kotlinx.coroutines.*
-import org.junit.Test
import kotlin.test.*
-class ChannelsJvmTest : TestBase() {
+class TrySendBlockingTest : TestBase() {
@Test
- fun testTrySendBlocking() {
+ fun testTrySendBlocking() = runBlocking<Unit> { // For old MM
val ch = Channel<Int>()
val sum = GlobalScope.async {
var sum = 0
diff --git a/kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt
index 3b5c36f9..f262e78f 100644
--- a/kotlinx-coroutines-core/jvm/test/flow/CombineStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/flow/CombineStressTest.kt
@@ -1,16 +1,16 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.flow
import kotlinx.coroutines.*
-import org.junit.*
+import kotlin.test.*
class CombineStressTest : TestBase() {
@Test
- public fun testCancellation() = runTest {
+ fun testCancellation() = runMtTest {
withContext(Dispatchers.Default + CoroutineExceptionHandler { _, _ -> expectUnreached() }) {
flow {
expect(1)
@@ -26,7 +26,7 @@ class CombineStressTest : TestBase() {
}
@Test
- public fun testFailure() = runTest {
+ fun testFailure() = runMtTest {
val innerIterations = 100 * stressTestMultiplierSqrt
val outerIterations = 10 * stressTestMultiplierSqrt
withContext(Dispatchers.Default + CoroutineExceptionHandler { _, _ -> expectUnreached() }) {
diff --git a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt
index 269805f9..286ba751 100644
--- a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/flow/FlowCancellationTest.kt
@@ -1,17 +1,18 @@
/*
- * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.flow
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
import kotlin.test.*
class FlowCancellationTest : TestBase() {
@Test
- fun testEmitIsCooperative() = runTest {
+ fun testEmitIsCooperative() = runMtTest {
val latch = Channel<Unit>(1)
val job = flow {
expect(1)
@@ -28,7 +29,7 @@ class FlowCancellationTest : TestBase() {
}
@Test
- fun testIsActiveOnCurrentContext() = runTest {
+ fun testIsActiveOnCurrentContext() = runMtTest {
val latch = Channel<Unit>(1)
val job = flow<Unit> {
expect(1)
@@ -45,7 +46,7 @@ class FlowCancellationTest : TestBase() {
}
@Test
- fun testFlowWithEmptyContext() = runTest {
+ fun testFlowWithEmptyContext() = runMtTest {
expect(1)
withEmptyContext {
val flow = flow {
@@ -59,4 +60,4 @@ class FlowCancellationTest : TestBase() {
}
finish(4)
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt
new file mode 100644
index 00000000..f2fb41a5
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowCommonStressTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.flow
+
+import kotlinx.coroutines.*
+import kotlin.random.*
+import kotlin.test.*
+
+// A simplified version of StateFlowStressTest
+class StateFlowCommonStressTest : TestBase() {
+ private val state = MutableStateFlow<Long>(0)
+
+ @Test
+ fun testSingleEmitterAndCollector() = runMtTest {
+ var collected = 0L
+ val collector = launch(Dispatchers.Default) {
+ // collect, but abort and collect again after every 1000 values to stress allocation/deallocation
+ do {
+ val batchSize = Random.nextInt(1..1000)
+ var index = 0
+ val cnt = state.onEach { value ->
+ // the first value in batch is allowed to repeat, but cannot go back
+ val ok = if (index++ == 0) value >= collected else value > collected
+ check(ok) {
+ "Values must be monotonic, but $value is not, was $collected"
+ }
+ collected = value
+ }.take(batchSize).map { 1 }.sum()
+ } while (cnt == batchSize)
+ }
+
+ var current = 1L
+ val emitter = launch {
+ while (true) {
+ state.value = current++
+ if (current % 1000 == 0L) yield() // make it cancellable
+ }
+ }
+
+ delay(3000)
+ emitter.cancelAndJoin()
+ collector.cancelAndJoin()
+ assertTrue { current >= collected / 2 }
+ }
+}
diff --git a/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt
new file mode 100644
index 00000000..1e797094
--- /dev/null
+++ b/kotlinx-coroutines-core/concurrent/test/flow/StateFlowUpdateCommonTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.flow
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+// A simplified version of StateFlowUpdateStressTest
+class StateFlowUpdateCommonTest : TestBase() {
+ private val iterations = 100_000 * stressTestMultiplier
+
+ @Test
+ fun testUpdate() = doTest { update { it + 1 } }
+
+ @Test
+ fun testUpdateAndGet() = doTest { updateAndGet { it + 1 } }
+
+ @Test
+ fun testGetAndUpdate() = doTest { getAndUpdate { it + 1 } }
+
+ private fun doTest(increment: MutableStateFlow<Int>.() -> Unit) = runMtTest {
+ val flow = MutableStateFlow(0)
+ val j1 = launch(Dispatchers.Default) {
+ repeat(iterations / 2) {
+ flow.increment()
+ }
+ }
+
+ repeat(iterations / 2) {
+ flow.increment()
+ }
+
+ joinAll(j1)
+ assertEquals(iterations, flow.value)
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt
index b9011448..7e85d495 100644
--- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt
@@ -1,10 +1,9 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.internal
-import org.junit.Test
import kotlin.test.*
class LockFreeLinkedListTest {
@@ -82,4 +81,4 @@ class LockFreeLinkedListTest {
for (i in 0 until n) assertEquals(expected[i], actual[i], "item $i")
assertEquals(expected.isEmpty(), list.isEmpty)
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt b/kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt
index 200cdc09..29c6c348 100644
--- a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/selects/SelectChannelStressTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.selects
@@ -11,61 +11,60 @@ import kotlin.test.*
class SelectChannelStressTest: TestBase() {
+ // Running less iterations on native platforms because of some performance regression
+ private val iterations = (if (isNative) 1_000 else 1_000_000) * stressTestMultiplier
+
@Test
- fun testSelectSendResourceCleanupArrayChannel() = runTest {
+ fun testSelectSendResourceCleanupArrayChannel() = runMtTest {
val channel = Channel<Int>(1)
- val n = 10_000_000 * stressTestMultiplier
expect(1)
channel.send(-1) // fill the buffer, so all subsequent sends cannot proceed
- repeat(n) { i ->
+ repeat(iterations) { i ->
select {
channel.onSend(i) { expectUnreached() }
default { expect(i + 2) }
}
}
- finish(n + 2)
+ finish(iterations + 2)
}
@Test
- fun testSelectReceiveResourceCleanupArrayChannel() = runTest {
+ fun testSelectReceiveResourceCleanupArrayChannel() = runMtTest {
val channel = Channel<Int>(1)
- val n = 10_000_000 * stressTestMultiplier
expect(1)
- repeat(n) { i ->
+ repeat(iterations) { i ->
select {
channel.onReceive { expectUnreached() }
default { expect(i + 2) }
}
}
- finish(n + 2)
+ finish(iterations + 2)
}
@Test
- fun testSelectSendResourceCleanupRendezvousChannel() = runTest {
+ fun testSelectSendResourceCleanupRendezvousChannel() = runMtTest {
val channel = Channel<Int>(Channel.RENDEZVOUS)
- val n = 1_000_000 * stressTestMultiplier
expect(1)
- repeat(n) { i ->
+ repeat(iterations) { i ->
select {
channel.onSend(i) { expectUnreached() }
default { expect(i + 2) }
}
}
- finish(n + 2)
+ finish(iterations + 2)
}
@Test
- fun testSelectReceiveResourceRendezvousChannel() = runTest {
+ fun testSelectReceiveResourceRendezvousChannel() = runMtTest {
val channel = Channel<Int>(Channel.RENDEZVOUS)
- val n = 1_000_000 * stressTestMultiplier
expect(1)
- repeat(n) { i ->
+ repeat(iterations) { i ->
select {
channel.onReceive { expectUnreached() }
default { expect(i + 2) }
}
}
- finish(n + 2)
+ finish(iterations + 2)
}
internal fun <R> SelectBuilder<R>.default(block: suspend () -> R) {
diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt b/kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.kt
index 5489ea5d..8f649c2f 100644
--- a/kotlinx-coroutines-core/jvm/test/selects/SelectMutexStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/selects/SelectMutexStressTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.selects
diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt
index 027f3c51..73b62aee 100644
--- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt
@@ -1,22 +1,49 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.sync
import kotlinx.coroutines.*
+import kotlinx.coroutines.exceptions.*
import kotlinx.coroutines.selects.*
import kotlin.test.*
class MutexStressTest : TestBase() {
+
+ private val n = (if (isNative) 1_000 else 10_000) * stressTestMultiplier
+
@Test
- fun testStress() = runBlocking(Dispatchers.Default) {
- val n = 1000 * stressTestMultiplier
+ fun testDefaultDispatcher() = runMtTest { testBody(Dispatchers.Default) }
+
+ @Test
+ fun testSingleThreadContext() = runMtTest {
+ newSingleThreadContext("testSingleThreadContext").use {
+ testBody(it)
+ }
+ }
+
+ @Test
+ fun testMultiThreadedContextWithSingleWorker() = runMtTest {
+ newFixedThreadPoolContext(1, "testMultiThreadedContextWithSingleWorker").use {
+ testBody(it)
+ }
+ }
+
+ @Test
+ fun testMultiThreadedContext() = runMtTest {
+ newFixedThreadPoolContext(8, "testMultiThreadedContext").use {
+ testBody(it)
+ }
+ }
+
+ @Suppress("SuspendFunctionOnCoroutineScope")
+ private suspend fun CoroutineScope.testBody(dispatcher: CoroutineDispatcher) {
val k = 100
var shared = 0
val mutex = Mutex()
val jobs = List(n) {
- launch {
+ launch(dispatcher) {
repeat(k) {
mutex.lock()
shared++
@@ -29,11 +56,11 @@ class MutexStressTest : TestBase() {
}
@Test
- fun stressUnlockCancelRace() = runTest {
+ fun stressUnlockCancelRace() = runMtTest {
val n = 10_000 * stressTestMultiplier
val mutex = Mutex(true) // create a locked mutex
newSingleThreadContext("SemaphoreStressTest").use { pool ->
- repeat (n) {
+ repeat(n) {
// Initially, we hold the lock and no one else can `lock`,
// otherwise it's a bug.
assertTrue(mutex.isLocked)
@@ -59,11 +86,11 @@ class MutexStressTest : TestBase() {
}
@Test
- fun stressUnlockCancelRaceWithSelect() = runTest {
+ fun stressUnlockCancelRaceWithSelect() = runMtTest {
val n = 10_000 * stressTestMultiplier
val mutex = Mutex(true) // create a locked mutex
newSingleThreadContext("SemaphoreStressTest").use { pool ->
- repeat (n) {
+ repeat(n) {
// Initially, we hold the lock and no one else can `lock`,
// otherwise it's a bug.
assertTrue(mutex.isLocked)
@@ -92,7 +119,7 @@ class MutexStressTest : TestBase() {
}
@Test
- fun testShouldBeUnlockedOnCancellation() = runTest {
+ fun testShouldBeUnlockedOnCancellation() = runMtTest {
val mutex = Mutex()
val n = 1000 * stressTestMultiplier
repeat(n) {
diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt
index 2ceed64b..c5f20389 100644
--- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt
+++ b/kotlinx-coroutines-core/concurrent/test/sync/SemaphoreStressTest.kt
@@ -1,18 +1,25 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
package kotlinx.coroutines.sync
import kotlinx.coroutines.*
-import org.junit.Test
+import kotlinx.coroutines.exceptions.*
import kotlin.test.*
class SemaphoreStressTest : TestBase() {
+
+ private val iterations = (if (isNative) 1_000 else 10_000) * stressTestMultiplier
+
@Test
- fun stressTestAsMutex() = runBlocking(Dispatchers.Default) {
- val n = 10_000 * stressTestMultiplier
+ fun testStressTestAsMutex() = runMtTest {
+ val n = iterations
val k = 100
var shared = 0
val semaphore = Semaphore(1)
val jobs = List(n) {
- launch {
+ launch(Dispatchers.Default) {
repeat(k) {
semaphore.acquire()
shared++
@@ -25,12 +32,12 @@ class SemaphoreStressTest : TestBase() {
}
@Test
- fun stressTest() = runBlocking(Dispatchers.Default) {
- val n = 10_000 * stressTestMultiplier
+ fun testStress() = runMtTest {
+ val n = iterations
val k = 100
val semaphore = Semaphore(10)
val jobs = List(n) {
- launch {
+ launch(Dispatchers.Default) {
repeat(k) {
semaphore.acquire()
semaphore.release()
@@ -41,12 +48,33 @@ class SemaphoreStressTest : TestBase() {
}
@Test
- fun stressCancellation() = runBlocking(Dispatchers.Default) {
- val n = 10_000 * stressTestMultiplier
+ fun testStressAsMutex() = runMtTest {
+ runBlocking(Dispatchers.Default) {
+ val n = iterations
+ val k = 100
+ var shared = 0
+ val semaphore = Semaphore(1)
+ val jobs = List(n) {
+ launch {
+ repeat(k) {
+ semaphore.acquire()
+ shared++
+ semaphore.release()
+ }
+ }
+ }
+ jobs.forEach { it.join() }
+ assertEquals(n * k, shared)
+ }
+ }
+
+ @Test
+ fun testStressCancellation() = runMtTest {
+ val n = iterations
val semaphore = Semaphore(1)
semaphore.acquire()
repeat(n) {
- val job = launch {
+ val job = launch(Dispatchers.Default) {
semaphore.acquire()
}
yield()
@@ -62,8 +90,8 @@ class SemaphoreStressTest : TestBase() {
* the semaphore into an incorrect state where permits are leaked.
*/
@Test
- fun stressReleaseCancelRace() = runTest {
- val n = 10_000 * stressTestMultiplier
+ fun testStressReleaseCancelRace() = runMtTest {
+ val n = iterations
val semaphore = Semaphore(1, 1)
newSingleThreadContext("SemaphoreStressTest").use { pool ->
repeat (n) {
@@ -92,7 +120,7 @@ class SemaphoreStressTest : TestBase() {
}
@Test
- fun testShouldBeUnlockedOnCancellation() = runTest {
+ fun testShouldBeUnlockedOnCancellation() = runMtTest {
val semaphore = Semaphore(1)
val n = 1000 * stressTestMultiplier
repeat(n) {
diff --git a/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt
new file mode 100644
index 00000000..0e239a42
--- /dev/null
+++ b/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+public actual abstract class CloseableCoroutineDispatcher actual constructor() : CoroutineDispatcher() {
+ public actual abstract fun close()
+}
diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
index a98ea973..8036c88a 100644
--- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt
+++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
@@ -12,7 +12,7 @@ private external val navigator: dynamic
private const val UNDEFINED = "undefined"
internal external val process: dynamic
-internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
+internal fun createDefaultDispatcher(): CoroutineDispatcher = when {
// Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source.
// It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md
// "It's missing a few semantics, especially around origins, as well as MessageEvent source."
@@ -42,6 +42,10 @@ public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext):
combined + Dispatchers.Default else combined
}
+public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
+ return this + addedContext
+}
+
// No debugging facilities on JS
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block()
internal actual inline fun <T> withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block()
diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt
index 8d3bac32..3eec5408 100644
--- a/kotlinx-coroutines-core/js/src/Dispatchers.kt
+++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt
@@ -8,8 +8,22 @@ import kotlin.coroutines.*
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
- public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false)
+ public actual val Main: MainCoroutineDispatcher
+ get() = injectedMainDispatcher ?: mainDispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
+
+ private val mainDispatcher = JsMainDispatcher(Default, false)
+ private var injectedMainDispatcher: MainCoroutineDispatcher? = null
+
+ @PublishedApi
+ internal fun injectMain(dispatcher: MainCoroutineDispatcher) {
+ injectedMainDispatcher = dispatcher
+ }
+
+ @PublishedApi
+ internal fun resetInjectedMain() {
+ injectedMainDispatcher = null
+ }
}
private class JsMainDispatcher(
diff --git a/kotlinx-coroutines-core/js/src/EventLoop.kt b/kotlinx-coroutines-core/js/src/EventLoop.kt
index b3a13641..13c33696 100644
--- a/kotlinx-coroutines-core/js/src/EventLoop.kt
+++ b/kotlinx-coroutines-core/js/src/EventLoop.kt
@@ -25,3 +25,5 @@ internal actual object DefaultExecutor {
private fun unsupported(): Nothing =
throw UnsupportedOperationException("runBlocking event loop is not supported")
+
+internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block()
diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
index 6ad7d41b..603005d5 100644
--- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt
+++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
@@ -31,6 +31,11 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay {
abstract fun scheduleQueueProcessing()
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ parallelism.checkParallelism()
+ return this
+ }
+
override fun dispatch(context: CoroutineContext, block: Runnable) {
messageQueue.enqueue(block)
}
diff --git a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt
index 0a1b0310..71f65227 100644
--- a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt
+++ b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt
@@ -16,3 +16,4 @@ internal class NoOpLock {
internal actual fun <E> subscriberList(): SubscribersList<E> = CopyOnWriteList()
internal actual fun <E> identitySet(expectedSize: Int): MutableSet<E> = HashSet(expectedSize)
+
diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt
index 147b31dc..d8c07f4e 100644
--- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt
+++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt
@@ -177,5 +177,5 @@ public open class LinkedListHead : LinkedListNode() {
}
// just a defensive programming -- makes sure that list head sentinel is never removed
- public final override fun remove(): Boolean = throw UnsupportedOperationException()
+ public final override fun remove(): Nothing = throw UnsupportedOperationException()
}
diff --git a/kotlinx-coroutines-core/js/test/PromiseTest.kt b/kotlinx-coroutines-core/js/test/PromiseTest.kt
index cc1297cd..6049a908 100644
--- a/kotlinx-coroutines-core/js/test/PromiseTest.kt
+++ b/kotlinx-coroutines-core/js/test/PromiseTest.kt
@@ -16,7 +16,7 @@ class PromiseTest : TestBase() {
val deferred = promise.asDeferred()
assertEquals("OK", deferred.await())
}
-
+
@Test
fun testPromiseRejectedAsDeferred() = GlobalScope.promise {
lateinit var promiseReject: (Throwable) -> Unit
diff --git a/kotlinx-coroutines-core/js/test/TestBase.kt b/kotlinx-coroutines-core/js/test/TestBase.kt
index cc7865ba..c930c200 100644
--- a/kotlinx-coroutines-core/js/test/TestBase.kt
+++ b/kotlinx-coroutines-core/js/test/TestBase.kt
@@ -8,10 +8,13 @@ import kotlin.js.*
public actual val isStressTest: Boolean = false
public actual val stressTestMultiplier: Int = 1
+public actual val stressTestMultiplierSqrt: Int = 1
@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
public actual typealias TestResult = Promise<Unit>
+public actual val isNative = false
+
public actual open class TestBase actual constructor() {
public actual val isBoundByJsTestTimeout = true
private var actionIndex = 0
diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin
index 397aaf67..a6ea0a40 100644
--- a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin
+++ b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin
Binary files differ
diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro
new file mode 100644
index 00000000..c3911b83
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro
@@ -0,0 +1,31 @@
+# When editing this file, update the following files as well:
+# - META-INF/proguard/coroutines.pro
+# - META-INF/com.android.tools/r8/coroutines.pro
+
+# ServiceLoader support
+-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
+-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
+
+# Most of volatile fields are updated with AFU and should not be mangled
+-keepclassmembers class kotlinx.coroutines.** {
+ volatile <fields>;
+}
+
+# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater
+-keepclassmembers class kotlin.coroutines.SafeContinuation {
+ volatile <fields>;
+}
+
+# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
+# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
+-dontwarn java.lang.instrument.ClassFileTransformer
+-dontwarn sun.misc.SignalHandler
+-dontwarn java.lang.instrument.Instrumentation
+-dontwarn sun.misc.Signal
+
+# Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`.
+# The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android.
+-dontwarn java.lang.ClassValue
+
+# An annotation used for build tooling, won't be directly accessed.
+-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro
new file mode 100644
index 00000000..1ac5ce57
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/r8/coroutines.pro
@@ -0,0 +1,27 @@
+# When editing this file, update the following files as well:
+# - META-INF/proguard/coroutines.pro
+# - META-INF/com.android.tools/proguard/coroutines.pro
+
+# Most of volatile fields are updated with AFU and should not be mangled
+-keepclassmembers class kotlinx.coroutines.** {
+ volatile <fields>;
+}
+
+# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater
+-keepclassmembers class kotlin.coroutines.SafeContinuation {
+ volatile <fields>;
+}
+
+# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
+# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
+-dontwarn java.lang.instrument.ClassFileTransformer
+-dontwarn sun.misc.SignalHandler
+-dontwarn java.lang.instrument.Instrumentation
+-dontwarn sun.misc.Signal
+
+# Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`.
+# The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android.
+-dontwarn java.lang.ClassValue
+
+# An annotation used for build tooling, won't be directly accessed.
+-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement \ No newline at end of file
diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
index 1a9ae1c7..6d29ed25 100644
--- a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
+++ b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
@@ -1,3 +1,7 @@
+# When editing this file, update the following files as well:
+# - META-INF/com.android.tools/proguard/coroutines.pro
+# - META-INF/com.android.tools/r8/coroutines.pro
+
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
@@ -18,3 +22,10 @@
-dontwarn sun.misc.SignalHandler
-dontwarn java.lang.instrument.Instrumentation
-dontwarn sun.misc.Signal
+
+# Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`.
+# The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android.
+-dontwarn java.lang.ClassValue
+
+# An annotation used for build tooling, won't be directly accessed.
+-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt
index edb43031..8c4b62b1 100644
--- a/kotlinx-coroutines-core/jvm/src/Builders.kt
+++ b/kotlinx-coroutines-core/jvm/src/Builders.kt
@@ -35,7 +35,7 @@ import kotlin.coroutines.*
* @param block the coroutine code.
*/
@Throws(InterruptedException::class)
-public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
+public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
diff --git a/kotlinx-coroutines-core/jvm/src/CommonPool.kt b/kotlinx-coroutines-core/jvm/src/CommonPool.kt
deleted file mode 100644
index 502630b0..00000000
--- a/kotlinx-coroutines-core/jvm/src/CommonPool.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines
-
-import java.util.concurrent.*
-import java.util.concurrent.atomic.*
-import kotlin.coroutines.*
-
-/**
- * Represents common pool of shared threads as coroutine dispatcher for compute-intensive tasks.
- *
- * If there isn't a SecurityManager present it uses [java.util.concurrent.ForkJoinPool] when available, which implements
- * efficient work-stealing algorithm for its queues, so every coroutine resumption is dispatched as a separate task even
- * when it already executes inside the pool. When available, it wraps `ForkJoinPool.commonPool` and provides a similar
- * shared pool where not.
- *
- * If there is a SecurityManager present (as would be if running inside a Java Web Start context) then a plain thread
- * pool is created. This is to work around the fact that ForkJoinPool creates threads that cannot perform
- * privileged actions.
- */
-internal object CommonPool : ExecutorCoroutineDispatcher() {
-
- /**
- * Name of the property that controls default parallelism level of [CommonPool].
- * If the property is not specified, `Runtime.getRuntime().availableProcessors() - 1` will be used instead (or `1` for single-core JVM).
- * Note that until Java 10, if an application is run within a container,
- * `Runtime.getRuntime().availableProcessors()` is not aware of container constraints and will return the real number of cores.
- */
- private const val DEFAULT_PARALLELISM_PROPERTY_NAME = "kotlinx.coroutines.default.parallelism"
-
- override val executor: Executor
- get() = pool ?: getOrCreatePoolSync()
-
- // Equals to -1 if not explicitly specified
- private val requestedParallelism = run<Int> {
- val property = Try { System.getProperty(DEFAULT_PARALLELISM_PROPERTY_NAME) } ?: return@run -1
- val parallelism = property.toIntOrNull()
- if (parallelism == null || parallelism < 1) {
- error("Expected positive number in $DEFAULT_PARALLELISM_PROPERTY_NAME, but has $property")
- }
- parallelism
- }
-
- private val parallelism: Int
- get() = requestedParallelism.takeIf { it > 0 }
- ?: (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)
-
- // For debug and tests
- private var usePrivatePool = false
-
- @Volatile
- private var pool: Executor? = null
-
- private inline fun <T> Try(block: () -> T) = try { block() } catch (e: Throwable) { null }
-
- private fun createPool(): ExecutorService {
- if (System.getSecurityManager() != null) return createPlainPool()
- // Reflection on ForkJoinPool class so that it works on JDK 6 (which is absent there)
- val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
- ?: return createPlainPool() // Fallback to plain thread pool
- // Try to use commonPool unless parallelism was explicitly specified or in debug privatePool mode
- if (!usePrivatePool && requestedParallelism < 0) {
- Try { fjpClass.getMethod("commonPool").invoke(null) as? ExecutorService }
- ?.takeIf { isGoodCommonPool(fjpClass, it) }
- ?.let { return it }
- }
- // Try to create private ForkJoinPool instance
- Try { fjpClass.getConstructor(Int::class.java).newInstance(parallelism) as? ExecutorService }
- ?. let { return it }
- // Fallback to plain thread pool
- return createPlainPool()
- }
-
- /**
- * Checks that this ForkJoinPool's parallelism is at least one to avoid pathological bugs.
- */
- internal fun isGoodCommonPool(fjpClass: Class<*>, executor: ExecutorService): Boolean {
- // We cannot use getParallelism, since it lies to us (always returns at least 1)
- // So we submit a task and check that getPoolSize is at least one after that
- // A broken FJP (that is configured for 0 parallelism) would not execute the task and
- // would report its pool size as zero.
- executor.submit {}
- val actual = Try { fjpClass.getMethod("getPoolSize").invoke(executor) as? Int }
- ?: return false
- return actual >= 1
- }
-
- private fun createPlainPool(): ExecutorService {
- val threadId = AtomicInteger()
- return Executors.newFixedThreadPool(parallelism) {
- Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }
- }
- }
-
- @Synchronized
- private fun getOrCreatePoolSync(): Executor =
- pool ?: createPool().also { pool = it }
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- try {
- (pool ?: getOrCreatePoolSync()).execute(wrapTask(block))
- } catch (e: RejectedExecutionException) {
- unTrackTask()
- // CommonPool only rejects execution when it is being closed and this behavior is reserved
- // for testing purposes, so we don't have to worry about cancelling the affected Job here.
- DefaultExecutor.enqueue(block)
- }
- }
-
- // used for tests
- @Synchronized
- internal fun usePrivatePool() {
- shutdown(0)
- usePrivatePool = true
- pool = null
- }
-
- // used for tests
- @Synchronized
- internal fun shutdown(timeout: Long) {
- (pool as? ExecutorService)?.apply {
- shutdown()
- if (timeout > 0)
- awaitTermination(timeout, TimeUnit.MILLISECONDS)
- shutdownNow().forEach { DefaultExecutor.enqueue(it) }
- }
- pool = Executor { throw RejectedExecutionException("CommonPool was shutdown") }
- }
-
- // used for tests
- @Synchronized
- internal fun restore() {
- shutdown(0)
- usePrivatePool = false
- pool = null
- }
-
- override fun toString(): String = "CommonPool"
-
- override fun close(): Unit = error("Close cannot be invoked on CommonPool")
-}
diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt
index e91bb9fd..7209bee8 100644
--- a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt
+++ b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt
@@ -5,38 +5,90 @@
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
-import kotlinx.coroutines.scheduling.*
import kotlin.coroutines.*
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
-internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"
-
-internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
- when (value) {
- null, "", "on" -> true
- "off" -> false
- else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
- }
-}
-
-internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
- if (useCoroutinesScheduler) DefaultScheduler else CommonPool
-
/**
- * Creates context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher nor
- * [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on).
- *
+ * Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
+ * [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on)
+ * and copyable-thread-local facilities on JVM.
* See [DEBUG_PROPERTY_NAME] for description of debugging facilities on JVM.
*/
@ExperimentalCoroutinesApi
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
- val combined = coroutineContext + context
+ val combined = foldCopies(coroutineContext, context, true)
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}
/**
+ * Creates a context for coroutine builder functions that do not launch a new coroutine, e.g. [withContext].
+ * @suppress
+ */
+@InternalCoroutinesApi
+public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
+ /*
+ * Fast-path: we only have to copy/merge if 'addedContext' (which typically has one or two elements)
+ * contains copyable elements.
+ */
+ if (!addedContext.hasCopyableElements()) return this + addedContext
+ return foldCopies(this, addedContext, false)
+}
+
+private fun CoroutineContext.hasCopyableElements(): Boolean =
+ fold(false) { result, it -> result || it is CopyableThreadContextElement<*> }
+
+/**
+ * Folds two contexts properly applying [CopyableThreadContextElement] rules when necessary.
+ * The rules are the following:
+ * * If neither context has CTCE, the sum of two contexts is returned
+ * * Every CTCE from the left-hand side context that does not have a matching (by key) element from right-hand side context
+ * is [copied][CopyableThreadContextElement.copyForChild] if [isNewCoroutine] is `true`.
+ * * Every CTCE from the left-hand side context that has a matching element in the right-hand side context is [merged][CopyableThreadContextElement.mergeForChild]
+ * * Every CTCE from the right-hand side context that hasn't been merged is copied
+ * * Everything else is added to the resulting context as is.
+ */
+private fun foldCopies(originalContext: CoroutineContext, appendContext: CoroutineContext, isNewCoroutine: Boolean): CoroutineContext {
+ // Do we have something to copy left-hand side?
+ val hasElementsLeft = originalContext.hasCopyableElements()
+ val hasElementsRight = appendContext.hasCopyableElements()
+
+ // Nothing to fold, so just return the sum of contexts
+ if (!hasElementsLeft && !hasElementsRight) {
+ return originalContext + appendContext
+ }
+
+ var leftoverContext = appendContext
+ val folded = originalContext.fold<CoroutineContext>(EmptyCoroutineContext) { result, element ->
+ if (element !is CopyableThreadContextElement<*>) return@fold result + element
+ // Will this element be overwritten?
+ val newElement = leftoverContext[element.key]
+ // No, just copy it
+ if (newElement == null) {
+ // For 'withContext'-like builders we do not copy as the element is not shared
+ return@fold result + if (isNewCoroutine) element.copyForChild() else element
+ }
+ // Yes, then first remove the element from append context
+ leftoverContext = leftoverContext.minusKey(element.key)
+ // Return the sum
+ @Suppress("UNCHECKED_CAST")
+ return@fold result + (element as CopyableThreadContextElement<Any?>).mergeForChild(newElement)
+ }
+
+ if (hasElementsRight) {
+ leftoverContext = leftoverContext.fold<CoroutineContext>(EmptyCoroutineContext) { result, element ->
+ // We're appending new context element -- we have to copy it, otherwise it may be shared with others
+ if (element is CopyableThreadContextElement<*>) {
+ return@fold result + element.copyForChild()
+ }
+ return@fold result + element
+ }
+ }
+ return folded + leftoverContext
+}
+
+/**
* Executes a block using a given coroutine context.
*/
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T {
@@ -72,7 +124,7 @@ internal actual inline fun <T> withContinuationContext(continuation: Continuatio
internal fun Continuation<*>.updateUndispatchedCompletion(context: CoroutineContext, oldValue: Any?): UndispatchedCoroutine<*>? {
if (this !is CoroutineStackFrame) return null
/*
- * Fast-path to detect whether we have unispatched coroutine at all in our stack.
+ * Fast-path to detect whether we have undispatched coroutine at all in our stack.
*
* Implementation note.
* If we ever find that stackwalking for thread-locals is way too slow, here is another idea:
@@ -83,8 +135,8 @@ internal fun Continuation<*>.updateUndispatchedCompletion(context: CoroutineCont
* Both options should work, but it requires more careful studying of the performance
* and, mostly, maintainability impact.
*/
- val potentiallyHasUndispatchedCorotuine = context[UndispatchedMarker] !== null
- if (!potentiallyHasUndispatchedCorotuine) return null
+ val potentiallyHasUndispatchedCoroutine = context[UndispatchedMarker] !== null
+ if (!potentiallyHasUndispatchedCoroutine) return null
val completion = undispatchedCompletion()
completion?.saveThreadContext(context, oldValue)
return completion
@@ -102,7 +154,7 @@ internal tailrec fun CoroutineStackFrame.undispatchedCompletion(): UndispatchedC
/**
* Marker indicating that [UndispatchedCoroutine] exists somewhere up in the stack.
- * Used as a performance optimization to avoid stack walking where it is not nesessary.
+ * Used as a performance optimization to avoid stack walking where it is not necessary.
*/
private object UndispatchedMarker: CoroutineContext.Element, CoroutineContext.Key<UndispatchedMarker> {
override val key: CoroutineContext.Key<*>
@@ -115,26 +167,65 @@ internal actual class UndispatchedCoroutine<in T>actual constructor (
uCont: Continuation<T>
) : ScopeCoroutine<T>(if (context[UndispatchedMarker] == null) context + UndispatchedMarker else context, uCont) {
- private var savedContext: CoroutineContext? = null
- private var savedOldValue: Any? = null
+ /*
+ * The state is thread-local because this coroutine can be used concurrently.
+ * Scenario of usage (withContinuationContext):
+ * val state = saveThreadContext(ctx)
+ * try {
+ * invokeSmthWithThisCoroutineAsCompletion() // Completion implies that 'afterResume' will be called
+ * // COROUTINE_SUSPENDED is returned
+ * } finally {
+ * thisCoroutine().clearThreadContext() // Concurrently the "smth" could've been already resumed on a different thread
+ * // and it also calls saveThreadContext and clearThreadContext
+ * }
+ */
+ private var threadStateToRecover = ThreadLocal<Pair<CoroutineContext, Any?>>()
+
+ init {
+ /*
+ * This is a hack for a very specific case in #2930 unless #3253 is implemented.
+ * 'ThreadLocalStressTest' covers this change properly.
+ *
+ * The scenario this change covers is the following:
+ * 1) The coroutine is being started as plain non kotlinx.coroutines related suspend function,
+ * e.g. `suspend fun main` or, more importantly, Ktor `SuspendFunGun`, that is invoking
+ * `withContext(tlElement)` which creates `UndispatchedCoroutine`.
+ * 2) It (original continuation) is then not wrapped into `DispatchedContinuation` via `intercept()`
+ * and goes neither through `DC.run` nor through `resumeUndispatchedWith` that both
+ * do thread context element tracking.
+ * 3) So thread locals never got chance to get properly set up via `saveThreadContext`,
+ * but when `withContext` finishes, it attempts to recover thread locals in its `afterResume`.
+ *
+ * Here we detect precisely this situation and properly setup context to recover later.
+ *
+ */
+ if (uCont.context[ContinuationInterceptor] !is CoroutineDispatcher) {
+ /*
+ * We cannot just "read" the elements as there is no such API,
+ * so we update-restore it immediately and use the intermediate value
+ * as the initial state, leveraging the fact that thread context element
+ * is idempotent and such situations are increasingly rare.
+ */
+ val values = updateThreadContext(context, null)
+ restoreThreadContext(context, values)
+ saveThreadContext(context, values)
+ }
+ }
fun saveThreadContext(context: CoroutineContext, oldValue: Any?) {
- savedContext = context
- savedOldValue = oldValue
+ threadStateToRecover.set(context to oldValue)
}
fun clearThreadContext(): Boolean {
- if (savedContext == null) return false
- savedContext = null
- savedOldValue = null
+ if (threadStateToRecover.get() == null) return false
+ threadStateToRecover.set(null)
return true
}
override fun afterResume(state: Any?) {
- savedContext?.let { context ->
- restoreThreadContext(context, savedOldValue)
- savedContext = null
- savedOldValue = null
+ threadStateToRecover.get()?.let { (ctx, value) ->
+ restoreThreadContext(ctx, value)
+ threadStateToRecover.set(null)
}
// resume undispatched -- update context but stay on the same dispatcher
val result = recoverResult(state, uCont)
@@ -153,6 +244,7 @@ internal actual val CoroutineContext.coroutineName: String? get() {
private const val DEBUG_THREAD_NAME_SEPARATOR = " @"
+@IgnoreJreRequirement // desugared hashcode implementation
internal data class CoroutineId(
val id: Long
) : ThreadContextElement<String>, AbstractCoroutineContextElement(CoroutineId) {
diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
index 6d069692..4259092e 100644
--- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
+++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
@@ -22,6 +22,25 @@ private val handlers: List<CoroutineExceptionHandler> = ServiceLoader.load(
CoroutineExceptionHandler::class.java.classLoader
).iterator().asSequence().toList()
+/**
+ * Private exception without stacktrace that is added to suppressed exceptions of the original exception
+ * when it is reported to the last-ditch current thread 'uncaughtExceptionHandler'.
+ *
+ * The purpose of this exception is to add an otherwise inaccessible diagnostic information and to
+ * be able to poke the failing coroutine context in the debugger.
+ */
+private class DiagnosticCoroutineContextException(private val context: CoroutineContext) : RuntimeException() {
+ override fun getLocalizedMessage(): String {
+ return context.toString()
+ }
+
+ override fun fillInStackTrace(): Throwable {
+ // Prevent Android <= 6.0 bug, #1866
+ stackTrace = emptyArray()
+ return this
+ }
+}
+
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
// use additional extension handlers
for (handler in handlers) {
@@ -36,5 +55,8 @@ internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exce
// use thread's handler
val currentThread = Thread.currentThread()
+ // addSuppressed is never user-defined and cannot normally throw with the only exception being OOM
+ // we do ignore that just in case to definitely deliver the exception
+ runCatching { exception.addSuppressed(DiagnosticCoroutineContextException(context)) }
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
index fe020276..c993bc23 100644
--- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
+++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt
@@ -4,10 +4,25 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.*
import java.util.concurrent.*
import kotlin.coroutines.*
-internal actual val DefaultDelay: Delay = DefaultExecutor
+private val defaultMainDelayOptIn = systemProp("kotlinx.coroutines.main.delay", false)
+
+internal actual val DefaultDelay: Delay = initializeDefaultDelay()
+
+private fun initializeDefaultDelay(): Delay {
+ // Opt-out flag
+ if (!defaultMainDelayOptIn) return DefaultExecutor
+ val main = Dispatchers.Main
+ /*
+ * When we already are working with UI and Main threads, it makes
+ * no sense to create a separate thread with timer that cannot be controller
+ * by the UI runtime.
+ */
+ return if (main.isMissing() || main !is Delay) DefaultExecutor else main
+}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
@@ -17,13 +32,13 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
incrementUseCount() // this event loop is never completed
}
- private const val DEFAULT_KEEP_ALIVE = 1000L // in milliseconds
+ private const val DEFAULT_KEEP_ALIVE_MS = 1000L // in milliseconds
private val KEEP_ALIVE_NANOS = TimeUnit.MILLISECONDS.toNanos(
try {
- java.lang.Long.getLong("kotlinx.coroutines.DefaultExecutor.keepAlive", DEFAULT_KEEP_ALIVE)
+ java.lang.Long.getLong("kotlinx.coroutines.DefaultExecutor.keepAlive", DEFAULT_KEEP_ALIVE_MS)
} catch (e: SecurityException) {
- DEFAULT_KEEP_ALIVE
+ DEFAULT_KEEP_ALIVE_MS
})
@Suppress("ObjectPropertyName")
@@ -37,15 +52,39 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
private const val ACTIVE = 1
private const val SHUTDOWN_REQ = 2
private const val SHUTDOWN_ACK = 3
+ private const val SHUTDOWN = 4
@Volatile
private var debugStatus: Int = FRESH
+ private val isShutDown: Boolean get() = debugStatus == SHUTDOWN
+
private val isShutdownRequested: Boolean get() {
val debugStatus = debugStatus
return debugStatus == SHUTDOWN_REQ || debugStatus == SHUTDOWN_ACK
}
+ actual override fun enqueue(task: Runnable) {
+ if (isShutDown) shutdownError()
+ super.enqueue(task)
+ }
+
+ override fun reschedule(now: Long, delayedTask: DelayedTask) {
+ // Reschedule on default executor can only be invoked after Dispatchers.shutdown
+ shutdownError()
+ }
+
+ private fun shutdownError() {
+ throw RejectedExecutionException("DefaultExecutor was shut down. " +
+ "This error indicates that Dispatchers.shutdown() was invoked prior to completion of exiting coroutines, leaving coroutines in incomplete state. " +
+ "Please refer to Dispatchers.shutdown documentation for more details")
+ }
+
+ override fun shutdown() {
+ debugStatus = SHUTDOWN
+ super.shutdown()
+ }
+
/**
* All event loops are using DefaultExecutor#invokeOnTimeout to avoid livelock on
* ```
@@ -118,9 +157,8 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
return true
}
- // used for tests
- @Synchronized
- fun shutdown(timeout: Long) {
+ @Synchronized // used _only_ for tests
+ fun shutdownForTests(timeout: Long) {
val deadline = System.currentTimeMillis() + timeout
if (!isShutdownRequested) debugStatus = SHUTDOWN_REQ
// loop while there is anything to do immediately or deadline passes
diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt
index d82598ea..251a567c 100644
--- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt
+++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt
@@ -21,7 +21,7 @@ public const val IO_PARALLELISM_PROPERTY_NAME: String = "kotlinx.coroutines.io.p
public actual object Dispatchers {
/**
* The default [CoroutineDispatcher] that is used by all standard builders like
- * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc
+ * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc.
* if no dispatcher nor any other [ContinuationInterceptor] is specified in their context.
*
* It is backed by a shared pool of threads on JVM. By default, the maximal level of parallelism used
@@ -29,7 +29,7 @@ public actual object Dispatchers {
* Level of parallelism X guarantees that no more than X tasks can be executed in this dispatcher in parallel.
*/
@JvmStatic
- public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
+ public actual val Default: CoroutineDispatcher = DefaultScheduler
/**
* A coroutine dispatcher that is confined to the Main thread operating with UI objects.
@@ -86,7 +86,7 @@ public actual object Dispatchers {
* Note that if you need your coroutine to be confined to a particular thread or a thread-pool after resumption,
* but still want to execute it in the current call-frame until its first suspension, then you can use
* an optional [CoroutineStart] parameter in coroutine builders like
- * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to the
+ * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to
* the value of [CoroutineStart.UNDISPATCHED].
*/
@JvmStatic
@@ -100,20 +100,64 @@ public actual object Dispatchers {
* "`kotlinx.coroutines.io.parallelism`" ([IO_PARALLELISM_PROPERTY_NAME]) system property.
* It defaults to the limit of 64 threads or the number of cores (whichever is larger).
*
- * Moreover, the maximum configurable number of threads is capped by the
- * `kotlinx.coroutines.scheduler.max.pool.size` system property.
- * If you need a higher number of parallel threads,
- * you should use a custom dispatcher backed by your own thread pool.
+ * ### Elasticity for limited parallelism
+ *
+ * `Dispatchers.IO` has a unique property of elasticity: its views
+ * obtained with [CoroutineDispatcher.limitedParallelism] are
+ * not restricted by the `Dispatchers.IO` parallelism. Conceptually, there is
+ * a dispatcher backed by an unlimited pool of threads, and both `Dispatchers.IO`
+ * and views of `Dispatchers.IO` are actually views of that dispatcher. In practice
+ * this means that, despite not abiding by `Dispatchers.IO`'s parallelism
+ * restrictions, its views share threads and resources with it.
+ *
+ * In the following example
+ * ```
+ * // 100 threads for MySQL connection
+ * val myMysqlDbDispatcher = Dispatchers.IO.limitedParallelism(100)
+ * // 60 threads for MongoDB connection
+ * val myMongoDbDispatcher = Dispatchers.IO.limitedParallelism(60)
+ * ```
+ * the system may have up to `64 + 100 + 60` threads dedicated to blocking tasks during peak loads,
+ * but during its steady state there is only a small number of threads shared
+ * among `Dispatchers.IO`, `myMysqlDbDispatcher` and `myMongoDbDispatcher`.
*
* ### Implementation note
*
- * This dispatcher shares threads with the [Default][Dispatchers.Default] dispatcher, so using
+ * This dispatcher and its views share threads with the [Default][Dispatchers.Default] dispatcher, so using
* `withContext(Dispatchers.IO) { ... }` when already running on the [Default][Dispatchers.Default]
- * dispatcher does not lead to an actual switching to another thread &mdash; typically execution
- * continues in the same thread.
+ * dispatcher typically does not lead to an actual switching to another thread. In such scenarios,
+ * the underlying implementation attempts to keep the execution on the same thread on a best-effort basis.
+ *
* As a result of thread sharing, more than 64 (default parallelism) threads can be created (but not used)
* during operations over IO dispatcher.
*/
@JvmStatic
- public val IO: CoroutineDispatcher = DefaultScheduler.IO
+ public val IO: CoroutineDispatcher = DefaultIoScheduler
+
+ /**
+ * Shuts down built-in dispatchers, such as [Default] and [IO],
+ * stopping all the threads associated with them and making them reject all new tasks.
+ * Dispatcher used as a fallback for time-related operations (`delay`, `withTimeout`)
+ * and to handle rejected tasks from other dispatchers is also shut down.
+ *
+ * This is a **delicate** API. It is not supposed to be called from a general
+ * application-level code and its invocation is irreversible.
+ * The invocation of shutdown affects most of the coroutines machinery and
+ * leaves the coroutines framework in an inoperable state.
+ * The shutdown method should only be invoked when there are no pending tasks or active coroutines.
+ * Otherwise, the behavior is unspecified: the call to `shutdown` may throw an exception without completing
+ * the shutdown, or it may finish successfully, but the remaining jobs will be in a permanent dormant state,
+ * never completing nor executing.
+ *
+ * The main goal of the shutdown is to stop all background threads associated with the coroutines
+ * framework in order to make kotlinx.coroutines classes unloadable by Java Virtual Machine.
+ * It is only recommended to be used in containerized environments (OSGi, Gradle plugins system,
+ * IDEA plugins) at the end of the container lifecycle.
+ */
+ @DelicateCoroutinesApi
+ public fun shutdown() {
+ DefaultExecutor.shutdown()
+ // Also shuts down Dispatchers.IO
+ DefaultScheduler.shutdown()
+ }
}
diff --git a/kotlinx-coroutines-core/jvm/src/EventLoop.kt b/kotlinx-coroutines-core/jvm/src/EventLoop.kt
index e49c7dc7..1ee651aa 100644
--- a/kotlinx-coroutines-core/jvm/src/EventLoop.kt
+++ b/kotlinx-coroutines-core/jvm/src/EventLoop.kt
@@ -13,8 +13,7 @@ internal actual abstract class EventLoopImplPlatform: EventLoop() {
unpark(thread)
}
- protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) {
- assert { this !== DefaultExecutor } // otherwise default execution was shutdown with tasks in it (cannot be)
+ protected actual open fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) {
DefaultExecutor.schedule(now, delayedTask)
}
}
@@ -47,3 +46,5 @@ internal actual fun createEventLoop(): EventLoop = BlockingEventLoop(Thread.curr
@InternalCoroutinesApi
public fun processNextEventInCurrentThread(): Long =
ThreadLocalEventLoop.currentOrNull()?.processNextEvent() ?: Long.MAX_VALUE
+
+internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block()
diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt
index 007a0c98..48b4788c 100644
--- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt
+++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt
@@ -29,7 +29,7 @@ public actual fun CancellationException(message: String?, cause: Throwable?) : C
internal actual class JobCancellationException public actual constructor(
message: String,
cause: Throwable?,
- @JvmField internal actual val job: Job
+ @JvmField @Transient internal actual val job: Job
) : CancellationException(message), CopyableThrowable<JobCancellationException> {
init {
diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt
index 7ea3cc68..4e98e7bc 100644
--- a/kotlinx-coroutines-core/jvm/src/Executors.kt
+++ b/kotlinx-coroutines-core/jvm/src/Executors.kt
@@ -37,6 +37,9 @@ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closea
public abstract override fun close()
}
+@ExperimentalCoroutinesApi
+public actual typealias CloseableCoroutineDispatcher = ExecutorCoroutineDispatcher
+
/**
* Converts an instance of [ExecutorService] to an implementation of [ExecutorCoroutineDispatcher].
*
diff --git a/kotlinx-coroutines-core/jvm/src/Interruptible.kt b/kotlinx-coroutines-core/jvm/src/Interruptible.kt
index b873eadf..0bded765 100644
--- a/kotlinx-coroutines-core/jvm/src/Interruptible.kt
+++ b/kotlinx-coroutines-core/jvm/src/Interruptible.kt
@@ -8,7 +8,7 @@ import kotlinx.atomicfu.*
import kotlin.coroutines.*
/**
- * Calls the specified [block] with a given coroutine context in a interruptible manner.
+ * Calls the specified [block] with a given coroutine context in an interruptible manner.
* The blocking code block will be interrupted and this function will throw [CancellationException]
* if the coroutine is cancelled.
*
@@ -30,6 +30,11 @@ import kotlin.coroutines.*
* suspend fun <T> BlockingQueue<T>.awaitTake(): T =
* runInterruptible(Dispatchers.IO) { queue.take() }
* ```
+ *
+ * `runInterruptible` uses [withContext] as an underlying mechanism for switching context,
+ * meaning that the supplied [block] is invoked in an [undispatched][CoroutineStart.UNDISPATCHED]
+ * manner directly by the caller if [CoroutineDispatcher] from the current [coroutineContext][currentCoroutineContext]
+ * is the same as the one supplied in [context].
*/
public suspend fun <T> runInterruptible(
context: CoroutineContext = EmptyCoroutineContext,
diff --git a/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt b/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt
index 37fd70a2..bfe99958 100644
--- a/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt
+++ b/kotlinx-coroutines-core/jvm/src/ThreadContextElement.kt
@@ -48,6 +48,15 @@ import kotlin.coroutines.*
* this coroutine suspends.
*
* To use [ThreadLocal] variable within the coroutine use [ThreadLocal.asContextElement][asContextElement] function.
+ *
+ * ### Reentrancy and thread-safety
+ *
+ * Correct implementations of this interface must expect that calls to [restoreThreadContext]
+ * may happen in parallel to the subsequent [updateThreadContext] and [restoreThreadContext] operations.
+ * See [CopyableThreadContextElement] for advanced interleaving details.
+ *
+ * All implementations of [ThreadContextElement] should be thread-safe and guard their internal mutable state
+ * within an element accordingly.
*/
public interface ThreadContextElement<S> : CoroutineContext.Element {
/**
@@ -78,6 +87,110 @@ public interface ThreadContextElement<S> : CoroutineContext.Element {
}
/**
+ * A [ThreadContextElement] copied whenever a child coroutine inherits a context containing it.
+ *
+ * When an API uses a _mutable_ [ThreadLocal] for consistency, a [CopyableThreadContextElement]
+ * can give coroutines "coroutine-safe" write access to that `ThreadLocal`.
+ *
+ * A write made to a `ThreadLocal` with a matching [CopyableThreadContextElement] by a coroutine
+ * will be visible to _itself_ and any child coroutine launched _after_ that write.
+ *
+ * Writes will not be visible to the parent coroutine, peer coroutines, or coroutines that happen
+ * to use the same thread. Writes made to the `ThreadLocal` by the parent coroutine _after_
+ * launching a child coroutine will not be visible to that child coroutine.
+ *
+ * This can be used to allow a coroutine to use a mutable ThreadLocal API transparently and
+ * correctly, regardless of the coroutine's structured concurrency.
+ *
+ * This example adapts a `ThreadLocal` method trace to be "coroutine local" while the method trace
+ * is in a coroutine:
+ *
+ * ```
+ * class TraceContextElement(private val traceData: TraceData?) : CopyableThreadContextElement<TraceData?> {
+ * companion object Key : CoroutineContext.Key<TraceContextElement>
+ *
+ * override val key: CoroutineContext.Key<TraceContextElement> = Key
+ *
+ * override fun updateThreadContext(context: CoroutineContext): TraceData? {
+ * val oldState = traceThreadLocal.get()
+ * traceThreadLocal.set(traceData)
+ * return oldState
+ * }
+ *
+ * override fun restoreThreadContext(context: CoroutineContext, oldState: TraceData?) {
+ * traceThreadLocal.set(oldState)
+ * }
+ *
+ * override fun copyForChild(): TraceContextElement {
+ * // Copy from the ThreadLocal source of truth at child coroutine launch time. This makes
+ * // ThreadLocal writes between resumption of the parent coroutine and the launch of the
+ * // child coroutine visible to the child.
+ * return TraceContextElement(traceThreadLocal.get()?.copy())
+ * }
+ *
+ * override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext {
+ * // Merge operation defines how to handle situations when both
+ * // the parent coroutine has an element in the context and
+ * // an element with the same key was also
+ * // explicitly passed to the child coroutine.
+ * // If merging does not require special behavior,
+ * // the copy of the element can be returned.
+ * return TraceContextElement(traceThreadLocal.get()?.copy())
+ * }
+ * }
+ * ```
+ *
+ * A coroutine using this mechanism can safely call Java code that assumes the corresponding thread local element's
+ * value is installed into the target thread local.
+ *
+ * ### Reentrancy and thread-safety
+ *
+ * Correct implementations of this interface must expect that calls to [restoreThreadContext]
+ * may happen in parallel to the subsequent [updateThreadContext] and [restoreThreadContext] operations.
+ *
+ * Even though an element is copied for each child coroutine, an implementation should be able to handle the following
+ * interleaving when a coroutine with the corresponding element is launched on a multithreaded dispatcher:
+ *
+ * ```
+ * coroutine.updateThreadContext() // Thread #1
+ * ... coroutine body ...
+ * // suspension + immediate dispatch happen here
+ * coroutine.updateThreadContext() // Thread #2, coroutine is already resumed
+ * // ... coroutine body after suspension point on Thread #2 ...
+ * coroutine.restoreThreadContext() // Thread #1, is invoked late because Thread #1 is slow
+ * coroutine.restoreThreadContext() // Thread #2, may happen in parallel with the previous restore
+ * ```
+ *
+ * All implementations of [CopyableThreadContextElement] should be thread-safe and guard their internal mutable state
+ * within an element accordingly.
+ */
+@DelicateCoroutinesApi
+@ExperimentalCoroutinesApi
+public interface CopyableThreadContextElement<S> : ThreadContextElement<S> {
+
+ /**
+ * Returns a [CopyableThreadContextElement] to replace `this` `CopyableThreadContextElement` in the child
+ * coroutine's context that is under construction if the added context does not contain an element with the same [key].
+ *
+ * This function is called on the element each time a new coroutine inherits a context containing it,
+ * and the returned value is folded into the context given to the child.
+ *
+ * Since this method is called whenever a new coroutine is launched in a context containing this
+ * [CopyableThreadContextElement], implementations are performance-sensitive.
+ */
+ public fun copyForChild(): CopyableThreadContextElement<S>
+
+ /**
+ * Returns a [CopyableThreadContextElement] to replace `this` `CopyableThreadContextElement` in the child
+ * coroutine's context that is under construction if the added context does contain an element with the same [key].
+ *
+ * This method is invoked on the original element, accepting as the parameter
+ * the element that is supposed to overwrite it.
+ */
+ public fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext
+}
+
+/**
* Wraps [ThreadLocal] into [ThreadContextElement]. The resulting [ThreadContextElement]
* maintains the given [value] of the given [ThreadLocal] for coroutine regardless of the actual thread its is resumed on.
* By default [ThreadLocal.get] is used as a value for the thread-local variable, but it can be overridden with [value] parameter.
diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt
index 99e3b46c..dc0b7e29 100644
--- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt
+++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt
@@ -17,11 +17,12 @@ import java.util.concurrent.atomic.AtomicInteger
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
*
- * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools
- * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed
- * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due
- * to coroutine-oriented scheduling policy and thread-switch minimization.
- * See [issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) for details.
+ * This is a **delicate** API. The result of this method is a closeable resource with the
+ * associated native resources (threads). It should not be allocated in place,
+ * should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint.
+ * If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher,
+ * it is recommended to use [CoroutineDispatcher.limitedParallelism] instead.
+ *
* If you need a completely separate thread-pool with scheduling policy that is based on the standard
* JDK executors, use the following expression:
* `Executors.newSingleThreadExecutor().asCoroutineDispatcher()`.
@@ -29,8 +30,8 @@ import java.util.concurrent.atomic.AtomicInteger
*
* @param name the base name of the created thread.
*/
-@ObsoleteCoroutinesApi
-public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher =
+@DelicateCoroutinesApi
+public actual fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher =
newFixedThreadPoolContext(1, name)
/**
@@ -43,11 +44,12 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher =
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
*
- * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools
- * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed
- * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due
- * to coroutine-oriented scheduling policy and thread-switch minimization.
- * See [issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) for details.
+ * This is a **delicate** API. The result of this method is a closeable resource with the
+ * associated native resources (threads). It should not be allocated in place,
+ * should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint.
+ * If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher,
+ * it is recommended to use [CoroutineDispatcher.limitedParallelism] instead.
+ *
* If you need a completely separate thread-pool with scheduling policy that is based on the standard
* JDK executors, use the following expression:
* `Executors.newFixedThreadPool().asCoroutineDispatcher()`.
@@ -56,8 +58,8 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher =
* @param nThreads the number of threads.
* @param name the base name of the created threads.
*/
-@ObsoleteCoroutinesApi
-public fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
+@DelicateCoroutinesApi
+public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
val threadNo = AtomicInteger()
val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt
index 4657bc7d..748f5283 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<E>(
return super.send(element)
}
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION", "DEPRECATION_ERROR")
override fun offer(element: E): Boolean {
start()
return super.offer(element)
diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt
index 8ef0c182..4b0ce3f3 100644
--- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt
+++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt
@@ -4,12 +4,13 @@
package kotlinx.coroutines.debug
-import kotlinx.coroutines.debug.internal.DebugProbesImpl
+import android.annotation.*
+import kotlinx.coroutines.debug.internal.*
+import org.codehaus.mojo.animal_sniffer.*
import sun.misc.*
import java.lang.instrument.*
import java.lang.instrument.ClassFileTransformer
import java.security.*
-import android.annotation.*
/*
* This class is loaded if and only if kotlinx-coroutines-core was used as -javaagent argument,
@@ -17,17 +18,16 @@ import android.annotation.*
*/
@Suppress("unused")
@SuppressLint("all")
+@IgnoreJRERequirement // Never touched on Android
internal object AgentPremain {
- public var isInstalledStatically = false
-
private val enableCreationStackTraces = runCatching {
System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean()
}.getOrNull() ?: DebugProbesImpl.enableCreationStackTraces
@JvmStatic
- public fun premain(args: String?, instrumentation: Instrumentation) {
- isInstalledStatically = true
+ fun premain(args: String?, instrumentation: Instrumentation) {
+ AgentInstallationType.isInstalledStatically = true
instrumentation.addTransformer(DebugProbesTransformer)
DebugProbesImpl.enableCreationStackTraces = enableCreationStackTraces
DebugProbesImpl.install()
@@ -52,7 +52,7 @@ internal object AgentPremain {
* on the fly (-> get rid of ASM dependency).
* You can verify its content either by using javap on it or looking at out integration test module.
*/
- isInstalledStatically = true
+ AgentInstallationType.isInstalledStatically = true
return loader.getResourceAsStream("DebugProbesKt.bin").readBytes()
}
}
diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt
new file mode 100644
index 00000000..0e9b26ce
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentInstallationType.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.internal
+
+/**
+ * Object used to differentiate between agent installed statically or dynamically.
+ * This is done in a separate object so [DebugProbesImpl] can check for static installation
+ * without having to depend on [kotlinx.coroutines.debug.AgentPremain], which is not compatible with Android.
+ * Otherwise, access to `AgentPremain.isInstalledStatically` triggers the load of its internal `ClassFileTransformer`
+ * that is not available on Android.
+ */
+internal object AgentInstallationType {
+ internal var isInstalledStatically = false
+}
diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt
index ffb9c2da..b02eac63 100644
--- a/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt
+++ b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt
@@ -10,11 +10,10 @@ import java.lang.ref.*
// This is very limited implementation, not suitable as a generic map replacement.
// It has lock-free get and put with synchronized rehash for simplicity (and better CPU usage on contention)
-@OptIn(ExperimentalStdlibApi::class)
@Suppress("UNCHECKED_CAST")
internal class ConcurrentWeakMap<K : Any, V: Any>(
/**
- * Weak reference queue is needed when a small key is mapped to a large value and we need to promptly release a
+ * Weak reference queue is needed when a small key is mapped to a large value, and we need to promptly release a
* reference to the value when the key was already disposed.
*/
weakRefQueue: Boolean = false
@@ -73,7 +72,7 @@ internal class ConcurrentWeakMap<K : Any, V: Any>(
while (true) {
cleanWeakRef(weakRefQueue.remove() as HashedWeakRef<*>)
}
- } catch(e: InterruptedException) {
+ } catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
}
diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt
index 05befc1a..d358d49d 100644
--- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt
+++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt
@@ -6,7 +6,6 @@ package kotlinx.coroutines.debug.internal
import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import kotlinx.coroutines.debug.*
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.internal.ScopeCoroutine
import java.io.*
@@ -82,7 +81,7 @@ internal object DebugProbesImpl {
public fun install(): Unit = coroutineStateLock.write {
if (++installations > 1) return
startWeakRefCleanerThread()
- if (AgentPremain.isInstalledStatically) return
+ if (AgentInstallationType.isInstalledStatically) return
dynamicAttach?.invoke(true) // attach
}
@@ -92,7 +91,7 @@ internal object DebugProbesImpl {
stopWeakRefCleanerThread()
capturedCoroutinesMap.clear()
callerInfoCache.clear()
- if (AgentPremain.isInstalledStatically) return
+ if (AgentInstallationType.isInstalledStatically) return
dynamicAttach?.invoke(false) // detach
}
@@ -103,8 +102,10 @@ internal object DebugProbesImpl {
}
private fun stopWeakRefCleanerThread() {
- weakRefCleanerThread?.interrupt()
+ val thread = weakRefCleanerThread ?: return
weakRefCleanerThread = null
+ thread.interrupt()
+ thread.join()
}
public fun hierarchyToString(job: Job): String = coroutineStateLock.write {
@@ -149,10 +150,11 @@ internal object DebugProbesImpl {
* Private method that dumps coroutines so that different public-facing method can use
* to produce different result types.
*/
- private inline fun <R : Any> dumpCoroutinesInfoImpl(create: (CoroutineOwner<*>, CoroutineContext) -> R): List<R> =
+ private inline fun <R : Any> dumpCoroutinesInfoImpl(crossinline create: (CoroutineOwner<*>, CoroutineContext) -> R): List<R> =
coroutineStateLock.write {
check(isInstalled) { "Debug probes are not installed" }
capturedCoroutines
+ .asSequence()
// Stable ordering of coroutines by their sequence number
.sortedBy { it.info.sequenceNumber }
// Leave in the dump only the coroutines that were not collected while we were dumping them
@@ -160,9 +162,86 @@ internal object DebugProbesImpl {
// Fuse map and filter into one operation to save an inline
if (owner.isFinished()) null
else owner.info.context?.let { context -> create(owner, context) }
+ }.toList()
+ }
+
+ /*
+ * This method optimises the number of packages sent by the IDEA debugger
+ * to a client VM to speed up fetching of coroutine information.
+ *
+ * The return value is an array of objects, which consists of four elements:
+ * 1) A string in a JSON format that stores information that is needed to display
+ * every coroutine in the coroutine panel in the IDEA debugger.
+ * 2) An array of last observed threads.
+ * 3) An array of last observed frames.
+ * 4) An array of DebugCoroutineInfo.
+ *
+ * ### Implementation note
+ * For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference`
+ * that does a roundtrip to client VM for *each* field or property read.
+ * To avoid that, we serialize most of the critical for UI data into a primitives
+ * to save an exponential number of roundtrips.
+ *
+ * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
+ */
+ @OptIn(ExperimentalStdlibApi::class)
+ public fun dumpCoroutinesInfoAsJsonAndReferences(): Array<Any> {
+ val coroutinesInfo = dumpCoroutinesInfo()
+ val size = coroutinesInfo.size
+ val lastObservedThreads = ArrayList<Thread?>(size)
+ val lastObservedFrames = ArrayList<CoroutineStackFrame?>(size)
+ val coroutinesInfoAsJson = ArrayList<String>(size)
+ for (info in coroutinesInfo) {
+ val context = info.context
+ val name = context[CoroutineName.Key]?.name?.toStringWithQuotes()
+ val dispatcher = context[CoroutineDispatcher.Key]?.toStringWithQuotes()
+ coroutinesInfoAsJson.add(
+ """
+ {
+ "name": $name,
+ "id": ${context[CoroutineId.Key]?.id},
+ "dispatcher": $dispatcher,
+ "sequenceNumber": ${info.sequenceNumber},
+ "state": "${info.state}"
+ }
+ """.trimIndent()
+ )
+ lastObservedFrames.add(info.lastObservedFrame)
+ lastObservedThreads.add(info.lastObservedThread)
+ }
+
+ return arrayOf(
+ "[${coroutinesInfoAsJson.joinToString()}]",
+ lastObservedThreads.toTypedArray(),
+ lastObservedFrames.toTypedArray(),
+ coroutinesInfo.toTypedArray()
+ )
+ }
+
+ /*
+ * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
+ */
+ public fun enhanceStackTraceWithThreadDumpAsJson(info: DebugCoroutineInfo): String {
+ val stackTraceElements = enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace)
+ val stackTraceElementsInfoAsJson = mutableListOf<String>()
+ for (element in stackTraceElements) {
+ stackTraceElementsInfoAsJson.add(
+ """
+ {
+ "declaringClass": "${element.className}",
+ "methodName": "${element.methodName}",
+ "fileName": ${element.fileName?.toStringWithQuotes()},
+ "lineNumber": ${element.lineNumber}
}
+ """.trimIndent()
+ )
}
+ return "[${stackTraceElementsInfoAsJson.joinToString()}]"
+ }
+
+ private fun Any.toStringWithQuotes() = "\"$this\""
+
/*
* Internal (JVM-public) method used by IDEA debugger as of 1.4-M3.
*/
diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt
index d178060d..cfe5b699 100644
--- a/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt
+++ b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt
@@ -8,7 +8,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
internal actual class AbortFlowException actual constructor(
- actual val owner: FlowCollector<*>
+ @JvmField @Transient actual val owner: FlowCollector<*>
) : CancellationException("Flow was aborted, no more elements needed") {
override fun fillInStackTrace(): Throwable {
diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
index ea973287..cad3f1ae 100644
--- a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
+++ b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt
@@ -29,15 +29,22 @@ internal actual class SafeCollector<T> actual constructor(
@JvmField // Note, it is non-capturing lambda, so no extra allocation during init of SafeCollector
internal actual val collectContextSize = collectContext.fold(0) { count, _ -> count + 1 }
+
+ // 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<Unit>? = null
- // ContinuationImpl
+ /*
+ * 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
+ */
override val context: CoroutineContext
- get() = completion?.context ?: EmptyCoroutineContext
+ get() = lastEmissionContext ?: EmptyCoroutineContext
override fun invokeSuspend(result: Result<Any?>): Any {
- result.onFailure { lastEmissionContext = DownstreamExceptionElement(it) }
+ result.onFailure { lastEmissionContext = DownstreamExceptionContext(it, context) }
completion?.resumeWith(result as Result<Unit>)
return COROUTINE_SUSPENDED
}
@@ -59,7 +66,9 @@ internal actual class SafeCollector<T> actual constructor(
emit(uCont, value)
} catch (e: Throwable) {
// Save the fact that exception from emit (or even check context) has been thrown
- lastEmissionContext = DownstreamExceptionElement(e)
+ // Note, that this can the first emit and lastEmissionContext may not be saved yet,
+ // hence we use `uCont.context` here.
+ lastEmissionContext = DownstreamExceptionContext(e, uCont.context)
throw e
}
}
@@ -72,9 +81,18 @@ internal actual class SafeCollector<T> actual constructor(
val previousContext = lastEmissionContext
if (previousContext !== currentContext) {
checkContext(currentContext, previousContext, value)
+ lastEmissionContext = currentContext
}
completion = uCont
- return emitFun(collector as FlowCollector<Any?>, value, this as Continuation<Unit>)
+ val result = emitFun(collector as FlowCollector<Any?>, value, this as Continuation<Unit>)
+ /*
+ * 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
+ }
+ return result
}
private fun checkContext(
@@ -82,14 +100,13 @@ internal actual class SafeCollector<T> actual constructor(
previousContext: CoroutineContext?,
value: T
) {
- if (previousContext is DownstreamExceptionElement) {
+ if (previousContext is DownstreamExceptionContext) {
exceptionTransparencyViolated(previousContext, value)
}
checkContext(currentContext)
- lastEmissionContext = currentContext
}
- private fun exceptionTransparencyViolated(exception: DownstreamExceptionElement, value: Any?) {
+ private fun exceptionTransparencyViolated(exception: DownstreamExceptionContext, value: Any?) {
/*
* Exception transparency ensures that if a `collect` block or any intermediate operator
* throws an exception, then no more values will be received by it.
@@ -122,14 +139,12 @@ internal actual class SafeCollector<T> actual constructor(
For a more detailed explanation, please refer to Flow documentation.
""".trimIndent())
}
-
}
-internal class DownstreamExceptionElement(@JvmField val e: Throwable) : CoroutineContext.Element {
- companion object Key : CoroutineContext.Key<DownstreamExceptionElement>
-
- override val key: CoroutineContext.Key<*> = Key
-}
+internal class DownstreamExceptionContext(
+ @JvmField val e: Throwable,
+ originalContext: CoroutineContext
+) : CoroutineContext by originalContext
private object NoOpContinuation : Continuation<Any?> {
override val context: CoroutineContext = EmptyCoroutineContext
diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt
new file mode 100644
index 00000000..a4b321d8
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import android.annotation.SuppressLint
+import kotlinx.coroutines.*
+import java.lang.reflect.*
+import java.util.*
+import java.util.concurrent.locks.*
+import kotlin.concurrent.*
+
+private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1)
+private typealias Ctor = (Throwable) -> Throwable?
+
+private val ctorCache = try {
+ if (ANDROID_DETECTED) WeakMapCtorCache
+ else ClassValueCtorCache
+} catch (e: Throwable) {
+ // Fallback on Java 6 or exotic setups
+ WeakMapCtorCache
+}
+
+@Suppress("UNCHECKED_CAST")
+internal fun <E : Throwable> tryCopyException(exception: E): E? {
+ // Fast path for CopyableThrowable
+ if (exception is CopyableThrowable<*>) {
+ return runCatching { exception.createCopy() as E? }.getOrNull()
+ }
+ return ctorCache.get(exception.javaClass).invoke(exception) as E?
+}
+
+private fun <E : Throwable> createConstructor(clz: Class<E>): Ctor {
+ val nullResult: Ctor = { null } // Pre-cache class
+ // Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors)
+ if (throwableFields != clz.fieldsCountOrDefault(0)) return nullResult
+ /*
+ * Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message).
+ * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
+ */
+ val constructors = clz.constructors.sortedByDescending { it.parameterTypes.size }
+ for (constructor in constructors) {
+ val result = createSafeConstructor(constructor)
+ if (result != null) return result
+ }
+ return nullResult
+}
+
+private fun createSafeConstructor(constructor: Constructor<*>): Ctor? {
+ val p = constructor.parameterTypes
+ return when (p.size) {
+ 2 -> when {
+ p[0] == String::class.java && p[1] == Throwable::class.java ->
+ safeCtor { e -> constructor.newInstance(e.message, e) as Throwable }
+ else -> null
+ }
+ 1 -> when (p[0]) {
+ Throwable::class.java ->
+ safeCtor { e -> constructor.newInstance(e) as Throwable }
+ String::class.java ->
+ safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } }
+ else -> null
+ }
+ 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } }
+ else -> null
+ }
+}
+
+private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor =
+ { e -> runCatching { block(e) }.getOrNull() }
+
+private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) =
+ kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
+
+private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {
+ val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) }
+ val totalFields = accumulator + fieldsCount
+ val superClass = superclass ?: return totalFields
+ return superClass.fieldsCount(totalFields)
+}
+
+internal abstract class CtorCache {
+ abstract fun get(key: Class<out Throwable>): Ctor
+}
+
+private object WeakMapCtorCache : CtorCache() {
+ private val cacheLock = ReentrantReadWriteLock()
+ private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMap()
+
+ override fun get(key: Class<out Throwable>): Ctor {
+ cacheLock.read { exceptionCtors[key]?.let { return it } }
+ cacheLock.write {
+ exceptionCtors[key]?.let { return it }
+ return createConstructor(key).also { exceptionCtors[key] = it }
+ }
+ }
+}
+
+@IgnoreJreRequirement
+@SuppressLint("NewApi")
+private object ClassValueCtorCache : CtorCache() {
+ private val cache = object : ClassValue<Ctor>() {
+ override fun computeValue(type: Class<*>?): Ctor {
+ @Suppress("UNCHECKED_CAST")
+ return createConstructor(type as Class<out Throwable>)
+ }
+ }
+
+ override fun get(key: Class<out Throwable>): Ctor = cache.get(key)
+}
diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt
deleted file mode 100644
index 60328ebd..00000000
--- a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.internal
-
-import kotlinx.coroutines.*
-import java.lang.reflect.*
-import java.util.*
-import java.util.concurrent.locks.*
-import kotlin.concurrent.*
-
-private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1)
-private val cacheLock = ReentrantReadWriteLock()
-private typealias Ctor = (Throwable) -> Throwable?
-// Replace it with ClassValue when Java 6 support is over
-private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMap()
-
-@Suppress("UNCHECKED_CAST")
-internal fun <E : Throwable> tryCopyException(exception: E): E? {
- // Fast path for CopyableThrowable
- if (exception is CopyableThrowable<*>) {
- return runCatching { exception.createCopy() as E? }.getOrNull()
- }
- // Use cached ctor if found
- cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor ->
- return cachedCtor(exception) as E?
- }
- /*
- * Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors)
- */
- if (throwableFields != exception.javaClass.fieldsCountOrDefault(0)) {
- cacheLock.write { exceptionCtors[exception.javaClass] = { null } }
- return null
- }
- /*
- * Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message).
- * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
- */
- var ctor: Ctor? = null
- val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size }
- for (constructor in constructors) {
- ctor = createConstructor(constructor)
- if (ctor != null) break
- }
- // Store the resulting ctor to cache
- cacheLock.write { exceptionCtors[exception.javaClass] = ctor ?: { null } }
- return ctor?.invoke(exception) as E?
-}
-
-private fun createConstructor(constructor: Constructor<*>): Ctor? {
- val p = constructor.parameterTypes
- return when (p.size) {
- 2 -> when {
- p[0] == String::class.java && p[1] == Throwable::class.java ->
- safeCtor { e -> constructor.newInstance(e.message, e) as Throwable }
- else -> null
- }
- 1 -> when (p[0]) {
- Throwable::class.java ->
- safeCtor { e -> constructor.newInstance(e) as Throwable }
- String::class.java ->
- safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } }
- else -> null
- }
- 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } }
- else -> null
- }
-}
-
-private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor =
- { e -> runCatching { block(e) }.getOrNull() }
-
-private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
-
-private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {
- val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) }
- val totalFields = accumulator + fieldsCount
- val superClass = superclass ?: return totalFields
- return superClass.fieldsCount(totalFields)
-}
diff --git a/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt b/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt
new file mode 100644
index 00000000..41707f7b
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/src/internal/InternalAnnotations.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+@Suppress("ACTUAL_WITHOUT_EXPECT") // Not the same name to WA the bug in the compiler
+internal actual typealias IgnoreJreRequirement = org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
index 2d447413..e87952b4 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
+++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
@@ -61,7 +61,9 @@ public fun MainDispatcherFactory.tryCreateDispatcher(factories: List<MainDispatc
/** @suppress */
@InternalCoroutinesApi
-public fun MainCoroutineDispatcher.isMissing(): Boolean = this is MissingMainCoroutineDispatcher
+public fun MainCoroutineDispatcher.isMissing(): Boolean =
+ // not checking `this`, as it may be wrapped in a `TestMainDispatcher`, whereas `immediate` never is.
+ this.immediate is MissingMainCoroutineDispatcher
// R8 optimization hook, not const on purpose to enable R8 optimizations via "assumenosideeffects"
@Suppress("MayBeConstant")
@@ -93,6 +95,9 @@ private class MissingMainCoroutineDispatcher(
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
missing()
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher =
+ missing()
+
override suspend fun delay(time: Long) =
missing()
diff --git a/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt b/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt
new file mode 100644
index 00000000..f949d9f5
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 java.util.concurrent.atomic.*
+
+/**
+ * Atomic array with lock-free reads and synchronized modifications. It logically has an unbounded size,
+ * is implicitly filled with nulls, and is resized on updates as needed to grow.
+ */
+internal class ResizableAtomicArray<T>(initialLength: Int) {
+ @Volatile
+ private var array = AtomicReferenceArray<T>(initialLength)
+
+ // for debug output
+ public fun currentLength(): Int = array.length()
+
+ public operator fun get(index: Int): T? {
+ val array = this.array // volatile read
+ return if (index < array.length()) array[index] else null
+ }
+
+ // Must not be called concurrently, e.g. always use synchronized(this) to call this function
+ fun setSynchronized(index: Int, value: T?) {
+ val curArray = this.array
+ val curLen = curArray.length()
+ if (index < curLen) {
+ curArray[index] = value
+ } else {
+ val newArray = AtomicReferenceArray<T>((index + 1).coerceAtLeast(2 * curLen))
+ for (i in 0 until curLen) newArray[i] = curArray[i]
+ newArray[index] = value
+ array = newArray // copy done
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
index 84d9d9f8..e08d1cef 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
@@ -9,7 +9,6 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import java.io.*
import java.util.concurrent.*
-import java.util.concurrent.atomic.*
import java.util.concurrent.locks.*
import kotlin.math.*
import kotlin.random.*
@@ -261,7 +260,7 @@ internal class CoroutineScheduler(
* works properly
*/
@JvmField
- val workers = AtomicReferenceArray<Worker?>(maxPoolSize + 1)
+ val workers = ResizableAtomicArray<Worker>(corePoolSize + 1)
/**
* Long describing state of workers in this pool.
@@ -480,7 +479,7 @@ internal class CoroutineScheduler(
* 3) Only then start the worker, otherwise it may miss its own creation
*/
val worker = Worker(newIndex)
- workers[newIndex] = worker
+ workers.setSynchronized(newIndex, worker)
require(newIndex == incrementCreatedWorkers())
worker.start()
return cpuWorkers + 1
@@ -525,7 +524,7 @@ internal class CoroutineScheduler(
var dormant = 0
var terminated = 0
val queueSizes = arrayListOf<String>()
- for (index in 1 until workers.length()) {
+ for (index in 1 until workers.currentLength()) {
val worker = workers[index] ?: continue
val queueSize = worker.localQueue.size
when (worker.state) {
@@ -684,6 +683,7 @@ internal class CoroutineScheduler(
* No tasks were found:
* 1) Either at least one of the workers has stealable task in its FIFO-buffer with a stealing deadline.
* Then its deadline is stored in [minDelayUntilStealableTask]
+ * // '2)' can be found below
*
* Then just park for that duration (ditto re-scanning).
* While it could potentially lead to short (up to WORK_STEALING_TIME_RESOLUTION_NS ns) starvations,
@@ -838,7 +838,7 @@ internal class CoroutineScheduler(
val lastIndex = decrementCreatedWorkers()
if (lastIndex != oldIndex) {
val lastWorker = workers[lastIndex]!!
- workers[oldIndex] = lastWorker
+ workers.setSynchronized(oldIndex, lastWorker)
lastWorker.indexInArray = oldIndex
/*
* Now lastWorker is available at both indices in the array, but it can
@@ -852,7 +852,7 @@ internal class CoroutineScheduler(
/*
* 5) It is safe to clear reference from workers array now.
*/
- workers[lastIndex] = null
+ workers.setSynchronized(lastIndex, null)
}
state = WorkerState.TERMINATED
}
@@ -968,7 +968,6 @@ internal class CoroutineScheduler(
* Checks if the thread is part of a thread pool that supports coroutines.
* This function is needed for integration with BlockHound.
*/
-@Suppress("UNUSED")
@JvmName("isSchedulerWorker")
internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker
@@ -976,7 +975,6 @@ internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Wo
* Checks if the thread is running a CPU-bound task.
* This function is needed for integration with BlockHound.
*/
-@Suppress("UNUSED")
@JvmName("mayNotBlock")
internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker &&
thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt
new file mode 100644
index 00000000..e5defbaf
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/Deprecated.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+@file:Suppress("unused")
+
+package kotlinx.coroutines.scheduling
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import java.util.concurrent.*
+import kotlin.coroutines.*
+
+/**
+ * This API was "public @InternalApi" and leaked into Ktor enabled-by-default sources.
+ * Since then, we refactored scheduler sources and its API and decided to get rid of it in
+ * its current shape.
+ *
+ * To preserve backwards compatibility with Ktor 1.x, previous version of the code is
+ * extracted here as is and isolated from the rest of code base, so R8 can get rid of it.
+ *
+ * It should be removed after Ktor 3.0.0 (EOL of Ktor 1.x) around 2022.
+ */
+@PublishedApi
+internal open class ExperimentalCoroutineDispatcher(
+ private val corePoolSize: Int,
+ private val maxPoolSize: Int,
+ private val idleWorkerKeepAliveNs: Long,
+ private val schedulerName: String = "CoroutineScheduler"
+) : ExecutorCoroutineDispatcher() {
+ public constructor(
+ corePoolSize: Int = CORE_POOL_SIZE,
+ maxPoolSize: Int = MAX_POOL_SIZE,
+ schedulerName: String = DEFAULT_SCHEDULER_NAME
+ ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)
+
+ @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN)
+ public constructor(
+ corePoolSize: Int = CORE_POOL_SIZE,
+ maxPoolSize: Int = MAX_POOL_SIZE
+ ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)
+
+ override val executor: Executor
+ get() = coroutineScheduler
+
+ // This is variable for test purposes, so that we can reinitialize from clean state
+ private var coroutineScheduler = createScheduler()
+
+ override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
+ try {
+ coroutineScheduler.dispatch(block)
+ } catch (e: RejectedExecutionException) {
+ // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
+ // for testing purposes, so we don't have to worry about cancelling the affected Job here.
+ DefaultExecutor.dispatch(context, block)
+ }
+
+ override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
+ try {
+ coroutineScheduler.dispatch(block, tailDispatch = true)
+ } catch (e: RejectedExecutionException) {
+ // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
+ // for testing purposes, so we don't have to worry about cancelling the affected Job here.
+ DefaultExecutor.dispatchYield(context, block)
+ }
+
+ override fun close(): Unit = coroutineScheduler.close()
+
+ override fun toString(): String {
+ return "${super.toString()}[scheduler = $coroutineScheduler]"
+ }
+
+ /**
+ * Creates a coroutine execution context with limited parallelism to execute tasks which may potentially block.
+ * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher],
+ * giving it additional hints to adjust its behaviour.
+ *
+ * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel.
+ */
+ fun blocking(parallelism: Int = 16): CoroutineDispatcher {
+ require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
+ return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
+ }
+
+ /**
+ * Creates a coroutine execution context with limited parallelism to execute CPU-intensive tasks.
+ * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher],
+ * giving it additional hints to adjust its behaviour.
+ *
+ * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel.
+ */
+ fun limited(parallelism: Int): CoroutineDispatcher {
+ require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
+ require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
+ return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
+ }
+
+ internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
+ try {
+ coroutineScheduler.dispatch(block, context, tailDispatch)
+ } catch (e: RejectedExecutionException) {
+ // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
+ // for testing purposes, so we don't have to worry about cancelling the affected Job here.
+ // TaskContext shouldn't be lost here to properly invoke before/after task
+ DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context))
+ }
+ }
+
+ private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
+}
+
+private class LimitingDispatcher(
+ private val dispatcher: ExperimentalCoroutineDispatcher,
+ private val parallelism: Int,
+ private val name: String?,
+ override val taskMode: Int
+) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
+
+ private val queue = ConcurrentLinkedQueue<Runnable>()
+ private val inFlightTasks = atomic(0)
+
+ override val executor: Executor
+ get() = this
+
+ override fun execute(command: Runnable) = dispatch(command, false)
+
+ override fun close(): Unit = error("Close cannot be invoked on LimitingBlockingDispatcher")
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)
+
+ private fun dispatch(block: Runnable, tailDispatch: Boolean) {
+ var taskToSchedule = block
+ while (true) {
+ // Commit in-flight tasks slot
+ val inFlight = inFlightTasks.incrementAndGet()
+
+ // Fast path, if parallelism limit is not reached, dispatch task and return
+ if (inFlight <= parallelism) {
+ dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
+ return
+ }
+
+ // Parallelism limit is reached, add task to the queue
+ queue.add(taskToSchedule)
+
+ /*
+ * We're not actually scheduled anything, so rollback committed in-flight task slot:
+ * If the amount of in-flight tasks is still above the limit, do nothing
+ * If the amount of in-flight tasks is lesser than parallelism, then
+ * it's a race with a thread which finished the task from the current context, we should resubmit the first task from the queue
+ * to avoid starvation.
+ *
+ * Race example #1 (TN is N-th thread, R is current in-flight tasks number), execution is sequential:
+ *
+ * T1: submit task, start execution, R == 1
+ * T2: commit slot for next task, R == 2
+ * T1: finish T1, R == 1
+ * T2: submit next task to local queue, decrement R, R == 0
+ * Without retries, task from T2 will be stuck in the local queue
+ */
+ if (inFlightTasks.decrementAndGet() >= parallelism) {
+ return
+ }
+
+ taskToSchedule = queue.poll() ?: return
+ }
+ }
+
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ dispatch(block, tailDispatch = true)
+ }
+
+ override fun toString(): String {
+ return name ?: "${super.toString()}[dispatcher = $dispatcher]"
+ }
+
+ /**
+ * Tries to dispatch tasks which were blocked due to reaching parallelism limit if there is any.
+ *
+ * Implementation note: blocking tasks are scheduled in a fair manner (to local queue tail) to avoid
+ * non-blocking continuations starvation.
+ * E.g. for
+ * ```
+ * foo()
+ * blocking()
+ * bar()
+ * ```
+ * it's more profitable to execute bar at the end of `blocking` rather than pending blocking task
+ */
+ override fun afterTask() {
+ var next = queue.poll()
+ // If we have pending tasks in current blocking context, dispatch first
+ if (next != null) {
+ dispatcher.dispatchWithContext(next, this, true)
+ return
+ }
+ inFlightTasks.decrementAndGet()
+
+ /*
+ * Re-poll again and try to submit task if it's required otherwise tasks may be stuck in the local queue.
+ * Race example #2 (TN is N-th thread, R is current in-flight tasks number), execution is sequential:
+ * T1: submit task, start execution, R == 1
+ * T2: commit slot for next task, R == 2
+ * T1: finish T1, poll queue (it's still empty), R == 2
+ * T2: submit next task to the local queue, decrement R, R == 1
+ * T1: decrement R, finish. R == 0
+ *
+ * The task from T2 is stuck is the local queue
+ */
+ next = queue.poll() ?: return
+ dispatch(next, true)
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
index 7227b07c..d55edec9 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
@@ -4,124 +4,108 @@
package kotlinx.coroutines.scheduling
-import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import java.util.concurrent.*
import kotlin.coroutines.*
-/**
- * Default instance of coroutine dispatcher.
- */
-internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
- val IO: CoroutineDispatcher = LimitingDispatcher(
- this,
- systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
- "Dispatchers.IO",
- TASK_PROBABLY_BLOCKING
- )
+// Instance of Dispatchers.Default
+internal object DefaultScheduler : SchedulerCoroutineDispatcher(
+ CORE_POOL_SIZE, MAX_POOL_SIZE,
+ IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
+) {
+ // Shuts down the dispatcher, used only by Dispatchers.shutdown()
+ internal fun shutdown() {
+ super.close()
+ }
+ // Overridden in case anyone writes (Dispatchers.Default as ExecutorCoroutineDispatcher).close()
override fun close() {
- throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed")
+ throw UnsupportedOperationException("Dispatchers.Default cannot be closed")
}
- override fun toString(): String = DEFAULT_DISPATCHER_NAME
+ override fun toString(): String = "Dispatchers.Default"
+}
+
+// The unlimited instance of Dispatchers.IO that utilizes all the threads CoroutineScheduler provides
+private object UnlimitedIoScheduler : CoroutineDispatcher() {
@InternalCoroutinesApi
- @Suppress("UNUSED")
- public fun toDebugString(): String = super.toString()
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ DefaultScheduler.dispatchWithContext(block, BlockingContext, true)
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ DefaultScheduler.dispatchWithContext(block, BlockingContext, false)
+ }
}
-/**
- * @suppress **This is unstable API and it is subject to change.**
- */
-// TODO make internal (and rename) after complete integration
-@InternalCoroutinesApi
-public open class ExperimentalCoroutineDispatcher(
- private val corePoolSize: Int,
- private val maxPoolSize: Int,
- private val idleWorkerKeepAliveNs: Long,
- private val schedulerName: String = "CoroutineScheduler"
-) : ExecutorCoroutineDispatcher() {
- public constructor(
- corePoolSize: Int = CORE_POOL_SIZE,
- maxPoolSize: Int = MAX_POOL_SIZE,
- schedulerName: String = DEFAULT_SCHEDULER_NAME
- ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)
-
- @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN)
- public constructor(
- corePoolSize: Int = CORE_POOL_SIZE,
- maxPoolSize: Int = MAX_POOL_SIZE
- ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)
+// Dispatchers.IO
+internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
+
+ private val default = UnlimitedIoScheduler.limitedParallelism(
+ systemProp(
+ IO_PARALLELISM_PROPERTY_NAME,
+ 64.coerceAtLeast(AVAILABLE_PROCESSORS)
+ )
+ )
override val executor: Executor
- get() = coroutineScheduler
+ get() = this
- // This is variable for test purposes, so that we can reinitialize from clean state
- private var coroutineScheduler = createScheduler()
+ override fun execute(command: java.lang.Runnable) = dispatch(EmptyCoroutineContext, command)
- override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
- try {
- coroutineScheduler.dispatch(block)
- } catch (e: RejectedExecutionException) {
- // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
- // for testing purposes, so we don't have to worry about cancelling the affected Job here.
- DefaultExecutor.dispatch(context, block)
- }
+ @ExperimentalCoroutinesApi
+ override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
+ // See documentation to Dispatchers.IO for the rationale
+ return UnlimitedIoScheduler.limitedParallelism(parallelism)
+ }
- override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
- try {
- coroutineScheduler.dispatch(block, tailDispatch = true)
- } catch (e: RejectedExecutionException) {
- // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
- // for testing purposes, so we don't have to worry about cancelling the affected Job here.
- DefaultExecutor.dispatchYield(context, block)
- }
-
- override fun close(): Unit = coroutineScheduler.close()
-
- override fun toString(): String {
- return "${super.toString()}[scheduler = $coroutineScheduler]"
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ default.dispatch(context, block)
}
- /**
- * Creates a coroutine execution context with limited parallelism to execute tasks which may potentially block.
- * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher],
- * giving it additional hints to adjust its behaviour.
- *
- * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel.
- */
- public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
- require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
- return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
+ @InternalCoroutinesApi
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ default.dispatchYield(context, block)
}
- /**
- * Creates a coroutine execution context with limited parallelism to execute CPU-intensive tasks.
- * Resulting [CoroutineDispatcher] doesn't own any resources (its threads) and provides a view of the original [ExperimentalCoroutineDispatcher],
- * giving it additional hints to adjust its behaviour.
- *
- * @param parallelism parallelism level, indicating how many threads can execute tasks in the resulting dispatcher parallel.
- */
- public fun limited(parallelism: Int): CoroutineDispatcher {
- require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
- require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
- return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
+ override fun close() {
+ error("Cannot be invoked on Dispatchers.IO")
}
+ override fun toString(): String = "Dispatchers.IO"
+}
+
+// Instantiated in tests so we can test it in isolation
+internal open class SchedulerCoroutineDispatcher(
+ private val corePoolSize: Int = CORE_POOL_SIZE,
+ private val maxPoolSize: Int = MAX_POOL_SIZE,
+ private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
+ private val schedulerName: String = "CoroutineScheduler",
+) : ExecutorCoroutineDispatcher() {
+
+ override val executor: Executor
+ get() = coroutineScheduler
+
+ // This is variable for test purposes, so that we can reinitialize from clean state
+ private var coroutineScheduler = createScheduler()
+
+ private fun createScheduler() =
+ CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
+
+ override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block)
+
+ override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
+ coroutineScheduler.dispatch(block, tailDispatch = true)
+
internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
- try {
- coroutineScheduler.dispatch(block, context, tailDispatch)
- } catch (e: RejectedExecutionException) {
- // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
- // for testing purposes, so we don't have to worry about cancelling the affected Job here.
- // TaskContext shouldn't be lost here to properly invoke before/after task
- DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context))
- }
+ coroutineScheduler.dispatch(block, context, tailDispatch)
}
- private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
+ override fun close() {
+ coroutineScheduler.close()
+ }
// fot tests only
@Synchronized
@@ -139,106 +123,3 @@ public open class ExperimentalCoroutineDispatcher(
// for tests only
internal fun restore() = usePrivateScheduler() // recreate scheduler
}
-
-private class LimitingDispatcher(
- private val dispatcher: ExperimentalCoroutineDispatcher,
- private val parallelism: Int,
- private val name: String?,
- override val taskMode: Int
-) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
-
- private val queue = ConcurrentLinkedQueue<Runnable>()
- private val inFlightTasks = atomic(0)
-
- override val executor: Executor
- get() = this
-
- override fun execute(command: Runnable) = dispatch(command, false)
-
- override fun close(): Unit = error("Close cannot be invoked on LimitingBlockingDispatcher")
-
- override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)
-
- private fun dispatch(block: Runnable, tailDispatch: Boolean) {
- var taskToSchedule = block
- while (true) {
- // Commit in-flight tasks slot
- val inFlight = inFlightTasks.incrementAndGet()
-
- // Fast path, if parallelism limit is not reached, dispatch task and return
- if (inFlight <= parallelism) {
- dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
- return
- }
-
- // Parallelism limit is reached, add task to the queue
- queue.add(taskToSchedule)
-
- /*
- * We're not actually scheduled anything, so rollback committed in-flight task slot:
- * If the amount of in-flight tasks is still above the limit, do nothing
- * If the amount of in-flight tasks is lesser than parallelism, then
- * it's a race with a thread which finished the task from the current context, we should resubmit the first task from the queue
- * to avoid starvation.
- *
- * Race example #1 (TN is N-th thread, R is current in-flight tasks number), execution is sequential:
- *
- * T1: submit task, start execution, R == 1
- * T2: commit slot for next task, R == 2
- * T1: finish T1, R == 1
- * T2: submit next task to local queue, decrement R, R == 0
- * Without retries, task from T2 will be stuck in the local queue
- */
- if (inFlightTasks.decrementAndGet() >= parallelism) {
- return
- }
-
- taskToSchedule = queue.poll() ?: return
- }
- }
-
- override fun dispatchYield(context: CoroutineContext, block: Runnable) {
- dispatch(block, tailDispatch = true)
- }
-
- override fun toString(): String {
- return name ?: "${super.toString()}[dispatcher = $dispatcher]"
- }
-
- /**
- * Tries to dispatch tasks which were blocked due to reaching parallelism limit if there is any.
- *
- * Implementation note: blocking tasks are scheduled in a fair manner (to local queue tail) to avoid
- * non-blocking continuations starvation.
- * E.g. for
- * ```
- * foo()
- * blocking()
- * bar()
- * ```
- * it's more profitable to execute bar at the end of `blocking` rather than pending blocking task
- */
- override fun afterTask() {
- var next = queue.poll()
- // If we have pending tasks in current blocking context, dispatch first
- if (next != null) {
- dispatcher.dispatchWithContext(next, this, true)
- return
- }
- inFlightTasks.decrementAndGet()
-
- /*
- * Re-poll again and try to submit task if it's required otherwise tasks may be stuck in the local queue.
- * Race example #2 (TN is N-th thread, R is current in-flight tasks number), execution is sequential:
- * T1: submit task, start execution, R == 1
- * T2: commit slot for next task, R == 2
- * T1: finish T1, poll queue (it's still empty), R == 2
- * T2: submit next task to the local queue, decrement R, R == 1
- * T1: decrement R, finish. R == 0
- *
- * The task from T2 is stuck is the local queue
- */
- next = queue.poll() ?: return
- dispatch(next, true)
- }
-}
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
index da867c98..5403cfc1 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
@@ -9,10 +9,6 @@ import kotlinx.coroutines.internal.*
import java.util.concurrent.*
-// TODO most of these fields will be moved to 'object ExperimentalDispatcher'
-
-// User-visible name
-internal const val DEFAULT_DISPATCHER_NAME = "Dispatchers.Default"
// Internal debuggability name + thread name prefixes
internal const val DEFAULT_SCHEDULER_NAME = "DefaultDispatcher"
@@ -22,27 +18,24 @@ internal val WORK_STEALING_TIME_RESOLUTION_NS = systemProp(
"kotlinx.coroutines.scheduler.resolution.ns", 100000L
)
-@JvmField
-internal val BLOCKING_DEFAULT_PARALLELISM = systemProp(
- "kotlinx.coroutines.scheduler.blocking.parallelism", 16
-)
-
-// NOTE: we coerce default to at least two threads to give us chances that multi-threading problems
-// get reproduced even on a single-core machine, but support explicit setting of 1 thread scheduler if needed.
+/**
+ * The maximum number of threads allocated for CPU-bound tasks at the default set of dispatchers.
+ *
+ * NOTE: we coerce default to at least two threads to give us chances that multi-threading problems
+ * get reproduced even on a single-core machine, but support explicit setting of 1 thread scheduler if needed
+ */
@JvmField
internal val CORE_POOL_SIZE = systemProp(
"kotlinx.coroutines.scheduler.core.pool.size",
- AVAILABLE_PROCESSORS.coerceAtLeast(2), // !!! at least two here
+ AVAILABLE_PROCESSORS.coerceAtLeast(2),
minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)
+/** The maximum number of threads allocated for blocking tasks at the default set of dispatchers. */
@JvmField
internal val MAX_POOL_SIZE = systemProp(
"kotlinx.coroutines.scheduler.max.pool.size",
- (AVAILABLE_PROCESSORS * 128).coerceIn(
- CORE_POOL_SIZE,
- CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE
- ),
+ CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE,
maxValue = CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE
)
@@ -69,14 +62,18 @@ internal interface TaskContext {
fun afterTask()
}
-internal object NonBlockingContext : TaskContext {
- override val taskMode: Int = TASK_NON_BLOCKING
-
+private class TaskContextImpl(override val taskMode: Int): TaskContext {
override fun afterTask() {
- // Nothing for non-blocking context
+ // Nothing for non-blocking context
}
}
+@JvmField
+internal val NonBlockingContext: TaskContext = TaskContextImpl(TASK_NON_BLOCKING)
+
+@JvmField
+internal val BlockingContext: TaskContext = TaskContextImpl(TASK_PROBABLY_BLOCKING)
+
internal abstract class Task(
@JvmField var submissionTime: Long,
@JvmField var taskContext: TaskContext
diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt
deleted file mode 100644
index 8526ca21..00000000
--- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.internal.*
-import java.util.concurrent.*
-import kotlin.coroutines.*
-
-/**
- * This [CoroutineContext] dispatcher can be used to simulate virtual time to speed up
- * code, especially tests, that deal with delays and timeouts in Coroutines.
- *
- * Provide an instance of this TestCoroutineContext when calling the *non-blocking*
- * [launch][CoroutineScope.launch] or [async][CoroutineScope.async]
- * and then advance time or trigger the actions to make the co-routines execute as soon as possible.
- *
- * This works much like the *TestScheduler* in RxJava2, which allows to speed up tests that deal
- * with non-blocking Rx chains that contain delays, timeouts, intervals and such.
- *
- * This dispatcher can also handle *blocking* coroutines that are started by [runBlocking].
- * This dispatcher's virtual time will be automatically advanced based on the delayed actions
- * within the Coroutine(s).
- *
- * **Note: This API will become obsolete in future updates due to integration with structured concurrency.**
- * See [issue #541](https://github.com/Kotlin/kotlinx.coroutines/issues/541).
- *
- * @param name A user-readable name for debugging purposes.
- */
-@Deprecated("This API has been deprecated to integrate with Structured Concurrency.",
- ReplaceWith("TestCoroutineScope", "kotlin.coroutines.test"),
- level = DeprecationLevel.WARNING)
-public class TestCoroutineContext(private val name: String? = null) : CoroutineContext {
- private val uncaughtExceptions = mutableListOf<Throwable>()
-
- private val ctxDispatcher = Dispatcher()
-
- private val ctxHandler = CoroutineExceptionHandler { _, exception ->
- uncaughtExceptions += exception
- }
-
- // The ordered queue for the runnable tasks.
- private val queue = ThreadSafeHeap<TimedRunnableObsolete>()
-
- // The per-scheduler global order counter.
- private var counter = 0L
-
- // Storing time in nanoseconds internally.
- private var time = 0L
-
- /**
- * Exceptions that were caught during a [launch][CoroutineScope.launch] or a [async][CoroutineScope.async] + [Deferred.await].
- */
- public val exceptions: List<Throwable> get() = uncaughtExceptions
-
- // -- CoroutineContext implementation
-
- public override fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R =
- operation(operation(initial, ctxDispatcher), ctxHandler)
-
- @Suppress("UNCHECKED_CAST")
- public override fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? = when {
- key === ContinuationInterceptor -> ctxDispatcher as E
- key === CoroutineExceptionHandler -> ctxHandler as E
- else -> null
- }
-
- public override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext = when {
- key === ContinuationInterceptor -> ctxHandler
- key === CoroutineExceptionHandler -> ctxDispatcher
- else -> this
- }
-
- /**
- * Returns the current virtual clock-time as it is known to this CoroutineContext.
- *
- * @param unit The [TimeUnit] in which the clock-time must be returned.
- * @return The virtual clock-time
- */
- public fun now(unit: TimeUnit = TimeUnit.MILLISECONDS): Long=
- unit.convert(time, TimeUnit.NANOSECONDS)
-
- /**
- * Moves the CoroutineContext's virtual clock forward by a specified amount of time.
- *
- * The returned delay-time can be larger than the specified delay-time if the code
- * under test contains *blocking* Coroutines.
- *
- * @param delayTime The amount of time to move the CoroutineContext's clock forward.
- * @param unit The [TimeUnit] in which [delayTime] and the return value is expressed.
- * @return The amount of delay-time that this CoroutineContext's clock has been forwarded.
- */
- public fun advanceTimeBy(delayTime: Long, unit: TimeUnit = TimeUnit.MILLISECONDS): Long {
- val oldTime = time
- advanceTimeTo(oldTime + unit.toNanos(delayTime), TimeUnit.NANOSECONDS)
- return unit.convert(time - oldTime, TimeUnit.NANOSECONDS)
- }
-
- /**
- * Moves the CoroutineContext's clock-time to a particular moment in time.
- *
- * @param targetTime The point in time to which to move the CoroutineContext's clock.
- * @param unit The [TimeUnit] in which [targetTime] is expressed.
- */
- public fun advanceTimeTo(targetTime: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) {
- val nanoTime = unit.toNanos(targetTime)
- triggerActions(nanoTime)
- if (nanoTime > time) time = nanoTime
- }
-
- /**
- * Triggers any actions that have not yet been triggered and that are scheduled to be triggered at or
- * before this CoroutineContext's present virtual clock-time.
- */
- public fun triggerActions(): Unit = triggerActions(time)
-
- /**
- * Cancels all not yet triggered actions. Be careful calling this, since it can seriously
- * mess with your coroutines work. This method should usually be called on tear-down of a
- * unit test.
- */
- public fun cancelAllActions() {
- // An 'is-empty' test is required to avoid a NullPointerException in the 'clear()' method
- if (!queue.isEmpty) queue.clear()
- }
-
- /**
- * This method does nothing if there is one unhandled exception that satisfies the given predicate.
- * Otherwise it throws an [AssertionError] with the given message.
- *
- * (this method will clear the list of unhandled exceptions)
- *
- * @param message Message of the [AssertionError]. Defaults to an empty String.
- * @param predicate The predicate that must be satisfied.
- */
- public fun assertUnhandledException(message: String = "", predicate: (Throwable) -> Boolean) {
- if (uncaughtExceptions.size != 1 || !predicate(uncaughtExceptions[0])) throw AssertionError(message)
- uncaughtExceptions.clear()
- }
-
- /**
- * This method does nothing if there are no unhandled exceptions or all of them satisfy the given predicate.
- * Otherwise it throws an [AssertionError] with the given message.
- *
- * (this method will clear the list of unhandled exceptions)
- *
- * @param message Message of the [AssertionError]. Defaults to an empty String.
- * @param predicate The predicate that must be satisfied.
- */
- public fun assertAllUnhandledExceptions(message: String = "", predicate: (Throwable) -> Boolean) {
- if (!uncaughtExceptions.all(predicate)) throw AssertionError(message)
- uncaughtExceptions.clear()
- }
-
- /**
- * This method does nothing if one or more unhandled exceptions satisfy the given predicate.
- * Otherwise it throws an [AssertionError] with the given message.
- *
- * (this method will clear the list of unhandled exceptions)
- *
- * @param message Message of the [AssertionError]. Defaults to an empty String.
- * @param predicate The predicate that must be satisfied.
- */
- public fun assertAnyUnhandledException(message: String = "", predicate: (Throwable) -> Boolean) {
- if (!uncaughtExceptions.any(predicate)) throw AssertionError(message)
- uncaughtExceptions.clear()
- }
-
- /**
- * This method does nothing if the list of unhandled exceptions satisfy the given predicate.
- * Otherwise it throws an [AssertionError] with the given message.
- *
- * (this method will clear the list of unhandled exceptions)
- *
- * @param message Message of the [AssertionError]. Defaults to an empty String.
- * @param predicate The predicate that must be satisfied.
- */
- public fun assertExceptions(message: String = "", predicate: (List<Throwable>) -> Boolean) {
- if (!predicate(uncaughtExceptions)) throw AssertionError(message)
- uncaughtExceptions.clear()
- }
-
- private fun enqueue(block: Runnable) =
- queue.addLast(TimedRunnableObsolete(block, counter++))
-
- private fun postDelayed(block: Runnable, delayTime: Long) =
- TimedRunnableObsolete(block, counter++, time + TimeUnit.MILLISECONDS.toNanos(delayTime))
- .also {
- queue.addLast(it)
- }
-
- private fun processNextEvent(): Long {
- val current = queue.peek()
- if (current != null) {
- // Automatically advance time for EventLoop callbacks
- triggerActions(current.time)
- }
- return if (queue.isEmpty) Long.MAX_VALUE else 0L
- }
-
- private fun triggerActions(targetTime: Long) {
- while (true) {
- val current = queue.removeFirstIf { it.time <= targetTime } ?: break
- // If the scheduled time is 0 (immediate) use current virtual time
- if (current.time != 0L) time = current.time
- current.run()
- }
- }
-
- public override fun toString(): String = name ?: "TestCoroutineContext@$hexAddress"
-
- private inner class Dispatcher : EventLoop(), Delay {
- init {
- incrementUseCount() // this event loop is never completed
- }
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- this@TestCoroutineContext.enqueue(block)
- }
-
- // override runBlocking to process this event loop
- override fun shouldBeProcessedFromContext(): Boolean = true
-
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- postDelayed(Runnable {
- with(continuation) { resumeUndispatched(Unit) }
- }, timeMillis)
- }
-
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val node = postDelayed(block, timeMillis)
- return object : DisposableHandle {
- override fun dispose() {
- queue.remove(node)
- }
- }
- }
-
- override fun processNextEvent() = this@TestCoroutineContext.processNextEvent()
-
- public override fun toString(): String = "Dispatcher(${this@TestCoroutineContext})"
- }
-}
-
-private class TimedRunnableObsolete(
- private val run: Runnable,
- private val count: Long = 0,
- @JvmField internal val time: Long = 0
-) : Comparable<TimedRunnableObsolete>, Runnable by run, ThreadSafeHeapNode {
- override var heap: ThreadSafeHeap<*>? = null
- override var index: Int = 0
-
- override fun run() = run.run()
-
- override fun compareTo(other: TimedRunnableObsolete) = if (time == other.time) {
- count.compareTo(other.count)
- } else {
- time.compareTo(other.time)
- }
-
- override fun toString() = "TimedRunnable(time=$time, run=$run)"
-}
-
-/**
- * Executes a block of code in which a unit-test can be written using the provided [TestCoroutineContext]. The provided
- * [TestCoroutineContext] is available in the [testBody] as the `this` receiver.
- *
- * The [testBody] is executed and an [AssertionError] is thrown if the list of unhandled exceptions is not empty and
- * contains any exception that is not a [CancellationException].
- *
- * If the [testBody] successfully executes one of the [TestCoroutineContext.assertAllUnhandledExceptions],
- * [TestCoroutineContext.assertAnyUnhandledException], [TestCoroutineContext.assertUnhandledException] or
- * [TestCoroutineContext.assertExceptions], the list of unhandled exceptions will have been cleared and this method will
- * not throw an [AssertionError].
- *
- * **Note: This API will become obsolete in future updates due to integration with structured concurrency.**
- * See [issue #541](https://github.com/Kotlin/kotlinx.coroutines/issues/541).
- *
- * @param testContext The provided [TestCoroutineContext]. If not specified, a default [TestCoroutineContext] will be
- * provided instead.
- * @param testBody The code of the unit-test.
- */
-@Deprecated("This API has been deprecated to integrate with Structured Concurrency.",
- ReplaceWith("testContext.runBlockingTest(testBody)", "kotlin.coroutines.test"),
- level = DeprecationLevel.WARNING)
-public fun withTestContext(testContext: TestCoroutineContext = TestCoroutineContext(), testBody: TestCoroutineContext.() -> Unit) {
- with (testContext) {
- testBody()
- if (!exceptions.all { it is CancellationException }) {
- throw AssertionError("Coroutine encountered unhandled exceptions:\n$exceptions")
- }
- }
-}
diff --git a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt b/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt
deleted file mode 100644
index 8f9f8554..00000000
--- a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines
-
-import org.junit.Test
-import java.lang.reflect.*
-import java.util.concurrent.*
-import kotlin.test.*
-
-@Suppress("DEPRECATION")
-class CommonPoolTest {
- private inline fun <T> Try(block: () -> T) = try { block() } catch (e: Throwable) { null }
-
- @Test
- fun testIsGoodCommonPool() {
- // Test only on JDKs that has all we need
- val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") } ?: return
- val wtfClass = Try { Class.forName("java.util.concurrent.ForkJoinPool${'$'}ForkJoinWorkerThreadFactory") } ?: return
- val dwtfClass = Try { Class.forName("java.util.concurrent.ForkJoinPool${'$'}DefaultForkJoinWorkerThreadFactory") } ?: return
- // We need private constructor to create "broken" FJP instance
- val fjpCtor = Try { fjpClass.getDeclaredConstructor(
- Int::class.java,
- wtfClass,
- Thread.UncaughtExceptionHandler::class.java,
- Int::class.java,
- String::class.java
- ) } ?: return
- fjpCtor.isAccessible = true
- val dwtfCtor = Try { dwtfClass.getDeclaredConstructor() } ?: return
- dwtfCtor.isAccessible = true
- // Create bad pool
- val fjp0: ExecutorService = createFJP(0, fjpCtor, dwtfCtor) ?: return
- assertFalse(CommonPool.isGoodCommonPool(fjpClass, fjp0))
- fjp0.shutdown()
- // Create good pool
- val fjp1: ExecutorService = createFJP(1, fjpCtor, dwtfCtor) ?: return
- assertTrue(CommonPool.isGoodCommonPool(fjpClass, fjp1))
- fjp1.shutdown()
- }
-
- private fun createFJP(
- parallelism: Int,
- fjpCtor: Constructor<out Any>,
- dwtfCtor: Constructor<out Any>
- ): ExecutorService? = Try {
- fjpCtor.newInstance(
- parallelism,
- dwtfCtor.newInstance(),
- Thread.getDefaultUncaughtExceptionHandler(),
- 0,
- "Worker"
- )
- } as? ExecutorService
-} \ No newline at end of file
diff --git a/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt b/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt
new file mode 100644
index 00000000..b46adda9
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/ConcurrentTestUtilities.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlin.random.*
+
+actual fun randomWait() {
+ val n = Random.nextInt(1000)
+ if (n < 500) return // no wait 50% of time
+ repeat(n) {
+ BlackHole.sink *= 3
+ }
+ if (n > 900) Thread.yield()
+}
+
+private object BlackHole {
+ @Volatile
+ var sink = 1
+}
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+internal actual typealias SuppressSupportingThrowable = Throwable
+
+@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
+actual fun Throwable.printStackTrace() = printStackTrace()
+
+actual fun currentThreadName(): String = Thread.currentThread().name
diff --git a/kotlinx-coroutines-core/jvm/test/ContinuationSerializationTest.kt b/kotlinx-coroutines-core/jvm/test/ContinuationSerializationTest.kt
deleted file mode 100644
index 12941fb1..00000000
--- a/kotlinx-coroutines-core/jvm/test/ContinuationSerializationTest.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines
-
-
-import com.esotericsoftware.kryo.*
-import com.esotericsoftware.kryo.io.*
-import kotlinx.atomicfu.*
-import org.junit.Test
-import org.objenesis.strategy.*
-import java.io.*
-import kotlin.coroutines.*
-import kotlin.coroutines.intrinsics.*
-import kotlin.reflect.*
-import kotlin.test.*
-
-@Ignore
-class ContinuationSerializationTest : TestBase() {
-
- companion object {
- @JvmStatic
- var flag = false
- }
-
-// private val atomicInt = atomic(1)
-
- private val kryo =
- Kryo().also { it.instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) }
-
- private var storage: ByteArrayOutputStream = ByteArrayOutputStream()
-
- @Test
- fun testSafeContinuationSerDe() = testSerization(::serialize, {
- it.javaClass.getDeclaredField("result").apply {
- isAccessible = true
- set(it, COROUTINE_SUSPENDED)
- }
- })
-
- @Test
- fun testUnsafeContinuationSerDe() = testSerization(::serializeUnsafe, {})
-
-// @Test
-// fun testCancellableContinuationSerDe() = testSerization(::serializeCancellable, {
-// it.javaClass.superclass.getDeclaredField("_decision").apply {
-// isAccessible = true
-// set(it, atomicInt)
-// }
-// })
-
- private suspend fun serialize() = suspendCoroutine<Unit> { cont ->
- Output(storage).use {
- kryo.writeClassAndObject(it, cont)
- }
- }
-
- private suspend fun serializeCancellable() = suspendCancellableCoroutine<Unit> { cont ->
- Output(storage).use {
- kryo.writeClassAndObject(it, cont)
- }
- }
-
- private suspend fun serializeUnsafe() = suspendCoroutineUninterceptedOrReturn<Unit> { cont ->
- Output(storage).use {
- kryo.writeClassAndObject(it, cont)
- }
- }
-
- private fun testSerization(serialize: KSuspendFunction0<Unit>, patcher: (Continuation<Unit>) -> Unit) =
- runBlocking {
- launch(Unconfined) {
- expect(1)
- serialize()
- flag = true
- }
-
- val cont = deserialise()
- patcher(cont)
- expect(2)
- cont.resume(Unit)
- finish(3)
- assertTrue(flag)
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun deserialise(): Continuation<Unit> {
- val input = Input(ByteArrayInputStream(storage.toByteArray()))
- input.use {
- return kryo.readClassAndObject(it) as Continuation<Unit>
- }
- }
-}
diff --git a/kotlinx-coroutines-core/jvm/test/DispatcherKeyTest.kt b/kotlinx-coroutines-core/jvm/test/DispatcherKeyTest.kt
index e2d8ffaa..303e8cc8 100644
--- a/kotlinx-coroutines-core/jvm/test/DispatcherKeyTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/DispatcherKeyTest.kt
@@ -8,7 +8,7 @@ import org.junit.Test
import kotlin.coroutines.*
import kotlin.test.*
-@UseExperimental(ExperimentalStdlibApi::class)
+@OptIn(ExperimentalStdlibApi::class)
class DispatcherKeyTest : TestBase() {
companion object CustomInterceptor : AbstractCoroutineContextElement(ContinuationInterceptor),
diff --git a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt
index 179b2e5e..7b2aaf63 100644
--- a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt
+++ b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt
@@ -9,6 +9,7 @@ import java.lang.reflect.*
import java.text.*
import java.util.*
import java.util.Collections.*
+import java.util.concurrent.*
import java.util.concurrent.atomic.*
import java.util.concurrent.locks.*
import kotlin.test.*
@@ -26,11 +27,11 @@ object FieldWalker {
// excluded/terminal classes (don't walk them)
fieldsCache += listOf(
Any::class, String::class, Thread::class, Throwable::class, StackTraceElement::class,
- WeakReference::class, ReferenceQueue::class, AbstractMap::class,
- ReentrantReadWriteLock::class, SimpleDateFormat::class
+ WeakReference::class, ReferenceQueue::class, AbstractMap::class, Enum::class,
+ ReentrantLock::class, ReentrantReadWriteLock::class, SimpleDateFormat::class, ThreadPoolExecutor::class,
)
.map { it.java }
- .associateWith { emptyList<Field>() }
+ .associateWith { emptyList() }
}
/*
@@ -78,9 +79,8 @@ object FieldWalker {
val path = ArrayList<String>()
var cur = element
while (true) {
- val ref = visited.getValue(cur)
- if (ref is Ref.RootRef) break
- when (ref) {
+ when (val ref = visited.getValue(cur)) {
+ Ref.RootRef -> break
is Ref.FieldRef -> {
cur = ref.parent
path += "|${ref.parent.javaClass.simpleName}::${ref.name}"
@@ -89,7 +89,9 @@ object FieldWalker {
cur = ref.parent
path += "[${ref.index}]"
}
- else -> error("Should not be reached")
+ else -> {
+ // Nothing, kludge for IDE
+ }
}
}
path.reverse()
@@ -158,6 +160,13 @@ object FieldWalker {
&& !(it.type.isArray && it.type.componentType.isPrimitive)
&& it.name != "previousOut" // System.out from TestBase that we store in a field to restore later
}
+ check(fields.isEmpty() || !type.name.startsWith("java.")) {
+ """
+ Trying to walk trough JDK's '$type' will get into illegal reflective access on JDK 9+.
+ Either modify your test to avoid usage of this class or update FieldWalker code to retrieve
+ the captured state of this class without going through reflection (see how collections are handled).
+ """.trimIndent()
+ }
fields.forEach { it.isAccessible = true } // make them all accessible
result.addAll(fields)
type = type.superclass
diff --git a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt
new file mode 100644
index 00000000..c909f27b
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import org.junit.*
+import java.io.*
+
+
+@Suppress("BlockingMethodInNonBlockingContext")
+class JobCancellationExceptionSerializerTest : TestBase() {
+
+ @Test
+ fun testSerialization() = runTest {
+ try {
+ coroutineScope {
+ expect(1)
+
+ launch {
+ expect(2)
+ try {
+ hang {}
+ } catch (e: CancellationException) {
+ throw RuntimeException("RE2", e)
+ }
+ }
+
+ launch {
+ expect(3)
+ throw RuntimeException("RE1")
+ }
+ }
+ } catch (e: Throwable) {
+ // Should not fail
+ ObjectOutputStream(ByteArrayOutputStream()).use {
+ it.writeObject(e)
+ }
+ finish(4)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt b/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt
new file mode 100644
index 00000000..58f2b6b3
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import org.junit.*
+import org.junit.Test
+import org.junit.runner.*
+import org.junit.runners.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import kotlin.test.*
+
+@RunWith(Parameterized::class)
+class LimitedParallelismStressTest(private val targetParallelism: Int) : TestBase() {
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun params(): Collection<Array<Any>> = listOf(1, 2, 3, 4).map { arrayOf(it) }
+ }
+
+ @get:Rule
+ val executor = ExecutorRule(targetParallelism * 2)
+ private val iterations = 100_000
+
+ private val parallelism = AtomicInteger(0)
+
+ private fun checkParallelism() {
+ val value = parallelism.incrementAndGet()
+ Thread.yield()
+ assertTrue { value <= targetParallelism }
+ parallelism.decrementAndGet()
+ }
+
+ @Test
+ fun testLimitedExecutor() = runTest {
+ val view = executor.limitedParallelism(targetParallelism)
+ doStress {
+ repeat(iterations) {
+ launch(view) {
+ checkParallelism()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testLimitedDispatchersIo() = runTest {
+ val view = Dispatchers.IO.limitedParallelism(targetParallelism)
+ doStress {
+ repeat(iterations) {
+ launch(view) {
+ checkParallelism()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testLimitedDispatchersIoDispatchYield() = runTest {
+ val view = Dispatchers.IO.limitedParallelism(targetParallelism)
+ doStress {
+ launch(view) {
+ yield()
+ checkParallelism()
+ }
+ }
+ }
+
+ @Test
+ fun testLimitedExecutorReachesTargetParallelism() = runTest {
+ val view = executor.limitedParallelism(targetParallelism)
+ doStress {
+ repeat(iterations) {
+ val barrier = CyclicBarrier(targetParallelism + 1)
+ repeat(targetParallelism) {
+ launch(view) {
+ barrier.await()
+ }
+ }
+ // Successfully awaited parallelism + 1
+ barrier.await()
+ coroutineContext.job.children.toList().joinAll()
+ }
+ }
+ }
+
+ private suspend inline fun doStress(crossinline block: suspend CoroutineScope.() -> Unit) {
+ repeat(stressTestMultiplier) {
+ coroutineScope {
+ block()
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/LimitedParallelismUnhandledExceptionTest.kt b/kotlinx-coroutines-core/jvm/test/LimitedParallelismUnhandledExceptionTest.kt
new file mode 100644
index 00000000..8d48aa43
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/LimitedParallelismUnhandledExceptionTest.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import org.junit.Test
+import java.util.concurrent.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class LimitedParallelismUnhandledExceptionTest : TestBase() {
+
+ @Test
+ fun testUnhandledException() = runTest {
+ var caughtException: Throwable? = null
+ val executor = Executors.newFixedThreadPool(
+ 1
+ ) {
+ Thread(it).also {
+ it.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e -> caughtException = e }
+ }
+ }.asCoroutineDispatcher()
+ val view = executor.limitedParallelism(1)
+ view.dispatch(EmptyCoroutineContext, Runnable { throw TestException() })
+ withContext(view) {
+ // Verify it is in working state and establish happens-before
+ }
+ assertTrue { caughtException is TestException }
+ executor.close()
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt b/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt
new file mode 100644
index 00000000..057a8bbc
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+import org.junit.*
+
+class RunBlockingJvmTest : TestBase() {
+ @Test
+ fun testContract() {
+ val rb: Int
+ runBlocking {
+ rb = 42
+ }
+ rb.hashCode() // unused
+ }
+}
+
diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt
index 61a2c8b8..ce94d33a 100644
--- a/kotlinx-coroutines-core/jvm/test/TestBase.kt
+++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt
@@ -20,15 +20,18 @@ private val VERBOSE = systemProp("test.verbose", false)
*/
public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false
-public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1
+public actual val stressTestMultiplierSqrt = if (isStressTest) 5 else 1
private const val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread
+public actual val isNative = false
+
/**
* Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test.
*/
public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt
+
@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias TestResult = Unit
@@ -152,7 +155,8 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
})
fun println(message: Any?) {
- previousOut.println(message)
+ if (disableOutCheck) kotlin.io.println(message)
+ else previousOut.println(message)
}
@Before
@@ -199,15 +203,12 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
}
fun initPoolsBeforeTest() {
- CommonPool.usePrivatePool()
DefaultScheduler.usePrivateScheduler()
}
fun shutdownPoolsAfterTest() {
- CommonPool.shutdown(SHUTDOWN_TIMEOUT)
DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT)
- DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
- CommonPool.restore()
+ DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT)
DefaultScheduler.restore()
}
@@ -235,8 +236,9 @@ public actual open class TestBase(private var disableOutCheck: Boolean) {
if (expected != null) {
if (!expected(e))
error("Unexpected exception: $e", e)
- } else
+ } else {
throw e
+ }
} finally {
if (ex == null && expected != null) error("Exception was expected but none produced")
}
diff --git a/kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt b/kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt
new file mode 100644
index 00000000..799e559a
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/TestBaseExtension.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+public actual fun TestBase.runMtTest(
+ expected: ((Throwable) -> Boolean)?,
+ unhandled: List<(Throwable) -> Boolean>,
+ block: suspend CoroutineScope.() -> Unit
+): TestResult = runTest(expected, unhandled, block)
diff --git a/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt b/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt
index ea43c7ad..ec45406b 100644
--- a/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt
@@ -54,7 +54,6 @@ class ThreadContextElementTest : TestBase() {
assertNull(myThreadLocal.get())
}
-
@Test
fun testWithContext() = runTest {
expect(1)
@@ -86,6 +85,77 @@ class ThreadContextElementTest : TestBase() {
finish(7)
}
+
+ @Test
+ fun testNonCopyableElementReferenceInheritedOnLaunch() = runTest {
+ var parentElement: MyElement? = null
+ var inheritedElement: MyElement? = null
+
+ newSingleThreadContext("withContext").use {
+ withContext(it + MyElement(MyData())) {
+ parentElement = coroutineContext[MyElement.Key]
+ launch {
+ inheritedElement = coroutineContext[MyElement.Key]
+ }
+ }
+ }
+
+ assertSame(inheritedElement, parentElement,
+ "Inner and outer coroutines did not have the same object reference to a" +
+ " ThreadContextElement that did not override `copyForChildCoroutine()`")
+ }
+
+ @Test
+ fun testCopyableElementCopiedOnLaunch() = runTest {
+ var parentElement: CopyForChildCoroutineElement? = null
+ var inheritedElement: CopyForChildCoroutineElement? = null
+
+ newSingleThreadContext("withContext").use {
+ withContext(it + CopyForChildCoroutineElement(MyData())) {
+ parentElement = coroutineContext[CopyForChildCoroutineElement.Key]
+ launch {
+ inheritedElement = coroutineContext[CopyForChildCoroutineElement.Key]
+ }
+ }
+ }
+
+ assertNotSame(inheritedElement, parentElement,
+ "Inner coroutine did not copy its copyable ThreadContextElement.")
+ }
+
+ @Test
+ fun testCopyableThreadContextElementImplementsWriteVisibility() = runTest {
+ newFixedThreadPoolContext(nThreads = 4, name = "withContext").use {
+ withContext(it + CopyForChildCoroutineElement(MyData())) {
+ val forBlockData = MyData()
+ myThreadLocal.setForBlock(forBlockData) {
+ assertSame(myThreadLocal.get(), forBlockData)
+ launch {
+ assertSame(myThreadLocal.get(), forBlockData)
+ }
+ launch {
+ assertSame(myThreadLocal.get(), forBlockData)
+ // Modify value in child coroutine. Writes to the ThreadLocal and
+ // the (copied) ThreadLocalElement's memory are not visible to peer or
+ // ancestor coroutines, so this write is both threadsafe and coroutinesafe.
+ val innerCoroutineData = MyData()
+ myThreadLocal.setForBlock(innerCoroutineData) {
+ assertSame(myThreadLocal.get(), innerCoroutineData)
+ }
+ assertSame(myThreadLocal.get(), forBlockData) // Asserts value was restored.
+ }
+ launch {
+ val innerCoroutineData = MyData()
+ myThreadLocal.setForBlock(innerCoroutineData) {
+ assertSame(myThreadLocal.get(), innerCoroutineData)
+ }
+ assertSame(myThreadLocal.get(), forBlockData)
+ }
+ }
+ assertNull(myThreadLocal.get()) // Asserts value was restored to its origin
+ }
+ }
+ }
}
class MyData
@@ -114,3 +184,64 @@ class MyElement(val data: MyData) : ThreadContextElement<MyData?> {
myThreadLocal.set(oldState)
}
}
+
+/**
+ * A [ThreadContextElement] that implements copy semantics in [copyForChild].
+ */
+class CopyForChildCoroutineElement(val data: MyData?) : CopyableThreadContextElement<MyData?> {
+ companion object Key : CoroutineContext.Key<CopyForChildCoroutineElement>
+
+ override val key: CoroutineContext.Key<CopyForChildCoroutineElement>
+ get() = Key
+
+ override fun updateThreadContext(context: CoroutineContext): MyData? {
+ val oldState = myThreadLocal.get()
+ myThreadLocal.set(data)
+ return oldState
+ }
+
+ override fun mergeForChild(overwritingElement: CoroutineContext.Element): CopyForChildCoroutineElement {
+ TODO("Not used in tests")
+ }
+
+ override fun restoreThreadContext(context: CoroutineContext, oldState: MyData?) {
+ myThreadLocal.set(oldState)
+ }
+
+ /**
+ * At coroutine launch time, the _current value of the ThreadLocal_ is inherited by the new
+ * child coroutine, and that value is copied to a new, unique, ThreadContextElement memory
+ * reference for the child coroutine to use uniquely.
+ *
+ * n.b. the value copied to the child must be the __current value of the ThreadLocal__ and not
+ * the value initially passed to the ThreadContextElement in order to reflect writes made to the
+ * ThreadLocal between coroutine resumption and the child coroutine launch point. Those writes
+ * will be reflected in the parent coroutine's [CopyForChildCoroutineElement] when it yields the
+ * thread and calls [restoreThreadContext].
+ */
+ override fun copyForChild(): CopyForChildCoroutineElement {
+ return CopyForChildCoroutineElement(myThreadLocal.get())
+ }
+}
+
+/**
+ * Calls [block], setting the value of [this] [ThreadLocal] for the duration of [block].
+ *
+ * When a [CopyForChildCoroutineElement] for `this` [ThreadLocal] is used within a
+ * [CoroutineContext], a ThreadLocal set this way will have the "correct" value expected lexically
+ * at every statement reached, whether that statement is reached immediately, across suspend and
+ * redispatch within one coroutine, or within a child coroutine. Writes made to the `ThreadLocal`
+ * by child coroutines will not be visible to the parent coroutine. Writes made to the `ThreadLocal`
+ * by the parent coroutine _after_ launching a child coroutine will not be visible to that child
+ * coroutine.
+ */
+private inline fun <ThreadLocalT, OutputT> ThreadLocal<ThreadLocalT>.setForBlock(
+ value: ThreadLocalT,
+ crossinline block: () -> OutputT
+) {
+ val priorValue = get()
+ set(value)
+ block()
+ set(priorValue)
+}
+
diff --git a/kotlinx-coroutines-core/jvm/test/ThreadContextMutableCopiesTest.kt b/kotlinx-coroutines-core/jvm/test/ThreadContextMutableCopiesTest.kt
new file mode 100644
index 00000000..34e5955f
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/ThreadContextMutableCopiesTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-2022 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 ThreadContextMutableCopiesTest : TestBase() {
+ companion object {
+ val threadLocalData: ThreadLocal<MutableList<String>> = ThreadLocal.withInitial { ArrayList() }
+ }
+
+ class MyMutableElement(
+ val mutableData: MutableList<String>
+ ) : CopyableThreadContextElement<MutableList<String>> {
+
+ companion object Key : CoroutineContext.Key<MyMutableElement>
+
+ override val key: CoroutineContext.Key<*>
+ get() = Key
+
+ override fun updateThreadContext(context: CoroutineContext): MutableList<String> {
+ val st = threadLocalData.get()
+ threadLocalData.set(mutableData)
+ return st
+ }
+
+ override fun restoreThreadContext(context: CoroutineContext, oldState: MutableList<String>) {
+ threadLocalData.set(oldState)
+ }
+
+ override fun copyForChild(): MyMutableElement {
+ return MyMutableElement(ArrayList(mutableData))
+ }
+
+ override fun mergeForChild(overwritingElement: CoroutineContext.Element): MyMutableElement {
+ overwritingElement as MyMutableElement // <- app-specific, may be another subtype
+ return MyMutableElement((mutableData.toSet() + overwritingElement.mutableData).toMutableList())
+ }
+ }
+
+ @Test
+ fun testDataIsCopied() = runTest {
+ val root = MyMutableElement(ArrayList())
+ runBlocking(root) {
+ val data = threadLocalData.get()
+ expect(1)
+ launch(root) {
+ assertNotSame(data, threadLocalData.get())
+ assertEquals(data, threadLocalData.get())
+ finish(2)
+ }
+ }
+ }
+
+ @Test
+ fun testDataIsNotOverwritten() = runTest {
+ val root = MyMutableElement(ArrayList())
+ runBlocking(root) {
+ expect(1)
+ val originalData = threadLocalData.get()
+ threadLocalData.get().add("X")
+ launch {
+ threadLocalData.get().add("Y")
+ // Note here, +root overwrites the data
+ launch(Dispatchers.Default + root) {
+ assertEquals(listOf("X", "Y"), threadLocalData.get())
+ assertNotSame(originalData, threadLocalData.get())
+ finish(2)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDataIsMerged() = runTest {
+ val root = MyMutableElement(ArrayList())
+ runBlocking(root) {
+ expect(1)
+ val originalData = threadLocalData.get()
+ threadLocalData.get().add("X")
+ launch {
+ threadLocalData.get().add("Y")
+ // Note here, +root overwrites the data
+ launch(Dispatchers.Default + MyMutableElement(mutableListOf("Z"))) {
+ assertEquals(listOf("X", "Y", "Z"), threadLocalData.get())
+ assertNotSame(originalData, threadLocalData.get())
+ finish(2)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDataIsNotOverwrittenWithContext() = runTest {
+ val root = MyMutableElement(ArrayList())
+ runBlocking(root) {
+ val originalData = threadLocalData.get()
+ threadLocalData.get().add("X")
+ expect(1)
+ launch {
+ threadLocalData.get().add("Y")
+ // Note here, +root overwrites the data
+ withContext(Dispatchers.Default + root) {
+ assertEquals(listOf("X", "Y"), threadLocalData.get())
+ assertNotSame(originalData, threadLocalData.get())
+ finish(2)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDataIsCopiedForRunBlocking() = runTest {
+ val root = MyMutableElement(ArrayList())
+ val originalData = root.mutableData
+ runBlocking(root) {
+ assertNotSame(originalData, threadLocalData.get())
+ }
+ }
+
+ @Test
+ fun testDataIsCopiedForCoroutine() = runTest {
+ val root = MyMutableElement(ArrayList())
+ val originalData = root.mutableData
+ expect(1)
+ launch(root) {
+ assertNotSame(originalData, threadLocalData.get())
+ finish(2)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/ThreadLocalStressTest.kt b/kotlinx-coroutines-core/jvm/test/ThreadLocalStressTest.kt
new file mode 100644
index 00000000..20621d12
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/ThreadLocalStressTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.coroutines.sync.*
+import java.util.concurrent.*
+import kotlin.coroutines.*
+import kotlin.coroutines.intrinsics.*
+import kotlin.test.*
+
+
+class ThreadLocalStressTest : TestBase() {
+
+ private val threadLocal = ThreadLocal<String>()
+
+ // See the comment in doStress for the machinery
+ @Test
+ fun testStress() = runTest {
+ repeat (100 * stressTestMultiplierSqrt) {
+ withContext(Dispatchers.Default) {
+ repeat(100) {
+ launch {
+ doStress(null)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testStressWithOuterValue() = runTest {
+ repeat (100 * stressTestMultiplierSqrt) {
+ withContext(Dispatchers.Default + threadLocal.asContextElement("bar")) {
+ repeat(100) {
+ launch {
+ doStress("bar")
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun doStress(expectedValue: String?) {
+ assertEquals(expectedValue, threadLocal.get())
+ try {
+ /*
+ * Here we are using very specific code-path to trigger the execution we want to.
+ * The bug, in general, has a larger impact, but this particular code pinpoints it:
+ *
+ * 1) We use _undispatched_ withContext with thread element
+ * 2) We cancel the coroutine
+ * 3) We use 'suspendCancellableCoroutineReusable' that does _postponed_ cancellation check
+ * which makes the reproduction of this race pretty reliable.
+ *
+ * Now the following code path is likely to be triggered:
+ *
+ * T1 from within 'withContinuationContext' method:
+ * Finds 'oldValue', finds undispatched completion, invokes its 'block' argument.
+ * 'block' is this coroutine, it goes to 'trySuspend', checks for postponed cancellation and *dispatches* it.
+ * The execution stops _right_ before 'undispatchedCompletion.clearThreadContext()'.
+ *
+ * T2 now executes the dispatched cancellation and concurrently mutates the state of the undispatched completion.
+ * All bets are off, now both threads can leave the thread locals state inconsistent.
+ */
+ withContext(threadLocal.asContextElement("foo")) {
+ yield()
+ cancel()
+ suspendCancellableCoroutineReusable<Unit> { }
+ }
+ } finally {
+ assertEquals(expectedValue, threadLocal.get())
+ }
+ }
+
+ /*
+ * Another set of tests for undispatcheable continuations that do not require stress test multiplier.
+ * Also note that `uncaughtExceptionHandler` is used as the only available mechanism to propagate error from
+ * `resumeWith`
+ */
+
+ @Test
+ fun testNonDispatcheableLeak() {
+ repeat(100) {
+ doTestWithPreparation(
+ ::doTest,
+ { threadLocal.set(null) }) { threadLocal.get() == null }
+ assertNull(threadLocal.get())
+ }
+ }
+
+ @Test
+ fun testNonDispatcheableLeakWithInitial() {
+ repeat(100) {
+ doTestWithPreparation(::doTest, { threadLocal.set("initial") }) { threadLocal.get() == "initial" }
+ assertEquals("initial", threadLocal.get())
+ }
+ }
+
+ @Test
+ fun testNonDispatcheableLeakWithContextSwitch() {
+ repeat(100) {
+ doTestWithPreparation(
+ ::doTestWithContextSwitch,
+ { threadLocal.set(null) }) { threadLocal.get() == null }
+ assertNull(threadLocal.get())
+ }
+ }
+
+ @Test
+ fun testNonDispatcheableLeakWithInitialWithContextSwitch() {
+ repeat(100) {
+ doTestWithPreparation(
+ ::doTestWithContextSwitch,
+ { threadLocal.set("initial") }) { true /* can randomly wake up on the non-main thread */ }
+ // Here we are always on the main thread
+ assertEquals("initial", threadLocal.get())
+ }
+ }
+
+ private fun doTestWithPreparation(testBody: suspend () -> Unit, setup: () -> Unit, isValid: () -> Boolean) {
+ setup()
+ val latch = CountDownLatch(1)
+ testBody.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) {
+ if (!isValid()) {
+ Thread.currentThread().uncaughtExceptionHandler.uncaughtException(
+ Thread.currentThread(),
+ IllegalStateException("Unexpected error: thread local was not cleaned")
+ )
+ }
+ latch.countDown()
+ })
+ latch.await()
+ }
+
+ private suspend fun doTest() {
+ withContext(threadLocal.asContextElement("foo")) {
+ try {
+ coroutineScope {
+ val semaphore = Semaphore(1, 1)
+ cancel()
+ semaphore.acquire()
+ }
+ } catch (e: CancellationException) {
+ // Ignore cancellation
+ }
+ }
+ }
+
+ private suspend fun doTestWithContextSwitch() {
+ withContext(threadLocal.asContextElement("foo")) {
+ try {
+ coroutineScope {
+ val semaphore = Semaphore(1, 1)
+ GlobalScope.launch { }.join()
+ cancel()
+ semaphore.acquire()
+ }
+ } catch (e: CancellationException) {
+ // Ignore cancellation
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt b/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt
index bd9a185f..b4bc96eb 100644
--- a/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt
+++ b/kotlinx-coroutines-core/jvm/test/VirtualTimeSource.kt
@@ -11,14 +11,14 @@ import java.util.concurrent.locks.*
private const val SHUTDOWN_TIMEOUT = 1000L
internal inline fun withVirtualTimeSource(log: PrintStream? = null, block: () -> Unit) {
- DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) // shutdown execution with old time source (in case it was working)
+ DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) // shutdown execution with old time source (in case it was working)
val testTimeSource = VirtualTimeSource(log)
timeSource = testTimeSource
DefaultExecutor.ensureStarted() // should start with new time source
try {
block()
} finally {
- DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
+ DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT)
testTimeSource.shutdown()
timeSource = null // restore time source
}
diff --git a/kotlinx-coroutines-core/jvm/test/WithTimeoutOrNullJvmTest.kt b/kotlinx-coroutines-core/jvm/test/WithTimeoutOrNullJvmTest.kt
index 377fcf46..cce77bb6 100644
--- a/kotlinx-coroutines-core/jvm/test/WithTimeoutOrNullJvmTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/WithTimeoutOrNullJvmTest.kt
@@ -59,7 +59,7 @@ class WithTimeoutOrNullJvmTest : TestBase() {
@Test
fun testIgnoredTimeoutOnNullThrowsOnYield() = runTest {
val value = withTimeoutOrNull(1) {
- Thread.sleep(10)
+ Thread.sleep(75)
yield()
}
assertNull(value)
diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt
deleted file mode 100644
index 221120af..00000000
--- a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelSubStressTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.channels
-
-import kotlinx.coroutines.*
-import org.junit.*
-import org.junit.runner.*
-import org.junit.runners.*
-import java.util.concurrent.atomic.*
-
-/**
- * Creates a broadcast channel and repeatedly opens new subscription, receives event, closes it,
- * to stress test the logic of opening the subscription
- * to broadcast channel while events are being concurrently sent to it.
- */
-@RunWith(Parameterized::class)
-class BroadcastChannelSubStressTest(
- private val kind: TestBroadcastChannelKind
-) : TestBase() {
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun params(): Collection<Array<Any>> =
- TestBroadcastChannelKind.values().map { arrayOf<Any>(it) }
- }
-
- private val nSeconds = 5 * stressTestMultiplier
- private val broadcast = kind.create<Long>()
-
- private val sentTotal = AtomicLong()
- private val receivedTotal = AtomicLong()
-
- @Test
- fun testStress() = runBlocking {
- println("--- BroadcastChannelSubStressTest $kind")
- val sender =
- launch(context = Dispatchers.Default + CoroutineName("Sender")) {
- while (isActive) {
- broadcast.send(sentTotal.incrementAndGet())
- }
- }
- val receiver =
- launch(context = Dispatchers.Default + CoroutineName("Receiver")) {
- var last = -1L
- while (isActive) {
- val channel = broadcast.openSubscription()
- val i = channel.receive()
- check(i >= last) { "Last was $last, got $i" }
- if (!kind.isConflated) check(i != last) { "Last was $last, got it again" }
- receivedTotal.incrementAndGet()
- last = i
- channel.cancel()
- }
- }
- var prevSent = -1L
- repeat(nSeconds) { sec ->
- delay(1000)
- val curSent = sentTotal.get()
- println("${sec + 1}: Sent $curSent, received ${receivedTotal.get()}")
- check(curSent > prevSent) { "Send stalled at $curSent events" }
- prevSent = curSent
- }
- withTimeout(5000) {
- sender.cancelAndJoin()
- receiver.cancelAndJoin()
- }
- }
-} \ No newline at end of file
diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt
index a6345cc5..7e55f2e6 100644
--- a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt
@@ -25,7 +25,10 @@ class ChannelSendReceiveStressTest(
fun params(): Collection<Array<Any>> =
listOf(1, 2, 10).flatMap { nSenders ->
listOf(1, 10).flatMap { nReceivers ->
- TestChannelKind.values().map { arrayOf(it, nSenders, nReceivers) }
+ TestChannelKind.values()
+ // Workaround for bug that won't be fixed unless new channel implementation, see #2443
+ .filter { it != TestChannelKind.LINKED_LIST }
+ .map { arrayOf(it, nSenders, nReceivers) }
}
}
}
diff --git a/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt
deleted file mode 100644
index a0541754..00000000
--- a/kotlinx-coroutines-core/jvm/test/channels/RandevouzChannelStressTest.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.channels
-
-import kotlinx.coroutines.*
-import org.junit.*
-
-class RandevouzChannelStressTest : TestBase() {
-
- @Test
- fun testStress() = runTest {
- val n = 100_000 * stressTestMultiplier
- val q = Channel<Int>(Channel.RENDEZVOUS)
- val sender = launch {
- for (i in 1..n) q.send(i)
- expect(2)
- }
- val receiver = launch {
- for (i in 1..n) check(q.receive() == i)
- expect(3)
- }
- expect(1)
- sender.join()
- receiver.join()
- finish(4)
- }
-}
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt
index d2a5d536..1bc3791c 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt
@@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay01
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt
index f74422e6..ac48af9a 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt
@@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay02
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt
index edaea742..1fa9dc46 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt
@@ -7,6 +7,7 @@ package kotlinx.coroutines.examples.exampleDelay03
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt
index a19e6cb1..8b3d2d24 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt
@@ -1,4 +1,3 @@
-@file:OptIn(ExperimentalTime::class)
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@@ -9,6 +8,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration01
import kotlin.time.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt
index 10ba88a5..6500ecd3 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt
@@ -1,4 +1,3 @@
-@file:OptIn(ExperimentalTime::class)
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@@ -9,6 +8,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration02
import kotlin.time.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt
index 5fa980a6..4d5e40d7 100644
--- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt
+++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt
@@ -1,4 +1,3 @@
-@file:OptIn(ExperimentalTime::class)
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@@ -9,6 +8,7 @@ package kotlinx.coroutines.examples.exampleDelayDuration03
import kotlin.time.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlin.time.Duration.Companion.milliseconds
fun main() = runBlocking {
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
index cea9713f..2095f148 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
@@ -39,4 +39,16 @@ class CoroutineExceptionHandlerJvmTest : TestBase() {
finish(3)
}
+
+ @Test
+ fun testLastDitchHandlerContainsContextualInformation() = runBlocking {
+ expect(1)
+ GlobalScope.launch(CoroutineName("last-ditch")) {
+ expect(2)
+ throw TestException()
+ }.join()
+ assertTrue(caughtException is TestException)
+ assertContains(caughtException.suppressed[0].toString(), "last-ditch")
+ finish(3)
+ }
}
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt
index 13023e31..4849f520 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt
@@ -15,7 +15,7 @@ import kotlin.test.*
* but run only under JDK 1.8
*/
@Suppress("ConflictingExtensionProperty")
-val Throwable.suppressed: Array<Throwable> get() {
+actual val Throwable.suppressed: Array<Throwable> get() {
val method = this::class.java.getMethod("getSuppressed") ?: error("This test can only be run using JDK 1.7")
@Suppress("UNCHECKED_CAST")
return method.invoke(this) as Array<Throwable>
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt
new file mode 100644
index 00000000..41fe0903
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/FlowSuppressionTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import org.junit.*
+import org.junit.Test
+import kotlin.test.*
+
+class FlowSuppressionTest : TestBase() {
+ @Test
+ fun testSuppressionForPrimaryException() = runTest {
+ val flow = flow {
+ try {
+ emit(1)
+ } finally {
+ throw TestException()
+ }
+ }.catch { expectUnreached() }.onEach { throw TestException2() }
+
+ try {
+ flow.collect()
+ } catch (e: Throwable) {
+ assertIs<TestException>(e)
+ assertIs<TestException2>(e.suppressed[0])
+ }
+ }
+
+ @Test
+ fun testSuppressionForPrimaryExceptionRetry() = runTest {
+ val flow = flow {
+ try {
+ emit(1)
+ } finally {
+ throw TestException()
+ }
+ }.retry { expectUnreached(); true }.onEach { throw TestException2() }
+
+ try {
+ flow.collect()
+ } catch (e: Throwable) {
+ assertIs<TestException>(e)
+ assertIs<TestException2>(e.suppressed[0])
+
+ }
+ }
+
+ @Test
+ fun testCancellationSuppression() = runTest {
+ val flow = flow {
+ try {
+ expect(1)
+ emit(1)
+ } finally {
+ expect(3)
+ throw CancellationException("")
+ }
+ }.catch { expectUnreached() }.onEach {
+ expect(2)
+ throw TestException("")
+ }
+
+ try {
+ flow.collect()
+ } catch (e: Throwable) {
+ assertIs<TestException>(e)
+ assertIs<CancellationException>(e.suppressed[0])
+ }
+ finish(4)
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt
index dba738a8..d4e19040 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt
@@ -124,4 +124,22 @@ class StackTraceRecoveryCustomExceptionsTest : TestBase() {
assertTrue(ex is CopyableWithCustomMessage)
assertEquals("Recovered: [OK]", ex.message)
}
+
+ @Test
+ fun testTryCopyThrows() = runTest {
+ class FailingException : Exception(), CopyableThrowable<FailingException> {
+ override fun createCopy(): FailingException? {
+ TODO("Not yet implemented")
+ }
+ }
+
+ val e = FailingException()
+ val result = runCatching {
+ coroutineScope<Unit> {
+ throw e
+ }
+ }
+
+ assertSame(e, result.exceptionOrNull())
+ }
}
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt
index 29954663..f148a3ae 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/WithContextCancellationStressTest.kt
@@ -10,10 +10,11 @@ import org.junit.Test
import java.util.concurrent.*
import kotlin.coroutines.*
import kotlin.test.*
+import kotlin.time.Duration.Companion.minutes
class WithContextCancellationStressTest : TestBase() {
- private val iterations = 15_000 * stressTestMultiplier
+ private val timeoutAfter = 1.minutes
private val pool = newFixedThreadPoolContext(3, "WithContextCancellationStressTest")
@After
@@ -28,56 +29,54 @@ class WithContextCancellationStressTest : TestBase() {
var e1Cnt = 0
var e2Cnt = 0
- repeat(iterations) {
- val barrier = CyclicBarrier(4)
- val ctx = pool + NonCancellable
- var e1 = false
- var e2 = false
- val jobWithContext = async(ctx) {
- withContext(wrapperDispatcher(coroutineContext)) {
- launch {
- barrier.await()
- e1 = true
- throw TestException1()
- }
+ withTimeout(timeoutAfter) {
+ while (eCnt == 0 || e1Cnt == 0 || e2Cnt == 0) {
+ val barrier = CyclicBarrier(4)
+ val ctx = pool + NonCancellable
+ var e1 = false
+ var e2 = false
+ val jobWithContext = async(ctx) {
+ withContext(wrapperDispatcher(coroutineContext)) {
+ launch {
+ barrier.await()
+ e1 = true
+ throw TestException1()
+ }
+
+ launch {
+ barrier.await()
+ e2 = true
+ throw TestException2()
+ }
- launch {
barrier.await()
- e2 = true
- throw TestException2()
+ throw TestException()
}
-
- barrier.await()
- throw TestException()
}
- }
- barrier.await()
+ barrier.await()
- try {
- jobWithContext.await()
- } catch (e: Throwable) {
- when (e) {
- is TestException -> {
- eCnt++
- e.checkSuppressed(e1 = e1, e2 = e2)
- }
- is TestException1 -> {
- e1Cnt++
- e.checkSuppressed(ex = true, e2 = e2)
+ try {
+ jobWithContext.await()
+ } catch (e: Throwable) {
+ when (e) {
+ is TestException -> {
+ eCnt++
+ e.checkSuppressed(e1 = e1, e2 = e2)
+ }
+ is TestException1 -> {
+ e1Cnt++
+ e.checkSuppressed(ex = true, e2 = e2)
+ }
+ is TestException2 -> {
+ e2Cnt++
+ e.checkSuppressed(ex = true, e1 = e1)
+ }
+ else -> error("Unexpected exception $e")
}
- is TestException2 -> {
- e2Cnt++
- e.checkSuppressed(ex = true, e1 = e1)
- }
- else -> error("Unexpected exception $e")
}
}
}
-
- require(eCnt > 0) { "At least one TestException expected" }
- require(e1Cnt > 0) { "At least one TestException1 expected" }
- require(e2Cnt > 0) { "At least one TestException2 expected" }
}
private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
diff --git a/kotlinx-coroutines-core/jvm/test/flow/SafeCollectorMemoryLeakTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SafeCollectorMemoryLeakTest.kt
new file mode 100644
index 00000000..b75ec60e
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/flow/SafeCollectorMemoryLeakTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.flow
+
+import kotlinx.coroutines.*
+import org.junit.*
+
+class SafeCollectorMemoryLeakTest : TestBase() {
+ // custom List.forEach impl to avoid using iterator (FieldWalker cannot scan it)
+ private inline fun <T> List<T>.listForEach(action: (T) -> Unit) {
+ for (i in indices) action(get(i))
+ }
+
+ @Test
+ fun testCompletionIsProperlyCleanedUp() = runBlocking {
+ val job = flow {
+ emit(listOf(239))
+ expect(2)
+ hang {}
+ }.transform { l -> l.listForEach { _ -> emit(42) } }
+ .onEach { expect(1) }
+ .launchIn(this)
+ yield()
+ expect(3)
+ FieldWalker.assertReachableCount(0, job) { it == 239 }
+ job.cancelAndJoin()
+ finish(4)
+ }
+
+ @Test
+ fun testCompletionIsNotCleanedUp() = runBlocking {
+ val job = flow {
+ emit(listOf(239))
+ hang {}
+ }.transform { l -> l.listForEach { _ -> emit(42) } }
+ .onEach {
+ expect(1)
+ hang { finish(3) }
+ }
+ .launchIn(this)
+ yield()
+ expect(2)
+ FieldWalker.assertReachableCount(1, job) { it == 239 }
+ job.cancelAndJoin()
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt
index 349b7c81..58e3ef40 100644
--- a/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt
@@ -10,9 +10,8 @@ import org.junit.*
import org.junit.Test
import kotlin.collections.ArrayList
import kotlin.test.*
-import kotlin.time.*
+import kotlin.time.Duration.Companion.seconds
-@ExperimentalTime
class SharedFlowStressTest : TestBase() {
private val nProducers = 5
private val nConsumers = 3
@@ -84,4 +83,4 @@ class SharedFlowStressTest : TestBase() {
jobs.forEach { it.join() }
println("total: produced = ${totalProduced.value}; consumed = ${totalConsumed.value}")
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt
index 7d346bdc..25c0c983 100644
--- a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt
@@ -189,5 +189,9 @@ class SharingStressTest : TestBase() {
var count = 0L
}
- private fun log(msg: String) = println("${testStarted.elapsedNow().toLongMilliseconds()} ms: $msg")
-} \ No newline at end of file
+ private fun log(msg: String) = println("${testStarted.elapsedNow().inWholeMilliseconds} ms: $msg")
+
+ private fun MutableStateFlow<Int>.increment(delta: Int) {
+ update { it + delta }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt
index 3daaf49b..2cf0770a 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt
@@ -8,15 +8,15 @@ package kotlinx.coroutines.guide.exampleCancel03
import kotlinx.coroutines.*
fun main() = runBlocking {
- val startTime = currentTimeMillis()
val job = launch(Dispatchers.Default) {
- var nextPrintTime = startTime
- var i = 0
- while (isActive) { // cancellable computation loop
- // print a message twice a second
- if (currentTimeMillis() >= nextPrintTime) {
- println("job: I'm sleeping ${i++} ...")
- nextPrintTime += 500L
+ repeat(5) { i ->
+ try {
+ // print a message twice a second
+ println("job: I'm sleeping $i ...")
+ delay(500)
+ } catch (e: Exception) {
+ // log the exception
+ println(e)
}
}
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt
index b1b9a9bf..7d7f3bde 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt
@@ -8,14 +8,16 @@ package kotlinx.coroutines.guide.exampleCancel04
import kotlinx.coroutines.*
fun main() = runBlocking {
- val job = launch {
- try {
- repeat(1000) { i ->
- println("job: I'm sleeping $i ...")
- delay(500L)
+ val startTime = currentTimeMillis()
+ val job = launch(Dispatchers.Default) {
+ var nextPrintTime = startTime
+ var i = 0
+ while (isActive) { // cancellable computation loop
+ // print a message twice a second
+ if (currentTimeMillis() >= nextPrintTime) {
+ println("job: I'm sleeping ${i++} ...")
+ nextPrintTime += 500L
}
- } finally {
- println("job: I'm running finally")
}
}
delay(1300L) // delay a bit
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt
index 9772ae57..97ea956e 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt
@@ -15,11 +15,7 @@ fun main() = runBlocking {
delay(500L)
}
} finally {
- withContext(NonCancellable) {
- println("job: I'm running finally")
- delay(1000L)
- println("job: And I've just delayed for 1 sec because I'm non-cancellable")
- }
+ println("job: I'm running finally")
}
}
delay(1300L) // delay a bit
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt
index e1afd057..ef66f5df 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt
@@ -8,10 +8,22 @@ package kotlinx.coroutines.guide.exampleCancel06
import kotlinx.coroutines.*
fun main() = runBlocking {
- withTimeout(1300L) {
- repeat(1000) { i ->
- println("I'm sleeping $i ...")
- delay(500L)
+ val job = launch {
+ try {
+ repeat(1000) { i ->
+ println("job: I'm sleeping $i ...")
+ delay(500L)
+ }
+ } finally {
+ withContext(NonCancellable) {
+ println("job: I'm running finally")
+ delay(1000L)
+ println("job: And I've just delayed for 1 sec because I'm non-cancellable")
+ }
}
}
+ delay(1300L) // delay a bit
+ println("main: I'm tired of waiting!")
+ job.cancelAndJoin() // cancels the job and waits for its completion
+ println("main: Now I can quit.")
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt
index 8c57b429..ea4a1035 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt
@@ -8,12 +8,10 @@ package kotlinx.coroutines.guide.exampleCancel07
import kotlinx.coroutines.*
fun main() = runBlocking {
- val result = withTimeoutOrNull(1300L) {
+ withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
- "Done" // will get cancelled before it produces this result
}
- println("Result is $result")
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt
index e7def132..79d08091 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt
@@ -7,25 +7,13 @@ package kotlinx.coroutines.guide.exampleCancel08
import kotlinx.coroutines.*
-var acquired = 0
-
-class Resource {
- init { acquired++ } // Acquire the resource
- fun close() { acquired-- } // Release the resource
-}
-
-fun main() {
- runBlocking {
- repeat(100_000) { // Launch 100K coroutines
- launch {
- val resource = withTimeout(60) { // Timeout of 60 ms
- delay(50) // Delay for 50 ms
- Resource() // Acquire a resource and return it from withTimeout block
- }
- resource.close() // Release the resource
- }
+fun main() = runBlocking {
+ val result = withTimeoutOrNull(1300L) {
+ repeat(1000) { i ->
+ println("I'm sleeping $i ...")
+ delay(500L)
}
+ "Done" // will get cancelled before it produces this result
}
- // Outside of runBlocking all coroutines have completed
- println(acquired) // Print the number of resources still acquired
+ println("Result is $result")
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt
index 95424f51..13910a6c 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt
@@ -18,16 +18,11 @@ fun main() {
runBlocking {
repeat(100_000) { // Launch 100K coroutines
launch {
- var resource: Resource? = null // Not acquired yet
- try {
- withTimeout(60) { // Timeout of 60 ms
- delay(50) // Delay for 50 ms
- resource = Resource() // Store a resource to the variable if acquired
- }
- // We can do something else with the resource here
- } finally {
- resource?.close() // Release the resource if it was acquired
+ val resource = withTimeout(60) { // Timeout of 60 ms
+ delay(50) // Delay for 50 ms
+ Resource() // Acquire a resource and return it from withTimeout block
}
+ resource.close() // Release the resource
}
}
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt
new file mode 100644
index 00000000..336f5e3a
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit.
+package kotlinx.coroutines.guide.exampleCancel10
+
+import kotlinx.coroutines.*
+
+var acquired = 0
+
+class Resource {
+ init { acquired++ } // Acquire the resource
+ fun close() { acquired-- } // Release the resource
+}
+
+fun main() {
+ runBlocking {
+ repeat(100_000) { // Launch 100K coroutines
+ launch {
+ var resource: Resource? = null // Not acquired yet
+ try {
+ withTimeout(60) { // Timeout of 60 ms
+ delay(50) // Delay for 50 ms
+ resource = Resource() // Store a resource to the variable if acquired
+ }
+ // We can do something else with the resource here
+ } finally {
+ resource?.close() // Release the resource if it was acquired
+ }
+ }
+ }
+ }
+ // Outside of runBlocking all coroutines have completed
+ println(acquired) // Print the number of resources still acquired
+}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-context-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-context-06.kt
index c6ad4516..67965322 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/example-context-06.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/example-context-06.kt
@@ -26,6 +26,6 @@ fun main() = runBlocking<Unit> {
}
delay(500)
request.cancel() // cancel processing of the request
- delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")
+ delay(1000) // delay the main thread for a second to see what happens
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt
index 0cff63a8..871e4911 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt
@@ -34,8 +34,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel03() {
- test("ExampleCancel03") { kotlinx.coroutines.guide.exampleCancel03.main() }.verifyLines(
+ fun testExampleCancel04() {
+ test("ExampleCancel04") { kotlinx.coroutines.guide.exampleCancel04.main() }.verifyLines(
"job: I'm sleeping 0 ...",
"job: I'm sleeping 1 ...",
"job: I'm sleeping 2 ...",
@@ -45,8 +45,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel04() {
- test("ExampleCancel04") { kotlinx.coroutines.guide.exampleCancel04.main() }.verifyLines(
+ fun testExampleCancel05() {
+ test("ExampleCancel05") { kotlinx.coroutines.guide.exampleCancel05.main() }.verifyLines(
"job: I'm sleeping 0 ...",
"job: I'm sleeping 1 ...",
"job: I'm sleeping 2 ...",
@@ -57,8 +57,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel05() {
- test("ExampleCancel05") { kotlinx.coroutines.guide.exampleCancel05.main() }.verifyLines(
+ fun testExampleCancel06() {
+ test("ExampleCancel06") { kotlinx.coroutines.guide.exampleCancel06.main() }.verifyLines(
"job: I'm sleeping 0 ...",
"job: I'm sleeping 1 ...",
"job: I'm sleeping 2 ...",
@@ -70,8 +70,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel06() {
- test("ExampleCancel06") { kotlinx.coroutines.guide.exampleCancel06.main() }.verifyLinesStartWith(
+ fun testExampleCancel07() {
+ test("ExampleCancel07") { kotlinx.coroutines.guide.exampleCancel07.main() }.verifyLinesStartWith(
"I'm sleeping 0 ...",
"I'm sleeping 1 ...",
"I'm sleeping 2 ...",
@@ -80,8 +80,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel07() {
- test("ExampleCancel07") { kotlinx.coroutines.guide.exampleCancel07.main() }.verifyLines(
+ fun testExampleCancel08() {
+ test("ExampleCancel08") { kotlinx.coroutines.guide.exampleCancel08.main() }.verifyLines(
"I'm sleeping 0 ...",
"I'm sleeping 1 ...",
"I'm sleeping 2 ...",
@@ -90,8 +90,8 @@ class CancellationGuideTest {
}
@Test
- fun testExampleCancel09() {
- test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines(
+ fun testExampleCancel10() {
+ test("ExampleCancel10") { kotlinx.coroutines.guide.exampleCancel10.main() }.verifyLines(
"0"
)
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt
index 1a84fb94..e18741e9 100644
--- a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt
@@ -59,8 +59,8 @@ class DispatcherGuideTest {
test("ExampleContext06") { kotlinx.coroutines.guide.exampleContext06.main() }.verifyLines(
"job1: I run in my own Job and execute independently!",
"job2: I am a child of the request coroutine",
- "job1: I am not affected by cancellation of the request",
- "main: Who has survived request cancellation?"
+ "main: Who has survived request cancellation?",
+ "job1: I am not affected by cancellation of the request"
)
}
diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/GuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/GuideTest.kt
deleted file mode 100644
index e69de29b..00000000
--- a/kotlinx-coroutines-core/jvm/test/guide/test/GuideTest.kt
+++ /dev/null
diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt
index 49e6cccd..63b58384 100644
--- a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt
@@ -34,7 +34,7 @@ class ConcurrentWeakMapOperationStressTest : TestBase() {
var generationOffset = 0L
while (!stop.value) {
val kvs = (generationOffset + batchSize * index until generationOffset + batchSize * (index + 1))
- .associateBy({ Key(it) }, { it * it })
+ .associateBy({ Key(it) }, { it * it })
generationOffset += batchSize * nThreads
for ((k, v) in kvs) {
assertEquals(null, m.put(k, v))
@@ -45,8 +45,8 @@ class ConcurrentWeakMapOperationStressTest : TestBase() {
for ((k, v) in kvs) {
assertEquals(v, m.remove(k))
}
- for ((k, v) in kvs) {
- assertEquals(null, m.get(k))
+ for ((k, _) in kvs) {
+ assertEquals(null, m[k])
}
count.incrementAndGet()
}
@@ -68,6 +68,6 @@ class ConcurrentWeakMapOperationStressTest : TestBase() {
}
stop.value = true
threads.forEach { it.join() }
- assertEquals(0, m.size)
+ assertEquals(0, m.size, "Unexpected map state: $m")
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListLongStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListLongStressTest.kt
index dde4b2f6..a70a32b5 100644
--- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListLongStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListLongStressTest.kt
@@ -9,7 +9,6 @@ import org.junit.Test
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
-import kotlin.sequences.buildIterator
/**
* This stress test has 2 threads adding on one side on list, 2 more threads adding on the other,
diff --git a/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapStressTest.kt
new file mode 100644
index 00000000..5bc0952e
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapStressTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlinx.coroutines.*
+import java.util.concurrent.*
+import java.util.concurrent.CancellationException
+import kotlin.test.*
+
+class ThreadSafeHeapStressTest : TestBase() {
+ private class DisposableNode : EventLoopImplBase.DelayedTask(1L) {
+ override fun run() {
+ }
+ }
+
+ @Test
+ fun testConcurrentRemoveDispose() = runTest {
+ val heap = EventLoopImplBase.DelayedTaskQueue(1)
+ repeat(10_000 * stressTestMultiplierSqrt) {
+ withContext(Dispatchers.Default) {
+ val node = DisposableNode()
+ val barrier = CyclicBarrier(2)
+ launch {
+ heap.addLast(node)
+ barrier.await()
+ heap.remove(node)
+ }
+ launch {
+ barrier.await()
+ Thread.yield()
+ node.dispose()
+ }
+ }
+ }
+ }
+
+ @Test()
+ fun testConcurrentAddDispose() = runTest {
+ repeat(10_000 * stressTestMultiplierSqrt) {
+ val jobToCancel = Job()
+ val barrier = CyclicBarrier(2)
+ val jobToJoin = launch(Dispatchers.Default) {
+ barrier.await()
+ jobToCancel.cancelAndJoin()
+ }
+
+ try {
+ runBlocking { // Use event loop impl
+ withContext(jobToCancel) {
+ // This one is to work around heap allocation optimization
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ delay(100_000)
+ }
+ barrier.await()
+ delay(100_000)
+ }
+ }
+ } catch (e: CancellationException) {
+ // Expected exception
+ }
+ jobToJoin.join()
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapTest.kt
index be7ed91a..ee0960c8 100644
--- a/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/internal/ThreadSafeHeapTest.kt
@@ -93,4 +93,4 @@ class ThreadSafeHeapTest : TestBase() {
assertEquals(set.size, h.size)
}
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt
index 2e61ec6b..80c98030 100644
--- a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt
+++ b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt
@@ -26,9 +26,8 @@ private val OUT_ENABLED = systemProp("guide.tests.sout", false)
fun <R> test(name: String, block: () -> R): List<String> = outputException(name) {
try {
captureOutput(name, stdoutEnabled = OUT_ENABLED) { log ->
- CommonPool.usePrivatePool()
DefaultScheduler.usePrivateScheduler()
- DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
+ DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT)
resetCoroutineId()
val threadsBefore = currentThreads()
try {
@@ -39,15 +38,13 @@ fun <R> test(name: String, block: () -> R): List<String> = outputException(name)
} finally {
// the shutdown
log.println("--- shutting down")
- CommonPool.shutdown(SHUTDOWN_TIMEOUT)
DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT)
shutdownDispatcherPools(SHUTDOWN_TIMEOUT)
- DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT) // the last man standing -- cleanup all pending tasks
+ DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT) // the last man standing -- cleanup all pending tasks
}
checkTestThreads(threadsBefore) // check thread if the main completed successfully
}
} finally {
- CommonPool.restore()
DefaultScheduler.restore()
}
}
diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt
new file mode 100644
index 00000000..1948a78e
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.lincheck
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import org.jetbrains.kotlinx.lincheck.annotations.*
+import org.jetbrains.kotlinx.lincheck.paramgen.*
+
+@Param(name = "index", gen = IntGen::class, conf = "0:4")
+@Param(name = "value", gen = IntGen::class, conf = "1:5")
+@OpGroupConfig(name = "sync", nonParallel = true)
+class ResizableAtomicArrayLincheckTest : AbstractLincheckTest() {
+ private val a = ResizableAtomicArray<Int>(2)
+
+ @Operation
+ fun get(@Param(name = "index") index: Int): Int? = a[index]
+
+ @Operation(group = "sync")
+ fun set(@Param(name = "index") index: Int, @Param(name = "value") value: Int) {
+ a.setSynchronized(index, value)
+ }
+
+ override fun extractState() = (0..4).map { a[it] }
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt
index 9c17e698..864ecdc0 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt
@@ -10,7 +10,7 @@ import java.util.*
import java.util.concurrent.*
class BlockingCoroutineDispatcherTerminationStressTest : TestBase() {
- private val baseDispatcher = ExperimentalCoroutineDispatcher(
+ private val baseDispatcher = SchedulerCoroutineDispatcher(
2, 20,
TimeUnit.MILLISECONDS.toNanos(10)
)
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt
index fe09440f..f8830fee 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt
@@ -125,71 +125,6 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() {
checkPoolThreadsCreated(101..100 + CORES_COUNT)
}
- @Test
- fun testBlockingFairness() = runBlocking {
- corePoolSize = 1
- maxPoolSize = 1
-
- val blocking = blockingDispatcher(1)
- val task = async(dispatcher) {
- expect(1)
-
- val nonBlocking = async(dispatcher) {
- expect(3)
- }
-
- val firstBlocking = async(blocking) {
- expect(2)
- }
-
- val secondBlocking = async(blocking) {
- // Already have 1 queued blocking task, so this one wouldn't be scheduled to head
- expect(4)
- }
-
- listOf(firstBlocking, nonBlocking, secondBlocking).joinAll()
- finish(5)
- }
-
- task.await()
- }
-
- @Test
- fun testBoundedBlockingFairness() = runBlocking {
- corePoolSize = 1
- maxPoolSize = 1
-
- val blocking = blockingDispatcher(2)
- val task = async(dispatcher) {
- expect(1)
-
- val nonBlocking = async(dispatcher) {
- expect(3)
- }
-
- val firstBlocking = async(blocking) {
- expect(4)
- }
-
- val secondNonBlocking = async(dispatcher) {
- expect(5)
- }
-
- val secondBlocking = async(blocking) {
- expect(2) // <- last submitted blocking is executed first
- }
-
- val thirdBlocking = async(blocking) {
- expect(6) // parallelism level is reached before this task
- }
-
- listOf(firstBlocking, nonBlocking, secondBlocking, secondNonBlocking, thirdBlocking).joinAll()
- finish(7)
- }
-
- task.await()
- }
-
@Test(timeout = 1_000)
fun testYield() = runBlocking {
corePoolSize = 1
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt
index 3280527f..3b3e0850 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt
@@ -18,7 +18,7 @@ class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase()
val iterations = 1000 * stressTestMultiplier
repeat(iterations) {
// Create a dispatcher every iteration to increase probability of race
- val dispatcher = ExperimentalCoroutineDispatcher(CORES_COUNT)
+ val dispatcher = SchedulerCoroutineDispatcher(CORES_COUNT)
val blockingDispatcher = dispatcher.blocking(100)
val blockingBarrier = CyclicBarrier(CORES_COUNT * 3 + 1)
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt
index 3cd77da7..c95415a8 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt
@@ -134,7 +134,7 @@ class CoroutineDispatcherTest : SchedulerTestBase() {
val initialCount = Thread.getAllStackTraces().keys.asSequence()
.count { it is CoroutineScheduler.Worker && it.name.contains("SomeTestName") }
assertEquals(0, initialCount)
- val dispatcher = ExperimentalCoroutineDispatcher(1, 1, IDLE_WORKER_KEEP_ALIVE_NS, "SomeTestName")
+ val dispatcher = SchedulerCoroutineDispatcher(1, 1, IDLE_WORKER_KEEP_ALIVE_NS, "SomeTestName")
dispatcher.use {
launch(dispatcher) {
}.join()
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt
index 473b4292..a50867d6 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerCloseStressTest.kt
@@ -22,15 +22,13 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
fun params(): Collection<Array<Any>> = Mode.values().map { arrayOf<Any>(it) }
}
- private val N_REPEAT = 2 * stressTestMultiplier
private val MAX_LEVEL = 5
private val N_COROS = (1 shl (MAX_LEVEL + 1)) - 1
private val N_THREADS = 4
private val rnd = Random()
- private lateinit var closeableDispatcher: ExperimentalCoroutineDispatcher
- private lateinit var dispatcher: ExecutorCoroutineDispatcher
- private var closeIndex = -1
+ private lateinit var closeableDispatcher: SchedulerCoroutineDispatcher
+ private lateinit var dispatcher: CoroutineDispatcher
private val started = atomic(0)
private val finished = atomic(0)
@@ -44,20 +42,12 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
}
}
- @Test
- fun testRacingClose() {
- repeat(N_REPEAT) {
- closeIndex = rnd.nextInt(N_COROS)
- launchCoroutines()
- }
- }
-
private fun launchCoroutines() = runBlocking {
- closeableDispatcher = ExperimentalCoroutineDispatcher(N_THREADS)
+ closeableDispatcher = SchedulerCoroutineDispatcher(N_THREADS)
dispatcher = when (mode) {
Mode.CPU -> closeableDispatcher
- Mode.CPU_LIMITED -> closeableDispatcher.limited(N_THREADS) as ExecutorCoroutineDispatcher
- Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS) as ExecutorCoroutineDispatcher
+ Mode.CPU_LIMITED -> closeableDispatcher.limitedParallelism(N_THREADS)
+ Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS)
}
started.value = 0
finished.value = 0
@@ -68,20 +58,16 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
assertEquals(N_COROS, finished.value)
}
+ // Index and level are used only for debugging purpose
private fun CoroutineScope.launchChild(index: Int, level: Int): Job = launch(start = CoroutineStart.ATOMIC) {
started.incrementAndGet()
try {
- if (index == closeIndex) closeableDispatcher.close()
if (level < MAX_LEVEL) {
launchChild(2 * index + 1, level + 1)
launchChild(2 * index + 2, level + 1)
} else {
if (rnd.nextBoolean()) {
delay(1000)
- val t = Thread.currentThread()
- if (!t.name.contains("DefaultDispatcher-worker")) {
- val a = 2
- }
} else {
yield()
}
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt
index cb49f054..7aefd4f7 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt
@@ -15,7 +15,7 @@ import kotlin.coroutines.*
import kotlin.test.*
class CoroutineSchedulerStressTest : TestBase() {
- private var dispatcher: ExperimentalCoroutineDispatcher = ExperimentalCoroutineDispatcher()
+ private var dispatcher: SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher()
private val observedThreads = ConcurrentHashMap<Thread, Long>()
private val tasksNum = 500_000 * stressMemoryMultiplier()
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
index b0a5954b..9d41c05d 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
@@ -106,22 +106,22 @@ class CoroutineSchedulerTest : TestBase() {
@Test(expected = IllegalArgumentException::class)
fun testNegativeCorePoolSize() {
- ExperimentalCoroutineDispatcher(-1, 4)
+ SchedulerCoroutineDispatcher(-1, 4)
}
@Test(expected = IllegalArgumentException::class)
fun testNegativeMaxPoolSize() {
- ExperimentalCoroutineDispatcher(1, -4)
+ SchedulerCoroutineDispatcher(1, -4)
}
@Test(expected = IllegalArgumentException::class)
fun testCorePoolSizeGreaterThanMaxPoolSize() {
- ExperimentalCoroutineDispatcher(4, 1)
+ SchedulerCoroutineDispatcher(4, 1)
}
@Test
fun testSelfClose() {
- val dispatcher = ExperimentalCoroutineDispatcher(1, 1)
+ val dispatcher = SchedulerCoroutineDispatcher(1, 1)
val latch = CountDownLatch(1)
dispatcher.dispatch(EmptyCoroutineContext, Runnable {
dispatcher.close(); latch.countDown()
@@ -131,7 +131,7 @@ class CoroutineSchedulerTest : TestBase() {
@Test
fun testInterruptionCleanup() {
- ExperimentalCoroutineDispatcher(1, 1).use {
+ SchedulerCoroutineDispatcher(1, 1).use {
val executor = it.executor
var latch = CountDownLatch(1)
executor.execute {
@@ -171,4 +171,4 @@ class CoroutineSchedulerTest : TestBase() {
private class TaskContextImpl(override val taskMode: Int) : TaskContext {
override fun afterTask() {}
}
-} \ No newline at end of file
+}
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt
new file mode 100644
index 00000000..56c66954
--- /dev/null
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/DefaultDispatchersTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.scheduling
+
+import kotlinx.coroutines.*
+import org.junit.Test
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import kotlin.test.*
+
+class DefaultDispatchersTest : TestBase() {
+
+ private /*const*/ val EXPECTED_PARALLELISM = 64
+
+ @Test(timeout = 10_000L)
+ fun testLimitedParallelismIsSeparatedFromDefaultIo() = runTest {
+ val barrier = CyclicBarrier(EXPECTED_PARALLELISM + 1)
+ val ioBlocker = CountDownLatch(1)
+ repeat(EXPECTED_PARALLELISM) {
+ launch(Dispatchers.IO) {
+ barrier.await()
+ ioBlocker.await()
+ }
+ }
+
+ barrier.await() // Ensure all threads are occupied
+ barrier.reset()
+ val limited = Dispatchers.IO.limitedParallelism(EXPECTED_PARALLELISM)
+ repeat(EXPECTED_PARALLELISM) {
+ launch(limited) {
+ barrier.await()
+ }
+ }
+ barrier.await()
+ ioBlocker.countDown()
+ }
+
+ @Test(timeout = 10_000L)
+ fun testDefaultDispatcherIsSeparateFromIO() = runTest {
+ val ioBarrier = CyclicBarrier(EXPECTED_PARALLELISM + 1)
+ val ioBlocker = CountDownLatch(1)
+ repeat(EXPECTED_PARALLELISM) {
+ launch(Dispatchers.IO) {
+ ioBarrier.await()
+ ioBlocker.await()
+ }
+ }
+
+ ioBarrier.await() // Ensure all threads are occupied
+ val parallelism = Runtime.getRuntime().availableProcessors()
+ val defaultBarrier = CyclicBarrier(parallelism + 1)
+ repeat(parallelism) {
+ launch(Dispatchers.Default) {
+ defaultBarrier.await()
+ }
+ }
+ defaultBarrier.await()
+ ioBlocker.countDown()
+ }
+
+ @Test
+ fun testHardCapOnParallelism() = runTest {
+ val iterations = 100_000 * stressTestMultiplierSqrt
+ val concurrency = AtomicInteger()
+ repeat(iterations) {
+ launch(Dispatchers.IO) {
+ val c = concurrency.incrementAndGet()
+ assertTrue("Got: $c") { c <= EXPECTED_PARALLELISM }
+ concurrency.decrementAndGet()
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt
index b4924277..e5705803 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/LimitingDispatcherTest.kt
@@ -11,11 +11,6 @@ import java.util.concurrent.*
class LimitingDispatcherTest : SchedulerTestBase() {
@Test(expected = IllegalArgumentException::class)
- fun testTooLargeView() {
- view(corePoolSize + 1)
- }
-
- @Test(expected = IllegalArgumentException::class)
fun testNegativeView() {
view(-1)
}
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt
index dd969bdd..fc4436f4 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt
@@ -6,7 +6,6 @@
package kotlinx.coroutines.scheduling
-import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import org.junit.*
@@ -61,11 +60,11 @@ abstract class SchedulerTestBase : TestBase() {
protected var maxPoolSize = 1024
protected var idleWorkerKeepAliveNs = IDLE_WORKER_KEEP_ALIVE_NS
- private var _dispatcher: ExperimentalCoroutineDispatcher? = null
+ private var _dispatcher: SchedulerCoroutineDispatcher? = null
protected val dispatcher: CoroutineDispatcher
get() {
if (_dispatcher == null) {
- _dispatcher = ExperimentalCoroutineDispatcher(
+ _dispatcher = SchedulerCoroutineDispatcher(
corePoolSize,
maxPoolSize,
idleWorkerKeepAliveNs
@@ -86,7 +85,7 @@ abstract class SchedulerTestBase : TestBase() {
protected fun view(parallelism: Int): CoroutineDispatcher {
val intitialize = dispatcher
- return _dispatcher!!.limited(parallelism)
+ return _dispatcher!!.limitedParallelism(parallelism)
}
@After
@@ -98,3 +97,17 @@ abstract class SchedulerTestBase : TestBase() {
}
}
}
+
+internal fun SchedulerCoroutineDispatcher.blocking(parallelism: Int = 16): CoroutineDispatcher {
+ return object : CoroutineDispatcher() {
+
+ @InternalCoroutinesApi
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ this@blocking.dispatchWithContext(block, BlockingContext, true)
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ this@blocking.dispatchWithContext(block, BlockingContext, false)
+ }
+ }.limitedParallelism(parallelism)
+}
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt
index 6a66da9f..743b4a61 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/SharingWorkerClassTest.kt
@@ -13,8 +13,8 @@ class SharingWorkerClassTest : SchedulerTestBase() {
@Test
fun testSharedThread() = runTest {
- val dispatcher = ExperimentalCoroutineDispatcher(1, schedulerName = "first")
- val dispatcher2 = ExperimentalCoroutineDispatcher(1, schedulerName = "second")
+ val dispatcher = SchedulerCoroutineDispatcher(1, schedulerName = "first")
+ val dispatcher2 = SchedulerCoroutineDispatcher(1, schedulerName = "second")
try {
withContext(dispatcher) {
@@ -39,7 +39,7 @@ class SharingWorkerClassTest : SchedulerTestBase() {
val cores = Runtime.getRuntime().availableProcessors()
repeat(cores + 1) {
CoroutineScope(Dispatchers.Default).launch {
- ExperimentalCoroutineDispatcher(1).close()
+ SchedulerCoroutineDispatcher(1).close()
}.join()
}
}
diff --git a/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt b/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt
deleted file mode 100644
index 4a6f4d24..00000000
--- a/kotlinx-coroutines-core/jvm/test/test/TestCoroutineContextTest.kt
+++ /dev/null
@@ -1,404 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import org.junit.*
-import org.junit.Test
-import kotlin.coroutines.*
-import kotlin.test.*
-
-class TestCoroutineContextTest {
- private val injectedContext = TestCoroutineContext()
-
- @After
- fun tearDown() {
- injectedContext.cancelAllActions()
- }
-
- @Test
- fun testDelayWithLaunch() = withTestContext(injectedContext) {
- val delay = 1000L
-
- var executed = false
- launch {
- suspendedDelayedAction(delay) {
- executed = true
- }
- }
-
- advanceTimeBy(delay / 2)
- assertFalse(executed)
-
- advanceTimeBy(delay / 2)
- assertTrue(executed)
- }
-
- @Test
- fun testTimeJumpWithLaunch() = withTestContext(injectedContext) {
- val delay = 1000L
-
- var executed = false
- launch {
- suspendedDelayedAction(delay) {
- executed = true
- }
- }
-
- advanceTimeTo(delay / 2)
- assertFalse(executed)
-
- advanceTimeTo(delay)
- assertTrue(executed)
- }
-
- @Test
- fun testDelayWithAsync() = withTestContext(injectedContext) {
- val delay = 1000L
-
- var executed = false
- async {
- suspendedDelayedAction(delay) {
- executed = true
- }
- }
-
- advanceTimeBy(delay / 2)
- assertFalse(executed)
-
- advanceTimeBy(delay / 2)
- assertTrue(executed)
- }
-
- @Test
- fun testDelayWithRunBlocking() = withTestContext(injectedContext) {
- val delay = 1000L
-
- var executed = false
- runBlocking {
- suspendedDelayedAction(delay) {
- executed = true
- }
- }
-
- assertTrue(executed)
- assertEquals(delay, now())
- }
-
- private suspend fun suspendedDelayedAction(delay: Long, action: () -> Unit) {
- delay(delay)
- action()
- }
-
- @Test
- fun testDelayedFunctionWithRunBlocking() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 16
-
- val result = runBlocking {
- suspendedDelayedFunction(delay) {
- expectedValue
- }
- }
-
- assertEquals(expectedValue, result)
- assertEquals(delay, now())
- }
-
- @Test
- fun testDelayedFunctionWithAsync() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 16
-
- val deferred = async {
- suspendedDelayedFunction(delay) {
- expectedValue
- }
- }
-
- advanceTimeBy(delay / 2)
- try {
- deferred.getCompleted()
- fail("The Job should not have been completed yet.")
- } catch (e: Exception) {
- // Success.
- }
-
- advanceTimeBy(delay / 2)
- assertEquals(expectedValue, deferred.getCompleted())
- }
-
- private suspend fun <T> TestCoroutineContext.suspendedDelayedFunction(delay: Long, function: () -> T): T {
- delay(delay / 4)
- return async {
- delay((delay / 4) * 3)
- function()
- }.await()
- }
-
- @Test
- fun testBlockingFunctionWithRunBlocking() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 16
- val result = runBlocking {
- suspendedBlockingFunction(delay) {
- expectedValue
- }
- }
- assertEquals(expectedValue, result)
- assertEquals(delay, now())
- }
-
- @Test
- fun testBlockingFunctionWithAsync() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 16
- var now = 0L
- val deferred = async {
- suspendedBlockingFunction(delay) {
- expectedValue
- }
- }
- now += advanceTimeBy((delay / 4) - 1)
- assertEquals((delay / 4) - 1, now)
- assertEquals(now, now())
- try {
- deferred.getCompleted()
- fail("The Job should not have been completed yet.")
- } catch (e: Exception) {
- // Success.
- }
- now += advanceTimeBy(1)
- assertEquals(delay, now())
- assertEquals(now, now())
- assertEquals(expectedValue, deferred.getCompleted())
- }
-
- private suspend fun <T> TestCoroutineContext.suspendedBlockingFunction(delay: Long, function: () -> T): T {
- delay(delay / 4)
- return runBlocking {
- delay((delay / 4) * 3)
- function()
- }
- }
-
- @Test
- fun testTimingOutFunctionWithAsyncAndNoTimeout() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 67
-
- val result = async {
- suspendedTimingOutFunction(delay, delay + 1) {
- expectedValue
- }
- }
-
- triggerActions()
- assertEquals(expectedValue, result.getCompleted())
- }
-
- @Test
- fun testTimingOutFunctionWithAsyncAndTimeout() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 67
-
- val result = async {
- suspendedTimingOutFunction(delay, delay) {
- expectedValue
- }
- }
-
- triggerActions()
- assertTrue(result.getCompletionExceptionOrNull() is TimeoutCancellationException)
- }
-
- @Test
- fun testTimingOutFunctionWithRunBlockingAndTimeout() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedValue = 67
-
- try {
- runBlocking {
- suspendedTimingOutFunction(delay, delay) {
- expectedValue
- }
- }
- fail("Expected TimeoutCancellationException to be thrown.")
- } catch (e: TimeoutCancellationException) {
- // Success
- } catch (e: Throwable) {
- fail("Expected TimeoutCancellationException to be thrown: $e")
- }
- }
-
- private suspend fun <T> TestCoroutineContext.suspendedTimingOutFunction(delay: Long, timeOut: Long, function: () -> T): T {
- return runBlocking {
- withTimeout(timeOut) {
- delay(delay / 2)
- val ret = function()
- delay(delay / 2)
- ret
- }
- }
- }
-
- @Test(expected = AssertionError::class)
- fun testWithTestContextThrowingAnAssertionError() = withTestContext(injectedContext) {
- val expectedError = IllegalAccessError("hello")
-
- launch {
- throw expectedError
- }
-
- triggerActions()
- }
-
- @Test
- fun testExceptionHandlingWithLaunch() = withTestContext(injectedContext) {
- val expectedError = IllegalAccessError("hello")
-
- launch {
- throw expectedError
- }
-
- triggerActions()
- assertUnhandledException { it === expectedError}
- }
-
- @Test
- fun testExceptionHandlingWithLaunchingChildCoroutines() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedError = TestException("hello")
- val expectedValue = 12
-
- launch {
- suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true)
- }
-
- advanceTimeBy(delay)
- assertUnhandledException { it === expectedError}
- }
-
- @Test
- fun testExceptionHandlingWithAsyncAndDontWaitForException() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedError = IllegalAccessError("hello")
- val expectedValue = 12
-
- val result = async {
- suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, false)
- }
-
- advanceTimeBy(delay)
-
- assertNull(result.getCompletionExceptionOrNull())
- assertEquals(expectedValue, result.getCompleted())
- }
-
- @Test
- fun testExceptionHandlingWithAsyncAndWaitForException() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedError = TestException("hello")
- val expectedValue = 12
-
- val result = async {
- suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true)
- }
-
- advanceTimeBy(delay)
-
- val e = result.getCompletionExceptionOrNull()
- assertTrue(expectedError === e, "Expected to be thrown: '$expectedError' but was '$e'")
- }
-
- @Test
- fun testExceptionHandlingWithRunBlockingAndDontWaitForException() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedError = IllegalAccessError("hello")
- val expectedValue = 12
-
- val result = runBlocking {
- suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, false)
- }
-
- advanceTimeBy(delay)
-
- assertEquals(expectedValue, result)
- }
-
- @Test
- fun testExceptionHandlingWithRunBlockingAndWaitForException() = withTestContext(injectedContext) {
- val delay = 1000L
- val expectedError = TestException("hello")
- val expectedValue = 12
-
- try {
- runBlocking {
- suspendedAsyncWithExceptionAfterDelay(delay, expectedError, expectedValue, true)
- }
- fail("Expected to be thrown: '$expectedError'")
- } catch (e: AssertionError) {
- throw e
- } catch (e: Throwable) {
- assertTrue(expectedError === e, "Expected to be thrown: '$expectedError' but was '$e'")
- }
- }
-
- private suspend fun <T> TestCoroutineContext.suspendedAsyncWithExceptionAfterDelay(delay: Long, exception: Throwable, value: T, await: Boolean): T {
- val deferred = async {
- delay(delay - 1)
- throw exception
- }
-
- if (await) {
- deferred.await()
- }
- return value
- }
-
- @Test
- fun testCancellationException() = withTestContext {
- val job = launch {
- delay(1000)
- }
-
- advanceTimeBy(500)
- job.cancel()
- assertAllUnhandledExceptions { it is CancellationException }
- }
-
- @Test
- fun testCancellationExceptionNotThrownByWithTestContext() = withTestContext {
- val job = launch {
- delay(1000)
- }
-
- advanceTimeBy(500)
- job.cancel()
- }
-}
-
-
-/* Some helper functions */
-// todo: deprecate, replace, see https://github.com/Kotlin/kotlinx.coroutines/issues/541
-private fun TestCoroutineContext.launch(
- start: CoroutineStart = CoroutineStart.DEFAULT,
- parent: Job? = null,
- block: suspend CoroutineScope.() -> Unit
-) =
- GlobalScope.launch(this + (parent ?: EmptyCoroutineContext), start, block)
-
-// todo: deprecate, replace, see https://github.com/Kotlin/kotlinx.coroutines/issues/541
-private fun <T> TestCoroutineContext.async(
- start: CoroutineStart = CoroutineStart.DEFAULT,
- parent: Job? = null,
- block: suspend CoroutineScope.() -> T
-
-) =
- GlobalScope.async(this + (parent ?: EmptyCoroutineContext), start, block)
-
-private fun <T> TestCoroutineContext.runBlocking(
- block: suspend CoroutineScope.() -> T
-) = runBlocking(this, block)
diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt
index 30c187fa..f5b22224 100644
--- a/kotlinx-coroutines-core/native/src/Builders.kt
+++ b/kotlinx-coroutines-core/native/src/Builders.kt
@@ -2,11 +2,14 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(ExperimentalContracts::class)
package kotlinx.coroutines
import kotlinx.cinterop.*
import platform.posix.*
+import kotlin.contracts.*
import kotlin.coroutines.*
+import kotlin.native.concurrent.*
/**
* Runs new coroutine and **blocks** current thread _interruptibly_ until its completion.
@@ -30,10 +33,13 @@ import kotlin.coroutines.*
* @param context context of the coroutine. The default value is an implementation of [EventLoop].
* @param block the coroutine code.
*/
-public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
+public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
- var newContext: CoroutineContext = context // todo: kludge for data flow analysis error
+ val newContext: CoroutineContext
if (contextInterceptor == null) {
// create or use private event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
@@ -54,20 +60,33 @@ private class BlockingCoroutine<T>(
parentContext: CoroutineContext,
private val eventLoop: EventLoop?
) : AbstractCoroutine<T>(parentContext, true, true) {
+ private val joinWorker = Worker.current
+
override val isScopedCoroutine: Boolean get() = true
+ override fun afterCompletion(state: Any?) {
+ // wake up blocked thread
+ if (joinWorker != Worker.current) {
+ // Unpark waiting worker
+ joinWorker.executeAfter(0L, {}) // send an empty task to unpark the waiting event loop
+ }
+ }
+
@Suppress("UNCHECKED_CAST")
- fun joinBlocking(): T = memScoped {
+ fun joinBlocking(): T {
try {
eventLoop?.incrementUseCount()
- val timespec = alloc<timespec>()
while (true) {
- val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
- // note: process next even may loose unpark flag, so check if completed before parking
+ var parkNanos: Long
+ // Workaround for bug in BE optimizer that cannot eliminate boxing here
+ if (eventLoop != null) {
+ parkNanos = eventLoop.processNextEvent()
+ } else {
+ parkNanos = Long.MAX_VALUE
+ }
+ // note: processNextEvent may lose unpark flag, so check if completed before parking
if (isCompleted) break
- timespec.tv_sec = (parkNanos / 1000000000L).convert() // 1e9 ns -> sec
- timespec.tv_nsec = (parkNanos % 1000000000L).convert() // % 1e9
- nanosleep(timespec.ptr, null)
+ joinWorker.park(parkNanos / 1000L, true)
}
} finally { // paranoia
eventLoop?.decrementUseCount()
@@ -75,6 +94,6 @@ private class BlockingCoroutine<T>(
// now return result
val state = state.unboxState()
(state as? CompletedExceptionally)?.let { throw it.cause }
- state as T
+ return state as T
}
}
diff --git a/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt
new file mode 100644
index 00000000..0e239a42
--- /dev/null
+++ b/kotlinx-coroutines-core/native/src/CloseableCoroutineDispatcher.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+public actual abstract class CloseableCoroutineDispatcher actual constructor() : CoroutineDispatcher() {
+ public actual abstract fun close()
+}
diff --git a/kotlinx-coroutines-core/native/src/CompletionHandler.kt b/kotlinx-coroutines-core/native/src/CompletionHandler.kt
deleted file mode 100644
index 4835f796..00000000
--- a/kotlinx-coroutines-core/native/src/CompletionHandler.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines
-
-import kotlinx.coroutines.internal.*
-
-internal actual abstract class CompletionHandlerBase actual constructor() : LockFreeLinkedListNode(), CompletionHandler {
- actual abstract override fun invoke(cause: Throwable?)
-}
-
-internal actual inline val CompletionHandlerBase.asHandler: CompletionHandler get() = this
-
-internal actual abstract class CancelHandlerBase actual constructor() : CompletionHandler {
- actual abstract override fun invoke(cause: Throwable?)
-}
-
-internal actual inline val CancelHandlerBase.asHandler: CompletionHandler get() = this
-
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun CompletionHandler.invokeIt(cause: Throwable?) = invoke(cause)
diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt
index 47afd8ad..6e2dac1a 100644
--- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt
+++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt
@@ -8,33 +8,49 @@ import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.native.concurrent.*
-private fun takeEventLoop(): EventLoopImpl =
- ThreadLocalEventLoop.currentOrNull() as? EventLoopImpl ?:
- error("There is no event loop. Use runBlocking { ... } to start one.")
-
internal actual object DefaultExecutor : CoroutineDispatcher(), Delay {
- override fun dispatch(context: CoroutineContext, block: Runnable) =
- takeEventLoop().dispatch(context, block)
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
- takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation)
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
- takeEventLoop().invokeOnTimeout(timeMillis, block, context)
-
- actual fun enqueue(task: Runnable): Unit = loopWasShutDown()
-}
-internal fun loopWasShutDown(): Nothing = error("Cannot execute task because event loop was shut down")
+ private val delegate = WorkerDispatcher(name = "DefaultExecutor")
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkState()
+ delegate.dispatch(context, block)
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ checkState()
+ delegate.scheduleResumeAfterDelay(timeMillis, continuation)
+ }
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ checkState()
+ return delegate.invokeOnTimeout(timeMillis, block, context)
+ }
-internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
- DefaultExecutor
+ actual fun enqueue(task: Runnable): Unit {
+ checkState()
+ delegate.dispatch(EmptyCoroutineContext, task)
+ }
+
+ private fun checkState() {
+ if (multithreadingSupported) return
+ error("DefaultExecutor should never be invoked in K/N with disabled new memory model. The top-most 'runBlocking' event loop has been shutdown")
+ }
+}
+
+internal expect fun createDefaultDispatcher(): CoroutineDispatcher
@SharedImmutable
-internal actual val DefaultDelay: Delay = DefaultExecutor
+internal actual val DefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = coroutineContext + context
- return if (combined !== DefaultExecutor && combined[ContinuationInterceptor] == null)
- combined + DefaultExecutor else combined
+ return if (combined !== DefaultDelay && combined[ContinuationInterceptor] == null)
+ combined + (DefaultDelay as CoroutineContext.Element) else combined
+}
+
+public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
+ return this + addedContext
}
// No debugging facilities on native
diff --git a/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt
index b0aa8633..434813dc 100644
--- a/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt
+++ b/kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt
@@ -5,8 +5,10 @@
package kotlinx.coroutines
import kotlin.coroutines.*
+import kotlin.native.*
+@OptIn(ExperimentalStdlibApi::class)
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
// log exception
- exception.printStackTrace()
+ processUnhandledException(exception)
}
diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt
index 4e5facfe..6c51a034 100644
--- a/kotlinx-coroutines-core/native/src/Dispatchers.kt
+++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt
@@ -4,15 +4,54 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.multithreadingSupported
import kotlin.coroutines.*
public actual object Dispatchers {
- public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
- public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default)
+ public actual val Default: CoroutineDispatcher = createDefaultDispatcherBasedOnMm()
+ public actual val Main: MainCoroutineDispatcher
+ get() = injectedMainDispatcher ?: mainDispatcher
public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing
+
+ private val mainDispatcher = createMainDispatcher(Default)
+
+ private var injectedMainDispatcher: MainCoroutineDispatcher? = null
+
+ @PublishedApi
+ internal fun injectMain(dispatcher: MainCoroutineDispatcher) {
+ if (!multithreadingSupported) {
+ throw IllegalStateException("Dispatchers.setMain is not supported in Kotlin/Native when new memory model is disabled")
+ }
+ injectedMainDispatcher = dispatcher
+ }
+
+ @PublishedApi
+ internal fun resetInjectedMain() {
+ injectedMainDispatcher = null
+ }
+}
+
+internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher
+
+private fun createDefaultDispatcherBasedOnMm(): CoroutineDispatcher {
+ return if (multithreadingSupported) createDefaultDispatcher()
+ else OldDefaultExecutor
+}
+
+private fun takeEventLoop(): EventLoopImpl =
+ ThreadLocalEventLoop.currentOrNull() as? EventLoopImpl ?:
+ error("There is no event loop. Use runBlocking { ... } to start one.")
+
+internal object OldDefaultExecutor : CoroutineDispatcher(), Delay {
+ override fun dispatch(context: CoroutineContext, block: Runnable) =
+ takeEventLoop().dispatch(context, block)
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
+ takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation)
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+ takeEventLoop().invokeOnTimeout(timeMillis, block, context)
}
-private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() {
+internal class OldMainDispatcher(private val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() {
override val immediate: MainCoroutineDispatcher
get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native")
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt
index 925cbe99..f4e5b8c9 100644
--- a/kotlinx-coroutines-core/native/src/EventLoop.kt
+++ b/kotlinx-coroutines-core/native/src/EventLoop.kt
@@ -4,18 +4,38 @@
package kotlinx.coroutines
+import kotlinx.cinterop.*
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.internal.multithreadingSupported
+import platform.posix.*
import kotlin.coroutines.*
+import kotlin.native.concurrent.*
import kotlin.system.*
-internal actual abstract class EventLoopImplPlatform: EventLoop() {
- protected actual fun unpark() { /* does nothing */ }
- protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask): Unit =
- loopWasShutDown()
+internal actual abstract class EventLoopImplPlatform : EventLoop() {
+
+ private val current = Worker.current
+
+ protected actual fun unpark() {
+ current.executeAfter(0L, {})// send an empty task to unpark the waiting event loop
+ }
+
+ protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) {
+ if (multithreadingSupported) {
+ DefaultExecutor.invokeOnTimeout(now, delayedTask, EmptyCoroutineContext)
+ } else {
+ error("Cannot execute task because event loop was shut down")
+ }
+ }
}
internal class EventLoopImpl: EventLoopImplBase() {
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
- scheduleInvokeOnTimeout(timeMillis, block)
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ if (!multithreadingSupported) {
+ return scheduleInvokeOnTimeout(timeMillis, block)
+ }
+ return DefaultDelay.invokeOnTimeout(timeMillis, block, context)
+ }
}
internal actual fun createEventLoop(): EventLoop = EventLoopImpl()
diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt
index da9979b6..1a923c40 100644
--- a/kotlinx-coroutines-core/native/src/Exceptions.kt
+++ b/kotlinx-coroutines-core/native/src/Exceptions.kt
@@ -4,6 +4,9 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.internal.SuppressSupportingThrowableImpl
+
/**
* Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending.
* It indicates _normal_ cancellation of a coroutine.
@@ -31,7 +34,9 @@ internal actual class JobCancellationException public actual constructor(
}
@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ }
+internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) {
+ if (this is SuppressSupportingThrowableImpl) addSuppressed(other)
+}
// For use in tests
internal actual val RECOVER_STACK_TRACES: Boolean = false
diff --git a/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt
new file mode 100644
index 00000000..1c1306c2
--- /dev/null
+++ b/kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+import kotlin.native.concurrent.*
+
+@ExperimentalCoroutinesApi
+public actual fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher {
+ if (!multithreadingSupported) throw IllegalStateException("This API is only supported for experimental K/N memory model")
+ return WorkerDispatcher(name)
+}
+
+public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher {
+ if (!multithreadingSupported) throw IllegalStateException("This API is only supported for experimental K/N memory model")
+ require(nThreads >= 1) { "Expected at least one thread, but got: $nThreads"}
+ return MultiWorkerDispatcher(name, nThreads)
+}
+
+internal class WorkerDispatcher(name: String) : CloseableCoroutineDispatcher(), Delay {
+ private val worker = Worker.start(name = name)
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ worker.executeAfter(0L) { block.run() }
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ worker.executeAfter(timeMillis.toMicrosSafe()) {
+ with(continuation) { resumeUndispatched(Unit) }
+ }
+ }
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ // Workers don't have an API to cancel sent "executeAfter" block, but we are trying
+ // to control the damage and reduce reachable objects by nulling out `block`
+ // that may retain a lot of references, and leaving only an empty shell after a timely disposal
+ // This is a class and not an object with `block` in a closure because that would defeat the purpose.
+ class DisposableBlock(block: Runnable) : DisposableHandle, Function0<Unit> {
+ private val disposableHolder = AtomicReference<Runnable?>(block)
+
+ override fun invoke() {
+ disposableHolder.value?.run()
+ }
+
+ override fun dispose() {
+ disposableHolder.value = null
+ }
+ }
+
+ val disposableBlock = DisposableBlock(block)
+ worker.executeAfter(timeMillis.toMicrosSafe(), disposableBlock)
+ return disposableBlock
+ }
+
+ override fun close() {
+ worker.requestTermination().result // Note: calling "result" blocks
+ }
+
+ private fun Long.toMicrosSafe(): Long {
+ val result = this * 1000
+ return if (result > this) result else Long.MAX_VALUE
+ }
+}
+
+private class MultiWorkerDispatcher(name: String, workersCount: Int) : CloseableCoroutineDispatcher() {
+ private val tasksQueue = Channel<Runnable>(Channel.UNLIMITED)
+ private val workers = Array(workersCount) { Worker.start(name = "$name-$it") }
+
+ init {
+ workers.forEach { w -> w.executeAfter(0L) { workerRunLoop() } }
+ }
+
+ private fun workerRunLoop() = runBlocking {
+ for (task in tasksQueue) {
+ // TODO error handling
+ task.run()
+ }
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ // TODO handle rejections
+ tasksQueue.trySend(block)
+ }
+
+ override fun close() {
+ tasksQueue.close()
+ workers.forEach { it.requestTermination().result }
+ }
+}
diff --git a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
index b379c6ac..f6e18dd5 100644
--- a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
+++ b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt
@@ -4,15 +4,35 @@
package kotlinx.coroutines.internal
-internal actual typealias ReentrantLock = NoOpLock
+import kotlinx.atomicfu.*
+import kotlin.native.concurrent.*
+import kotlinx.atomicfu.locks.withLock as withLock2
-internal actual inline fun <T> ReentrantLock.withLock(action: () -> T) = action()
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject
-internal class NoOpLock {
- fun tryLock() = true
- fun unlock(): Unit {}
-}
+internal actual inline fun <T> ReentrantLock.withLock(action: () -> T): T = this.withLock2(action)
internal actual fun <E> subscriberList(): MutableList<E> = CopyOnWriteList<E>()
internal actual fun <E> identitySet(expectedSize: Int): MutableSet<E> = HashSet()
+
+
+// "Suppress-supporting throwable" is currently used for tests only
+internal open class SuppressSupportingThrowableImpl : Throwable() {
+ private val _suppressed = atomic<Array<Throwable>>(emptyArray())
+
+ val suppressed: Array<Throwable>
+ get() = _suppressed.value
+
+ fun addSuppressed(other: Throwable) {
+ _suppressed.update { current ->
+ current + other
+ }
+ }
+}
+
+// getter instead of a property due to the bug in the initialization dependencies tracking with '-Xir-property-lazy-initialization=disabled' that Ktor uses
+@OptIn(ExperimentalStdlibApi::class)
+internal val multithreadingSupported: Boolean
+ get() = kotlin.native.isExperimentalMM()
diff --git a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt
index 30f063a5..2896c2ea 100644
--- a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt
+++ b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt
@@ -4,101 +4,76 @@
package kotlinx.coroutines.internal
+import kotlinx.atomicfu.*
+
@Suppress("UNCHECKED_CAST")
-internal class CopyOnWriteList<E>(private var array: Array<Any?> = arrayOfNulls(4)) : AbstractMutableList<E>() {
+internal class CopyOnWriteList<E> : AbstractMutableList<E>() {
+
+ private val _array = atomic(arrayOfNulls<Any?>(0))
+ private var array: Array<Any?>
+ get() = _array.value
+ set(value) { _array.value = value }
- private var _size = 0
- override val size: Int get() = _size
+ override val size: Int
+ get() = array.size
override fun add(element: E): Boolean {
- val newSize = if (_size == array.size) array.size * 2 else array.size
- val update = array.copyOf(newSize)
- update[_size++] = element
+ val n = size
+ val update = array.copyOf(n + 1)
+ update[n] = element
array = update
return true
}
override fun add(index: Int, element: E) {
rangeCheck(index)
- val update = arrayOfNulls<Any?>(if (array.size == _size) array.size * 2 else array.size)
- array.copyInto(
- destination = update,
- endIndex = index
- )
+ val n = size
+ val update = arrayOfNulls<Any?>(n + 1)
+ array.copyInto(destination = update, endIndex = index)
update[index] = element
- array.copyInto(
- destination = update,
- destinationOffset = index + 1,
- startIndex = index,
- endIndex = _size + 1
- )
- ++_size
+ array.copyInto(destination = update, destinationOffset = index + 1, startIndex = index, endIndex = n + 1)
array = update
}
override fun remove(element: E): Boolean {
val index = array.indexOf(element as Any)
- if (index == -1) {
- return false
- }
-
+ if (index == -1) return false
removeAt(index)
return true
}
override fun removeAt(index: Int): E {
rangeCheck(index)
- modCount++
- val n = array.size
+ val n = size
val element = array[index]
- val update = arrayOfNulls<Any>(n)
- array.copyInto(
- destination = update,
- endIndex = index
- )
- array.copyInto(
- destination = update,
- destinationOffset = index,
- startIndex = index + 1,
- endIndex = n
- )
+ val update = arrayOfNulls<Any>(n - 1)
+ array.copyInto(destination = update, endIndex = index)
+ array.copyInto(destination = update, destinationOffset = index, startIndex = index + 1, endIndex = n)
array = update
- --_size
return element as E
}
- override fun iterator(): MutableIterator<E> = IteratorImpl(array as Array<E>, size)
-
+ override fun iterator(): MutableIterator<E> = IteratorImpl(array as Array<E>)
override fun listIterator(): MutableListIterator<E> = throw UnsupportedOperationException("Operation is not supported")
-
override fun listIterator(index: Int): MutableListIterator<E> = throw UnsupportedOperationException("Operation is not supported")
-
override fun isEmpty(): Boolean = size == 0
-
override fun set(index: Int, element: E): E = throw UnsupportedOperationException("Operation is not supported")
+ override fun get(index: Int): E = array[rangeCheck(index)] as E
- override fun get(index: Int): E = array[rangeCheck(index)]!! as E
-
- private class IteratorImpl<E>(private var array: Array<E>, private val size: Int) : MutableIterator<E> {
-
+ private class IteratorImpl<E>(private val array: Array<E>) : MutableIterator<E> {
private var current = 0
- override fun hasNext(): Boolean = current != size
+ override fun hasNext(): Boolean = current != array.size
override fun next(): E {
- if (!hasNext()) {
- throw NoSuchElementException()
- }
-
- return array[current++]!!
+ if (!hasNext()) throw NoSuchElementException()
+ return array[current++]
}
override fun remove() = throw UnsupportedOperationException("Operation is not supported")
}
private fun rangeCheck(index: Int) = index.apply {
- if (index < 0 || index >= _size) {
- throw IndexOutOfBoundsException("index: $index, size: $size")
- }
+ if (index < 0 || index >= size) throw IndexOutOfBoundsException("index: $index, size: $size")
}
}
diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt
deleted file mode 100644
index a8aed044..00000000
--- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-@file:Suppress("NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", "NO_EXPLICIT_VISIBILITY_IN_API_MODE")
-
-package kotlinx.coroutines.internal
-
-private typealias Node = LinkedListNode
-
-/** @suppress **This is unstable API and it is subject to change.** */
-@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // :TODO: Remove when fixed: https://youtrack.jetbrains.com/issue/KT-23703
-public actual typealias LockFreeLinkedListNode = LinkedListNode
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public actual typealias LockFreeLinkedListHead = LinkedListHead
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public open class LinkedListNode {
- @PublishedApi internal var _next = this
- @PublishedApi internal var _prev = this
- @PublishedApi internal var _removed: Boolean = false
-
- public inline val nextNode get() = _next
- public inline val prevNode get() = _prev
- public inline val isRemoved get() = _removed
-
- public fun addLast(node: Node) {
- val prev = this._prev
- node._next = this
- node._prev = prev
- prev._next = node
- this._prev = node
- }
-
- public open fun remove(): Boolean {
- if (_removed) return false
- val prev = this._prev
- val next = this._next
- prev._next = next
- next._prev = prev
- _removed = true
- return true
- }
-
- public fun addOneIfEmpty(node: Node): Boolean {
- if (_next !== this) return false
- addLast(node)
- return true
- }
-
- public inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
- if (!condition()) return false
- addLast(node)
- return true
- }
-
- public inline fun addLastIfPrev(node: Node, predicate: (Node) -> Boolean): Boolean {
- if (!predicate(_prev)) return false
- addLast(node)
- return true
- }
-
- public inline fun addLastIfPrevAndIf(
- node: Node,
- predicate: (Node) -> Boolean, // prev node predicate
- crossinline condition: () -> Boolean // atomically checked condition
- ): Boolean {
- if (!predicate(_prev)) return false
- if (!condition()) return false
- addLast(node)
- return true
- }
-
- public fun helpRemove() {} // no-op without multithreading
-
- public fun removeFirstOrNull(): Node? {
- val next = _next
- if (next === this) return null
- check(next.remove()) { "Should remove" }
- return next
- }
-
- public inline fun <reified T> removeFirstIfIsInstanceOfOrPeekIf(predicate: (T) -> Boolean): T? {
- val next = _next
- if (next === this) return null
- if (next !is T) return null
- if (predicate(next)) return next
- check(next.remove()) { "Should remove" }
- return next
- }
-}
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public actual open class AddLastDesc<T : Node> actual constructor(
- actual val queue: Node,
- actual val node: T
-) : AbstractAtomicDesc() {
- override val affectedNode: Node get() = queue._prev
- actual override fun finishPrepare(prepareOp: PrepareOp) {}
- override fun onComplete() = queue.addLast(node)
- actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit
-}
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public actual open class RemoveFirstDesc<T> actual constructor(
- actual val queue: LockFreeLinkedListNode
-) : AbstractAtomicDesc() {
- @Suppress("UNCHECKED_CAST")
- actual val result: T get() = affectedNode as T
- override val affectedNode: Node = queue.nextNode
- actual override fun finishPrepare(prepareOp: PrepareOp) {}
- override fun onComplete() { queue.removeFirstOrNull() }
- actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit
-}
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public actual abstract class AbstractAtomicDesc : AtomicDesc() {
- protected abstract val affectedNode: Node
- actual abstract fun finishPrepare(prepareOp: PrepareOp)
- protected abstract fun onComplete()
-
- actual open fun onPrepare(prepareOp: PrepareOp): Any? {
- finishPrepare(prepareOp)
- return null
- }
-
- actual open fun onRemoved(affected: Node) {}
-
- actual final override fun prepare(op: AtomicOp<*>): Any? {
- val affected = affectedNode
- val failure = failure(affected)
- if (failure != null) return failure
- @Suppress("UNCHECKED_CAST")
- return onPrepare(PrepareOp(affected, this, op))
- }
-
- actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete()
- protected actual open fun failure(affected: LockFreeLinkedListNode): Any? = null // Never fails by default
- protected actual open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean = false // Always succeeds
- protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode)
-}
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public actual class PrepareOp(
- actual val affected: LockFreeLinkedListNode,
- actual val desc: AbstractAtomicDesc,
- actual override val atomicOp: AtomicOp<*>
-): OpDescriptor() {
- override fun perform(affected: Any?): Any? = null
- actual fun finishPrepare() {}
-}
-
-/** @suppress **This is unstable API and it is subject to change.** */
-public open class LinkedListHead : LinkedListNode() {
- public val isEmpty get() = _next === this
-
- /**
- * Iterates over all elements in this list of a specified type.
- */
- public inline fun <reified T : Node> forEach(block: (T) -> Unit) {
- var cur: Node = _next
- while (cur != this) {
- if (cur is T) block(cur)
- cur = cur._next
- }
- }
-
- // just a defensive programming -- makes sure that list head sentinel is never removed
- public final override fun remove(): Boolean = throw UnsupportedOperationException()
-}
diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
index dcbb2021..edbd3fde 100644
--- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
+++ b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt
@@ -5,16 +5,16 @@
package kotlinx.coroutines.internal
import kotlinx.coroutines.*
+import kotlinx.atomicfu.locks.withLock as withLock2
/**
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
-public actual typealias SynchronizedObject = Any
+public actual typealias SynchronizedObject = kotlinx.atomicfu.locks.SynchronizedObject
/**
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
-public actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T =
- block()
+public actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block)
diff --git a/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt b/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt
new file mode 100644
index 00000000..639b5fb1
--- /dev/null
+++ b/kotlinx-coroutines-core/native/test/ConcurrentTestUtilities.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.internal.*
+import platform.posix.*
+import kotlin.native.concurrent.*
+import kotlin.random.*
+
+actual fun randomWait() {
+ val n = Random.nextInt(1000)
+ if (n < 500) return // no wait 50% of time
+ repeat(n) {
+ BlackHole.sink *= 3
+ }
+ // use the BlackHole value somehow, so even if the compiler gets smarter, it won't remove the object
+ val sinkValue = if (BlackHole.sink > 16) 1 else 0
+ if (n + sinkValue > 900) sched_yield()
+}
+
+@ThreadLocal
+private object BlackHole {
+ var sink = 1
+}
+
+internal actual typealias SuppressSupportingThrowable = SuppressSupportingThrowableImpl
+
+actual val Throwable.suppressed: Array<Throwable>
+ get() = (this as? SuppressSupportingThrowableImpl)?.suppressed ?: emptyArray()
+
+@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
+actual fun Throwable.printStackTrace() = printStackTrace()
+
+actual fun currentThreadName(): String = Worker.current.name
diff --git a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt
index a39a59e1..b0409935 100644
--- a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt
+++ b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt
@@ -8,22 +8,6 @@ import kotlin.coroutines.*
import kotlin.test.*
class DelayExceptionTest : TestBase() {
- private object Dispatcher : CoroutineDispatcher() {
- override fun isDispatchNeeded(context: CoroutineContext): Boolean = true
- override fun dispatch(context: CoroutineContext, block: Runnable) { block.run() }
- }
-
- private lateinit var exception: Throwable
-
-
- @Test
- fun testThrowsTce() {
- CoroutineScope(Dispatcher + CoroutineExceptionHandler { _, e -> exception = e }).launch {
- delay(10)
- }
-
- assertTrue(exception is IllegalStateException)
- }
@Test
fun testMaxDelay() = runBlocking {
diff --git a/kotlinx-coroutines-core/native/test/RunBlockingTest.kt b/kotlinx-coroutines-core/native/test/RunBlockingTest.kt
deleted file mode 100644
index c5d08af5..00000000
--- a/kotlinx-coroutines-core/native/test/RunBlockingTest.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines
-import kotlin.test.*
-
-class RunBlockingTest : TestBase() {
-
- @Test
- fun testIncompleteState() {
- val handle = runBlocking {
- coroutineContext[Job]!!.invokeOnCompletion { }
- }
-
- handle.dispose()
- }
-} \ No newline at end of file
diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt
index 4ffa6c0b..6fef4752 100644
--- a/kotlinx-coroutines-core/native/test/TestBase.kt
+++ b/kotlinx-coroutines-core/native/test/TestBase.kt
@@ -4,16 +4,21 @@
package kotlinx.coroutines
+import kotlinx.atomicfu.*
+
public actual val isStressTest: Boolean = false
public actual val stressTestMultiplier: Int = 1
+public actual val stressTestMultiplierSqrt: Int = 1
+
+public actual val isNative = true
@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias TestResult = Unit
public actual open class TestBase actual constructor() {
public actual val isBoundByJsTestTimeout = false
- private var actionIndex = 0
- private var finished = false
+ private var actionIndex = atomic(0)
+ private var finished = atomic(false)
private var error: Throwable? = null
/**
@@ -36,7 +41,7 @@ public actual open class TestBase actual constructor() {
* Asserts that this invocation is `index`-th in the execution sequence (counting from one).
*/
public actual fun expect(index: Int) {
- val wasIndex = ++actionIndex
+ val wasIndex = actionIndex.incrementAndGet()
check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
}
@@ -52,21 +57,21 @@ public actual open class TestBase actual constructor() {
*/
public actual fun finish(index: Int) {
expect(index)
- check(!finished) { "Should call 'finish(...)' at most once" }
- finished = true
+ check(!finished.value) { "Should call 'finish(...)' at most once" }
+ finished.value = true
}
/**
* Asserts that [finish] was invoked
*/
- public actual fun ensureFinished() {
- require(finished) { "finish(...) should be caller prior to this check" }
+ actual fun ensureFinished() {
+ require(finished.value) { "finish(...) should be caller prior to this check" }
}
- public actual fun reset() {
- check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
- actionIndex = 0
- finished = false
+ actual fun reset() {
+ check(actionIndex.value == 0 || finished.value) { "Expecting that 'finish(...)' was invoked, but it was not" }
+ actionIndex.value = 0
+ finished.value = false
}
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
@@ -78,7 +83,7 @@ public actual open class TestBase actual constructor() {
var exCount = 0
var ex: Throwable? = null
try {
- runBlocking(block = block, context = CoroutineExceptionHandler { context, e ->
+ runBlocking(block = block, context = CoroutineExceptionHandler { _, e ->
if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
exCount++
when {
diff --git a/kotlinx-coroutines-core/native/test/TestBaseExtension.kt b/kotlinx-coroutines-core/native/test/TestBaseExtension.kt
new file mode 100644
index 00000000..fde2cde5
--- /dev/null
+++ b/kotlinx-coroutines-core/native/test/TestBaseExtension.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines
+
+import kotlinx.coroutines.internal.*
+
+actual fun TestBase.runMtTest(
+ expected: ((Throwable) -> Boolean)?,
+ unhandled: List<(Throwable) -> Boolean>,
+ block: suspend CoroutineScope.() -> Unit
+) {
+ if (!multithreadingSupported) return
+ return runTest(expected, unhandled, block)
+}
diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt
index d6b5fad1..8252ca65 100644
--- a/kotlinx-coroutines-core/native/test/WorkerTest.kt
+++ b/kotlinx-coroutines-core/native/test/WorkerTest.kt
@@ -23,12 +23,12 @@ class WorkerTest : TestBase() {
}
@Test
- fun testLaunchInWorkerTroughGlobalScope() {
+ fun testLaunchInWorkerThroughGlobalScope() {
val worker = Worker.start()
worker.execute(TransferMode.SAFE, { }) {
runBlocking {
CoroutineScope(EmptyCoroutineContext).launch {
- delay(1)
+ delay(10)
}.join()
}
}.result
diff --git a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt b/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt
index 6c1fddfc..44ddf471 100644
--- a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt
+++ b/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt
@@ -10,11 +10,11 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class LinkedListTest {
- data class IntNode(val i: Int) : LinkedListNode()
+ data class IntNode(val i: Int) : LockFreeLinkedListNode()
@Test
fun testSimpleAddLastRemove() {
- val list = LinkedListHead()
+ val list = LockFreeLinkedListHead()
assertContents(list)
val n1 = IntNode(1).apply { list.addLast(this) }
assertContents(list, 1)
@@ -35,7 +35,7 @@ class LinkedListTest {
assertContents(list)
}
- private fun assertContents(list: LinkedListHead, vararg expected: Int) {
+ private fun assertContents(list: LockFreeLinkedListHead, vararg expected: Int) {
val n = expected.size
val actual = IntArray(n)
var index = 0
diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt
new file mode 100644
index 00000000..ace20422
--- /dev/null
+++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.cinterop.*
+import kotlinx.coroutines.internal.*
+import platform.CoreFoundation.*
+import platform.darwin.*
+import kotlin.coroutines.*
+import kotlin.native.concurrent.*
+import kotlin.native.internal.NativePtr
+
+internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
+
+internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
+ if (multithreadingSupported) DarwinMainDispatcher(false) else OldMainDispatcher(Dispatchers.Default)
+
+internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGlobalQueueDispatcher
+
+private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() {
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ autoreleasepool {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.convert(), 0)) {
+ block.run()
+ }
+ }
+ }
+}
+
+private class DarwinMainDispatcher(
+ private val invokeImmediately: Boolean
+) : MainCoroutineDispatcher(), Delay {
+
+ override val immediate: MainCoroutineDispatcher =
+ if (invokeImmediately) this else DarwinMainDispatcher(true)
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = !(invokeImmediately && isMainThread())
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ autoreleasepool {
+ dispatch_async(dispatch_get_main_queue()) {
+ block.run()
+ }
+ }
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ val timer = Timer()
+ val timerBlock: TimerBlock = {
+ timer.dispose()
+ continuation.resume(Unit)
+ }
+ timer.start(timeMillis, timerBlock)
+ continuation.disposeOnCancellation(timer)
+ }
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ val timer = Timer()
+ val timerBlock: TimerBlock = {
+ timer.dispose()
+ block.run()
+ }
+ timer.start(timeMillis, timerBlock)
+ return timer
+ }
+
+ override fun toString(): String =
+ "MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }"
+}
+
+private typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit
+
+private val TIMER_NEW = NativePtr.NULL
+private val TIMER_DISPOSED = NativePtr.NULL.plus(1)
+
+private class Timer : DisposableHandle {
+ private val ref = AtomicNativePtr(TIMER_NEW)
+
+ fun start(timeMillis: Long, timerBlock: TimerBlock) {
+ val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0
+ val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock)
+ CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
+ if (!ref.compareAndSet(TIMER_NEW, timer.rawValue)) {
+ // dispose was already called concurrently
+ release(timer)
+ }
+ }
+
+ override fun dispose() {
+ while (true) {
+ val ptr = ref.value
+ if (ptr == TIMER_DISPOSED) return
+ if (ref.compareAndSet(ptr, TIMER_DISPOSED)) {
+ if (ptr != TIMER_NEW) release(interpretCPointer(ptr))
+ return
+ }
+ }
+ }
+
+ private fun release(timer: CFRunLoopTimerRef?) {
+ CFRunLoopRemoveTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
+ CFRelease(timer)
+ }
+}
+
+internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit): Unit = autoreleasepool { block() }
diff --git a/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt
new file mode 100644
index 00000000..d460bd6e
--- /dev/null
+++ b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlinx.coroutines.internal.*
+import platform.CoreFoundation.*
+import platform.darwin.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class MainDispatcherTest : TestBase() {
+
+ private fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
+ private fun canTestMainDispatcher() = !isMainThread() && multithreadingSupported
+
+ private fun runTestNotOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) {
+ // skip if already on the main thread, run blocking doesn't really work well with that
+ if (!canTestMainDispatcher()) return
+ runTest(block = block)
+ }
+
+ @Test
+ fun testDispatchNecessityCheckWithMainImmediateDispatcher() = runTestNotOnMainDispatcher {
+ val main = Dispatchers.Main.immediate
+ assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
+ withContext(Dispatchers.Default) {
+ assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
+ withContext(Dispatchers.Main) {
+ assertFalse(main.isDispatchNeeded(EmptyCoroutineContext))
+ }
+ assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
+ }
+ }
+
+ @Test
+ fun testWithContext() = runTestNotOnMainDispatcher {
+ expect(1)
+ assertFalse(isMainThread())
+ withContext(Dispatchers.Main) {
+ assertTrue(isMainThread())
+ expect(2)
+ }
+ assertFalse(isMainThread())
+ finish(3)
+ }
+
+ @Test
+ fun testWithContextDelay() = runTestNotOnMainDispatcher {
+ expect(1)
+ withContext(Dispatchers.Main) {
+ assertTrue(isMainThread())
+ expect(2)
+ delay(100)
+ assertTrue(isMainThread())
+ expect(3)
+ }
+ assertFalse(isMainThread())
+ finish(4)
+ }
+
+ @Test
+ fun testWithTimeoutContextDelayNoTimeout() = runTestNotOnMainDispatcher {
+ expect(1)
+ withTimeout(1000) {
+ withContext(Dispatchers.Main) {
+ assertTrue(isMainThread())
+ expect(2)
+ delay(100)
+ assertTrue(isMainThread())
+ expect(3)
+ }
+ }
+ assertFalse(isMainThread())
+ finish(4)
+ }
+
+ @Test
+ fun testWithTimeoutContextDelayTimeout() = runTestNotOnMainDispatcher {
+ expect(1)
+ assertFailsWith<TimeoutCancellationException> {
+ withTimeout(100) {
+ withContext(Dispatchers.Main) {
+ assertTrue(isMainThread())
+ expect(2)
+ delay(1000)
+ expectUnreached()
+ }
+ }
+ expectUnreached()
+ }
+ assertFalse(isMainThread())
+ finish(3)
+ }
+
+ @Test
+ fun testWithContextTimeoutDelayNoTimeout() = runTestNotOnMainDispatcher {
+ expect(1)
+ withContext(Dispatchers.Main) {
+ withTimeout(1000) {
+ assertTrue(isMainThread())
+ expect(2)
+ delay(100)
+ assertTrue(isMainThread())
+ expect(3)
+ }
+ }
+ assertFalse(isMainThread())
+ finish(4)
+ }
+
+ @Test
+ fun testWithContextTimeoutDelayTimeout() = runTestNotOnMainDispatcher {
+ expect(1)
+ assertFailsWith<TimeoutCancellationException> {
+ withContext(Dispatchers.Main) {
+ withTimeout(100) {
+ assertTrue(isMainThread())
+ expect(2)
+ delay(1000)
+ expectUnreached()
+ }
+ }
+ expectUnreached()
+ }
+ assertFalse(isMainThread())
+ finish(3)
+ }
+}
diff --git a/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt
new file mode 100644
index 00000000..517190d0
--- /dev/null
+++ b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.coroutines.*
+
+internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
+ MissingMainDispatcher
+
+internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DefaultDispatcher
+
+private object DefaultDispatcher : CoroutineDispatcher() {
+
+ // Delegated, so users won't be able to downcast and call 'close'
+ // The precise number of threads cannot be obtained until KT-48179 is implemented, 4 is just "good enough" number.
+ private val ctx = newFixedThreadPoolContext(4, "Dispatchers.Default")
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ ctx.dispatch(context, block)
+ }
+}
+
+private object MissingMainDispatcher : MainCoroutineDispatcher() {
+ override val immediate: MainCoroutineDispatcher
+ get() = notImplemented()
+ override fun dispatch(context: CoroutineContext, block: Runnable) = notImplemented()
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = notImplemented()
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) = notImplemented()
+
+ private fun notImplemented(): Nothing = TODO("Dispatchers.Main is missing on the current platform")
+}
+
+internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block()
diff --git a/kotlinx-coroutines-core/npm/README.md b/kotlinx-coroutines-core/npm/README.md
index 7f88ea39..868fb656 100644
--- a/kotlinx-coroutines-core/npm/README.md
+++ b/kotlinx-coroutines-core/npm/README.md
@@ -16,4 +16,4 @@ suspend fun main() = coroutineScope {
## Documentation
* [Guide to kotlinx.coroutines by example on JVM](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html) (**read it first**)
-* [Full kotlinx.coroutines API reference](https://kotlin.github.io/kotlinx.coroutines)
+* [Full kotlinx.coroutines API reference](https://kotlinlang.org/api/kotlinx.coroutines/)
diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md
index cd71f580..5f302d20 100644
--- a/kotlinx-coroutines-debug/README.md
+++ b/kotlinx-coroutines-debug/README.md
@@ -61,7 +61,7 @@ stacktraces will be dumped to the console.
### Using as JVM agent
Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup.
-You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.5.2.jar`.
+You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.6.4.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.
@@ -154,9 +154,8 @@ of the dump programmatically.
### Debug agent and Android
-Unfortunately, Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`.
-
-Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support android-gradle 3.3
+Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`,
+and it is not possible to use coroutine debugger along with Android emulator.
<!---
Make an exception googlable
@@ -265,22 +264,22 @@ More than one file was found with OS independent path 'win32-x86-64/attach_hotsp
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
<!--- MODULE kotlinx-coroutines-debug -->
<!--- INDEX kotlinx.coroutines.debug -->
-[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
-[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html
-[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html
-[DebugProbes.dumpCoroutinesInfo]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-info.html
-[DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html
-[DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html
-[DebugProbes.enableCreationStackTraces]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/enable-creation-stack-traces.html
+[DebugProbes]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
+[DebugProbes.install]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html
+[DebugProbes.dumpCoroutines]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html
+[DebugProbes.dumpCoroutinesInfo]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-info.html
+[DebugProbes.printJob]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html
+[DebugProbes.printScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html
+[DebugProbes.enableCreationStackTraces]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/enable-creation-stack-traces.html
<!--- INDEX kotlinx.coroutines.debug.junit4 -->
-[CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html
+[CoroutinesTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html
<!--- END -->
diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle
index 43d94d18..9165a41f 100644
--- a/kotlinx-coroutines-debug/build.gradle
+++ b/kotlinx-coroutines-debug/build.gradle
@@ -8,25 +8,18 @@ configurations {
shadowDeps // shaded dependencies, not included into the resulting .pom file
compileOnly.extendsFrom(shadowDeps)
runtimeOnly.extendsFrom(shadowDeps)
-
- /*
- * It is possible to extend a particular configuration with shadow,
- * but in that case it changes dependency type to "runtime" and resolves it
- * (so it cannot be further modified). Otherwise, shadow just ignores all dependencies.
- */
- shadow.extendsFrom(api) // shadow - resulting configuration with shaded jar file
- configureKotlinJvmPlatform(shadow)
}
dependencies {
compileOnly "junit:junit:$junit_version"
compileOnly "org.junit.jupiter:junit-jupiter-api:$junit5_version"
- testCompile "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
- testCompile "org.junit.platform:junit-platform-testkit:1.7.0"
+ 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"
}
@@ -38,15 +31,48 @@ java {
}
jar {
- manifest {
- attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
- attributes "Can-Redefine-Classes": "true"
+ setEnabled(false)
+}
+
+// This is a rough estimation of what shadow plugin has been doing with our default configuration prior to
+// 1.6.2: https://github.com/johnrengelman/shadow/blob/1ff12fc816629ae5bc331fa3889c8ecfcaee7b27/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy#L72-L82
+// We just emulate it here for backwards compatibility
+shadowJar.configure {
+ def classpath = project.objects.fileCollection().from { ->
+ project.configurations.findByName('runtimeClasspath')
+ }
+ doFirst {
+ manifest.attributes 'Class-Path': classpath.collect { "${it.name}" }.findAll { it }.join(' ')
}
}
-shadowJar {
+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)
+ }
+}
+
+def commonKoverExcludes =
+ // Never used, safety mechanism
+ ["kotlinx.coroutines.debug.internal.NoOpProbesKt"]
+
+tasks.koverHtmlReport {
+ excludes = commonKoverExcludes
+}
+
+tasks.koverVerify {
+ excludes = commonKoverExcludes
}
diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
index 190476c4..a26c1928 100644
--- a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
+++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
@@ -10,7 +10,6 @@ import kotlinx.coroutines.scheduling.*
import reactor.blockhound.*
import reactor.blockhound.integration.*
-@Suppress("UNUSED")
public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) {
@@ -19,6 +18,9 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
allowServiceLoaderInvocationsOnInit()
allowBlockingCallsInReflectionImpl()
allowBlockingCallsInDebugProbes()
+ allowBlockingCallsInWorkQueue()
+ // Stacktrace recovery cache is guarded by lock
+ allowBlockingCallsInside("kotlinx.coroutines.internal.ExceptionsConstructorKt", "tryCopyException")
/* The predicates that define that BlockHound should only report blocking calls from threads that are part of
the coroutine thread pool and currently execute a CPU-bound coroutine computation. */
addDynamicThreadPredicate { isSchedulerWorker(it) }
@@ -61,15 +63,20 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
}
/**
+ * Allow blocking calls inside [kotlinx.coroutines.scheduling.WorkQueue]
+ */
+ private fun BlockHound.Builder.allowBlockingCallsInWorkQueue() {
+ /** uses [Thread.yield] in a benign way. */
+ allowBlockingCallsInside("kotlinx.coroutines.scheduling.WorkQueue", "addLast")
+ }
+
+ /**
* Allows blocking inside [kotlinx.coroutines.internal.ThreadSafeHeap].
*/
private fun BlockHound.Builder.allowBlockingCallsInThreadSafeHeap() {
for (method in listOf("clear", "peek", "removeFirstOrNull", "addLast")) {
allowBlockingCallsInside("kotlinx.coroutines.internal.ThreadSafeHeap", method)
}
- // [addLastIf] is only used in [EventLoop.common]. Users of [removeFirstIf]:
- allowBlockingCallsInside("kotlinx.coroutines.test.TestCoroutineDispatcher", "doActionsUntil")
- allowBlockingCallsInside("kotlinx.coroutines.test.TestCoroutineContext", "triggerActions")
}
private fun BlockHound.Builder.allowBlockingCallsInFlow() {
@@ -110,7 +117,7 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
private fun BlockHound.Builder.allowBlockingCallsInArrayChannel() {
for (method in listOf(
"pollInternal", "isEmpty", "isFull", "isClosedForReceive", "offerInternal", "offerSelectInternal",
- "enqueueSend", "pollInternal", "pollSelectInternal", "enqueueReceiveInternal", "onCancelIdempotent"))
+ "enqueueSend", "pollSelectInternal", "enqueueReceiveInternal", "onCancelIdempotent"))
{
allowBlockingCallsInside("kotlinx.coroutines.channels.ArrayChannel", method)
}
@@ -133,7 +140,7 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
*/
private fun BlockHound.Builder.allowBlockingCallsInConflatedChannel() {
for (method in listOf("offerInternal", "offerSelectInternal", "pollInternal", "pollSelectInternal",
- "onCancelIdempotent"))
+ "onCancelIdempotent", "isEmpty", "enqueueReceiveInternal"))
{
allowBlockingCallsInside("kotlinx.coroutines.channels.ConflatedChannel", method)
}
diff --git a/kotlinx-coroutines-debug/test/BlockHoundTest.kt b/kotlinx-coroutines-debug/test/BlockHoundTest.kt
index 571daca1..3f588785 100644
--- a/kotlinx-coroutines-debug/test/BlockHoundTest.kt
+++ b/kotlinx-coroutines-debug/test/BlockHoundTest.kt
@@ -4,6 +4,7 @@ import kotlinx.coroutines.channels.*
import org.junit.*
import reactor.blockhound.*
+@Suppress("UnusedEquals", "DeferredResultUnused", "BlockingMethodInNonBlockingContext")
class BlockHoundTest : TestBase() {
@Before
@@ -12,21 +13,21 @@ class BlockHoundTest : TestBase() {
}
@Test(expected = BlockingOperationError::class)
- fun shouldDetectBlockingInDefault() = runTest {
+ fun testShouldDetectBlockingInDefault() = runTest {
withContext(Dispatchers.Default) {
Thread.sleep(1)
}
}
@Test
- fun shouldNotDetectBlockingInIO() = runTest {
+ fun testShouldNotDetectBlockingInIO() = runTest {
withContext(Dispatchers.IO) {
Thread.sleep(1)
}
}
@Test
- fun shouldNotDetectNonblocking() = runTest {
+ fun testShouldNotDetectNonblocking() = runTest {
withContext(Dispatchers.Default) {
val a = 1
val b = 2
@@ -54,7 +55,7 @@ class BlockHoundTest : TestBase() {
}
@Test
- fun testChannelsNotBeingConsideredBlocking() = runTest {
+ fun testChannelNotBeingConsideredBlocking() = runTest {
withContext(Dispatchers.Default) {
// Copy of kotlinx.coroutines.channels.ArrayChannelTest.testSimple
val q = Channel<Int>(1)
@@ -74,6 +75,24 @@ class BlockHoundTest : TestBase() {
}
}
+ @Test
+ fun testConflatedChannelsNotBeingConsideredBlocking() = runTest {
+ withContext(Dispatchers.Default) {
+ val q = Channel<Int>(Channel.CONFLATED)
+ check(q.isEmpty)
+ check(!q.isClosedForReceive)
+ check(!q.isClosedForSend)
+ val sender = launch {
+ q.send(1)
+ }
+ val receiver = launch {
+ q.receive() == 1
+ }
+ sender.join()
+ receiver.join()
+ }
+ }
+
@Test(expected = BlockingOperationError::class)
fun testReusingThreadsFailure() = runTest {
val n = 100
diff --git a/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt
new file mode 100644
index 00000000..4808470e
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+package kotlinx.coroutines.debug
+
+import com.google.gson.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.debug.internal.*
+import org.junit.Test
+import kotlin.coroutines.*
+import kotlin.test.*
+
+@ExperimentalStdlibApi
+class DumpCoroutineInfoAsJsonAndReferencesTest : DebugTestBase() {
+ private data class CoroutineInfoFromJson(
+ val name: String?,
+ val id: Long?,
+ val dispatcher: String?,
+ val sequenceNumber: Long?,
+ val state: String?
+ )
+
+ @Test
+ fun testDumpOfUnnamedCoroutine() =
+ runTestWithNamedDeferred(name = null)
+
+ @Test
+ fun testDumpOfNamedCoroutine() =
+ runTestWithNamedDeferred("Name")
+
+ @Test
+ fun testDumpWithNoCoroutines() {
+ val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences()
+ assertEquals(dumpResult.size, 4)
+ assertIsEmptyArray(dumpResult[1])
+ assertIsEmptyArray(dumpResult[2])
+ assertIsEmptyArray(dumpResult[3])
+ }
+
+ private fun assertIsEmptyArray(obj: Any) =
+ assertTrue(obj is Array<*> && obj.isEmpty())
+
+ private fun runTestWithNamedDeferred(name: String?) = runTest {
+ val context = if (name == null) EmptyCoroutineContext else CoroutineName(name)
+ val deferred = async(context) {
+ suspendingMethod()
+ assertTrue(true)
+ }
+ yield()
+ verifyDump()
+ deferred.cancelAndJoin()
+ }
+
+ private suspend fun suspendingMethod() {
+ delay(Long.MAX_VALUE)
+ }
+
+ private fun verifyDump() {
+ val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences()
+
+ assertEquals(dumpResult.size, 4)
+
+ val coroutinesInfoAsJsonString = dumpResult[0]
+ val lastObservedThreads = dumpResult[1]
+ val lastObservedFrames = dumpResult[2]
+ val coroutinesInfo = dumpResult[3]
+
+ assertTrue(coroutinesInfoAsJsonString is String)
+ assertTrue(lastObservedThreads is Array<*>)
+ assertTrue(lastObservedFrames is Array<*>)
+ assertTrue(coroutinesInfo is Array<*>)
+
+ val coroutinesInfoFromJson = Gson().fromJson(coroutinesInfoAsJsonString, Array<CoroutineInfoFromJson>::class.java)
+
+ val size = coroutinesInfo.size
+ assertTrue(size != 0)
+ assertEquals(size, coroutinesInfoFromJson.size)
+ assertEquals(size, lastObservedFrames.size)
+ assertEquals(size, lastObservedThreads.size)
+
+ for (i in 0 until size) {
+ val info = coroutinesInfo[i]
+ val infoFromJson = coroutinesInfoFromJson[i]
+ assertTrue(info is DebugCoroutineInfo)
+ assertEquals(info.lastObservedThread, lastObservedThreads[i])
+ assertEquals(info.lastObservedFrame, lastObservedFrames[i])
+ assertEquals(info.sequenceNumber, infoFromJson.sequenceNumber)
+ assertEquals(info.state, infoFromJson.state)
+ val context = info.context
+ assertEquals(context[CoroutineName.Key]?.name, infoFromJson.name)
+ assertEquals(context[CoroutineId.Key]?.id, infoFromJson.id)
+ assertEquals(context[CoroutineDispatcher.Key]?.toString(), infoFromJson.dispatcher)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt b/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt
new file mode 100644
index 00000000..fcf9f1a9
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/EnhanceStackTraceWithTreadDumpAsJsonTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+package kotlinx.coroutines.debug
+
+import com.google.gson.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.debug.internal.*
+import org.junit.Test
+import kotlin.test.*
+
+class EnhanceStackTraceWithTreadDumpAsJsonTest : DebugTestBase() {
+ private data class StackTraceElementInfoFromJson(
+ val declaringClass: String,
+ val methodName: String,
+ val fileName: String?,
+ val lineNumber: Int
+ )
+
+ @Test
+ fun testEnhancedStackTraceFormatWithDeferred() = runTest {
+ val deferred = async {
+ suspendingMethod()
+ assertTrue(true)
+ }
+ yield()
+
+ val coroutineInfo = DebugProbesImpl.dumpCoroutinesInfo()
+ assertEquals(coroutineInfo.size, 2)
+ val info = coroutineInfo[1]
+ val enhancedStackTraceAsJsonString = DebugProbesImpl.enhanceStackTraceWithThreadDumpAsJson(info)
+ val enhancedStackTraceFromJson = Gson().fromJson(enhancedStackTraceAsJsonString, Array<StackTraceElementInfoFromJson>::class.java)
+ val enhancedStackTrace = DebugProbesImpl.enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace)
+ assertEquals(enhancedStackTrace.size, enhancedStackTraceFromJson.size)
+ for ((frame, frameFromJson) in enhancedStackTrace.zip(enhancedStackTraceFromJson)) {
+ assertEquals(frame.className, frameFromJson.declaringClass)
+ assertEquals(frame.methodName, frameFromJson.methodName)
+ assertEquals(frame.fileName, frameFromJson.fileName)
+ assertEquals(frame.lineNumber, frameFromJson.lineNumber)
+ }
+
+ deferred.cancelAndJoin()
+ }
+
+ private suspend fun suspendingMethod() {
+ delay(Long.MAX_VALUE)
+ }
+}
diff --git a/kotlinx-coroutines-debug/test/StacktraceUtils.kt b/kotlinx-coroutines-debug/test/StacktraceUtils.kt
index 8c591ebd..9cc626f1 100644
--- a/kotlinx-coroutines-debug/test/StacktraceUtils.kt
+++ b/kotlinx-coroutines-debug/test/StacktraceUtils.kt
@@ -90,7 +90,8 @@ private fun cleanBlockHoundTraces(frames: List<String>): List<String> {
public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
val baos = ByteArrayOutputStream()
DebugProbes.dumpCoroutines(PrintStream(baos))
- val trace = baos.toString().split("\n\n")
+ val wholeDump = baos.toString()
+ val trace = wholeDump.split("\n\n")
if (traces.isEmpty()) {
val filtered = trace.filter { ignoredCoroutine == null || !it.contains(ignoredCoroutine) }
assertEquals(1, filtered.count())
@@ -105,7 +106,7 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
- assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input")
+ assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input. Whole dump:\n$wholeDump")
expected.withIndex().forEach { (index, trace) ->
val actualTrace = actual[index].trimStackTrace().sanitizeAddresses()
@@ -113,7 +114,7 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
val actualLines = cleanBlockHoundTraces(actualTrace.split("\n"))
val expectedLines = expectedTrace.split("\n")
for (i in expectedLines.indices) {
- assertEquals(expectedLines[i], actualLines[i])
+ assertEquals(expectedLines[i], actualLines[i], "Whole dump:\n$wholeDump")
}
}
}
diff --git a/kotlinx-coroutines-debug/test/ToStringTest.kt b/kotlinx-coroutines-debug/test/ToStringTest.kt
index 0a9e84ef..0ea412b5 100644
--- a/kotlinx-coroutines-debug/test/ToStringTest.kt
+++ b/kotlinx-coroutines-debug/test/ToStringTest.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import org.junit.*
import org.junit.Test
+import java.io.*
import kotlin.coroutines.*
import kotlin.test.*
@@ -105,6 +106,8 @@ class ToStringTest : TestBase() {
expect(6)
assertEquals(expected, DebugProbes.jobToString(root).trimEnd().trimStackTrace().trimPackage())
assertEquals(expected, DebugProbes.scopeToString(CoroutineScope(root)).trimEnd().trimStackTrace().trimPackage())
+ assertEquals(expected, printToString { DebugProbes.printScope(CoroutineScope(root), it) }.trimEnd().trimStackTrace().trimPackage())
+ assertEquals(expected, printToString { DebugProbes.printJob(root, it) }.trimEnd().trimStackTrace().trimPackage())
root.cancelAndJoin()
finish(7)
@@ -145,4 +148,12 @@ class ToStringTest : TestBase() {
}
}
}
+
+ private inline fun printToString(block: (PrintStream) -> Unit): String {
+ val baos = ByteArrayOutputStream()
+ val ps = PrintStream(baos)
+ block(ps)
+ ps.close()
+ return baos.toString()
+ }
}
diff --git a/kotlinx-coroutines-test/MIGRATION.md b/kotlinx-coroutines-test/MIGRATION.md
new file mode 100644
index 00000000..10c197f4
--- /dev/null
+++ b/kotlinx-coroutines-test/MIGRATION.md
@@ -0,0 +1,447 @@
+# Migration to the new kotlinx-coroutines-test API
+
+In version 1.6.0, the API of the test module changed significantly.
+This is a guide for gradually adapting the existing test code to the new API.
+This guide is written step-by-step; the idea is to separate the migration into several sets of small changes.
+
+## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations
+
+We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that
+you don't need to do anything for this section.
+
+### `UncaughtExceptionCaptor`
+
+If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler`
+was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure
+was called.
+
+We currently don't provide a replacement for this.
+However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines
+are propagated structurally, which makes uncaught exception handlers less useful.
+
+If you have a use case for this, please tell us about it at the issue tracker.
+Meanwhile, it should be possible to use a custom exception captor, which should only implement
+`CoroutineExceptionHandler` now, like this:
+
+```kotlin
+@Test
+fun testFoo() = runTest {
+ val customCaptor = MyUncaughtExceptionCaptor()
+ launch(customCaptor) {
+ // ...
+ }
+ advanceUntilIdle()
+ customCaptor.cleanupTestCoroutines()
+}
+```
+
+### `DelayController`
+
+We don't provide a way to define custom dispatching strategies that support virtual time.
+That said, we significantly enhanced this mechanism:
+* Using multiple test dispatchers simultaneously is supported.
+ For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be
+ passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test
+ dispatcher.
+* Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided.
+
+If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue
+tracker.
+
+### `TestCoroutineScope`
+
+This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of
+`TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used.
+So, there could be two reasons for defining a custom implementation:
+
+* Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function.
+ These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and
+ `ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining
+ conforming instances. In this case, follow the instructions about replacing them.
+* Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else
+ accepts a `TestCoroutineScope` specifically as an argument.
+
+## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions`
+
+It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of
+`TestCoroutineExceptionHandler` include:
+
+* Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions
+ *yet*.
+ If there are any, they will be thrown by the cleanup procedure anyway.
+ We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the
+ following one.
+* Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected.
+ In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later.
+ It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be
+ found by the cleanup procedure are not superseded by the exceptions that are expected.
+ An example is shown below.
+
+```kotlin
+val exceptions = mutableListOf<Throwable>()
+val customCaptor = CoroutineExceptionHandler { ctx, throwable ->
+ exceptions.add(throwable) // add proper synchronization if the test is multithreaded
+}
+
+@Test
+fun testFoo() = runTest {
+ launch(customCaptor) {
+ // ...
+ }
+ advanceUntilIdle()
+ // check the list of the caught exceptions
+}
+```
+
+## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope`
+
+This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`.
+If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case,
+also pass this scheduler as the argument to the dispatcher.
+
+## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher`
+
+* In places where `pauseDispatcher` in its block form is called, replace it with a call to
+ `withContext(StandardTestDispatcher(testScheduler))`
+ (`testScheduler` is available as a field of `TestCoroutineScope`,
+ or `scheduler` is available as a field of `TestCoroutineDispatcher`),
+ followed by `advanceUntilIdle()`.
+ This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused
+ when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`.
+* Often, `pauseDispatcher()` in a non-block form is used at the start of the test.
+ Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`,
+ if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used,
+ or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`.
+ This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming,
+ instead of the deprecated `TestCoroutineDispatcher`.
+* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test.
+ In this case, attempt to wrap everything until the next `resumeDispatcher()` in
+ a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of
+ `StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where
+ execution happens).
+
+## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()`
+
+For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated.
+It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the
+tasks scheduled *at* `currentTime + n`.
+
+There is an automatic replacement for this deprecation, which produces correct but inelegant code.
+
+Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not
+encounter this edge case.
+
+## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())`
+
+This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with
+`TestScope`.
+
+Significant differences of `runTest` from `runBlockingTest` are each given a section below.
+
+### It works properly with other dispatchers and asynchronous completions.
+
+No action on your part is required, other than replacing `runBlocking` with `runTest` as well.
+
+### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`.
+
+By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
+variant of `TestCoroutineDispatcher` should be used.
+This version of the dispatcher has the property of eagerly entering `launch` and `async` blocks:
+code until the first suspension is executed without dispatching.
+
+There are two common ways in which this property is useful.
+
+#### `TestCoroutineDispatcher` for the top-level coroutine
+
+Some tests that rely on `launch` and `async` blocks being entered immediately have a form similar to this:
+```kotlin
+runTest(TestCoroutineDispatcher()) {
+ launch {
+ updateSomething()
+ }
+ checkThatSomethingWasUpdated()
+ launch {
+ updateSomethingElse()
+ }
+ checkThatSomethingElseWasUpdated()
+}
+```
+
+If the `TestCoroutineDispatcher()` is simply removed, `StandardTestDispatcher()` will be used, which will cause
+the test to fail.
+
+In these cases, `UnconfinedTestDispatcher()` should be used.
+We ensured that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
+blocks.
+
+Note though that *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
+any guarantees about their dispatching order.
+
+#### `TestCoroutineDispatcher` for testing intermediate emissions
+
+Some code tests `StateFlow` or channels in a manner similar to this:
+
+```kotlin
+@Test
+fun testAllEmissions() = runTest(TestCoroutineDispatcher()) {
+ val values = mutableListOf<Int>()
+ val stateFlow = MutableStateFlow(0)
+ val job = launch {
+ stateFlow.collect {
+ values.add(it)
+ }
+ }
+ stateFlow.value = 1
+ stateFlow.value = 2
+ stateFlow.value = 3
+ job.cancel()
+ // each assignment will immediately resume the collecting child coroutine,
+ // so no values will be skipped.
+ assertEquals(listOf(0, 1, 2, 3), values)
+}
+```
+
+Such code will fail when `TestCoroutineDispatcher()` is not used: not every emission will be listed.
+In this particular case, none will be listed at all.
+
+The reason for this is that setting `stateFlow.value` (as is sending to a channel, as are some other things) wakes up
+the coroutine waiting for the new value, but *typically* does not immediately run the collecting code, instead simply
+dispatching it.
+The exceptions are the coroutines running in dispatchers that don't (always) go through a dispatch,
+`Dispatchers.Unconfined`, `Dispatchers.Main.immediate`, `UnconfinedTestDispatcher`, or `TestCoroutineDispatcher` in
+the unpaused state.
+
+Therefore, a solution is to launch the collection in an unconfined dispatcher:
+
+```kotlin
+@Test
+fun testAllEmissions() = runTest {
+ val values = mutableListOf<Int>()
+ val stateFlow = MutableStateFlow(0)
+ val job = launch(UnconfinedTestDispatcher(testScheduler)) { // <------
+ stateFlow.collect {
+ values.add(it)
+ }
+ }
+ stateFlow.value = 1
+ stateFlow.value = 2
+ stateFlow.value = 3
+ job.cancel()
+ // each assignment will immediately resume the collecting child coroutine,
+ // so no values will be skipped.
+ assertEquals(listOf(0, 1, 2, 3), values)
+}
+```
+
+Note that `testScheduler` is passed so that the unconfined dispatcher is linked to `runTest`.
+Also, note that `UnconfinedTestDispatcher` is not passed to `runTest`.
+This is due to the fact that, *inside* the `UnconfinedTestDispatcher`, there are no execution order guarantees,
+so it would not be guaranteed that setting `stateFlow.value` would immediately run the collecting code
+(though in this case, it does).
+
+#### Other considerations
+
+Using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
+did, but it's still possible that the test relies on the specific dispatching order of `TestCoroutineDispatcher`,
+so it will need to be tweaked.
+
+If some code is expected to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
+at this moment of time to run.
+For example, the `StateFlow` example above can also be forced to succeed by doing this:
+
+```kotlin
+@Test
+fun testAllEmissions() = runTest {
+ val values = mutableListOf<Int>()
+ val stateFlow = MutableStateFlow(0)
+ val job = launch {
+ stateFlow.collect {
+ values.add(it)
+ }
+ }
+ runCurrent()
+ stateFlow.value = 1
+ runCurrent()
+ stateFlow.value = 2
+ runCurrent()
+ stateFlow.value = 3
+ runCurrent()
+ job.cancel()
+ // each assignment will immediately resume the collecting child coroutine,
+ // so no values will be skipped.
+ assertEquals(listOf(0, 1, 2, 3), values)
+}
+```
+
+Be wary though of this approach: using `runCurrent`, `advanceTimeBy`, or `advanceUntilIdle` is, essentially,
+simulating some particular execution order, which is not guaranteed to happen in production code.
+For example, using `UnconfinedTestDispatcher` to fix this test reflects how, in production code, one could use
+`Dispatchers.Unconfined` to observe all emitted values without conflation, but the `runCurrent()` approach only
+states that the behavior would be observed if a dispatch were to happen at some chosen points.
+It is, therefore, recommended to structure tests in a way that does not rely on a particular interleaving, unless
+that is the intention.
+
+### The job hierarchy is completely different.
+
+- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the
+ created coroutine.
+- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`.
+- The job passed as an argument is used as a parent job.
+
+Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a
+`SupervisorJob`; this should make the job hierarchy resemble what it used to be.
+
+```kotlin
+@Test
+fun testFoo() = runTest {
+ val deferred = async(SupervisorJob()) {
+ // test code
+ }
+ advanceUntilIdle()
+ deferred.getCompletionExceptionOrNull()?.let {
+ throw it
+ }
+}
+```
+
+### Only a single call to `runTest` is permitted per test.
+
+In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned
+immediately:
+
+```kotlin
+@Test
+fun testFoo(): TestResult {
+ // arbitrary code here
+ return runTest {
+ // ...
+ }
+}
+```
+
+When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported.
+Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue
+tracker.
+
+### It uses `TestScope`, not `TestCoroutineScope`, by default.
+
+There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating
+from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and
+`TestScope` will not suffice.
+
+## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest`
+
+Likely can be done together with the next step.
+
+Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base.
+Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside
+the `runTest` block.
+
+The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup.
+If a test must check that no other delays are remaining after it has finished, the following form may help:
+```kotlin
+runTest {
+ testBody()
+ val timeAfterTest = currentTime()
+ advanceUntilIdle() // run the remaining tasks
+ assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment
+}
+```
+Note that this will report time advancement even if the job scheduled at a later point was cancelled.
+
+It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens
+outside the test itself.
+In this case, we propose that you write a wrapper of the form:
+
+```kotlin
+fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest {
+ try {
+ body()
+ } finally {
+ // the usual cleanup procedures that used to happen before `cleanupTestCoroutines`
+ }
+}
+```
+
+## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope`
+
+Also, replace `runTestWithLegacyScope` with just `runTest`.
+All of this can be done in parallel with replacing `runBlockingTest` with `runTest`.
+
+This step should remove all uses of `TestCoroutineScope`, explicit or implicit.
+
+Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be
+straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it.
+Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest`
+handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of
+`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them.
+
+Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`,
+and its usages should have been removed during the previous step.
+
+## Replace `runBlocking` with `runTest`
+
+Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful.
+As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other
+threads, like `Dispatchers.IO` or `Dispatchers.Default`.
+
+## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher`
+
+`TestCoroutineDispatcher` is a dispatcher with two modes:
+* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks.
+* ("paused") Behaving like a `StandardTestDispatcher`.
+
+In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the
+implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to
+`runTest`.
+
+Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate.
+
+## Simplify code by removing unneeded entities
+
+Likely, now some code has the form
+
+```kotlin
+val dispatcher = StandardTestDispatcher()
+val scope = TestScope(dispatcher)
+
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(dispatcher)
+}
+
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+}
+
+@Test
+fun testFoo() = scope.runTest {
+ // ...
+}
+```
+
+The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for
+`Dispatchers.Main`.
+
+However, now this can be simplified to just
+
+```kotlin
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+}
+
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+}
+
+@Test
+fun testFoo() = runTest {
+ // ...
+}
+```
+
+The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
+the current `Dispatchers.Main`.
diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md
index 43ae18f5..f45ccd0c 100644
--- a/kotlinx-coroutines-test/README.md
+++ b/kotlinx-coroutines-test/README.md
@@ -2,35 +2,54 @@
Test utilities for `kotlinx.coroutines`.
-This package provides testing utilities for effectively testing coroutines.
+## Overview
+
+This package provides utilities for efficiently testing coroutines.
+
+| Name | Description |
+| ---- | ----------- |
+| [runTest] | Runs the test code, automatically skipping delays and handling uncaught exceptions. |
+| [TestCoroutineScheduler] | The shared source of virtual time, used for controlling execution order and skipping delays. |
+| [TestScope] | A [CoroutineScope] that integrates with [runTest], providing access to [TestCoroutineScheduler]. |
+| [TestDispatcher] | A [CoroutineDispatcher] whose delays are controlled by a [TestCoroutineScheduler]. |
+| [Dispatchers.setMain] | Mocks the main dispatcher using the provided one. If mocked with a [TestDispatcher], its [TestCoroutineScheduler] is used everywhere by default. |
+
+Provided [TestDispatcher] implementations:
+
+| Name | Description |
+| ---- | ----------- |
+| [StandardTestDispatcher] | A simple dispatcher with no special behavior other than being linked to a [TestCoroutineScheduler]. |
+| [UnconfinedTestDispatcher] | A dispatcher that behaves like [Dispatchers.Unconfined]. |
## Using in your project
Add `kotlinx-coroutines-test` to your project test dependencies:
```
dependencies {
- testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
}
```
-**Do not** depend on this project in your main sources, all utilities are intended and designed to be used only from tests.
+**Do not** depend on this project in your main sources, all utilities here are intended and designed to be used only from tests.
## Dispatchers.Main Delegation
-`Dispatchers.setMain` will override the `Main` dispatcher in test situations. This is helpful when you want to execute a
-test in situations where the platform `Main` dispatcher is not available, or you wish to replace `Dispatchers.Main` with a
-testing dispatcher.
+`Dispatchers.setMain` will override the `Main` dispatcher in test scenarios.
+This is helpful when one wants to execute a test in situations where the platform `Main` dispatcher is not available,
+or to replace `Dispatchers.Main` with a testing dispatcher.
-Once you have this dependency in the runtime,
-[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism will overwrite
-[Dispatchers.Main] with a testable implementation.
+On the JVM,
+the [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism is responsible
+for overwriting [Dispatchers.Main] with a testable implementation, which by default will delegate its calls to the real
+`Main` dispatcher, if any.
-You can override the `Main` implementation using [setMain][setMain] method with any [CoroutineDispatcher] implementation, e.g.:
+The `Main` implementation can be overridden using [Dispatchers.setMain][setMain] method with any [CoroutineDispatcher]
+implementation, e.g.:
```kotlin
class SomeTest {
-
+
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
@Before
@@ -40,10 +59,10 @@ class SomeTest {
@After
fun tearDown() {
- Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
+ Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
-
+
@Test
fun testSomeUI() = runBlocking {
launch(Dispatchers.Main) { // Will be launched in the mainThreadSurrogate dispatcher
@@ -52,372 +71,289 @@ class SomeTest {
}
}
```
-Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. The testable version of
-`Dispatchers.Main` installed by the `ServiceLoader` will delegate to the dispatcher provided by `setMain`.
-## runBlockingTest
+Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally.
-To test regular suspend functions or coroutines started with `launch` or `async` use the [runBlockingTest] coroutine
-builder that provides extra test control to coroutines.
+If `Main` is overridden with a [TestDispatcher], then its [TestCoroutineScheduler] is used when new [TestDispatcher] or
+[TestScope] instances are created without [TestCoroutineScheduler] being passed as an argument.
-1. Auto-advancing of time for regular suspend functions
-2. Explicit time control for testing multiple coroutines
-3. Eager execution of `launch` or `async` code blocks
-4. Pause, manually advance, and restart the execution of coroutines in a test
-5. Report uncaught exceptions as test failures
+## runTest
-### Testing regular suspend functions
+[runTest] is the way to test code that involves coroutines. `suspend` functions can be called inside it.
-To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing
-coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed.
+**IMPORTANT: in order to work with on Kotlin/JS, the result of `runTest` must be immediately `return`-ed from each test.**
+The typical invocation of [runTest] thus looks like this:
```kotlin
@Test
-fun testFoo() = runBlockingTest { // a coroutine with an extra test control
- val actual = foo()
- // ...
+fun testFoo() = runTest {
+ // code under test
}
+```
-suspend fun foo() {
- delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest
- // ...
+In more advanced scenarios, it's possible instead to use the following form:
+```kotlin
+@Test
+fun testFoo(): TestResult {
+ // initialize some test state
+ return runTest {
+ // code under test
+ }
}
```
-`runBlockingTest` returns `Unit` so it may be used in a single expression with common testing libraries.
+[runTest] is similar to running the code with `runBlocking` on Kotlin/JVM and Kotlin/Native, or launching a new promise
+on Kotlin/JS. The main differences are the following:
-### Testing `launch` or `async`
+* **The calls to `delay` are automatically skipped**, preserving the relative execution order of the tasks. This way,
+ it's possible to make tests finish more-or-less immediately.
+* **Controlling the virtual time**: in case just skipping delays is not sufficient, it's possible to more carefully
+ guide the execution, advancing the virtual time by a duration, draining the queue of the awaiting tasks, or running
+ the tasks scheduled at the present moment.
+* **Handling uncaught exceptions** spawned in the child coroutines by throwing them at the end of the test.
+* **Waiting for asynchronous callbacks**.
+ Sometimes, especially when working with third-party code, it's impossible to mock all the dispatchers in use.
+ [runTest] will handle the situations where some code runs in dispatchers not integrated with the test module.
-Inside of [runBlockingTest], both [launch] and [async] will start a new coroutine that may run concurrently with the
-test case.
+## Delay-skipping
-To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until
-the first call to [delay] or [yield].
+To test regular suspend functions, which may have a delay, just run them inside the [runTest] block.
```kotlin
@Test
-fun testFooWithLaunch() = runBlockingTest {
- foo()
- // the coroutine launched by foo() is completed before foo() returns
+fun testFoo() = runTest { // a coroutine with an extra test control
+ val actual = foo()
// ...
}
-fun CoroutineScope.foo() {
- // This coroutines `Job` is not shared with the test code
- launch {
- bar() // executes eagerly when foo() is called due to runBlockingTest
- println(1) // executes eagerly when foo() is called due to runBlockingTest
- }
+suspend fun foo() {
+ delay(1_000) // when run in `runTest`, will finish immediately instead of delaying
+ // ...
}
-
-suspend fun bar() {}
```
-`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines
-are not able to complete, an [UncompletedCoroutinesError] will be thrown.
-
-*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters.
+## `launch` and `async`
-### Testing `launch` or `async` with `delay`
+The coroutine dispatcher used for tests is single-threaded, meaning that the child coroutines of the [runTest] block
+will run on the thread that started the test, and will never run in parallel.
-If the coroutine created by `launch` or `async` calls `delay` then the [runBlockingTest] will not auto-progress time
-right away. This allows tests to observe the interaction of multiple coroutines with different delays.
-
-To control time in the test you can use the [DelayController] interface. The block passed to
-[runBlockingTest] can call any method on the `DelayController` interface.
+If several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen.
+The virtual time will automatically advance to the point of its resumption.
```kotlin
@Test
-fun testFooWithLaunchAndDelay() = runBlockingTest {
- foo()
- // the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000)
- advanceTimeBy(1_000) // progress time, this will cause the delay to resume
- // the coroutine launched by foo has completed here
- // ...
-}
-
-suspend fun CoroutineScope.foo() {
+fun testWithMultipleDelays() = runTest {
launch {
- println(1) // executes eagerly when foo() is called due to runBlockingTest
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeBy(1_000)
+ delay(1_000)
+ println("1. $currentTime") // 1000
+ delay(200)
+ println("2. $currentTime") // 1200
+ delay(2_000)
+ println("4. $currentTime") // 3200
}
+ val deferred = async {
+ delay(3_000)
+ println("3. $currentTime") // 3000
+ delay(500)
+ println("5. $currentTime") // 3500
+ }
+ deferred.await()
}
```
-*Note:* `runBlockingTest` will always attempt to auto-progress time until all coroutines are completed just before
-exiting. This is a convenience to avoid having to call [advanceUntilIdle][DelayController.advanceUntilIdle]
-as the last line of many common test cases.
-If any coroutines cannot complete by advancing time, an [UncompletedCoroutinesError] is thrown.
-
-### Testing `withTimeout` using `runBlockingTest`
-
-Time control can be used to test timeout code. To do so, ensure that the function under test is suspended inside a
-`withTimeout` block and advance time until the timeout is triggered.
+## Controlling the virtual time
-Depending on the code, causing the code to suspend may need to use different mocking or fake techniques. For this
-example an uncompleted `Deferred<Foo>` is provided to the function under test via parameter injection.
+Inside [runTest], the following operations are supported:
+* `currentTime` gets the current virtual time.
+* `runCurrent()` runs the tasks that are scheduled at this point of virtual time.
+* `advanceUntilIdle()` runs all enqueued tasks until there are no more.
+* `advanceTimeBy(timeDelta)` runs the enqueued tasks until the current virtual time advances by `timeDelta`.
```kotlin
-@Test(expected = TimeoutCancellationException::class)
-fun testFooWithTimeout() = runBlockingTest {
- val uncompleted = CompletableDeferred<Foo>() // this Deferred<Foo> will never complete
- foo(uncompleted)
- advanceTimeBy(1_000) // advance time, which will cause the timeout to throw an exception
- // ...
-}
-
-fun CoroutineScope.foo(resultDeferred: Deferred<Foo>) {
+@Test
+fun testFoo() = runTest {
launch {
- withTimeout(1_000) {
- resultDeferred.await() // await() will suspend forever waiting for uncompleted
- // ...
- }
+ println(1) // executes during runCurrent()
+ delay(1_000) // suspends until time is advanced by at least 1_000
+ println(2) // executes during advanceTimeBy(2_000)
+ delay(500) // suspends until the time is advanced by another 500 ms
+ println(3) // also executes during advanceTimeBy(2_000)
+ delay(5_000) // will suspend by another 4_500 ms
+ println(4) // executes during advanceUntilIdle()
}
+ // the child coroutine has not run yet
+ runCurrent()
+ // the child coroutine has called println(1), and is suspended on delay(1_000)
+ advanceTimeBy(2_000) // progress time, this will cause two calls to `delay` to resume
+ // the child coroutine has called println(2) and println(3) and suspends for another 4_500 virtual milliseconds
+ advanceUntilIdle() // will run the child coroutine to completion
+ assertEquals(6500, currentTime) // the child coroutine finished at virtual time of 6_500 milliseconds
}
```
-*Note:* Testing timeouts is simpler with a second coroutine that can be suspended (as in this example). If the
-call to `withTimeout` is in a regular suspend function, consider calling `launch` or `async` inside your test body to
-create a second coroutine.
+## Using multiple test dispatchers
-### Using `pauseDispatcher` for explicit execution of `runBlockingTest`
+The virtual time is controlled by an entity called the [TestCoroutineScheduler], which behaves as the shared source of
+virtual time.
-The eager execution of `launch` and `async` bodies makes many tests easier, but some tests need more fine grained
-control of coroutine execution.
+Several dispatchers can be created that use the same [TestCoroutineScheduler], in which case they will share their
+knowledge of the virtual time.
-To disable eager execution, you can call [pauseDispatcher][DelayController.pauseDispatcher]
-to pause the [TestCoroutineDispatcher] that [runBlockingTest] uses.
-
-When the dispatcher is paused, all coroutines will be added to a queue instead running. In addition, time will never
-auto-progress due to `delay` on a paused dispatcher.
+To access the scheduler used for this test, use the [TestScope.testScheduler] property.
```kotlin
@Test
-fun testFooWithPauseDispatcher() = runBlockingTest {
- pauseDispatcher {
- foo()
- // the coroutine started by foo has not run yet
- runCurrent() // the coroutine started by foo advances to delay(1_000)
- // the coroutine started by foo has called println(1), and is suspended on delay(1_000)
- advanceTimeBy(1_000) // progress time, this will cause the delay to resume
- // the coroutine started by foo has called println(2) and has completed here
- }
- // ...
-}
-
-fun CoroutineScope.foo() {
- launch {
- println(1) // executes after runCurrent() is called
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeBy(1_000)
+fun testWithMultipleDispatchers() = runTest {
+ val scheduler = testScheduler // the scheduler used for this test
+ val dispatcher1 = StandardTestDispatcher(scheduler, name = "IO dispatcher")
+ val dispatcher2 = StandardTestDispatcher(scheduler, name = "Background dispatcher")
+ launch(dispatcher1) {
+ delay(1_000)
+ println("1. $currentTime") // 1000
+ delay(200)
+ println("2. $currentTime") // 1200
+ delay(2_000)
+ println("4. $currentTime") // 3200
+ }
+ val deferred = async(dispatcher2) {
+ delay(3_000)
+ println("3. $currentTime") // 3000
+ delay(500)
+ println("5. $currentTime") // 3500
+ }
+ deferred.await()
}
-}
```
-Using `pauseDispatcher` gives tests explicit control over the progress of time as well as the ability to enqueue all
-coroutines. As a best practice consider adding two tests, one paused and one eager, to test coroutines that have
-non-trivial external dependencies and side effects in their launch body.
-
-*Important:* When passed a lambda block, `pauseDispatcher` will resume eager execution immediately after the block.
-This will cause time to auto-progress if there are any outstanding `delay` calls that were not resolved before the
-`pauseDispatcher` block returned. In advanced situations tests can call [pauseDispatcher][DelayController.pauseDispatcher]
-without a lambda block and then explicitly resume the dispatcher with [resumeDispatcher][DelayController.resumeDispatcher].
+**Note: if [Dispatchers.Main] is replaced by a [TestDispatcher], [runTest] will automatically use its scheduler.
+This is done so that there is no need to go through the ceremony of passing the correct scheduler to [runTest].**
-## Integrating tests with structured concurrency
+## Accessing the test coroutine scope
-Code that uses structured concurrency needs a [CoroutineScope] in order to launch a coroutine. In order to integrate
-[runBlockingTest] with code that uses common structured concurrency patterns tests can provide one (or both) of these
-classes to application code.
+Structured concurrency ties coroutines to scopes in which they are launched.
+[TestScope] is a special coroutine scope designed for testing coroutines, and a new one is automatically created
+for [runTest] and used as the receiver for the test body.
- | Name | Description |
- | ---- | ----------- |
- | [TestCoroutineScope] | A [CoroutineScope] which provides detailed control over the execution of coroutines for tests and integrates with [runBlockingTest]. |
- | [TestCoroutineDispatcher] | A [CoroutineDispatcher] which can be used for tests and integrates with [runBlockingTest]. |
-
- Both classes are provided to allow for various testing needs. Depending on the code that's being
- tested, it may be easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] will accept
- a [TestCoroutineDispatcher] but not a [TestCoroutineScope].
-
- [TestCoroutineScope] will always use a [TestCoroutineDispatcher] to execute coroutines. It
- also uses [TestCoroutineExceptionHandler] to convert uncaught exceptions into test failures.
+However, it can be convenient to access a `CoroutineScope` before the test has started, for example, to perform mocking
+of some
+parts of the system in `@BeforeTest` via dependency injection.
+In these cases, it is possible to manually create [TestScope], the scope for the test coroutines, in advance,
+before the test begins.
-By providing [TestCoroutineScope] a test case is able to control execution of coroutines, as well as ensure that
-uncaught exceptions thrown by coroutines are converted into test failures.
+[TestScope] on its own does not automatically run the code launched in it.
+In addition, it is stateful in order to keep track of executing coroutines and uncaught exceptions.
+Therefore, it is important to ensure that [TestScope.runTest] is called eventually.
-### Providing `TestCoroutineScope` from `runBlockingTest`
+```kotlin
+val scope = TestScope()
-In simple cases, tests can use the [TestCoroutineScope] created by [runBlockingTest] directly.
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler))
+ TestSubject.setScope(scope)
+}
-```kotlin
-@Test
-fun testFoo() = runBlockingTest {
- foo() // runBlockingTest passed in a TestCoroutineScope as this
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+ TestSubject.resetScope()
}
-fun CoroutineScope.foo() {
- launch { // CoroutineScope for launch is the TestCoroutineScope provided by runBlockingTest
- // ...
- }
+@Test
+fun testSubject() = scope.runTest {
+ // the receiver here is `testScope`
}
```
-This style is preferred when the `CoroutineScope` is passed through an extension function style.
+## Eagerly entering `launch` and `async` blocks
-### Providing an explicit `TestCoroutineScope`
+Some tests only test functionality and don't particularly care about the precise order in which coroutines are
+dispatched.
+In these cases, it can be cumbersome to always call [runCurrent] or [yield] to observe the effects of the coroutines
+after they are launched.
-In many cases, the direct style is not preferred because [CoroutineScope] may need to be provided through another means
-such as dependency injection or service locators.
-
-Tests can declare a [TestCoroutineScope] explicitly in the class to support these use cases.
-
-Since [TestCoroutineScope] is stateful in order to keep track of executing coroutines and uncaught exceptions, it is
-important to ensure that [cleanupTestCoroutines][TestCoroutineScope.cleanupTestCoroutines] is called after every test case.
+If [runTest] executes with an [UnconfinedTestDispatcher], the child coroutines launched at the top level are entered
+*eagerly*, that is, they don't go through a dispatch until the first suspension.
```kotlin
-class TestClass {
- private val testScope = TestCoroutineScope()
- private lateinit var subject: Subject
-
- @Before
- fun setup() {
- // provide the scope explicitly, in this example using a constructor parameter
- subject = Subject(testScope)
- }
-
- @After
- fun cleanUp() {
- testScope.cleanupTestCoroutines()
- }
-
- @Test
- fun testFoo() = testScope.runBlockingTest {
- // TestCoroutineScope.runBlockingTest uses the Dispatcher and exception handler provided by `testScope`
- subject.foo()
- }
-}
-
-class Subject(val scope: CoroutineScope) {
- fun foo() {
- scope.launch {
- // launch uses the testScope injected in setup
- }
+@Test
+fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered = false
+ val deferred = CompletableDeferred<Unit>()
+ var completed = false
+ launch {
+ entered = true
+ deferred.await()
+ completed = true
}
+ assertTrue(entered) // `entered = true` already executed.
+ assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ deferred.complete(Unit) // resume the coroutine.
+ assertTrue(completed) // now the child coroutine is immediately completed.
}
```
-*Note:* [TestCoroutineScope], [TestCoroutineDispatcher], and [TestCoroutineExceptionHandler] are interfaces to enable
-test libraries to provide library specific integrations. For example, a JUnit4 `@Rule` may call
-[Dispatchers.setMain][setMain] then expose [TestCoroutineScope] for use in tests.
-
-### Providing an explicit `TestCoroutineDispatcher`
-
-While providing a [TestCoroutineScope] is slightly preferred due to the improved uncaught exception handling, there are
-many situations where it is easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain]
-does not accept a [TestCoroutineScope] and requires a [TestCoroutineDispatcher] to control coroutine execution in
-tests.
-
-The main difference between `TestCoroutineScope` and `TestCoroutineDispatcher` is how uncaught exceptions are handled.
-When using `TestCoroutineDispatcher` uncaught exceptions thrown in coroutines will use regular
-[coroutine exception handling](https://kotlinlang.org/docs/reference/coroutines/exception-handling.html).
-`TestCoroutineScope` will always use `TestCoroutineDispatcher` as it's dispatcher.
-
-A test can use a `TestCoroutineDispatcher` without declaring an explicit `TestCoroutineScope`. This is preferred
-when the class under test allows a test to provide a [CoroutineDispatcher] but does not allow the test to provide a
-[CoroutineScope].
-
-Since [TestCoroutineDispatcher] is stateful in order to keep track of executing coroutines, it is
-important to ensure that [cleanupTestCoroutines][DelayController.cleanupTestCoroutines] is called after every test case.
+If this behavior is desirable, but some parts of the test still require accurate dispatching, for example, to ensure
+that the code executes on the correct thread, then simply `launch` a new coroutine with the [StandardTestDispatcher].
```kotlin
-class TestClass {
- private val testDispatcher = TestCoroutineDispatcher()
-
- @Before
- fun setup() {
- // provide the scope explicitly, in this example using a constructor parameter
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- fun cleanUp() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
- @Test
- fun testFoo() = testDispatcher.runBlockingTest {
- // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines
- foo()
+@Test
+fun testEagerlyEnteringSomeChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered1 = false
+ launch {
+ entered1 = true
}
-}
+ assertTrue(entered1) // `entered1 = true` already executed
-fun foo() {
- MainScope().launch {
- // launch will use the testDispatcher provided by setMain
+ var entered2 = false
+ launch(StandardTestDispatcher(testScheduler)) {
+ // this block and every coroutine launched inside it will explicitly go through the needed dispatches
+ entered2 = true
}
+ assertFalse(entered2)
+ runCurrent() // need to explicitly run the dispatched continuation
+ assertTrue(entered2)
}
```
-*Note:* Prefer to provide `TestCoroutineScope` when it does not complicate code since it will also elevate exceptions
-to test failures. However, exposing a `CoroutineScope` to callers of a function may lead to complicated code, in which
-case this is the preferred pattern.
-
-### Using `TestCoroutineScope` and `TestCoroutineDispatcher` without `runBlockingTest`
+### Using `withTimeout` inside `runTest`
-It is supported to use both [TestCoroutineScope] and [TestCoroutineDispatcher] without using the [runBlockingTest]
-builder. Tests may need to do this in situations such as introducing multiple dispatchers and library writers may do
-this to provide alternatives to `runBlockingTest`.
+Timeouts are also susceptible to time control, so the code below will immediately finish.
```kotlin
@Test
-fun testFooWithAutoProgress() {
- val scope = TestCoroutineScope()
- scope.foo()
- // foo is suspended waiting for time to progress
- scope.advanceUntilIdle()
- // foo's coroutine will be completed before here
-}
-
-fun CoroutineScope.foo() {
- launch {
- println(1) // executes eagerly when foo() is called due to TestCoroutineScope
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeUntilIdle
+fun testFooWithTimeout() = runTest {
+ assertFailsWith<TimeoutCancellationException> {
+ withTimeout(1_000) {
+ delay(999)
+ delay(2)
+ println("this won't be reached")
+ }
}
-}
+}
```
-## Using time control with `withContext`
+## Virtual time support with other dispatchers
-Calls to `withContext(Dispatchers.IO)` or `withContext(Dispatchers.Default)` are common in coroutines based codebases.
-Both dispatchers are not designed to interact with `TestCoroutineDispatcher`.
-
-Tests should provide a `TestCoroutineDispatcher` to replace these dispatchers if the `withContext` calls `delay` in the
-function under test. For example, a test that calls `veryExpensiveOne` should provide a `TestCoroutineDispatcher` using
-either dependency injection, a service locator, or a default parameter.
+Calls to `withContext(Dispatchers.IO)`, `withContext(Dispatchers.Default)` ,and `withContext(Dispatchers.Main)` are
+common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers
+using the virtual time source, so delays will not be skipped in them.
```kotlin
-suspend fun veryExpensiveOne() = withContext(Dispatchers.Default) {
+suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) {
delay(1_000)
- 1 // for very expensive values of 1
+ 1
}
-```
-
-In situations where the code inside the `withContext` is very simple, it is not as important to provide a test
-dispatcher. The function `veryExpensiveTwo` will behave identically in a `TestCoroutineDispatcher` and
-`Dispatchers.Default` after the thread switch for `Dispatchers.Default`. Because `withContext` always returns a value by
-directly, there is no need to inject a `TestCoroutineDispatcher` into this function.
-```kotlin
-suspend fun veryExpensiveTwo() = withContext(Dispatchers.Default) {
- 2 // for very expensive values of 2
+fun testExpensiveFunction() = runTest {
+ val result = veryExpensiveFunction() // will take a whole real-time second to execute
+ // the virtual time at this point is still 0
}
```
-Tests should provide a `TestCoroutineDispatcher` to code that calls `withContext` to provide time control for
-delays, or when execution control is needed to test complex logic.
-
+Tests should, when possible, replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the
+function under test. For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using
+either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time.
### Status of the API
@@ -426,36 +362,32 @@ This API is experimental and it is may change before migrating out of experiment
Changes during experimental may have deprecation applied when possible, but it is not
advised to use the API in stable code before it leaves experimental due to possible breaking changes.
-If you have any suggestions for improvements to this experimental API please share them them on the
+If you have any suggestions for improvements to this experimental API please share them on the
[issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[Dispatchers.Unconfined]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
+[Dispatchers.Main]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
+[yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
+[ExperimentalCoroutinesApi]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
<!--- MODULE kotlinx-coroutines-test -->
<!--- INDEX kotlinx.coroutines.test -->
-[setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
-[runBlockingTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html
-[UncompletedCoroutinesError]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-uncompleted-coroutines-error/index.html
-[DelayController]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/index.html
-[DelayController.advanceUntilIdle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/advance-until-idle.html
-[DelayController.pauseDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/pause-dispatcher.html
-[TestCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html
-[DelayController.resumeDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/resume-dispatcher.html
-[TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html
-[TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html
-[TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html
-[DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html
+[runTest]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html
+[TestCoroutineScheduler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/index.html
+[TestScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/index.html
+[TestDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-dispatcher/index.html
+[Dispatchers.setMain]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
+[StandardTestDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-standard-test-dispatcher.html
+[UnconfinedTestDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-unconfined-test-dispatcher.html
+[setMain]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
+[TestScope.testScheduler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/test-scheduler.html
+[TestScope.runTest]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html
+[runCurrent]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-current.html
<!--- END -->
diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
index c99ec5cb..bf639235 100644
--- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
+++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
@@ -13,27 +13,45 @@ public final class kotlinx/coroutines/test/TestBuildersKt {
public static final fun runBlockingTest (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineDispatcher;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V
+ public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestScope;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun runBlockingTest$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runBlockingTestOnTestScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runBlockingTestOnTestScope$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runTest (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
+ public static final fun runTest (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;)V
+ public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runTestWithLegacyScope (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runTestWithLegacyScope$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}
-public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/DelayController {
+public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/SchedulerAsDelayController {
public fun <init> ()V
+ public fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;)V
+ public synthetic fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun advanceTimeBy (J)J
public fun advanceUntilIdle ()J
public fun cleanupTestCoroutines ()V
- public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun getCurrentTime ()J
- public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
+ public fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
public fun pauseDispatcher ()V
public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resumeDispatcher ()V
public fun runCurrent ()V
- public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
public fun toString ()Ljava/lang/String;
}
+public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt {
+ public static final fun StandardTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static synthetic fun StandardTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static final fun UnconfinedTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static synthetic fun UnconfinedTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
+}
+
public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
public fun <init> ()V
public fun cleanupTestCoroutines ()V
@@ -41,13 +59,44 @@ public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotli
public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V
}
-public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope, kotlinx/coroutines/test/DelayController, kotlinx/coroutines/test/UncaughtExceptionCaptor {
+public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/coroutines/AbstractCoroutineContextElement, kotlin/coroutines/CoroutineContext$Element {
+ public static final field Key Lkotlinx/coroutines/test/TestCoroutineScheduler$Key;
+ public fun <init> ()V
+ public final fun advanceTimeBy (J)V
+ public final fun advanceUntilIdle ()V
+ public final fun getCurrentTime ()J
+ public final fun getTimeSource ()Lkotlin/time/TimeSource;
+ public final fun runCurrent ()V
+}
+
+public final class kotlinx/coroutines/test/TestCoroutineScheduler$Key : kotlin/coroutines/CoroutineContext$Key {
+}
+
+public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope {
public abstract fun cleanupTestCoroutines ()V
+ public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
}
public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
public static final fun TestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V
+ public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J
+ public static final fun getUncaughtExceptions (Lkotlinx/coroutines/test/TestCoroutineScope;)Ljava/util/List;
+ public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static final fun resumeDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun runCurrent (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+}
+
+public abstract class kotlinx/coroutines/test/TestDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay {
+ public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
+ public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
+ public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
}
public final class kotlinx/coroutines/test/TestDispatchers {
@@ -55,13 +104,23 @@ public final class kotlinx/coroutines/test/TestDispatchers {
public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V
}
+public abstract interface class kotlinx/coroutines/test/TestScope : kotlinx/coroutines/CoroutineScope {
+ public abstract fun getBackgroundScope ()Lkotlinx/coroutines/CoroutineScope;
+ public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
+}
+
+public final class kotlinx/coroutines/test/TestScopeKt {
+ public static final fun TestScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestScope;
+ public static synthetic fun TestScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestScope;
+ public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestScope;J)V
+ public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestScope;)V
+ public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestScope;)J
+ public static final fun getTestTimeSource (Lkotlinx/coroutines/test/TestScope;)Lkotlin/time/TimeSource;
+ public static final fun runCurrent (Lkotlinx/coroutines/test/TestScope;)V
+}
+
public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
public abstract fun cleanupTestCoroutines ()V
public abstract fun getUncaughtExceptions ()Ljava/util/List;
}
-public final class kotlinx/coroutines/test/UncompletedCoroutinesError : java/lang/AssertionError {
- public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
- public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
-}
-
diff --git a/kotlinx-coroutines-test/build.gradle.kts b/kotlinx-coroutines-test/build.gradle.kts
index fef0a146..7b244bb0 100644
--- a/kotlinx-coroutines-test/build.gradle.kts
+++ b/kotlinx-coroutines-test/build.gradle.kts
@@ -2,6 +2,12 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-dependencies {
- implementation(project(":kotlinx-coroutines-debug"))
+val experimentalAnnotations = listOf(
+ "kotlin.Experimental",
+ "kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "kotlinx.coroutines.InternalCoroutinesApi"
+)
+
+kotlin {
+ sourceSets.all { configureMultiplatform() }
}
diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt
new file mode 100644
index 00000000..80dc8d60
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:JvmName("TestBuildersKt")
+@file:JvmMultifileClass
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * A test result.
+ *
+ * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
+ * platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
+ * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
+ * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
+ *
+ * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
+ * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
+ * test finishes.
+ * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
+ * with a [TestResult] is to immediately `return` it from a test.
+ * * Don't nest functions returning a [TestResult].
+ */
+@Suppress("NO_ACTUAL_FOR_EXPECT")
+@ExperimentalCoroutinesApi
+public expect class TestResult
+
+/**
+ * Executes [testBody] as a test in a new coroutine, returning [TestResult].
+ *
+ * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
+ * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
+ * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
+ *
+ * ```
+ * @Test
+ * fun exampleTest() = runTest {
+ * val deferred = async {
+ * delay(1_000)
+ * async {
+ * delay(1_000)
+ * }.await()
+ * }
+ *
+ * deferred.await() // result available immediately
+ * }
+ * ```
+ *
+ * The platform difference entails that, in order to use this function correctly in common code, one must always
+ * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
+ * [TestResult] for details on this.
+ *
+ * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
+ * Because of this, child coroutines are not executed in parallel to the test body.
+ * In order to for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
+ * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
+ *
+ * ```
+ * @Test
+ * fun exampleWaitingForAsyncTasks1() = runTest {
+ * // 1
+ * val job = launch {
+ * // 3
+ * }
+ * // 2
+ * job.join() // the main test coroutine suspends here, so the child is executed
+ * // 4
+ * }
+ *
+ * @Test
+ * fun exampleWaitingForAsyncTasks2() = runTest {
+ * // 1
+ * launch {
+ * // 3
+ * }
+ * // 2
+ * advanceUntilIdle() // runs the tasks until their queue is empty
+ * // 4
+ * }
+ * ```
+ *
+ * ### Task scheduling
+ *
+ * Delay-skipping is achieved by using virtual time.
+ * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
+ * then its [TestCoroutineScheduler] is used;
+ * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
+ * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
+ * Some convenience methods are available on [TestScope] to control the scheduler.
+ *
+ * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
+ * ```
+ * @Test
+ * fun exampleTest() = runTest {
+ * val elapsed = TimeSource.Monotonic.measureTime {
+ * val deferred = async {
+ * delay(1_000) // will be skipped
+ * withContext(Dispatchers.Default) {
+ * delay(5_000) // Dispatchers.Default doesn't know about TestCoroutineScheduler
+ * }
+ * }
+ * deferred.await()
+ * }
+ * println(elapsed) // about five seconds
+ * }
+ * ```
+ *
+ * ### Failures
+ *
+ * #### Test body failures
+ *
+ * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
+ *
+ * #### Reported exceptions
+ *
+ * Unhandled exceptions will be thrown at the end of the test.
+ * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
+ * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
+ *
+ * #### Uncompleted coroutines
+ *
+ * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
+ * [testBody] also complete, or are cancelled.
+ * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
+ * [AssertionError], whereas on JS, the `Promise` will fail with it).
+ *
+ * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
+ * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
+ * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
+ * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
+ * task during that time, the timer gets reset.
+ *
+ * ### Configuration
+ *
+ * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
+ * scope created for the test, [context] also can be used to change how the test is executed.
+ * See the [TestScope] constructor function documentation for details.
+ *
+ * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
+ */
+@ExperimentalCoroutinesApi
+public fun runTest(
+ context: CoroutineContext = EmptyCoroutineContext,
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestScope.() -> Unit
+): TestResult {
+ if (context[RunningInRunTest] != null)
+ throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
+ return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs, testBody)
+}
+
+/**
+ * Performs [runTest] on an existing [TestScope].
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.runTest(
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestScope.() -> Unit
+): TestResult = asSpecificImplementation().let {
+ it.enter()
+ createTestResult {
+ runTestCoroutine(it, dispatchTimeoutMs, TestScopeImpl::tryGetCompletionCause, testBody) {
+ backgroundScope.cancel()
+ testScheduler.advanceUntilIdleOr { false }
+ it.leave()
+ }
+ }
+}
+
+/**
+ * Runs [testProcedure], creating a [TestResult].
+ */
+@Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
+internal expect fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult
+
+/** A coroutine context element indicating that the coroutine is running inside `runTest`. */
+internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
+ override val key: CoroutineContext.Key<*>
+ get() = this
+
+ override fun toString(): String = "RunningInRunTest"
+}
+
+/** The default timeout to use when waiting for asynchronous completions of the coroutines managed by
+ * a [TestCoroutineScheduler]. */
+internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
+
+/**
+ * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
+ * [dispatchTimeoutMs] milliseconds, and performing the [cleanup] procedure at the end.
+ *
+ * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
+ *
+ * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
+ * return a list of uncaught exceptions that should be reported at the end of the test.
+ */
+internal suspend fun <T: AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutine(
+ coroutine: T,
+ dispatchTimeoutMs: Long,
+ tryGetCompletionCause: T.() -> Throwable?,
+ testBody: suspend T.() -> Unit,
+ cleanup: () -> List<Throwable>,
+) {
+ val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!!
+ /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
+ coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) {
+ testBody()
+ }
+ /**
+ * The general procedure here is as follows:
+ * 1. Try running the work that the scheduler knows about, both background and foreground.
+ *
+ * 2. Wait until we run out of foreground work to do. This could mean one of the following:
+ * * The main coroutine is already completed. This is checked separately; then we leave the procedure.
+ * * It's switched to another dispatcher that doesn't know about the [TestCoroutineScheduler].
+ * * Generally, it's waiting for something external (like a network request, or just an arbitrary callback).
+ * * The test simply hanged.
+ * * The main coroutine is waiting for some background work.
+ *
+ * 3. We await progress from things that are not the code under test:
+ * the background work that the scheduler knows about, the external callbacks,
+ * the work on dispatchers not linked to the scheduler, etc.
+ *
+ * When we observe that the code under test can proceed, we go to step 1 again.
+ * If there is no activity for [dispatchTimeoutMs] milliseconds, we consider the test to have hanged.
+ *
+ * The background work is not running on a dedicated thread.
+ * Instead, the test thread itself is used, by spawning a separate coroutine.
+ */
+ var completed = false
+ while (!completed) {
+ scheduler.advanceUntilIdle()
+ if (coroutine.isCompleted) {
+ /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
+ non-trivial dispatches. */
+ completed = true
+ continue
+ }
+ // in case progress depends on some background work, we need to keep spinning it.
+ val backgroundWorkRunner = launch(CoroutineName("background work runner")) {
+ while (true) {
+ scheduler.tryRunNextTaskUnless { !isActive }
+ // yield so that the `select` below has a chance to check if its conditions are fulfilled
+ yield()
+ }
+ }
+ try {
+ select<Unit> {
+ coroutine.onJoin {
+ // observe that someone completed the test coroutine and leave without waiting for the timeout
+ completed = true
+ }
+ scheduler.onDispatchEvent {
+ // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
+ }
+ onTimeout(dispatchTimeoutMs) {
+ handleTimeout(coroutine, dispatchTimeoutMs, tryGetCompletionCause, cleanup)
+ }
+ }
+ } finally {
+ backgroundWorkRunner.cancelAndJoin()
+ }
+ }
+ coroutine.getCompletionExceptionOrNull()?.let { exception ->
+ val exceptions = try {
+ cleanup()
+ } catch (e: UncompletedCoroutinesError) {
+ // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
+ emptyList()
+ }
+ (listOf(exception) + exceptions).throwAll()
+ }
+ cleanup().throwAll()
+}
+
+/**
+ * Invoked on timeout in [runTest]. Almost always just builds a nice [UncompletedCoroutinesError] and throws it.
+ * However, sometimes it detects that the coroutine completed, in which case it returns normally.
+ */
+private inline fun<T: AbstractCoroutine<Unit>> handleTimeout(
+ coroutine: T,
+ dispatchTimeoutMs: Long,
+ tryGetCompletionCause: T.() -> Throwable?,
+ cleanup: () -> List<Throwable>,
+) {
+ val uncaughtExceptions = try {
+ cleanup()
+ } catch (e: UncompletedCoroutinesError) {
+ // we expect these and will instead throw a more informative exception.
+ emptyList()
+ }
+ val activeChildren = coroutine.children.filter { it.isActive }.toList()
+ val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null
+ var message = "After waiting for $dispatchTimeoutMs ms"
+ if (completionCause == null)
+ message += ", the test coroutine is not completing"
+ if (activeChildren.isNotEmpty())
+ message += ", there were active child jobs: $activeChildren"
+ if (completionCause != null && activeChildren.isEmpty()) {
+ if (coroutine.isCompleted)
+ return
+ // TODO: can this really ever happen?
+ message += ", the test coroutine was not completed"
+ }
+ val error = UncompletedCoroutinesError(message)
+ completionCause?.let { cause -> error.addSuppressed(cause) }
+ uncaughtExceptions.forEach { error.addSuppressed(it) }
+ throw error
+}
+
+internal fun List<Throwable>.throwAll() {
+ firstOrNull()?.apply {
+ drop(1).forEach { addSuppressed(it) }
+ throw this
+ }
+}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
new file mode 100644
index 00000000..e99fe8b1
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.test.internal.TestMainDispatcher
+import kotlin.coroutines.*
+
+/**
+ * Creates an instance of an unconfined [TestDispatcher].
+ *
+ * This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular
+ * thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do.
+ *
+ * Like [Dispatchers.Unconfined], this one does not provide guarantees about the execution order when several coroutines
+ * are queued in this dispatcher. However, we ensure that the [launch] and [async] blocks at the top level of [runTest]
+ * are entered eagerly. This allows launching child coroutines and not calling [runCurrent] for them to start executing.
+ *
+ * ```
+ * @Test
+ * fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ * var entered = false
+ * val deferred = CompletableDeferred<Unit>()
+ * var completed = false
+ * launch {
+ * entered = true
+ * deferred.await()
+ * completed = true
+ * }
+ * assertTrue(entered) // `entered = true` already executed.
+ * assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ * deferred.complete(Unit) // resume the coroutine.
+ * assertTrue(completed) // now the child coroutine is immediately completed.
+ * }
+ * ```
+ *
+ * Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and
+ * in which order the queued coroutines are executed.
+ * Another typical use case for this dispatcher is launching child coroutines that are resumed immediately, without
+ * going through a dispatch; this can be helpful for testing [Channel] and [StateFlow] usages.
+ *
+ * ```
+ * @Test
+ * fun testUnconfinedDispatcher() = runTest {
+ * val values = mutableListOf<Int>()
+ * val stateFlow = MutableStateFlow(0)
+ * val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ * stateFlow.collect {
+ * values.add(it)
+ * }
+ * }
+ * stateFlow.value = 1
+ * stateFlow.value = 2
+ * stateFlow.value = 3
+ * job.cancel()
+ * // each assignment will immediately resume the collecting child coroutine,
+ * // so no values will be skipped.
+ * assertEquals(listOf(0, 1, 2, 3), values)
+ * }
+ * ```
+ *
+ * Please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order
+ * guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing
+ * functionality, not the specific order of actions.
+ * See [Dispatchers.Unconfined] for a discussion of the execution order guarantees.
+ *
+ * In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control
+ * the virtual time and can be shared among many test dispatchers.
+ * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
+ * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
+ * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
+ *
+ * Additionally, [name] can be set to distinguish each dispatcher instance when debugging.
+ *
+ * @see StandardTestDispatcher for a more predictable [TestDispatcher].
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun UnconfinedTestDispatcher(
+ scheduler: TestCoroutineScheduler? = null,
+ name: String? = null
+): TestDispatcher = UnconfinedTestDispatcherImpl(
+ scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name)
+
+private class UnconfinedTestDispatcherImpl(
+ override val scheduler: TestCoroutineScheduler,
+ private val name: String? = null
+) : TestDispatcher() {
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
+
+ @Suppress("INVISIBLE_MEMBER")
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ scheduler.sendDispatchEvent(context)
+
+ /** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */
+ /** It can only be called by the [yield] function. See also code of [yield] function. */
+ val yieldContext = context[YieldContext]
+ if (yieldContext !== null) {
+ // report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
+ yieldContext.dispatcherWasUnconfined = true
+ return
+ }
+ throw UnsupportedOperationException(
+ "Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " +
+ "the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
+ "isDispatchNeeded and dispatch calls."
+ )
+ }
+
+ override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]"
+}
+
+/**
+ * Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler].
+ *
+ * This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its
+ * [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent],
+ * [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these
+ * tasks in a blocking manner.
+ *
+ * In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are
+ * parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to
+ * run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when
+ * inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines.
+ *
+ * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
+ * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
+ * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
+ *
+ * One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging.
+ *
+ * @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun StandardTestDispatcher(
+ scheduler: TestCoroutineScheduler? = null,
+ name: String? = null
+): TestDispatcher = StandardTestDispatcherImpl(
+ scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name)
+
+private class StandardTestDispatcherImpl(
+ override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
+ private val name: String? = null
+) : TestDispatcher() {
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ scheduler.registerEvent(this, 0, block, context) { false }
+ }
+
+ override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]"
+}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt
new file mode 100644
index 00000000..e735c6d4
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.selects.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+import kotlin.time.*
+
+/**
+ * This is a scheduler for coroutines used in tests, providing the delay-skipping behavior.
+ *
+ * [Test dispatchers][TestDispatcher] are parameterized with a scheduler. Several dispatchers can share the
+ * same scheduler, in which case their knowledge about the virtual time will be synchronized. When the dispatchers
+ * require scheduling an event at a later point in time, they notify the scheduler, which will establish the order of
+ * the tasks.
+ *
+ * The scheduler can be queried to advance the time (via [advanceTimeBy]), run all the scheduled tasks advancing the
+ * virtual time as needed (via [advanceUntilIdle]), or run the tasks that are scheduled to run as soon as possible but
+ * haven't yet been dispatched (via [runCurrent]).
+ */
+@ExperimentalCoroutinesApi
+public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCoroutineScheduler),
+ CoroutineContext.Element {
+
+ /** @suppress */
+ public companion object Key : CoroutineContext.Key<TestCoroutineScheduler>
+
+ /** This heap stores the knowledge about which dispatchers are interested in which moments of virtual time. */
+ // TODO: all the synchronization is done via a separate lock, so a non-thread-safe priority queue can be used.
+ private val events = ThreadSafeHeap<TestDispatchEvent<Any>>()
+
+ /** Establishes that [currentTime] can't exceed the time of the earliest event in [events]. */
+ private val lock = SynchronizedObject()
+
+ /** This counter establishes some order on the events that happen at the same virtual time. */
+ private val count = atomic(0L)
+
+ /** The current virtual time in milliseconds. */
+ @ExperimentalCoroutinesApi
+ public var currentTime: Long = 0
+ get() = synchronized(lock) { field }
+ private set
+
+ /** A channel for notifying about the fact that a dispatch recently happened. */
+ private val dispatchEvents: Channel<Unit> = Channel(CONFLATED)
+
+ /**
+ * Registers a request for the scheduler to notify [dispatcher] at a virtual moment [timeDeltaMillis] milliseconds
+ * later via [TestDispatcher.processEvent], which will be called with the provided [marker] object.
+ *
+ * Returns the handler which can be used to cancel the registration.
+ */
+ internal fun <T : Any> registerEvent(
+ dispatcher: TestDispatcher,
+ timeDeltaMillis: Long,
+ marker: T,
+ context: CoroutineContext,
+ isCancelled: (T) -> Boolean
+ ): DisposableHandle {
+ require(timeDeltaMillis >= 0) { "Attempted scheduling an event earlier in time (with the time delta $timeDeltaMillis)" }
+ checkSchedulerInContext(this, context)
+ val count = count.getAndIncrement()
+ val isForeground = context[BackgroundWork] === null
+ return synchronized(lock) {
+ val time = addClamping(currentTime, timeDeltaMillis)
+ val event = TestDispatchEvent(dispatcher, count, time, marker as Any, isForeground) { isCancelled(marker) }
+ events.addLast(event)
+ /** can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's
+ * actually anything in the event queue. */
+ sendDispatchEvent(context)
+ DisposableHandle {
+ synchronized(lock) {
+ events.remove(event)
+ }
+ }
+ }
+ }
+
+ /**
+ * Runs the next enqueued task, advancing the virtual time to the time of its scheduled awakening,
+ * unless [condition] holds.
+ */
+ internal fun tryRunNextTaskUnless(condition: () -> Boolean): Boolean {
+ val event = synchronized(lock) {
+ if (condition()) return false
+ val event = events.removeFirstOrNull() ?: return false
+ if (currentTime > event.time)
+ currentTimeAheadOfEvents()
+ currentTime = event.time
+ event
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ return true
+ }
+
+ /**
+ * Runs the enqueued tasks in the specified order, advancing the virtual time as needed until there are no more
+ * tasks associated with the dispatchers linked to this scheduler.
+ *
+ * A breaking change from [TestCoroutineDispatcher.advanceTimeBy] is that it no longer returns the total number of
+ * milliseconds by which the execution of this method has advanced the virtual time. If you want to recreate that
+ * functionality, query [currentTime] before and after the execution to achieve the same result.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceUntilIdle(): Unit = advanceUntilIdleOr { events.none(TestDispatchEvent<*>::isForeground) }
+
+ /**
+ * [condition]: guaranteed to be invoked under the lock.
+ */
+ internal fun advanceUntilIdleOr(condition: () -> Boolean) {
+ while (true) {
+ if (!tryRunNextTaskUnless(condition))
+ return
+ }
+ }
+
+ /**
+ * Runs the tasks that are scheduled to execute at this moment of virtual time.
+ */
+ @ExperimentalCoroutinesApi
+ public fun runCurrent() {
+ val timeMark = synchronized(lock) { currentTime }
+ while (true) {
+ val event = synchronized(lock) {
+ events.removeFirstIf { it.time <= timeMark } ?: return
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ }
+ }
+
+ /**
+ * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
+ * scheduled tasks in the meantime.
+ *
+ * Breaking changes from [TestCoroutineDispatcher.advanceTimeBy]:
+ * * Intentionally doesn't return a `Long` value, as its use cases are unclear. We may restore it in the future;
+ * please describe your use cases at [the issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues/).
+ * For now, it's possible to query [currentTime] before and after execution of this method, to the same effect.
+ * * It doesn't run the tasks that are scheduled at exactly [currentTime] + [delayTimeMillis]. For example,
+ * advancing the time by one millisecond used to run the tasks at the current millisecond *and* the next
+ * millisecond, but now will stop just before executing any task starting at the next millisecond.
+ * * Overflowing the target time used to lead to nothing being done, but will now run the tasks scheduled at up to
+ * (but not including) [Long.MAX_VALUE].
+ *
+ * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceTimeBy(delayTimeMillis: Long) {
+ require(delayTimeMillis >= 0) { "Can not advance time by a negative delay: $delayTimeMillis" }
+ val startingTime = currentTime
+ val targetTime = addClamping(startingTime, delayTimeMillis)
+ while (true) {
+ val event = synchronized(lock) {
+ val timeMark = currentTime
+ val event = events.removeFirstIf { targetTime > it.time }
+ when {
+ event == null -> {
+ currentTime = targetTime
+ return
+ }
+ timeMark > event.time -> currentTimeAheadOfEvents()
+ else -> {
+ currentTime = event.time
+ event
+ }
+ }
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ }
+ }
+
+ /**
+ * Checks that the only tasks remaining in the scheduler are cancelled.
+ */
+ internal fun isIdle(strict: Boolean = true): Boolean =
+ synchronized(lock) {
+ if (strict) events.isEmpty else events.none { !it.isCancelled() }
+ }
+
+ /**
+ * Notifies this scheduler about a dispatch event.
+ *
+ * [context] is the context in which the task will be dispatched.
+ */
+ internal fun sendDispatchEvent(context: CoroutineContext) {
+ if (context[BackgroundWork] !== BackgroundWork)
+ dispatchEvents.trySend(Unit)
+ }
+
+ /**
+ * Consumes the knowledge that a dispatch event happened recently.
+ */
+ internal val onDispatchEvent: SelectClause1<Unit> get() = dispatchEvents.onReceive
+
+ /**
+ * Returns the [TimeSource] representation of the virtual time of this scheduler.
+ */
+ @ExperimentalCoroutinesApi
+ @ExperimentalTime
+ public val timeSource: TimeSource = object : AbstractLongTimeSource(DurationUnit.MILLISECONDS) {
+ override fun read(): Long = currentTime
+ }
+}
+
+// Some error-throwing functions for pretty stack traces
+private fun currentTimeAheadOfEvents(): Nothing = invalidSchedulerState()
+
+private fun invalidSchedulerState(): Nothing =
+ throw IllegalStateException("The test scheduler entered an invalid state. Please report this at https://github.com/Kotlin/kotlinx.coroutines/issues.")
+
+/** [ThreadSafeHeap] node representing a scheduled task, ordered by the planned execution time. */
+private class TestDispatchEvent<T>(
+ @JvmField val dispatcher: TestDispatcher,
+ private val count: Long,
+ @JvmField val time: Long,
+ @JvmField val marker: T,
+ @JvmField val isForeground: Boolean,
+ // TODO: remove once the deprecated API is gone
+ @JvmField val isCancelled: () -> Boolean
+) : Comparable<TestDispatchEvent<*>>, ThreadSafeHeapNode {
+ override var heap: ThreadSafeHeap<*>? = null
+ override var index: Int = 0
+
+ override fun compareTo(other: TestDispatchEvent<*>) =
+ compareValuesBy(this, other, TestDispatchEvent<*>::time, TestDispatchEvent<*>::count)
+
+ override fun toString() = "TestDispatchEvent(time=$time, dispatcher=$dispatcher${if (isForeground) "" else ", background"})"
+}
+
+// works with positive `a`, `b`
+private fun addClamping(a: Long, b: Long): Long = (a + b).let { if (it >= 0) it else Long.MAX_VALUE }
+
+internal fun checkSchedulerInContext(scheduler: TestCoroutineScheduler, context: CoroutineContext) {
+ context[TestCoroutineScheduler]?.let {
+ check(it === scheduler) {
+ "Detected use of different schedulers. If you need to use several test coroutine dispatchers, " +
+ "create one `TestCoroutineScheduler` and pass it to each of them."
+ }
+ }
+}
+
+/**
+ * A coroutine context key denoting that the work is to be executed in the background.
+ * @see [TestScope.backgroundScope]
+ */
+internal object BackgroundWork : CoroutineContext.Key<BackgroundWork>, CoroutineContext.Element {
+ override val key: CoroutineContext.Key<*>
+ get() = this
+
+ override fun toString(): String = "BackgroundWork"
+}
+
+private fun<T> ThreadSafeHeap<T>.none(predicate: (T) -> Boolean) where T: ThreadSafeHeapNode, T: Comparable<T> =
+ find(predicate) == null
diff --git a/kotlinx-coroutines-test/common/src/TestDispatcher.kt b/kotlinx-coroutines-test/common/src/TestDispatcher.kt
new file mode 100644
index 00000000..348cc2f1
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * A test dispatcher that can interface with a [TestCoroutineScheduler].
+ *
+ * The available implementations are:
+ * * [StandardTestDispatcher] is a dispatcher that places new tasks into a queue.
+ * * [UnconfinedTestDispatcher] is a dispatcher that behaves like [Dispatchers.Unconfined] while allowing to control
+ * the virtual time.
+ */
+@ExperimentalCoroutinesApi
+public abstract class TestDispatcher internal constructor() : CoroutineDispatcher(), Delay {
+ /** The scheduler that this dispatcher is linked to. */
+ @ExperimentalCoroutinesApi
+ public abstract val scheduler: TestCoroutineScheduler
+
+ /** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */
+ internal fun processEvent(time: Long, marker: Any) {
+ check(marker is Runnable)
+ marker.run()
+ }
+
+ /** @suppress */
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ val timedRunnable = CancellableContinuationRunnable(continuation, this)
+ scheduler.registerEvent(this, timeMillis, timedRunnable, continuation.context, ::cancellableRunnableIsCancelled)
+ }
+
+ /** @suppress */
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+ scheduler.registerEvent(this, timeMillis, block, context) { false }
+}
+
+/**
+ * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled
+ * in the future.
+ */
+private class CancellableContinuationRunnable(
+ @JvmField val continuation: CancellableContinuation<Unit>,
+ private val dispatcher: CoroutineDispatcher
+) : Runnable {
+ override fun run() = with(dispatcher) { with(continuation) { resumeUndispatched(Unit) } }
+}
+
+private fun cancellableRunnableIsCancelled(runnable: CancellableContinuationRunnable): Boolean =
+ !runnable.continuation.isActive
diff --git a/kotlinx-coroutines-test/src/TestDispatchers.kt b/kotlinx-coroutines-test/common/src/TestDispatchers.kt
index bf068f9d..4454597e 100644
--- a/kotlinx-coroutines-test/src/TestDispatchers.kt
+++ b/kotlinx-coroutines-test/common/src/TestDispatchers.kt
@@ -1,38 +1,38 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-@file:Suppress("unused")
@file:JvmName("TestDispatchers")
package kotlinx.coroutines.test
import kotlinx.coroutines.*
import kotlinx.coroutines.test.internal.*
+import kotlin.jvm.*
/**
* Sets the given [dispatcher] as an underlying dispatcher of [Dispatchers.Main].
- * All consecutive usages of [Dispatchers.Main] will use given [dispatcher] under the hood.
+ * All subsequent usages of [Dispatchers.Main] will use the given [dispatcher] under the hood.
+ *
+ * Using [TestDispatcher] as an argument has special behavior: subsequently-called [runTest], as well as
+ * [TestScope] and test dispatcher constructors, will use the [TestCoroutineScheduler] of the provided dispatcher.
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
@ExperimentalCoroutinesApi
public fun Dispatchers.setMain(dispatcher: CoroutineDispatcher) {
require(dispatcher !is TestMainDispatcher) { "Dispatchers.setMain(Dispatchers.Main) is prohibited, probably Dispatchers.resetMain() should be used instead" }
- val mainDispatcher = Dispatchers.Main
- require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
- mainDispatcher.setDispatcher(dispatcher)
+ getTestMainDispatcher().setDispatcher(dispatcher)
}
/**
* Resets state of the [Dispatchers.Main] to the original main dispatcher.
- * For example, in Android Main thread dispatcher will be set as [Dispatchers.Main].
- * Used to clean up all possible dependencies, should be used in tear down (`@After`) methods.
+ *
+ * For example, in Android, the Main thread dispatcher will be set as [Dispatchers.Main].
+ * This method undoes a dependency injection performed for tests, and so should be used in tear down (`@After`) methods.
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
@ExperimentalCoroutinesApi
public fun Dispatchers.resetMain() {
- val mainDispatcher = Dispatchers.Main
- require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
- mainDispatcher.resetDispatcher()
+ getTestMainDispatcher().resetDispatcher()
}
diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt
new file mode 100644
index 00000000..15d48a2a
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestScope.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.test.internal.*
+import kotlin.coroutines.*
+import kotlin.time.*
+
+/**
+ * A coroutine scope that for launching test coroutines.
+ *
+ * The scope provides the following functionality:
+ * * The [coroutineContext] includes a [coroutine dispatcher][TestDispatcher] that supports delay-skipping, using
+ * a [TestCoroutineScheduler] for orchestrating the virtual time.
+ * This scheduler is also available via the [testScheduler] property, and some helper extension
+ * methods are defined to more conveniently interact with it: see [TestScope.currentTime], [TestScope.runCurrent],
+ * [TestScope.advanceTimeBy], and [TestScope.advanceUntilIdle].
+ * * When inside [runTest], uncaught exceptions from the child coroutines of this scope will be reported at the end of
+ * the test.
+ * It is invalid for child coroutines to throw uncaught exceptions when outside the call to [TestScope.runTest]:
+ * the only guarantee in this case is the best effort to deliver the exception.
+ *
+ * The usual way to access a [TestScope] is to call [runTest], but it can also be constructed manually, in order to
+ * use it to initialize the components that participate in the test.
+ *
+ * #### Differences from the deprecated [TestCoroutineScope]
+ *
+ * * This doesn't provide an equivalent of [TestCoroutineScope.cleanupTestCoroutines], and so can't be used as a
+ * standalone mechanism for writing tests: it does require that [runTest] is eventually called.
+ * The reason for this is that a proper cleanup procedure that supports using non-test dispatchers and arbitrary
+ * coroutine suspensions would be equivalent to [runTest], but would also be more error-prone, due to the potential
+ * for forgetting to perform the cleanup.
+ * * [TestCoroutineScope.advanceTimeBy] also calls [TestCoroutineScheduler.runCurrent] after advancing the virtual time.
+ * * No support for dispatcher pausing, like [DelayController] allows. [TestCoroutineDispatcher], which supported
+ * pausing, is deprecated; now, instead of pausing a dispatcher, one can use [withContext] to run a dispatcher that's
+ * paused by default, like [StandardTestDispatcher].
+ * * No access to the list of unhandled exceptions.
+ */
+@ExperimentalCoroutinesApi
+public sealed interface TestScope : CoroutineScope {
+ /**
+ * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
+ */
+ @ExperimentalCoroutinesApi
+ public val testScheduler: TestCoroutineScheduler
+
+ /**
+ * A scope for background work.
+ *
+ * This scope is automatically cancelled when the test finishes.
+ * Additionally, while the coroutines in this scope are run as usual when
+ * using [advanceTimeBy] and [runCurrent], [advanceUntilIdle] will stop advancing the virtual time
+ * once only the coroutines in this scope are left unprocessed.
+ *
+ * Failures in coroutines in this scope do not terminate the test.
+ * Instead, they are reported at the end of the test.
+ * Likewise, failure in the [TestScope] itself will not affect its [backgroundScope],
+ * because there's no parent-child relationship between them.
+ *
+ * A typical use case for this scope is to launch tasks that would outlive the tested code in
+ * the production environment.
+ *
+ * In this example, the coroutine that continuously sends new elements to the channel will get
+ * cancelled:
+ * ```
+ * @Test
+ * fun testExampleBackgroundJob() = runTest {
+ * val channel = Channel<Int>()
+ * backgroundScope.launch {
+ * var i = 0
+ * while (true) {
+ * channel.send(i++)
+ * }
+ * }
+ * repeat(100) {
+ * assertEquals(it, channel.receive())
+ * }
+ * }
+ * ```
+ */
+ @ExperimentalCoroutinesApi
+ public val backgroundScope: CoroutineScope
+}
+
+/**
+ * The current virtual time on [testScheduler][TestScope.testScheduler].
+ * @see TestCoroutineScheduler.currentTime
+ */
+@ExperimentalCoroutinesApi
+public val TestScope.currentTime: Long
+ get() = testScheduler.currentTime
+
+/**
+ * Advances the [testScheduler][TestScope.testScheduler] to the point where there are no tasks remaining.
+ * @see TestCoroutineScheduler.advanceUntilIdle
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.advanceUntilIdle(): Unit = testScheduler.advanceUntilIdle()
+
+/**
+ * Run any tasks that are pending at the current virtual time, according to
+ * the [testScheduler][TestScope.testScheduler].
+ *
+ * @see TestCoroutineScheduler.runCurrent
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.runCurrent(): Unit = testScheduler.runCurrent()
+
+/**
+ * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
+ * scheduled tasks in the meantime.
+ *
+ * In contrast with `TestCoroutineScope.advanceTimeBy`, this function does not run the tasks scheduled at the moment
+ * [currentTime] + [delayTimeMillis].
+ *
+ * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
+ * @see TestCoroutineScheduler.advanceTimeBy
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.advanceTimeBy(delayTimeMillis: Long): Unit = testScheduler.advanceTimeBy(delayTimeMillis)
+
+/**
+ * The [test scheduler][TestScope.testScheduler] as a [TimeSource].
+ * @see TestCoroutineScheduler.timeSource
+ */
+@ExperimentalCoroutinesApi
+@ExperimentalTime
+public val TestScope.testTimeSource: TimeSource get() = testScheduler.timeSource
+
+/**
+ * Creates a [TestScope].
+ *
+ * It ensures that all the test module machinery is properly initialized.
+ * * If [context] doesn't provide a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
+ * a new one is created, unless either
+ * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
+ * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
+ * its [TestCoroutineScheduler] is used.
+ * * If [context] doesn't have a [TestDispatcher], a [StandardTestDispatcher] is created.
+ * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
+ * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
+ * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
+ * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
+ * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
+ * [TestCoroutineScope] and share your use case at
+ * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
+ * * If [context] provides a [Job], that job is used as a parent for the new scope.
+ *
+ * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
+ * different scheduler.
+ * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
+ * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
+ * [UncaughtExceptionCaptor].
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
+ val ctxWithDispatcher = context.withDelaySkipping()
+ var scope: TestScopeImpl? = null
+ val exceptionHandler = when (ctxWithDispatcher[CoroutineExceptionHandler]) {
+ null -> CoroutineExceptionHandler { _, exception ->
+ scope!!.reportException(exception)
+ }
+ else -> throw IllegalArgumentException(
+ "A CoroutineExceptionHandler was passed to TestScope. " +
+ "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
+ "if uncaught exceptions require special treatment."
+ )
+ }
+ return TestScopeImpl(ctxWithDispatcher + exceptionHandler).also { scope = it }
+}
+
+/**
+ * Adds a [TestDispatcher] and a [TestCoroutineScheduler] to the context if there aren't any already.
+ *
+ * @throws IllegalArgumentException if both a [TestCoroutineScheduler] and a [TestDispatcher] are passed.
+ * @throws IllegalArgumentException if a [ContinuationInterceptor] is passed that is not a [TestDispatcher].
+ */
+internal fun CoroutineContext.withDelaySkipping(): CoroutineContext {
+ val dispatcher: TestDispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
+ is TestDispatcher -> {
+ val ctxScheduler = get(TestCoroutineScheduler)
+ if (ctxScheduler != null) {
+ require(dispatcher.scheduler === ctxScheduler) {
+ "Both a TestCoroutineScheduler $ctxScheduler and TestDispatcher $dispatcher linked to " +
+ "another scheduler were passed."
+ }
+ }
+ dispatcher
+ }
+ null -> StandardTestDispatcher(get(TestCoroutineScheduler))
+ else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
+ }
+ return this + dispatcher + dispatcher.scheduler
+}
+
+internal class TestScopeImpl(context: CoroutineContext) :
+ AbstractCoroutine<Unit>(context, initParentJob = true, active = true), TestScope {
+
+ override val testScheduler get() = context[TestCoroutineScheduler]!!
+
+ private var entered = false
+ private var finished = false
+ private val uncaughtExceptions = mutableListOf<Throwable>()
+ private val lock = SynchronizedObject()
+
+ override val backgroundScope: CoroutineScope =
+ CoroutineScope(coroutineContext + BackgroundWork + ReportingSupervisorJob {
+ if (it !is CancellationException) reportException(it)
+ })
+
+ /** Called upon entry to [runTest]. Will throw if called more than once. */
+ fun enter() {
+ val exceptions = synchronized(lock) {
+ if (entered)
+ throw IllegalStateException("Only a single call to `runTest` can be performed during one test.")
+ entered = true
+ check(!finished)
+ uncaughtExceptions
+ }
+ if (exceptions.isNotEmpty()) {
+ throw UncaughtExceptionsBeforeTest().apply {
+ for (e in exceptions)
+ addSuppressed(e)
+ }
+ }
+ }
+
+ /** Called at the end of the test. May only be called once. */
+ fun leave(): List<Throwable> {
+ val exceptions = synchronized(lock) {
+ check(entered && !finished)
+ finished = true
+ uncaughtExceptions
+ }
+ val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest`
+ if (exceptions.isEmpty()) {
+ if (activeJobs.isNotEmpty())
+ throw UncompletedCoroutinesError(
+ "Active jobs found during the tear-down. " +
+ "Ensure that all coroutines are completed or cancelled by your test. " +
+ "The active jobs: $activeJobs"
+ )
+ if (!testScheduler.isIdle())
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines found during the tear-down. " +
+ "Ensure that all coroutines are completed or cancelled by your test."
+ )
+ }
+ return exceptions
+ }
+
+ /** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */
+ fun reportException(throwable: Throwable) {
+ synchronized(lock) {
+ if (finished) {
+ throw throwable
+ } else {
+ @Suppress("INVISIBLE_MEMBER")
+ for (existingThrowable in uncaughtExceptions) {
+ // avoid reporting exceptions that already were reported.
+ if (unwrap(throwable) == unwrap(existingThrowable))
+ return
+ }
+ uncaughtExceptions.add(throwable)
+ if (!entered)
+ throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) }
+ }
+ }
+ }
+
+ /** Throws an exception if the coroutine is not completing. */
+ fun tryGetCompletionCause(): Throwable? = completionCause
+
+ override fun toString(): String =
+ "TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]"
+}
+
+/** Use the knowledge that any [TestScope] that we receive is necessarily a [TestScopeImpl]. */
+@Suppress("NO_ELSE_IN_WHEN") // TODO: a problem with `sealed` in MPP not allowing total pattern-matching
+internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) {
+ is TestScopeImpl -> this
+}
+
+internal class UncaughtExceptionsBeforeTest : IllegalStateException(
+ "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," +
+ " as such exceptions are also reported in a platform-dependent manner so that they are not lost."
+)
+
+/**
+ * Thrown when a test has completed and there are tasks that are not completed or cancelled.
+ */
+@ExperimentalCoroutinesApi
+internal class UncompletedCoroutinesError(message: String) : AssertionError(message)
diff --git a/kotlinx-coroutines-test/common/src/internal/ReportingSupervisorJob.kt b/kotlinx-coroutines-test/common/src/internal/ReportingSupervisorJob.kt
new file mode 100644
index 00000000..e3091bc2
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/internal/ReportingSupervisorJob.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016-2022 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.*
+
+/**
+ * A variant of [SupervisorJob] that additionally notifies about child failures via a callback.
+ */
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
+internal class ReportingSupervisorJob(parent: Job? = null, val onChildCancellation: (Throwable) -> Unit) :
+ JobImpl(parent) {
+ override fun childCancelled(cause: Throwable): Boolean =
+ try {
+ onChildCancellation(cause)
+ } catch (e: Throwable) {
+ cause.addSuppressed(e)
+ /* the coroutine context does not matter here, because we're only interested in reporting this exception
+ to the platform-specific global handler, not to a [CoroutineExceptionHandler] of any sort. */
+ handleCoroutineException(this, cause)
+ }.let { false }
+}
diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
new file mode 100644
index 00000000..24e093be
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test.internal
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.test.*
+import kotlin.coroutines.*
+
+/**
+ * The testable main dispatcher used by kotlinx-coroutines-test.
+ * It is a [MainCoroutineDispatcher] that delegates all actions to a settable delegate.
+ */
+internal class TestMainDispatcher(delegate: CoroutineDispatcher):
+ MainCoroutineDispatcher(),
+ Delay
+{
+ private val mainDispatcher = delegate
+ private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main")
+
+ private val delay
+ get() = delegate.value as? Delay ?: defaultDelay
+
+ override val immediate: MainCoroutineDispatcher
+ get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block)
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context)
+
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block)
+
+ fun setDispatcher(dispatcher: CoroutineDispatcher) {
+ delegate.value = dispatcher
+ }
+
+ fun resetDispatcher() {
+ delegate.value = mainDispatcher
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
+ delay.scheduleResumeAfterDelay(timeMillis, continuation)
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+ delay.invokeOnTimeout(timeMillis, block, context)
+
+ companion object {
+ internal val currentTestDispatcher
+ get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher
+
+ internal val currentTestScheduler
+ get() = currentTestDispatcher?.scheduler
+ }
+
+ /**
+ * A wrapper around a value that attempts to throw when writing happens concurrently with reading.
+ *
+ * The read operations never throw. Instead, the failures detected inside them will be remembered and thrown on the
+ * next modification.
+ */
+ private class NonConcurrentlyModifiable<T>(initialValue: T, private val name: String) {
+ private val readers = atomic(0) // number of concurrent readers
+ private val isWriting = atomic(false) // a modification is happening currently
+ private val exceptionWhenReading: AtomicRef<Throwable?> = atomic(null) // exception from reading
+ private val _value = atomic(initialValue) // the backing field for the value
+
+ private fun concurrentWW() = IllegalStateException("$name is modified concurrently")
+ private fun concurrentRW() = IllegalStateException("$name is used concurrently with setting it")
+
+ var value: T
+ get() {
+ readers.incrementAndGet()
+ if (isWriting.value) exceptionWhenReading.value = concurrentRW()
+ val result = _value.value
+ readers.decrementAndGet()
+ return result
+ }
+ set(value) {
+ exceptionWhenReading.getAndSet(null)?.let { throw it }
+ if (readers.value != 0) throw concurrentRW()
+ if (!isWriting.compareAndSet(expect = false, update = true)) throw concurrentWW()
+ _value.value = value
+ isWriting.value = false
+ if (readers.value != 0) throw concurrentRW()
+ }
+ }
+}
+
+@Suppress("INVISIBLE_MEMBER")
+private val defaultDelay
+ inline get() = DefaultDelay
+
+@Suppress("INVISIBLE_MEMBER")
+internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher
diff --git a/kotlinx-coroutines-test/common/test/Helpers.kt b/kotlinx-coroutines-test/common/test/Helpers.kt
new file mode 100644
index 00000000..98375b09
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/Helpers.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.atomicfu.*
+import kotlin.test.*
+import kotlin.time.*
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * The number of milliseconds that is sure not to pass [assertRunsFast].
+ */
+const val SLOW = 100_000L
+
+/**
+ * Asserts that a block completed within [timeout].
+ */
+@OptIn(ExperimentalTime::class)
+inline fun <T> assertRunsFast(timeout: Duration, block: () -> T): T {
+ val result: T
+ val elapsed = TimeSource.Monotonic.measureTime { result = block() }
+ assertTrue("Should complete in $timeout, but took $elapsed") { elapsed < timeout }
+ return result
+}
+
+/**
+ * Asserts that a block completed within two seconds.
+ */
+inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(2.seconds, block)
+
+/**
+ * Passes [test] as an argument to [block], but as a function returning not a [TestResult] but [Unit].
+*/
+expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult
+
+class TestException(message: String? = null): Exception(message)
+
+/**
+ * A class inheriting from which allows to check the execution order inside tests.
+ *
+ * @see TestBase
+ */
+open class OrderedExecutionTestBase {
+ private val actionIndex = atomic(0)
+ private val finished = atomic(false)
+
+ /** Expect the next action to be [index] in order. */
+ protected fun expect(index: Int) {
+ val wasIndex = actionIndex.incrementAndGet()
+ check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
+ }
+
+ /** Expect this action to be final, with the given [index]. */
+ protected fun finish(index: Int) {
+ expect(index)
+ check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
+ }
+
+ @AfterTest
+ fun ensureFinishCalls() {
+ assertTrue(finished.value || actionIndex.value == 0, "Expected `finish` to be called")
+ }
+}
+
+internal fun <T> T.void() { }
+
+@OptionalExpectation
+expect annotation class NoJs()
+
+@OptionalExpectation
+expect annotation class NoNative()
diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt
new file mode 100644
index 00000000..1430d830
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.flow.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class RunTestTest {
+
+ /** Tests that [withContext] that sends work to other threads works in [runTest]. */
+ @Test
+ fun testWithContextDispatching() = runTest {
+ var counter = 0
+ withContext(Dispatchers.Default) {
+ counter += 1
+ }
+ assertEquals(counter, 1)
+ }
+
+ /** Tests that joining [GlobalScope.launch] works in [runTest]. */
+ @Test
+ fun testJoiningForkedJob() = runTest {
+ var counter = 0
+ val job = GlobalScope.launch {
+ counter += 1
+ }
+ job.join()
+ assertEquals(counter, 1)
+ }
+
+ /** Tests [suspendCoroutine] not failing [runTest]. */
+ @Test
+ fun testSuspendCoroutine() = runTest {
+ val answer = suspendCoroutine<Int> {
+ it.resume(42)
+ }
+ assertEquals(42, answer)
+ }
+
+ /** Tests that [runTest] attempts to detect it being run inside another [runTest] and failing in such scenarios. */
+ @Test
+ fun testNestedRunTestForbidden() = runTest {
+ assertFailsWith<IllegalStateException> {
+ runTest { }
+ }
+ }
+
+ /** Tests that even the dispatch timeout of `0` is fine if all the dispatches go through the same scheduler. */
+ @Test
+ fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTest(dispatchTimeoutMs = 0) {
+ // below is some arbitrary concurrent code where all dispatches go through the same scheduler.
+ launch {
+ delay(2000)
+ }
+ val deferred = async {
+ val job = launch(StandardTestDispatcher(testScheduler)) {
+ launch {
+ delay(500)
+ }
+ delay(1000)
+ }
+ job.join()
+ }
+ deferred.await()
+ }
+
+ /** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */
+ @Test
+ @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
+ fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 0) {
+ withContext(Dispatchers.Default) {
+ delay(10)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that too low of a dispatch timeout causes crashes. */
+ @Test
+ @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
+ fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 100) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that, on timeout, the names of the active coroutines are listed,
+ * whereas the names of the completed ones are not. */
+ @Test
+ @NoJs
+ @NoNative
+ fun testListingActiveCoroutinesOnTimeout(): TestResult {
+ val name1 = "GoodUniqueName"
+ val name2 = "BadUniqueName"
+ return testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: UncompletedCoroutinesError) {
+ assertTrue((e.message ?: "").contains(name1))
+ assertFalse((e.message ?: "").contains(name2))
+ }
+ }) {
+ runTest(dispatchTimeoutMs = 10) {
+ launch(CoroutineName(name1)) {
+ CompletableDeferred<Unit>().await()
+ }
+ launch(CoroutineName(name2)) {
+ }
+ }
+ }
+ }
+
+ /** Tests that the [UncompletedCoroutinesError] suppresses an exception with which the coroutine is completing. */
+ @Test
+ fun testFailureWithPendingCoroutine() = testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: UncompletedCoroutinesError) {
+ @Suppress("INVISIBLE_MEMBER")
+ val suppressed = unwrap(e).suppressedExceptions
+ assertEquals(1, suppressed.size)
+ assertIs<TestException>(suppressed[0]).also {
+ assertEquals("A", it.message)
+ }
+ }
+ }) {
+ runTest(dispatchTimeoutMs = 10) {
+ launch {
+ withContext(NonCancellable) {
+ awaitCancellation()
+ }
+ }
+ yield()
+ throw TestException("A")
+ }
+ }
+
+ /** Tests that real delays can be accounted for with a large enough dispatch timeout. */
+ @Test
+ fun testRunTestWithLargeTimeout() = runTest(dispatchTimeoutMs = 5000) {
+ withContext(Dispatchers.Default) {
+ delay(50)
+ }
+ }
+
+ /** Tests uncaught exceptions being suppressed by the dispatch timeout error. */
+ @Test
+ @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
+ fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
+ try {
+ fn()
+ fail("unreached")
+ } catch (e: UncompletedCoroutinesError) {
+ @Suppress("INVISIBLE_MEMBER")
+ val suppressed = unwrap(e).suppressedExceptions
+ assertEquals(1, suppressed.size)
+ assertIs<TestException>(suppressed[0]).also {
+ assertEquals("A", it.message)
+ }
+ }
+ }) {
+ runTest(dispatchTimeoutMs = 1) {
+ coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A"))
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that passing invalid contexts to [runTest] causes it to fail (on JS, without forking). */
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runTest(ctx) { }
+ }
+ }
+ }
+
+ /** Tests that throwing exceptions in [runTest] fails the test with them. */
+ @Test
+ fun testThrowingInRunTestBody() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTest {
+ throw RuntimeException()
+ }
+ }
+
+ /** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */
+ @Test
+ fun testThrowingInRunTestPendingTask() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTest {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runTest {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ /** Tests that, once the test body has thrown, the child coroutines are cancelled. */
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure(): TestResult {
+ var job: Job? = null
+ return testResultMap({
+ assertFailsWith<AssertionError> { it() }
+ assertTrue(job!!.isCancelled)
+ }) {
+ runTest {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ }
+
+ /** Tests that [runTest] reports [TimeoutCancellationException]. */
+ @Test
+ fun testTimeout() = testResultMap({
+ assertFailsWith<TimeoutCancellationException> { it() }
+ }) {
+ runTest {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+
+ /** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */
+ @Test
+ fun testRunTestThrowsRootCause() = testResultMap({
+ assertFailsWith<TestException> { it() }
+ }) {
+ runTest {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+
+ /** Tests that [runTest] completes its job. */
+ @Test
+ fun testCompletesOwnJob(): TestResult {
+ var handlerCalled = false
+ return testResultMap({
+ it()
+ assertTrue(handlerCalled)
+ }) {
+ runTest {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ }
+ }
+
+ /** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */
+ @Test
+ fun testDoesNotCompleteGivenJob(): TestResult {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ return testResultMap({
+ it()
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }) {
+ runTest(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ }
+ }
+
+ /** Tests that, when the test body fails, the reported exceptions are suppressed. */
+ @Test
+ fun testSuppressedExceptions() = testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }) {
+ runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ }
+
+ /** Tests that [TestCoroutineScope.runTest] does not inherit the exception handler and works. */
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+
+ /**
+ * Tests that if the main coroutine is completed without a dispatch, [runTest] will not consider this to be
+ * inactivity.
+ *
+ * The test will hang if this is not the case.
+ */
+ @Test
+ fun testCoroutineCompletingWithoutDispatch() = runTest(dispatchTimeoutMs = Long.MAX_VALUE) {
+ launch(Dispatchers.Default) { delay(100) }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
new file mode 100644
index 00000000..d66be9bd
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+class StandardTestDispatcherTest: OrderedExecutionTestBase() {
+
+ private val scope = TestScope(StandardTestDispatcher())
+
+ @BeforeTest
+ fun init() {
+ scope.asSpecificImplementation().enter()
+ }
+
+ @AfterTest
+ fun cleanup() {
+ scope.runCurrent()
+ assertEquals(listOf(), scope.asSpecificImplementation().leave())
+ }
+
+ /** Tests that the [StandardTestDispatcher] follows an execution order similar to `runBlocking`. */
+ @Test
+ fun testFlowsNotSkippingValues() = scope.launch {
+ // https://github.com/Kotlin/kotlinx.coroutines/issues/1626#issuecomment-554632852
+ val list = flowOf(1).onStart { emit(0) }
+ .combine(flowOf("A")) { int, str -> "$str$int" }
+ .toList()
+ assertEquals(list, listOf("A0", "A1"))
+ }.void()
+
+ /** Tests that each [launch] gets dispatched. */
+ @Test
+ fun testLaunchDispatched() = scope.launch {
+ expect(1)
+ launch {
+ expect(3)
+ }
+ finish(2)
+ }.void()
+
+ /** Tests that dispatching is done in a predictable order and [yield] puts this task at the end of the queue. */
+ @Test
+ fun testYield() = scope.launch {
+ expect(1)
+ scope.launch {
+ expect(3)
+ yield()
+ expect(6)
+ }
+ scope.launch {
+ expect(4)
+ yield()
+ finish(7)
+ }
+ expect(2)
+ yield()
+ expect(5)
+ }.void()
+
+ /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */
+ @Test
+ @NoNative
+ fun testSchedulerReuse() {
+ val dispatcher1 = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher1)
+ try {
+ val dispatcher2 = StandardTestDispatcher()
+ assertSame(dispatcher1.scheduler, dispatcher2.scheduler)
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt
new file mode 100644
index 00000000..d050e9c8
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+import kotlin.time.*
+import kotlin.time.Duration.Companion.seconds
+
+class TestCoroutineSchedulerTest {
+ /** Tests that `TestCoroutineScheduler` attempts to detect if there are several instances of it. */
+ @Test
+ fun testContextElement() = runTest {
+ assertFailsWith<IllegalStateException> {
+ withContext(StandardTestDispatcher()) {
+ }
+ }
+ }
+
+ /** Tests that, as opposed to [DelayController.advanceTimeBy] or [TestCoroutineScope.advanceTimeBy],
+ * [TestCoroutineScheduler.advanceTimeBy] doesn't run the tasks scheduled at the target moment. */
+ @Test
+ fun testAdvanceTimeByDoesNotRunCurrent() = runTest {
+ var entered = false
+ launch {
+ delay(15)
+ entered = true
+ }
+ testScheduler.advanceTimeBy(15)
+ assertFalse(entered)
+ testScheduler.runCurrent()
+ assertTrue(entered)
+ }
+
+ /** Tests that [TestCoroutineScheduler.advanceTimeBy] doesn't accept negative delays. */
+ @Test
+ fun testAdvanceTimeByWithNegativeDelay() {
+ val scheduler = TestCoroutineScheduler()
+ assertFailsWith<IllegalArgumentException> {
+ scheduler.advanceTimeBy(-1)
+ }
+ }
+
+ /** Tests that if [TestCoroutineScheduler.advanceTimeBy] encounters an arithmetic overflow, all the tasks scheduled
+ * until the moment [Long.MAX_VALUE] get run. */
+ @Test
+ fun testAdvanceTimeByEnormousDelays() = forTestDispatchers {
+ assertRunsFast {
+ with (TestScope(it)) {
+ launch {
+ val initialDelay = 10L
+ delay(initialDelay)
+ assertEquals(initialDelay, currentTime)
+ var enteredInfinity = false
+ launch {
+ delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing
+ assertEquals(Long.MAX_VALUE, currentTime)
+ enteredInfinity = true
+ }
+ var enteredNearInfinity = false
+ launch {
+ delay(Long.MAX_VALUE - initialDelay - 1)
+ assertEquals(Long.MAX_VALUE - 1, currentTime)
+ enteredNearInfinity = true
+ }
+ testScheduler.advanceTimeBy(Long.MAX_VALUE)
+ assertFalse(enteredInfinity)
+ assertTrue(enteredNearInfinity)
+ assertEquals(Long.MAX_VALUE, currentTime)
+ testScheduler.runCurrent()
+ assertTrue(enteredInfinity)
+ }
+ testScheduler.advanceUntilIdle()
+ }
+ }
+ }
+
+ /** Tests the basic functionality of [TestCoroutineScheduler.advanceTimeBy]. */
+ @Test
+ fun testAdvanceTimeBy() = runTest {
+ assertRunsFast {
+ var stage = 1
+ launch {
+ delay(1_000)
+ assertEquals(1_000, currentTime)
+ stage = 2
+ delay(500)
+ assertEquals(1_500, currentTime)
+ stage = 3
+ delay(501)
+ assertEquals(2_001, currentTime)
+ stage = 4
+ }
+ assertEquals(1, stage)
+ assertEquals(0, currentTime)
+ advanceTimeBy(2_000)
+ assertEquals(3, stage)
+ assertEquals(2_000, currentTime)
+ advanceTimeBy(2)
+ assertEquals(4, stage)
+ assertEquals(2_002, currentTime)
+ }
+ }
+
+ /** Tests the basic functionality of [TestCoroutineScheduler.runCurrent]. */
+ @Test
+ fun testRunCurrent() = runTest {
+ var stage = 0
+ launch {
+ delay(1)
+ ++stage
+ delay(1)
+ stage += 10
+ }
+ launch {
+ delay(1)
+ ++stage
+ delay(1)
+ stage += 10
+ }
+ testScheduler.advanceTimeBy(1)
+ assertEquals(0, stage)
+ runCurrent()
+ assertEquals(2, stage)
+ testScheduler.advanceTimeBy(1)
+ assertEquals(2, stage)
+ runCurrent()
+ assertEquals(22, stage)
+ }
+
+ /** Tests that [TestCoroutineScheduler.runCurrent] will not run new tasks after the current time has advanced. */
+ @Test
+ fun testRunCurrentNotDrainingQueue() = forTestDispatchers {
+ assertRunsFast {
+ val scheduler = it.scheduler
+ val scope = TestScope(it)
+ var stage = 1
+ scope.launch {
+ delay(SLOW)
+ launch {
+ delay(SLOW)
+ stage = 3
+ }
+ scheduler.advanceTimeBy(SLOW)
+ stage = 2
+ }
+ scheduler.advanceTimeBy(SLOW)
+ assertEquals(1, stage)
+ scheduler.runCurrent()
+ assertEquals(2, stage)
+ scheduler.runCurrent()
+ assertEquals(3, stage)
+ }
+ }
+
+ /** Tests that [TestCoroutineScheduler.advanceUntilIdle] doesn't hang when itself running in a scheduler task. */
+ @Test
+ fun testNestedAdvanceUntilIdle() = forTestDispatchers {
+ assertRunsFast {
+ val scheduler = it.scheduler
+ val scope = TestScope(it)
+ var executed = false
+ scope.launch {
+ launch {
+ delay(SLOW)
+ executed = true
+ }
+ scheduler.advanceUntilIdle()
+ }
+ scheduler.advanceUntilIdle()
+ assertTrue(executed)
+ }
+ }
+
+ /** Tests [yield] scheduling tasks for future execution and not executing immediately. */
+ @Test
+ fun testYield() = forTestDispatchers {
+ val scope = TestScope(it)
+ var stage = 0
+ scope.launch {
+ yield()
+ assertEquals(1, stage)
+ stage = 2
+ }
+ scope.launch {
+ yield()
+ assertEquals(2, stage)
+ stage = 3
+ }
+ assertEquals(0, stage)
+ stage = 1
+ scope.runCurrent()
+ }
+
+ /** Tests that dispatching the delayed tasks is ordered by their waking times. */
+ @Test
+ fun testDelaysPriority() = forTestDispatchers {
+ val scope = TestScope(it)
+ var lastMeasurement = 0L
+ fun checkTime(time: Long) {
+ assertTrue(lastMeasurement < time)
+ assertEquals(time, scope.currentTime)
+ lastMeasurement = scope.currentTime
+ }
+ scope.launch {
+ launch {
+ delay(100)
+ checkTime(100)
+ val deferred = async {
+ delay(70)
+ checkTime(170)
+ }
+ delay(1)
+ checkTime(101)
+ deferred.await()
+ delay(1)
+ checkTime(171)
+ }
+ launch {
+ delay(200)
+ checkTime(200)
+ }
+ launch {
+ delay(150)
+ checkTime(150)
+ delay(22)
+ checkTime(172)
+ }
+ delay(201)
+ }
+ scope.advanceUntilIdle()
+ checkTime(201)
+ }
+
+ private fun TestScope.checkTimeout(
+ timesOut: Boolean, timeoutMillis: Long = SLOW, block: suspend () -> Unit
+ ) = assertRunsFast {
+ var caughtException = false
+ asSpecificImplementation().enter()
+ launch {
+ try {
+ withTimeout(timeoutMillis) {
+ block()
+ }
+ } catch (e: TimeoutCancellationException) {
+ caughtException = true
+ }
+ }
+ advanceUntilIdle()
+ asSpecificImplementation().leave().throwAll()
+ if (timesOut)
+ assertTrue(caughtException)
+ else
+ assertFalse(caughtException)
+ }
+
+ /** Tests that timeouts get triggered. */
+ @Test
+ fun testSmallTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ scope.checkTimeout(true) {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half)
+ }
+ }
+
+ /** Tests that timeouts don't get triggered if the code finishes in time. */
+ @Test
+ fun testLargeTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ scope.checkTimeout(false) {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half - 1)
+ }
+ }
+
+ /** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */
+ @Test
+ fun testSmallAsynchronousTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ val deferred = CompletableDeferred<Unit>()
+ scope.launch {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half)
+ deferred.complete(Unit)
+ }
+ scope.checkTimeout(true) {
+ deferred.await()
+ }
+ }
+
+ /** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */
+ @Test
+ fun testLargeAsynchronousTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ val deferred = CompletableDeferred<Unit>()
+ scope.launch {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half - 1)
+ deferred.complete(Unit)
+ }
+ scope.checkTimeout(false) {
+ deferred.await()
+ }
+ }
+
+ @Test
+ @ExperimentalTime
+ fun testAdvanceTimeSource() = runTest {
+ val expected = 1.seconds
+ val actual = testTimeSource.measureTime {
+ delay(expected)
+ }
+ assertEquals(expected, actual)
+ }
+
+ private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit =
+ @Suppress("DEPRECATION")
+ listOf(
+ StandardTestDispatcher(),
+ UnconfinedTestDispatcher()
+ ).forEach {
+ try {
+ block(it)
+ } catch (e: Throwable) {
+ throw RuntimeException("Test failed for dispatcher $it", e)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
new file mode 100644
index 00000000..bcf016b3
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.test.internal.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+@NoNative
+class TestDispatchersTest: OrderedExecutionTestBase() {
+
+ @BeforeTest
+ fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+ }
+
+ @AfterTest
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ /** Tests that asynchronous execution of tests does not happen concurrently with [AfterTest]. */
+ @Test
+ fun testMainMocking() = runTest {
+ val mainAtStart = TestMainDispatcher.currentTestDispatcher
+ assertNotNull(mainAtStart)
+ withContext(Dispatchers.Main) {
+ delay(10)
+ }
+ withContext(Dispatchers.Default) {
+ delay(10)
+ }
+ withContext(Dispatchers.Main) {
+ delay(10)
+ }
+ assertSame(mainAtStart, TestMainDispatcher.currentTestDispatcher)
+ }
+
+ /** Tests that the mocked [Dispatchers.Main] correctly forwards [Delay] methods. */
+ @Test
+ fun testMockedMainImplementsDelay() = runTest {
+ val main = Dispatchers.Main
+ withContext(main) {
+ delay(10)
+ }
+ withContext(Dispatchers.Default) {
+ delay(10)
+ }
+ withContext(main) {
+ delay(10)
+ }
+ }
+
+ /** Tests that [Distpachers.setMain] fails when called with [Dispatchers.Main]. */
+ @Test
+ fun testSelfSet() {
+ assertFailsWith<IllegalArgumentException> { Dispatchers.setMain(Dispatchers.Main) }
+ }
+
+ @Test
+ fun testImmediateDispatcher() = runTest {
+ Dispatchers.setMain(ImmediateDispatcher())
+ expect(1)
+ withContext(Dispatchers.Main) {
+ expect(3)
+ }
+
+ Dispatchers.setMain(RegularDispatcher())
+ withContext(Dispatchers.Main) {
+ expect(6)
+ }
+
+ finish(7)
+ }
+
+ private inner class ImmediateDispatcher : CoroutineDispatcher() {
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean {
+ expect(2)
+ return false
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) = throw RuntimeException("Shouldn't be reached")
+ }
+
+ private inner class RegularDispatcher : CoroutineDispatcher() {
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean {
+ expect(4)
+ return true
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ expect(5)
+ block.run()
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/TestScopeTest.kt b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
new file mode 100644
index 00000000..4138ca05
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class TestScopeTest {
+ /** Tests failing to create a [TestScope] with incorrect contexts. */
+ @Test
+ fun testCreateThrowsOnInvalidArguments() {
+ for (ctx in invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ TestScope(ctx)
+ }
+ }
+ }
+
+ /** Tests that a newly-created [TestScope] provides the correct scheduler. */
+ @Test
+ fun testCreateProvidesScheduler() {
+ // Creates a new scheduler.
+ run {
+ val scope = TestScope()
+ assertNotNull(scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Reuses the scheduler that the dispatcher is linked to.
+ run {
+ val dispatcher = StandardTestDispatcher()
+ val scope = TestScope(dispatcher)
+ assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Uses the scheduler passed to it.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val scope = TestScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler)
+ }
+ // Doesn't touch the passed dispatcher and the scheduler if they match.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val scope = TestScope(scheduler + dispatcher)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
+ }
+ }
+
+ /** Part of [testCreateProvidesScheduler], disabled for Native */
+ @Test
+ @NoNative
+ fun testCreateReusesScheduler() {
+ // Reuses the scheduler of `Dispatchers.Main`
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val mainDispatcher = StandardTestDispatcher(scheduler)
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scope = TestScope()
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed
+ run {
+ val mainDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scheduler = TestCoroutineScheduler()
+ val scope = TestScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ }
+
+ /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */
+ @Test
+ fun testPresentDelaysThrowing() {
+ val scope = TestScope()
+ var result = false
+ scope.launch {
+ delay(5)
+ result = true
+ }
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws if there were active jobs by the end. */
+ @Test
+ fun testActiveJobsThrowing() {
+ val scope = TestScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ scope.launch {
+ deferred.await()
+ result = true
+ }
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws even if it detects that the job is already cancelled. */
+ @Test
+ fun testCancelledDelaysThrowing() {
+ val scope = TestScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ val job = scope.launch {
+ deferred.await()
+ result = true
+ }
+ job.cancel()
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that uncaught exceptions are thrown at the cleanup. */
+ @Test
+ fun testGetsCancelledOnChildFailure(): TestResult {
+ val scope = TestScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ return testResultMap({
+ try {
+ it()
+ fail("should not reach")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ }
+ }
+ }
+
+ /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
+ @Test
+ fun testSuppressedExceptions() {
+ TestScope().apply {
+ asSpecificImplementation().enter()
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ runCurrent()
+ val e = asSpecificImplementation().leave()
+ assertEquals(3, e.size)
+ assertEquals("x", e[0].message)
+ assertEquals("y", e[1].message)
+ assertEquals("z", e[2].message)
+ }
+ }
+
+ /** Tests that the background work is being run at all. */
+ @Test
+ fun testBackgroundWorkBeingRun(): TestResult = runTest {
+ var i = 0
+ var j = 0
+ backgroundScope.launch {
+ ++i
+ }
+ backgroundScope.launch {
+ delay(10)
+ ++j
+ }
+ assertEquals(0, i)
+ assertEquals(0, j)
+ delay(1)
+ assertEquals(1, i)
+ assertEquals(0, j)
+ delay(10)
+ assertEquals(1, i)
+ assertEquals(1, j)
+ }
+
+ /**
+ * Tests that the background work gets cancelled after the test body finishes.
+ */
+ @Test
+ fun testBackgroundWorkCancelled(): TestResult {
+ var cancelled = false
+ return testResultMap({
+ it()
+ assertTrue(cancelled)
+ }) {
+ runTest {
+ var i = 0
+ backgroundScope.launch {
+ try {
+ while (isActive) {
+ ++i
+ yield()
+ }
+ } catch (e: CancellationException) {
+ cancelled = true
+ }
+ }
+ repeat(5) {
+ assertEquals(i, it)
+ yield()
+ }
+ }
+ }
+ }
+
+ /** Tests the interactions between the time-control commands and the background work. */
+ @Test
+ fun testBackgroundWorkTimeControl(): TestResult = runTest {
+ var i = 0
+ var j = 0
+ backgroundScope.launch {
+ while (true) {
+ ++i
+ delay(100)
+ }
+ }
+ backgroundScope.launch {
+ while (true) {
+ ++j
+ delay(50)
+ }
+ }
+ advanceUntilIdle() // should do nothing, as only background work is left.
+ assertEquals(0, i)
+ assertEquals(0, j)
+ val job = launch {
+ delay(1)
+ // the background work scheduled for earlier gets executed before the normal work scheduled for later does
+ assertEquals(1, i)
+ assertEquals(1, j)
+ }
+ job.join()
+ advanceTimeBy(199) // should work the same for the background tasks
+ assertEquals(2, i)
+ assertEquals(4, j)
+ advanceUntilIdle() // once again, should do nothing
+ assertEquals(2, i)
+ assertEquals(4, j)
+ runCurrent() // should behave the same way as for the normal work
+ assertEquals(3, i)
+ assertEquals(5, j)
+ launch {
+ delay(1001)
+ assertEquals(13, i)
+ assertEquals(25, j)
+ }
+ advanceUntilIdle() // should execute the normal work, and with that, the background one, too
+ }
+
+ /**
+ * Tests that an error in a background coroutine does not cancel the test, but is reported at the end.
+ */
+ @Test
+ fun testBackgroundWorkErrorReporting(): TestResult {
+ var testFinished = false
+ val exception = RuntimeException("x")
+ return testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: Throwable) {
+ assertSame(e, exception)
+ assertTrue(testFinished)
+ }
+ }) {
+ runTest {
+ backgroundScope.launch {
+ throw exception
+ }
+ delay(1000)
+ testFinished = true
+ }
+ }
+ }
+
+ /**
+ * Tests that the background work gets to finish what it's doing after the test is completed.
+ */
+ @Test
+ fun testBackgroundWorkFinalizing(): TestResult {
+ var taskEnded = 0
+ val nTasks = 10
+ return testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals(2, e.suppressedExceptions.size)
+ assertEquals(nTasks, taskEnded)
+ }
+ }) {
+ runTest {
+ repeat(nTasks) {
+ backgroundScope.launch {
+ try {
+ while (true) {
+ delay(1)
+ }
+ } finally {
+ ++taskEnded
+ if (taskEnded <= 2)
+ throw TestException()
+ }
+ }
+ }
+ delay(100)
+ throw TestException()
+ }
+ }
+ }
+
+ /**
+ * Tests using [Flow.stateIn] as a background job.
+ */
+ @Test
+ fun testExampleBackgroundJob1() = runTest {
+ val myFlow = flow {
+ var i = 0
+ while (true) {
+ emit(++i)
+ delay(1)
+ }
+ }
+ val stateFlow = myFlow.stateIn(backgroundScope, SharingStarted.Eagerly, 0)
+ var j = 0
+ repeat(100) {
+ assertEquals(j++, stateFlow.value)
+ delay(1)
+ }
+ }
+
+ /**
+ * A test from the documentation of [TestScope.backgroundScope].
+ */
+ @Test
+ fun testExampleBackgroundJob2() = runTest {
+ val channel = Channel<Int>()
+ backgroundScope.launch {
+ var i = 0
+ while (true) {
+ channel.send(i++)
+ }
+ }
+ repeat(100) {
+ assertEquals(it, channel.receive())
+ }
+ }
+
+ /**
+ * Tests that the test will timeout due to idleness even if some background tasks are running.
+ */
+ @Test
+ fun testBackgroundWorkNotPreventingTimeout(): TestResult = testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (_: UncompletedCoroutinesError) {
+
+ }
+ }) {
+ runTest(dispatchTimeoutMs = 100) {
+ backgroundScope.launch {
+ while (true) {
+ yield()
+ }
+ }
+ backgroundScope.launch {
+ while (true) {
+ delay(1)
+ }
+ }
+ val deferred = CompletableDeferred<Unit>()
+ deferred.await()
+ }
+
+ }
+
+ /**
+ * Tests that the background work will not prevent the test from timing out even in some cases
+ * when the unconfined dispatcher is used.
+ */
+ @Test
+ fun testUnconfinedBackgroundWorkNotPreventingTimeout(): TestResult = testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (_: UncompletedCoroutinesError) {
+
+ }
+ }) {
+ runTest(UnconfinedTestDispatcher(), dispatchTimeoutMs = 100) {
+ /**
+ * Having a coroutine like this will still cause the test to hang:
+ backgroundScope.launch {
+ while (true) {
+ yield()
+ }
+ }
+ * The reason is that even the initial [advanceUntilIdle] will never return in this case.
+ */
+ backgroundScope.launch {
+ while (true) {
+ delay(1)
+ }
+ }
+ val deferred = CompletableDeferred<Unit>()
+ deferred.await()
+ }
+ }
+
+ /**
+ * Tests that even the exceptions in the background scope that don't typically get reported and need to be queried
+ * (like failures in [async]) will still surface in some simple scenarios.
+ */
+ @Test
+ fun testAsyncFailureInBackgroundReported() = testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals("z", e.message)
+ assertEquals(setOf("x", "y"), e.suppressedExceptions.map { it.message }.toSet())
+ }
+ }) {
+ runTest {
+ backgroundScope.async {
+ throw TestException("x")
+ }
+ backgroundScope.produce<Unit> {
+ throw TestException("y")
+ }
+ delay(1)
+ throw TestException("z")
+ }
+ }
+
+ /**
+ * Tests that, if an exception reaches the [TestScope] exception reporting mechanism via several
+ * channels, it will only be reported once.
+ */
+ @Test
+ fun testNoDuplicateExceptions() = testResultMap({
+ try {
+ it()
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals("y", e.message)
+ assertEquals(listOf("x"), e.suppressedExceptions.map { it.message })
+ }
+ }) {
+ runTest {
+ backgroundScope.launch {
+ throw TestException("x")
+ }
+ delay(1)
+ throw TestException("y")
+ }
+ }
+
+ companion object {
+ internal val invalidContexts = listOf(
+ Dispatchers.Default, // not a [TestDispatcher]
+ CoroutineExceptionHandler { _, _ -> }, // exception handlers can't be overridden
+ StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
+ )
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt
new file mode 100644
index 00000000..ee63e6d1
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+class UnconfinedTestDispatcherTest {
+
+ @Test
+ fun reproducer1742() {
+ class ObservableValue<T>(initial: T) {
+ var value: T = initial
+ private set
+
+ private val listeners = mutableListOf<(T) -> Unit>()
+
+ fun set(value: T) {
+ this.value = value
+ listeners.forEach { it(value) }
+ }
+
+ fun addListener(listener: (T) -> Unit) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: (T) -> Unit) {
+ listeners.remove(listener)
+ }
+ }
+
+ fun <T> ObservableValue<T>.observe(): Flow<T> =
+ callbackFlow {
+ val listener = { value: T ->
+ if (!isClosedForSend) {
+ trySend(value)
+ }
+ }
+ addListener(listener)
+ listener(value)
+ awaitClose { removeListener(listener) }
+ }
+
+ val intProvider = ObservableValue(0)
+ val stringProvider = ObservableValue("")
+ var data = Pair(0, "")
+ val scope = CoroutineScope(UnconfinedTestDispatcher())
+ scope.launch {
+ combine(
+ intProvider.observe(),
+ stringProvider.observe()
+ ) { intValue, stringValue -> Pair(intValue, stringValue) }
+ .collect { pair ->
+ data = pair
+ }
+ }
+
+ intProvider.set(1)
+ stringProvider.set("3")
+ intProvider.set(2)
+ intProvider.set(3)
+
+ scope.cancel()
+ assertEquals(Pair(3, "3"), data)
+ }
+
+ @Test
+ fun reproducer2082() = runTest {
+ val subject1 = MutableStateFlow(1)
+ val subject2 = MutableStateFlow("a")
+ val values = mutableListOf<Pair<Int, String>>()
+
+ val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ combine(subject1, subject2) { intVal, strVal -> intVal to strVal }
+ .collect {
+ delay(10000)
+ values += it
+ }
+ }
+
+ subject1.value = 2
+ delay(10000)
+ subject2.value = "b"
+ delay(10000)
+
+ subject1.value = 3
+ delay(10000)
+ subject2.value = "c"
+ delay(10000)
+ delay(10000)
+ delay(1)
+
+ job.cancel()
+
+ assertEquals(listOf(Pair(1, "a"), Pair(2, "a"), Pair(2, "b"), Pair(3, "b"), Pair(3, "c")), values)
+ }
+
+ @Test
+ fun reproducer2405() = createTestResult {
+ val dispatcher = UnconfinedTestDispatcher()
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ /** An example from the [UnconfinedTestDispatcher] documentation. */
+ @Test
+ fun testUnconfinedDispatcher() = runTest {
+ val values = mutableListOf<Int>()
+ val stateFlow = MutableStateFlow(0)
+ val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ stateFlow.collect {
+ values.add(it)
+ }
+ }
+ stateFlow.value = 1
+ stateFlow.value = 2
+ stateFlow.value = 3
+ job.cancel()
+ assertEquals(listOf(0, 1, 2, 3), values)
+ }
+
+ /** Tests that child coroutines are eagerly entered. */
+ @Test
+ fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered = false
+ val deferred = CompletableDeferred<Unit>()
+ var completed = false
+ launch {
+ entered = true
+ deferred.await()
+ completed = true
+ }
+ assertTrue(entered) // `entered = true` already executed.
+ assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ deferred.complete(Unit) // resume the coroutine.
+ assertTrue(completed) // now the child coroutine is immediately completed.
+ }
+
+ /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */
+ @Test
+ @NoNative
+ fun testSchedulerReuse() {
+ val dispatcher1 = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher1)
+ try {
+ val dispatcher2 = UnconfinedTestDispatcher()
+ assertSame(dispatcher1.scheduler, dispatcher2.scheduler)
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/js/src/TestBuilders.kt b/kotlinx-coroutines-test/js/src/TestBuilders.kt
new file mode 100644
index 00000000..9da91ffc
--- /dev/null
+++ b/kotlinx-coroutines-test/js/src/TestBuilders.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+import kotlinx.coroutines.*
+import kotlin.js.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
+public actual typealias TestResult = Promise<Unit>
+
+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
new file mode 100644
index 00000000..4d865f83
--- /dev/null
+++ b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test.internal
+import kotlinx.coroutines.*
+
+@Suppress("INVISIBLE_MEMBER")
+internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher =
+ when (val mainDispatcher = Main) {
+ is TestMainDispatcher -> mainDispatcher
+ else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) }
+ }
diff --git a/kotlinx-coroutines-test/js/test/Helpers.kt b/kotlinx-coroutines-test/js/test/Helpers.kt
new file mode 100644
index 00000000..5f19d1ac
--- /dev/null
+++ b/kotlinx-coroutines-test/js/test/Helpers.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlin.test.*
+
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult =
+ test().then(
+ {
+ block {
+ }
+ }, {
+ block {
+ throw it
+ }
+ })
+
+actual typealias NoJs = Ignore
diff --git a/kotlinx-coroutines-test/js/test/PromiseTest.kt b/kotlinx-coroutines-test/js/test/PromiseTest.kt
new file mode 100644
index 00000000..ff09d9ab
--- /dev/null
+++ b/kotlinx-coroutines-test/js/test/PromiseTest.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+class PromiseTest {
+ @Test
+ fun testCompletionFromPromise() = runTest {
+ var promiseEntered = false
+ val p = promise {
+ delay(1)
+ promiseEntered = true
+ }
+ delay(2)
+ p.await()
+ assertTrue(promiseEntered)
+ }
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/resources/META-INF/proguard/coroutines.pro b/kotlinx-coroutines-test/jvm/resources/META-INF/proguard/coroutines.pro
index 1fdfb787..1fdfb787 100644
--- a/kotlinx-coroutines-test/resources/META-INF/proguard/coroutines.pro
+++ b/kotlinx-coroutines-test/jvm/resources/META-INF/proguard/coroutines.pro
diff --git a/kotlinx-coroutines-test/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/kotlinx-coroutines-test/jvm/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
index 0ec0c9d5..0ec0c9d5 100644
--- a/kotlinx-coroutines-test/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
+++ b/kotlinx-coroutines-test/jvm/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
diff --git a/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
new file mode 100644
index 00000000..06fbe810
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+public actual typealias TestResult = Unit
+
+internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit) {
+ runBlocking {
+ testProcedure()
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt
new file mode 100644
index 00000000..f86b08ea
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test.internal
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+
+internal class TestMainDispatcherFactory : MainDispatcherFactory {
+
+ override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
+ val otherFactories = allFactories.filter { it !== this }
+ val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory
+ val dispatcher = secondBestFactory.tryCreateDispatcher(otherFactories)
+ return TestMainDispatcher(dispatcher)
+ }
+
+ /**
+ * [Int.MAX_VALUE] -- test dispatcher always wins no matter what factories are present in the classpath.
+ * By default, all actions are delegated to the second-priority dispatcher, so that it won't be the issue.
+ */
+ override val loadPriority: Int
+ get() = Int.MAX_VALUE
+}
+
+internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher {
+ val mainDispatcher = Main
+ require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
+ return mainDispatcher
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt
new file mode 100644
index 00000000..3ccf2cad
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+
+/**
+ * Control the virtual clock time of a [CoroutineDispatcher].
+ *
+ * Testing libraries may expose this interface to the tests instead of [TestCoroutineDispatcher].
+ *
+ * This interface is deprecated without replacement.
+ * Instead, [TestCoroutineScheduler] is supposed to be used to control the virtual time.
+ * Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "Use `TestCoroutineScheduler` to control virtual time.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public interface DelayController {
+ /**
+ * Returns the current virtual clock-time as it is known to this Dispatcher.
+ *
+ * @return The virtual clock-time
+ */
+ @ExperimentalCoroutinesApi
+ public val currentTime: Long
+
+ /**
+ * Moves the Dispatcher's virtual clock forward by a specified amount of time.
+ *
+ * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
+ * blocking coroutines.
+ *
+ * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
+ * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
+ * calls to delay.
+ *
+ * ```
+ * @Test
+ * fun advanceTimeTest() = runBlockingTest {
+ * foo()
+ * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
+ * // virtual time is 2_000, next resume is at 2_001
+ * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
+ * // virtual time is 2_0002
+ * }
+ *
+ * fun CoroutineScope.foo() {
+ * launch {
+ * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
+ * // virtual time is 1_000
+ * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
+ * // virtual time is 1_500
+ * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
+ * // virtual time is 2_001
+ * }
+ * }
+ * ```
+ *
+ * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
+ * @return The amount of delay-time that this Dispatcher's clock has been forwarded.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceTimeBy(delayTimeMillis: Long): Long
+
+ /**
+ * Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
+ *
+ * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
+ * returns.
+ *
+ * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceUntilIdle(): Long
+
+ /**
+ * Run any tasks that are pending at or before the current virtual clock-time.
+ *
+ * Calling this function will never advance the clock.
+ */
+ @ExperimentalCoroutinesApi
+ public fun runCurrent()
+
+ /**
+ * Call after test code completes to ensure that the dispatcher is properly cleaned up.
+ *
+ * @throws AssertionError if any pending tasks are active, however it will not throw for suspended
+ * coroutines.
+ */
+ @ExperimentalCoroutinesApi
+ @Throws(AssertionError::class)
+ public fun cleanupTestCoroutines()
+
+ /**
+ * Run a block of code in a paused dispatcher.
+ *
+ * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
+ * will resume auto-advancing.
+ *
+ * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
+ * setup may be done between the time the coroutine is created and started.
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public suspend fun pauseDispatcher(block: suspend () -> Unit)
+
+ /**
+ * Pause the dispatcher.
+ *
+ * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
+ * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun pauseDispatcher()
+
+ /**
+ * Resume the dispatcher from a paused state.
+ *
+ * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
+ * time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
+ * or [advanceUntilIdle].
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun resumeDispatcher()
+}
+
+internal interface SchedulerAsDelayController : DelayController {
+ val scheduler: TestCoroutineScheduler
+
+ /** @suppress */
+ @Deprecated(
+ "This property delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.currentTime"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override val currentTime: Long
+ get() = scheduler.currentTime
+
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun advanceTimeBy(delayTimeMillis: Long): Long {
+ val oldTime = scheduler.currentTime
+ scheduler.advanceTimeBy(delayTimeMillis)
+ scheduler.runCurrent()
+ return scheduler.currentTime - oldTime
+ }
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.advanceUntilIdle()"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun advanceUntilIdle(): Long {
+ val oldTime = scheduler.currentTime
+ scheduler.advanceUntilIdle()
+ return scheduler.currentTime - oldTime
+ }
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.runCurrent()"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun runCurrent(): Unit = scheduler.runCurrent()
+
+ /** @suppress */
+ @ExperimentalCoroutinesApi
+ override fun cleanupTestCoroutines() {
+ // process any pending cancellations or completions, but don't advance time
+ scheduler.runCurrent()
+ if (!scheduler.isIdle(strict = false)) {
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines during tear-down. Ensure all coroutines are" +
+ " completed or cancelled by your test."
+ )
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt
new file mode 100644
index 00000000..eabdffb2
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+@file:JvmName("TestBuildersKt")
+@file:JvmMultifileClass
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * Executes a [testBody] inside an immediate execution dispatcher.
+ *
+ * This method is deprecated in favor of [runTest]. Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ *
+ * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
+ * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
+ * extra time.
+ *
+ * ```
+ * @Test
+ * fun exampleTest() = runBlockingTest {
+ * val deferred = async {
+ * delay(1_000)
+ * async {
+ * delay(1_000)
+ * }.await()
+ * }
+ *
+ * deferred.await() // result available immediately
+ * }
+ *
+ * ```
+ *
+ * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
+ * conditions.
+ *
+ * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
+ *
+ * @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches
+ * (including coroutines suspended on join/await).
+ *
+ * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
+ * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
+ * @param testBody The code of the unit-test.
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers. " +
+ "Please see the migration guide for details: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runBlockingTest(
+ context: CoroutineContext = EmptyCoroutineContext,
+ testBody: suspend TestCoroutineScope.() -> Unit
+) {
+ val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
+ val scheduler = scope.testScheduler
+ val deferred = scope.async {
+ scope.testBody()
+ }
+ scheduler.advanceUntilIdle()
+ deferred.getCompletionExceptionOrNull()?.let {
+ throw it
+ }
+ scope.cleanupTestCoroutines()
+}
+
+/**
+ * A version of [runBlockingTest] that works with [TestScope].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runBlockingTestOnTestScope(
+ context: CoroutineContext = EmptyCoroutineContext,
+ testBody: suspend TestScope.() -> Unit
+) {
+ val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context
+ val startJobs = completeContext.activeJobs()
+ val scope = TestScope(completeContext).asSpecificImplementation()
+ scope.enter()
+ scope.start(CoroutineStart.UNDISPATCHED, scope) {
+ scope.testBody()
+ }
+ scope.testScheduler.advanceUntilIdle()
+ val throwable = try {
+ scope.getCompletionExceptionOrNull()
+ } catch (e: IllegalStateException) {
+ null // the deferred was not completed yet; `scope.leave()` should complain then about unfinished jobs
+ }
+ scope.backgroundScope.cancel()
+ scope.testScheduler.advanceUntilIdleOr { false }
+ throwable?.let {
+ val exceptions = try {
+ scope.leave()
+ } catch (e: UncompletedCoroutinesError) {
+ listOf()
+ }
+ (listOf(it) + exceptions).throwAll()
+ return
+ }
+ scope.leave().throwAll()
+ val jobs = completeContext.activeJobs() - startJobs
+ if (jobs.isNotEmpty())
+ throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs")
+}
+
+/**
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
+ *
+ * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
+ * Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers. " +
+ "Please see the migration guide for details: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
+ runBlockingTest(coroutineContext, block)
+
+/**
+ * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit =
+ runBlockingTestOnTestScope(coroutineContext, block)
+
+/**
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
+ *
+ * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
+ * Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers. " +
+ "Please see the migration guide for details: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
+ runBlockingTest(this, block)
+
+/**
+ * This is an overload of [runTest] that works with [TestCoroutineScope].
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runTestWithLegacyScope(
+ context: CoroutineContext = EmptyCoroutineContext,
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestCoroutineScope.() -> Unit
+): TestResult {
+ if (context[RunningInRunTest] != null)
+ throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
+ val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
+ return createTestResult {
+ runTestCoroutine(testScope, dispatchTimeoutMs, TestBodyCoroutine::tryGetCompletionCause, testBody) {
+ try {
+ testScope.cleanup()
+ emptyList()
+ } catch (e: UncompletedCoroutinesError) {
+ throw e
+ } catch (e: Throwable) {
+ listOf(e)
+ }
+ }
+ }
+}
+
+/**
+ * Runs a test in a [TestCoroutineScope] based on this one.
+ *
+ * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
+ * [block] will be different from this one, but will use its [Job] as a parent.
+ *
+ * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
+ * immediately from the test body. See the docs for [TestResult] for details.
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.runTest(
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ block: suspend TestCoroutineScope.() -> Unit
+): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block)
+
+private class TestBodyCoroutine(
+ private val testScope: TestCoroutineScope,
+) : AbstractCoroutine<Unit>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
+
+ override val testScheduler get() = testScope.testScheduler
+
+ @Deprecated(
+ "This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
+ ReplaceWith("this.cleanup()"),
+ DeprecationLevel.ERROR
+ )
+ override fun cleanupTestCoroutines() =
+ throw UnsupportedOperationException(
+ "Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
+ "it will be called at the end of the test in any case."
+ )
+
+ fun cleanup() = testScope.cleanupTestCoroutines()
+
+ /** Throws an exception if the coroutine is not completing. */
+ fun tryGetCompletionCause(): Throwable? = completionCause
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt
new file mode 100644
index 00000000..08f428f2
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+
+/**
+ * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
+ * and uses a [TestCoroutineScheduler] to control its virtual clock.
+ *
+ * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are
+ * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the
+ * methods on the dispatcher's [scheduler].
+ *
+ * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will
+ * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the
+ * methods on [DelayController].
+ *
+ * @see DelayController
+ */
+@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " +
+ "pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()):
+ TestDispatcher(), Delay, SchedulerAsDelayController
+{
+ private var dispatchImmediately = true
+ set(value) {
+ field = value
+ if (value) {
+ // there may already be tasks from setup code we need to run
+ scheduler.advanceUntilIdle()
+ }
+ }
+
+ /** @suppress */
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ if (dispatchImmediately) {
+ scheduler.sendDispatchEvent(context)
+ block.run()
+ } else {
+ post(block, context)
+ }
+ }
+
+ /** @suppress */
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ post(block, context)
+ }
+
+ /** @suppress */
+ override fun toString(): String = "TestCoroutineDispatcher[scheduler=$scheduler]"
+
+ private fun post(block: Runnable, context: CoroutineContext) =
+ scheduler.registerEvent(this, 0, block, context) { false }
+
+ /** @suppress */
+ override suspend fun pauseDispatcher(block: suspend () -> Unit) {
+ val previous = dispatchImmediately
+ dispatchImmediately = false
+ try {
+ block()
+ } finally {
+ dispatchImmediately = previous
+ }
+ }
+
+ /** @suppress */
+ override fun pauseDispatcher() {
+ dispatchImmediately = false
+ }
+
+ /** @suppress */
+ override fun resumeDispatcher() {
+ dispatchImmediately = true
+ }
+}
diff --git a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
index 66eb2359..9da521f0 100644
--- a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
@@ -5,12 +5,20 @@
package kotlinx.coroutines.test
import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
/**
* Access uncaught coroutine exceptions captured during test execution.
*/
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
+@Deprecated(
+ "Deprecated for removal without a replacement. " +
+ "Consider whether the default mechanism of handling uncaught exceptions is sufficient. " +
+ "If not, try writing your own `CoroutineExceptionHandler` and " +
+ "please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public interface UncaughtExceptionCaptor {
/**
* List of uncaught coroutine exceptions.
@@ -34,26 +42,34 @@ public interface UncaughtExceptionCaptor {
/**
* An exception handler that captures uncaught exceptions in tests.
*/
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
+@Deprecated(
+ "Deprecated for removal without a replacement. " +
+ "It may be to define one's own `CoroutineExceptionHandler` if you just need to handle '" +
+ "uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public class TestCoroutineExceptionHandler :
- AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
-{
+ AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor {
private val _exceptions = mutableListOf<Throwable>()
+ private val _lock = SynchronizedObject()
+ private var _coroutinesCleanedUp = false
- /** @suppress **/
+ @Suppress("INVISIBLE_MEMBER")
override fun handleException(context: CoroutineContext, exception: Throwable) {
- synchronized(_exceptions) {
+ synchronized(_lock) {
+ if (_coroutinesCleanedUp) {
+ handleCoroutineExceptionImpl(context, exception)
+ }
_exceptions += exception
}
}
- /** @suppress **/
- override val uncaughtExceptions: List<Throwable>
- get() = synchronized(_exceptions) { _exceptions.toList() }
+ public override val uncaughtExceptions: List<Throwable>
+ get() = synchronized(_lock) { _exceptions.toList() }
- /** @suppress **/
- override fun cleanupTestCoroutines() {
- synchronized(_exceptions) {
+ public override fun cleanupTestCoroutines() {
+ synchronized(_lock) {
+ _coroutinesCleanedUp = true
val exception = _exceptions.firstOrNull() ?: return
// log the rest
_exceptions.drop(1).forEach { it.printStackTrace() }
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt
new file mode 100644
index 00000000..4a2cbc5c
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+
+/**
+ * A scope which provides detailed control over the execution of coroutines for tests.
+ *
+ * This scope is deprecated in favor of [TestScope].
+ * Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `TestScope` in combination with `runTest` instead." +
+ "Please see the migration guide for details: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public interface TestCoroutineScope : CoroutineScope {
+ /**
+ * Called after the test completes.
+ *
+ * * It checks that there were no uncaught exceptions caught by its [CoroutineExceptionHandler].
+ * If there were any, then the first one is thrown, whereas the rest are suppressed by it.
+ * * It runs the tasks pending in the scheduler at the current time. If there are any uncompleted tasks afterwards,
+ * it fails with [UncompletedCoroutinesError].
+ * * It checks whether some new child [Job]s were created but not completed since this [TestCoroutineScope] was
+ * created. If so, it fails with [UncompletedCoroutinesError].
+ *
+ * For backward compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its
+ * [TestCoroutineExceptionHandler.cleanupTestCoroutines] behavior is performed.
+ * Likewise, if the [ContinuationInterceptor] is a [DelayController], its [DelayController.cleanupTestCoroutines]
+ * is called.
+ *
+ * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
+ * @throws AssertionError if any pending tasks are active.
+ * @throws IllegalStateException if called more than once.
+ */
+ @ExperimentalCoroutinesApi
+ @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.")
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun cleanupTestCoroutines()
+
+ /**
+ * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
+ */
+ @ExperimentalCoroutinesApi
+ public val testScheduler: TestCoroutineScheduler
+}
+
+private class TestCoroutineScopeImpl(
+ override val coroutineContext: CoroutineContext
+) : TestCoroutineScope {
+ private val lock = SynchronizedObject()
+ private var exceptions = mutableListOf<Throwable>()
+ private var cleanedUp = false
+
+ /**
+ * Reports an exception so that it is thrown on [cleanupTestCoroutines].
+ *
+ * If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
+ * it.
+ *
+ * Returns `false` if [cleanupTestCoroutines] was already called.
+ */
+ fun reportException(throwable: Throwable): Boolean =
+ synchronized(lock) {
+ if (cleanedUp) {
+ false
+ } else {
+ exceptions.add(throwable)
+ true
+ }
+ }
+
+ override val testScheduler: TestCoroutineScheduler
+ get() = coroutineContext[TestCoroutineScheduler]!!
+
+ /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */
+ private val initialJobs = coroutineContext.activeJobs()
+
+ override fun cleanupTestCoroutines() {
+ val delayController = coroutineContext.delayController
+ val hasUnfinishedJobs = if (delayController != null) {
+ try {
+ delayController.cleanupTestCoroutines()
+ false
+ } catch (e: UncompletedCoroutinesError) {
+ true
+ }
+ } else {
+ testScheduler.runCurrent()
+ !testScheduler.isIdle(strict = false)
+ }
+ (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.cleanupTestCoroutines()
+ synchronized(lock) {
+ if (cleanedUp)
+ throw IllegalStateException("Attempting to clean up a test coroutine scope more than once.")
+ cleanedUp = true
+ }
+ exceptions.firstOrNull()?.let { toThrow ->
+ exceptions.drop(1).forEach { toThrow.addSuppressed(it) }
+ throw toThrow
+ }
+ if (hasUnfinishedJobs)
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines during teardown. Ensure all coroutines are" +
+ " completed or cancelled by your test."
+ )
+ val jobs = coroutineContext.activeJobs()
+ if ((jobs - initialJobs).isNotEmpty())
+ throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
+ }
+}
+
+internal fun CoroutineContext.activeJobs(): Set<Job> {
+ return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
+}
+
+/**
+ * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher].
+ *
+ * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
+ */
+@Deprecated(
+ "This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
+ "Please use `createTestCoroutineScope` instead.",
+ ReplaceWith(
+ "createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
+ "kotlin.coroutines.EmptyCoroutineContext"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
+ val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
+ return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context)
+}
+
+/**
+ * A coroutine scope for launching test coroutines.
+ *
+ * This is a function for aiding in migration from [TestCoroutineScope] to [TestScope].
+ * Please see the
+ * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
+ * for an instruction on how to update the code for the new API.
+ *
+ * It ensures that all the test module machinery is properly initialized.
+ * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
+ * a new one is created, unless either
+ * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
+ * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
+ * its [TestCoroutineScheduler] is used.
+ * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
+ * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
+ * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
+ * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
+ * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
+ * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
+ * [TestCoroutineScope] and share your use case at
+ * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
+ * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
+ *
+ * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
+ * different scheduler.
+ * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
+ * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
+ * [UncaughtExceptionCaptor].
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "This function was introduced in order to help migrate from TestCoroutineScope to TestScope. " +
+ "Please use TestScope() construction instead, or just runTest(), without creating a scope.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
+ val ctxWithDispatcher = context.withDelaySkipping()
+ var scope: TestCoroutineScopeImpl? = null
+ val ownExceptionHandler =
+ object : AbstractCoroutineContextElement(CoroutineExceptionHandler), TestCoroutineScopeExceptionHandler {
+ override fun handleException(context: CoroutineContext, exception: Throwable) {
+ if (!scope!!.reportException(exception))
+ throw exception // let this exception crash everything
+ }
+ }
+ val exceptionHandler = when (val exceptionHandler = ctxWithDispatcher[CoroutineExceptionHandler]) {
+ is UncaughtExceptionCaptor -> exceptionHandler
+ null -> ownExceptionHandler
+ is TestCoroutineScopeExceptionHandler -> ownExceptionHandler
+ else -> throw IllegalArgumentException(
+ "A CoroutineExceptionHandler was passed to TestCoroutineScope. " +
+ "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
+ "if uncaught exceptions require special treatment."
+ )
+ }
+ val job: Job = ctxWithDispatcher[Job] ?: Job()
+ return TestCoroutineScopeImpl(ctxWithDispatcher + exceptionHandler + job).also {
+ scope = it
+ }
+}
+
+/** A marker that shows that this [CoroutineExceptionHandler] was created for [TestCoroutineScope]. With this,
+ * constructing a new [TestCoroutineScope] with the [CoroutineScope.coroutineContext] of an existing one will override
+ * the exception handler, instead of failing. */
+private interface TestCoroutineScopeExceptionHandler : CoroutineExceptionHandler
+
+private inline val CoroutineContext.delayController: DelayController?
+ get() {
+ val handler = this[ContinuationInterceptor]
+ return handler as? DelayController
+ }
+
+
+/**
+ * The current virtual time on [testScheduler][TestCoroutineScope.testScheduler].
+ * @see TestCoroutineScheduler.currentTime
+ */
+@ExperimentalCoroutinesApi
+public val TestCoroutineScope.currentTime: Long
+ get() = coroutineContext.delayController?.currentTime ?: testScheduler.currentTime
+
+/**
+ * Advances the [testScheduler][TestCoroutineScope.testScheduler] by [delayTimeMillis] and runs the tasks up to that
+ * moment (inclusive).
+ *
+ * @see TestCoroutineScheduler.advanceTimeBy
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The name of this function is misleading: it not only advances the time, but also runs the tasks " +
+ "scheduled *at* the ending moment.",
+ ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
+ DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
+ when (val controller = coroutineContext.delayController) {
+ null -> {
+ testScheduler.advanceTimeBy(delayTimeMillis)
+ testScheduler.runCurrent()
+ }
+ else -> {
+ controller.advanceTimeBy(delayTimeMillis)
+ Unit
+ }
+ }
+
+/**
+ * Advances the [testScheduler][TestCoroutineScope.testScheduler] to the point where there are no tasks remaining.
+ * @see TestCoroutineScheduler.advanceUntilIdle
+ */
+@ExperimentalCoroutinesApi
+public fun TestCoroutineScope.advanceUntilIdle() {
+ coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle()
+}
+
+/**
+ * Run any tasks that are pending at the current virtual time, according to
+ * the [testScheduler][TestCoroutineScope.testScheduler].
+ *
+ * @see TestCoroutineScheduler.runCurrent
+ */
+@ExperimentalCoroutinesApi
+public fun TestCoroutineScope.runCurrent() {
+ coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent()
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) {
+ delayControllerForPausing.pauseDispatcher(block)
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.pauseDispatcher() {
+ delayControllerForPausing.pauseDispatcher()
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.resumeDispatcher() {
+ delayControllerForPausing.resumeDispatcher()
+}
+
+/**
+ * List of uncaught coroutine exceptions, for backward compatibility.
+ *
+ * The returned list is a copy of the exceptions caught during execution.
+ * During [TestCoroutineScope.cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
+ *
+ * Exceptions are only collected in this list if the [UncaughtExceptionCaptor] is in the test context.
+ */
+@Deprecated(
+ "This list is only populated if `UncaughtExceptionCaptor` is in the test context, and so can be " +
+ "easily misused. It is only present for backward compatibility and will be removed in the subsequent " +
+ "releases. If you need to check the list of exceptions, please consider creating your own " +
+ "`CoroutineExceptionHandler`.",
+ level = DeprecationLevel.WARNING
+)
+public val TestCoroutineScope.uncaughtExceptions: List<Throwable>
+ get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions
+ ?: emptyList()
+
+private val TestCoroutineScope.delayControllerForPausing: DelayController
+ get() = coroutineContext.delayController
+ ?: throw IllegalStateException("This scope isn't able to pause its dispatchers")
diff --git a/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt
new file mode 100644
index 00000000..e9aa3ff7
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) {
+ block {
+ test()
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
new file mode 100644
index 00000000..90a16d06
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.test.*
+import kotlin.concurrent.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class MultithreadingTest {
+
+ @Test
+ fun incorrectlyCalledRunBlocking_doesNotHaveSameInterceptor() = runBlockingTest {
+ // this code is an error as a production test, please do not use this as an example
+
+ // this test exists to document this error condition, if it's possible to make this code work please update
+ val outerInterceptor = coroutineContext[ContinuationInterceptor]
+ // runBlocking always requires an argument to pass the context in tests
+ runBlocking {
+ assertNotSame(coroutineContext[ContinuationInterceptor], outerInterceptor)
+ }
+ }
+
+ @Test
+ fun testSingleThreadExecutor() = runBlocking {
+ val mainThread = Thread.currentThread()
+ Dispatchers.setMain(Dispatchers.Unconfined)
+ newSingleThreadContext("testSingleThread").use { threadPool ->
+ withContext(Dispatchers.Main) {
+ assertSame(mainThread, Thread.currentThread())
+ }
+
+ Dispatchers.setMain(threadPool)
+ withContext(Dispatchers.Main) {
+ assertNotSame(mainThread, Thread.currentThread())
+ }
+ assertSame(mainThread, Thread.currentThread())
+
+ withContext(Dispatchers.Main.immediate) {
+ assertNotSame(mainThread, Thread.currentThread())
+ }
+ assertSame(mainThread, Thread.currentThread())
+
+ Dispatchers.setMain(Dispatchers.Unconfined)
+ withContext(Dispatchers.Main.immediate) {
+ assertSame(mainThread, Thread.currentThread())
+ }
+ assertSame(mainThread, Thread.currentThread())
+ }
+ }
+
+ @Test
+ fun whenDispatchCalled_runsOnCurrentThread() {
+ val currentThread = Thread.currentThread()
+ val subject = TestCoroutineDispatcher()
+ val scope = TestCoroutineScope(subject)
+
+ val deferred = scope.async(Dispatchers.Default) {
+ withContext(subject) {
+ assertNotSame(currentThread, Thread.currentThread())
+ 3
+ }
+ }
+
+ runBlocking {
+ // just to ensure the above code terminates
+ assertEquals(3, deferred.await())
+ }
+ }
+
+ @Test
+ fun whenAllDispatchersMocked_runsOnSameThread() {
+ val currentThread = Thread.currentThread()
+ val subject = TestCoroutineDispatcher()
+ val scope = TestCoroutineScope(subject)
+
+ val deferred = scope.async(subject) {
+ withContext(subject) {
+ assertSame(currentThread, Thread.currentThread())
+ 3
+ }
+ }
+
+ runBlocking {
+ // just to ensure the above code terminates
+ assertEquals(3, deferred.await())
+ }
+ }
+
+ /** Tests that resuming the coroutine of [runTest] asynchronously in reasonable time succeeds. */
+ @Test
+ fun testResumingFromAnotherThread() = runTest {
+ suspendCancellableCoroutine<Unit> { cont ->
+ thread {
+ Thread.sleep(10)
+ cont.resume(Unit)
+ }
+ }
+ }
+
+ /** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */
+ @Test
+ fun testStandardTestDispatcherIsConfined() = runTest {
+ val initialThread = Thread.currentThread()
+ withContext(Dispatchers.IO) {
+ val ioThread = Thread.currentThread()
+ assertNotSame(initialThread, ioThread)
+ }
+ assertEquals(initialThread, Thread.currentThread())
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt
new file mode 100644
index 00000000..3edaa48f
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.concurrent.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class RunTestStressTest {
+ /** Tests that notifications about asynchronous resumptions aren't lost. */
+ @Test
+ fun testRunTestActivityNotificationsRace() {
+ val n = 1_000 * stressTestMultiplier
+ for (i in 0 until n) {
+ runTest {
+ suspendCancellableCoroutine<Unit> { cont ->
+ thread {
+ cont.resume(Unit)
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt
new file mode 100644
index 00000000..80659207
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+/** Copy of [RunTestTest], but for [runBlockingTestOnTestScope], where applicable. */
+@Suppress("DEPRECATION")
+class RunBlockingTestOnTestScopeTest {
+
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTestOnTestScope(ctx) { }
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestBody() {
+ assertFailsWith<RuntimeException> {
+ runBlockingTestOnTestScope {
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestPendingTask() {
+ assertFailsWith<RuntimeException> {
+ runBlockingTestOnTestScope {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runBlockingTestOnTestScope {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure() {
+ var job: Job? = null
+ assertFailsWith<AssertionError> {
+ runBlockingTestOnTestScope {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ assertTrue(job!!.isCancelled)
+ }
+
+ @Test
+ fun testTimeout() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTestOnTestScope {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRunTestThrowsRootCause() {
+ assertFailsWith<TestException> {
+ runBlockingTestOnTestScope {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testCompletesOwnJob() {
+ var handlerCalled = false
+ runBlockingTestOnTestScope {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ assertTrue(handlerCalled)
+ }
+
+ @Test
+ fun testDoesNotCompleteGivenJob() {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ runBlockingTestOnTestScope(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }
+
+ @Test
+ fun testSuppressedExceptions() {
+ try {
+ runBlockingTestOnTestScope {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }
+
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestCoroutineScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+
+ @Test
+ fun testBackgroundWorkBeingRun() = runBlockingTestOnTestScope {
+ var i = 0
+ var j = 0
+ backgroundScope.launch {
+ yield()
+ ++i
+ }
+ backgroundScope.launch {
+ yield()
+ delay(10)
+ ++j
+ }
+ assertEquals(0, i)
+ assertEquals(0, j)
+ delay(1)
+ assertEquals(1, i)
+ assertEquals(0, j)
+ delay(10)
+ assertEquals(1, i)
+ assertEquals(1, j)
+ }
+
+ @Test
+ fun testBackgroundWorkCancelled() {
+ var cancelled = false
+ runBlockingTestOnTestScope {
+ var i = 0
+ backgroundScope.launch {
+ yield()
+ try {
+ while (isActive) {
+ ++i
+ yield()
+ }
+ } catch (e: CancellationException) {
+ cancelled = true
+ }
+ }
+ repeat(5) {
+ assertEquals(i, it)
+ yield()
+ }
+ }
+ assertTrue(cancelled)
+ }
+
+ @Test
+ fun testBackgroundWorkTimeControl(): TestResult = runBlockingTestOnTestScope {
+ var i = 0
+ var j = 0
+ backgroundScope.launch {
+ yield()
+ while (true) {
+ ++i
+ delay(100)
+ }
+ }
+ backgroundScope.launch {
+ yield()
+ while (true) {
+ ++j
+ delay(50)
+ }
+ }
+ advanceUntilIdle() // should do nothing, as only background work is left.
+ assertEquals(0, i)
+ assertEquals(0, j)
+ val job = launch {
+ delay(1)
+ // the background work scheduled for earlier gets executed before the normal work scheduled for later does
+ assertEquals(1, i)
+ assertEquals(1, j)
+ }
+ job.join()
+ advanceTimeBy(199) // should work the same for the background tasks
+ assertEquals(2, i)
+ assertEquals(4, j)
+ advanceUntilIdle() // once again, should do nothing
+ assertEquals(2, i)
+ assertEquals(4, j)
+ runCurrent() // should behave the same way as for the normal work
+ assertEquals(3, i)
+ assertEquals(5, j)
+ launch {
+ delay(1001)
+ assertEquals(13, i)
+ assertEquals(25, j)
+ }
+ advanceUntilIdle() // should execute the normal work, and with that, the background one, too
+ }
+
+ @Test
+ fun testBackgroundWorkErrorReporting() {
+ var testFinished = false
+ val exception = RuntimeException("x")
+ try {
+ runBlockingTestOnTestScope {
+ backgroundScope.launch {
+ throw exception
+ }
+ delay(1000)
+ testFinished = true
+ }
+ fail("unreached")
+ } catch (e: Throwable) {
+ assertSame(e, exception)
+ assertTrue(testFinished)
+ }
+ }
+
+ @Test
+ fun testBackgroundWorkFinalizing() {
+ var taskEnded = 0
+ val nTasks = 10
+ try {
+ runBlockingTestOnTestScope {
+ repeat(nTasks) {
+ backgroundScope.launch {
+ try {
+ while (true) {
+ delay(1)
+ }
+ } finally {
+ ++taskEnded
+ if (taskEnded <= 2)
+ throw TestException()
+ }
+ }
+ }
+ delay(100)
+ throw TestException()
+ }
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals(2, e.suppressedExceptions.size)
+ assertEquals(nTasks, taskEnded)
+ }
+ }
+
+ @Test
+ fun testExampleBackgroundJob1() = runBlockingTestOnTestScope {
+ val myFlow = flow {
+ yield()
+ var i = 0
+ while (true) {
+ emit(++i)
+ delay(1)
+ }
+ }
+ val stateFlow = myFlow.stateIn(backgroundScope, SharingStarted.Eagerly, 0)
+ var j = 0
+ repeat(100) {
+ assertEquals(j++, stateFlow.value)
+ delay(1)
+ }
+ }
+
+ @Test
+ fun testExampleBackgroundJob2() = runBlockingTestOnTestScope {
+ val channel = Channel<Int>()
+ backgroundScope.launch {
+ var i = 0
+ while (true) {
+ channel.send(i++)
+ }
+ }
+ repeat(100) {
+ assertEquals(it, channel.receive())
+ }
+ }
+
+ @Test
+ fun testAsyncFailureInBackgroundReported() =
+ try {
+ runBlockingTestOnTestScope {
+ backgroundScope.async {
+ throw TestException("x")
+ }
+ backgroundScope.produce<Unit> {
+ throw TestException("y")
+ }
+ delay(1)
+ throw TestException("z")
+ }
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals("z", e.message)
+ assertEquals(setOf("x", "y"), e.suppressedExceptions.map { it.message }.toSet())
+ }
+
+ @Test
+ fun testNoDuplicateExceptions() =
+ try {
+ runBlockingTestOnTestScope {
+ backgroundScope.launch {
+ throw TestException("x")
+ }
+ delay(1)
+ throw TestException("y")
+ }
+ fail("unreached")
+ } catch (e: TestException) {
+ assertEquals("y", e.message)
+ assertEquals(listOf("x"), e.suppressedExceptions.map { it.message })
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
new file mode 100644
index 00000000..7f1dd009
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+/** Copy of [RunTestTest], but for [TestCoroutineScope] */
+@Suppress("DEPRECATION")
+class RunTestLegacyScopeTest {
+
+ @Test
+ fun testWithContextDispatching() = runTestWithLegacyScope {
+ var counter = 0
+ withContext(Dispatchers.Default) {
+ counter += 1
+ }
+ assertEquals(counter, 1)
+ }
+
+ @Test
+ fun testJoiningForkedJob() = runTestWithLegacyScope {
+ var counter = 0
+ val job = GlobalScope.launch {
+ counter += 1
+ }
+ job.join()
+ assertEquals(counter, 1)
+ }
+
+ @Test
+ fun testSuspendCoroutine() = runTestWithLegacyScope {
+ val answer = suspendCoroutine<Int> {
+ it.resume(42)
+ }
+ assertEquals(42, answer)
+ }
+
+ @Test
+ fun testNestedRunTestForbidden() = runTestWithLegacyScope {
+ assertFailsWith<IllegalStateException> {
+ runTest { }
+ }
+ }
+
+ @Test
+ fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTestWithLegacyScope(dispatchTimeoutMs = 0) {
+ // below is some arbitrary concurrent code where all dispatches go through the same scheduler.
+ launch {
+ delay(2000)
+ }
+ val deferred = async {
+ val job = launch(StandardTestDispatcher(testScheduler)) {
+ launch {
+ delay(500)
+ }
+ delay(1000)
+ }
+ job.join()
+ }
+ deferred.await()
+ }
+
+ @Test
+ fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 0) {
+ withContext(Dispatchers.Default) {
+ delay(10)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 100) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithLargeTimeout() = runTestWithLegacyScope(dispatchTimeoutMs = 5000) {
+ withContext(Dispatchers.Default) {
+ delay(50)
+ }
+ }
+
+ @Test
+ fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
+ try {
+ fn()
+ fail("unreached")
+ } catch (e: UncompletedCoroutinesError) {
+ @Suppress("INVISIBLE_MEMBER")
+ val suppressed = unwrap(e).suppressedExceptions
+ assertEquals(1, suppressed.size)
+ assertIs<TestException>(suppressed[0]).also {
+ assertEquals("A", it.message)
+ }
+ }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 1) {
+ coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A"))
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runTestWithLegacyScope(ctx) { }
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestBody() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ throw RuntimeException()
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestPendingTask() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runTestWithLegacyScope {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure(): TestResult {
+ var job: Job? = null
+ return testResultMap({
+ assertFailsWith<AssertionError> { it() }
+ assertTrue(job!!.isCancelled)
+ }) {
+ runTestWithLegacyScope {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ }
+
+ @Test
+ fun testTimeout() = testResultMap({
+ assertFailsWith<TimeoutCancellationException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRunTestThrowsRootCause() = testResultMap({
+ assertFailsWith<TestException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+
+ @Test
+ fun testCompletesOwnJob(): TestResult {
+ var handlerCalled = false
+ return testResultMap({
+ it()
+ assertTrue(handlerCalled)
+ }) {
+ runTestWithLegacyScope {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDoesNotCompleteGivenJob(): TestResult {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ return testResultMap({
+ it()
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }) {
+ runTestWithLegacyScope(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ }
+ }
+
+ @Test
+ fun testSuppressedExceptions() = testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }) {
+ runTestWithLegacyScope {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ }
+
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestCoroutineScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/test/TestBuildersTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt
index 27c8f5fb..6d49a01f 100644
--- a/kotlinx-coroutines-test/test/TestBuildersTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt
@@ -5,10 +5,10 @@
package kotlinx.coroutines.test
import kotlinx.coroutines.*
-import org.junit.Test
import kotlin.coroutines.*
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestBuildersTest {
@Test
@@ -59,7 +59,7 @@ class TestBuildersTest {
}
@Test
- fun scopeRunBlocking_disablesImmedateOnExit() {
+ fun scopeRunBlocking_disablesImmediatelyOnExit() {
val scope = TestCoroutineScope()
scope.runBlockingTest {
assertRunsFast {
@@ -105,7 +105,7 @@ class TestBuildersTest {
}
@Test
- fun whenInrunBlocking_runBlockingTest_nestsProperly() {
+ fun whenInRunBlocking_runBlockingTest_nestsProperly() {
// this is not a supported use case, but it is possible so ensure it works
val scope = TestCoroutineScope()
diff --git a/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt
index 116aadcf..93fcd909 100644
--- a/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt
@@ -1,11 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
package kotlinx.coroutines.test
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import org.junit.*
-import kotlin.coroutines.*
-import kotlin.test.assertEquals
+import kotlin.test.*
-class TestCoroutineDispatcherOrderTest : TestBase() {
+@Suppress("DEPRECATION")
+class TestCoroutineDispatcherOrderTest: OrderedExecutionTestBase() {
@Test
fun testAdvanceTimeBy_progressesOnEachDelay() {
diff --git a/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt
new file mode 100644
index 00000000..a78d923d
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+@Suppress("DEPRECATION")
+class TestCoroutineDispatcherTest {
+ @Test
+ fun whenDispatcherPaused_doesNotAutoProgressCurrent() {
+ val subject = TestCoroutineDispatcher()
+ subject.pauseDispatcher()
+ val scope = CoroutineScope(subject)
+ var executed = 0
+ scope.launch {
+ executed++
+ }
+ assertEquals(0, executed)
+ }
+
+ @Test
+ fun whenDispatcherResumed_doesAutoProgressCurrent() {
+ val subject = TestCoroutineDispatcher()
+ val scope = CoroutineScope(subject)
+ var executed = 0
+ scope.launch {
+ executed++
+ }
+
+ assertEquals(1, executed)
+ }
+
+ @Test
+ fun whenDispatcherResumed_doesNotAutoProgressTime() {
+ val subject = TestCoroutineDispatcher()
+ val scope = CoroutineScope(subject)
+ var executed = 0
+ scope.launch {
+ delay(1_000)
+ executed++
+ }
+
+ assertEquals(0, executed)
+ subject.advanceUntilIdle()
+ assertEquals(1, executed)
+ }
+
+ @Test
+ fun whenDispatcherPaused_thenResume_itDoesDispatchCurrent() {
+ val subject = TestCoroutineDispatcher()
+ subject.pauseDispatcher()
+ val scope = CoroutineScope(subject)
+ var executed = 0
+ scope.launch {
+ executed++
+ }
+
+ assertEquals(0, executed)
+ subject.resumeDispatcher()
+ assertEquals(1, executed)
+ }
+
+ @Test
+ fun whenDispatcherHasUncompletedCoroutines_itThrowsErrorInCleanup() {
+ val subject = TestCoroutineDispatcher()
+ subject.pauseDispatcher()
+ val scope = CoroutineScope(subject)
+ scope.launch {
+ delay(1_000)
+ }
+ assertFailsWith<UncompletedCoroutinesError> { subject.cleanupTestCoroutines() }
+ }
+
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt
index 1a0833af..20da1307 100644
--- a/kotlinx-coroutines-test/test/TestCoroutineExceptionHandlerTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt
@@ -1,15 +1,15 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.test
-import org.junit.Test
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestCoroutineExceptionHandlerTest {
@Test
- fun whenExceptionsCaught_avaliableViaProperty() {
+ fun whenExceptionsCaught_availableViaProperty() {
val subject = TestCoroutineExceptionHandler()
val expected = IllegalArgumentException()
subject.handleException(subject, expected)
diff --git a/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt
new file mode 100644
index 00000000..1a626137
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class TestCoroutineScopeTest {
+ /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */
+ @Test
+ fun testCreateThrowsOnInvalidArguments() {
+ for (ctx in invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ createTestCoroutineScope(ctx)
+ }
+ }
+ }
+
+ /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */
+ @Test
+ fun testCreateProvidesScheduler() {
+ // Creates a new scheduler.
+ run {
+ val scope = createTestCoroutineScope()
+ assertNotNull(scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Reuses the scheduler that the dispatcher is linked to.
+ run {
+ val dispatcher = StandardTestDispatcher()
+ val scope = createTestCoroutineScope(dispatcher)
+ assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Uses the scheduler passed to it.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val scope = createTestCoroutineScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler)
+ }
+ // Doesn't touch the passed dispatcher and the scheduler if they match.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val scope = createTestCoroutineScope(scheduler + dispatcher)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
+ }
+ // Reuses the scheduler of `Dispatchers.Main`
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val mainDispatcher = StandardTestDispatcher(scheduler)
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scope = createTestCoroutineScope()
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed
+ run {
+ val mainDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scheduler = TestCoroutineScheduler()
+ val scope = createTestCoroutineScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ }
+
+ /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */
+ @Test
+ fun testPresentDelaysThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ scope.launch {
+ delay(5)
+ result = true
+ }
+ assertFalse(result)
+ assertFailsWith<AssertionError> { scope.cleanupTestCoroutines() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws if there were active jobs by the end. */
+ @Test
+ fun testActiveJobsThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ scope.launch {
+ deferred.await()
+ result = true
+ }
+ assertFalse(result)
+ assertFailsWith<AssertionError> { scope.cleanupTestCoroutines() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure doesn't throw if it detects that the job is already cancelled. */
+ @Test
+ fun testCancelledDelaysNotThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ val job = scope.launch {
+ deferred.await()
+ result = true
+ }
+ job.cancel()
+ assertFalse(result)
+ scope.cleanupTestCoroutines()
+ assertFalse(result)
+ }
+
+ /** Tests that uncaught exceptions are thrown at the cleanup. */
+ @Test
+ fun testThrowsUncaughtExceptionsOnCleanup() {
+ val scope = createTestCoroutineScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ assertFailsWith<TestException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that uncaught exceptions take priority over uncompleted jobs when throwing on cleanup. */
+ @Test
+ fun testUncaughtExceptionsPrioritizedOnCleanup() {
+ val scope = createTestCoroutineScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ scope.launch {
+ delay(1000)
+ }
+ assertFailsWith<TestException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that cleaning up twice is forbidden. */
+ @Test
+ fun testClosingTwice() {
+ val scope = createTestCoroutineScope()
+ scope.cleanupTestCoroutines()
+ assertFailsWith<IllegalStateException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
+ @Test
+ fun testSuppressedExceptions() {
+ createTestCoroutineScope().apply {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ try {
+ cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("x", e.message)
+ assertEquals(2, e.suppressedExceptions.size)
+ assertEquals("y", e.suppressedExceptions[0].message)
+ assertEquals("z", e.suppressedExceptions[1].message)
+ }
+ }
+ }
+
+ /** Tests that constructing a new [TestCoroutineScope] using another one's scope works and overrides the exception
+ * handler. */
+ @Test
+ fun testCopyingContexts() {
+ val deferred = CompletableDeferred<Unit>()
+ val scope1 = createTestCoroutineScope()
+ scope1.launch { deferred.await() } // a pending job in the outer scope
+ val scope2 = createTestCoroutineScope(scope1.coroutineContext)
+ val scope3 = createTestCoroutineScope(scope1.coroutineContext)
+ assertEquals(
+ scope1.coroutineContext.minusKey(CoroutineExceptionHandler),
+ scope2.coroutineContext.minusKey(CoroutineExceptionHandler))
+ scope2.launch(SupervisorJob()) { throw TestException("x") } // will fail the cleanup of scope2
+ try {
+ scope2.cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: TestException) { }
+ scope3.cleanupTestCoroutines() // the pending job in the outer scope will not cause this to fail
+ try {
+ scope1.cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: UncompletedCoroutinesError) {
+ // the pending job in the outer scope
+ }
+ }
+
+ companion object {
+ internal val invalidContexts = listOf(
+ Dispatchers.Default, // not a [TestDispatcher]
+ CoroutineExceptionHandler { _, _ -> }, // not an [UncaughtExceptionCaptor]
+ StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
+ )
+ }
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt
index e21c82b9..32514d90 100644
--- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt
@@ -1,14 +1,15 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.test
import kotlinx.coroutines.*
-import org.junit.*
-import kotlin.coroutines.*
+import kotlin.test.*
+
+@Suppress("DEPRECATION")
+class TestRunBlockingOrderTest: OrderedExecutionTestBase() {
-class TestRunBlockingOrderTest : TestBase() {
@Test
fun testLaunchImmediate() = runBlockingTest {
expect(1)
@@ -76,4 +77,4 @@ class TestRunBlockingOrderTest : TestBase() {
}
finish(2)
}
-}
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/test/TestRunBlockingTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt
index e0c70915..af3b2489 100644
--- a/kotlinx-coroutines-test/test/TestRunBlockingTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt
@@ -5,9 +5,9 @@
package kotlinx.coroutines.test
import kotlinx.coroutines.*
-import kotlin.coroutines.*
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestRunBlockingTest {
@Test
@@ -53,22 +53,14 @@ class TestRunBlockingTest {
}
@Test
- fun incorrectlyCalledRunblocking_doesNotHaveSameInterceptor() = runBlockingTest {
- // this code is an error as a production test, please do not use this as an example
-
- // this test exists to document this error condition, if it's possible to make this code work please update
- val outerInterceptor = coroutineContext[ContinuationInterceptor]
- // runBlocking always requires an argument to pass the context in tests
- runBlocking {
- assertNotSame(coroutineContext[ContinuationInterceptor], outerInterceptor)
- }
- }
-
- @Test(expected = TimeoutCancellationException::class)
- fun whenUsingTimeout_triggersWhenDelayed() = runBlockingTest {
- assertRunsFast {
- withTimeout(SLOW) {
- delay(SLOW)
+ fun whenUsingTimeout_triggersWhenDelayed() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTest {
+ assertRunsFast {
+ withTimeout(SLOW) {
+ delay(SLOW)
+ }
+ }
}
}
}
@@ -82,12 +74,16 @@ class TestRunBlockingTest {
}
}
- @Test(expected = TimeoutCancellationException::class)
- fun whenUsingTimeout_triggersWhenWaiting() = runBlockingTest {
- val uncompleted = CompletableDeferred<Unit>()
- assertRunsFast {
- withTimeout(SLOW) {
- uncompleted.await()
+ @Test
+ fun whenUsingTimeout_triggersWhenWaiting() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTest {
+ val uncompleted = CompletableDeferred<Unit>()
+ assertRunsFast {
+ withTimeout(SLOW) {
+ uncompleted.await()
+ }
+ }
}
}
}
@@ -114,22 +110,25 @@ class TestRunBlockingTest {
}
}
- @Test(expected = TimeoutCancellationException::class)
- fun whenUsingTimeout_inAsync_triggersWhenDelayed() = runBlockingTest {
- val deferred = async {
- withTimeout(SLOW) {
- delay(SLOW)
- }
- }
+ @Test
+ fun whenUsingTimeout_inAsync_triggersWhenDelayed() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTest {
+ val deferred = async {
+ withTimeout(SLOW) {
+ delay(SLOW)
+ }
+ }
- assertRunsFast {
- deferred.await()
+ assertRunsFast {
+ deferred.await()
+ }
+ }
}
}
@Test
fun whenUsingTimeout_inAsync_doesNotTriggerWhenNotDelayed() = runBlockingTest {
- val testScope = this
val deferred = async {
withTimeout(SLOW) {
delay(0)
@@ -141,18 +140,21 @@ class TestRunBlockingTest {
}
}
- @Test(expected = TimeoutCancellationException::class)
- fun whenUsingTimeout_inLaunch_triggersWhenDelayed() = runBlockingTest {
- val job= launch {
- withTimeout(1) {
- delay(SLOW + 1)
- 3
- }
- }
+ @Test
+ fun whenUsingTimeout_inLaunch_triggersWhenDelayed() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTest {
+ val job = launch {
+ withTimeout(1) {
+ delay(SLOW + 1)
+ }
+ }
- assertRunsFast {
- job.join()
- throw job.getCancellationException()
+ assertRunsFast {
+ job.join()
+ throw job.getCancellationException()
+ }
+ }
}
}
@@ -170,36 +172,48 @@ class TestRunBlockingTest {
}
}
- @Test(expected = IllegalArgumentException::class)
- fun throwingException_throws() = runBlockingTest {
- assertRunsFast {
- delay(SLOW)
- throw IllegalArgumentException("Test")
+ @Test
+ fun throwingException_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest {
+ assertRunsFast {
+ delay(SLOW)
+ throw IllegalArgumentException("Test")
+ }
+ }
}
}
- @Test(expected = IllegalArgumentException::class)
- fun throwingException_inLaunch_throws() = runBlockingTest {
- val job = launch {
- delay(SLOW)
- throw IllegalArgumentException("Test")
- }
+ @Test
+ fun throwingException_inLaunch_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest {
+ val job = launch {
+ delay(SLOW)
+ throw IllegalArgumentException("Test")
+ }
- assertRunsFast {
- job.join()
- throw job.getCancellationException().cause ?: assertFails { "expected exception" }
+ assertRunsFast {
+ job.join()
+ throw job.getCancellationException().cause ?: AssertionError("expected exception")
+ }
+ }
}
}
- @Test(expected = IllegalArgumentException::class)
- fun throwingException__inAsync_throws() = runBlockingTest {
- val deferred = async {
- delay(SLOW)
- throw IllegalArgumentException("Test")
- }
+ @Test
+ fun throwingException__inAsync_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest {
+ val deferred: Deferred<Unit> = async {
+ delay(SLOW)
+ throw IllegalArgumentException("Test")
+ }
- assertRunsFast {
- deferred.await()
+ assertRunsFast {
+ deferred.await()
+ }
+ }
}
}
@@ -221,12 +235,13 @@ class TestRunBlockingTest {
fun callingAsyncFunction_executesAsyncBlockImmediately() = runBlockingTest {
assertRunsFast {
var executed = false
- async {
+ val deferred = async {
delay(SLOW)
executed = true
}
advanceTimeBy(SLOW)
+ assertTrue(deferred.isCompleted)
assertTrue(executed)
}
}
@@ -273,25 +288,33 @@ class TestRunBlockingTest {
job.join()
}
- @Test(expected = UncompletedCoroutinesError::class)
- fun whenACoroutineLeaks_errorIsThrown() = runBlockingTest {
- val uncompleted = CompletableDeferred<Unit>()
- launch {
- uncompleted.await()
+ @Test
+ fun whenACoroutineLeaks_errorIsThrown() {
+ assertFailsWith<UncompletedCoroutinesError> {
+ runBlockingTest {
+ val uncompleted = CompletableDeferred<Unit>()
+ launch {
+ uncompleted.await()
+ }
+ }
}
}
- @Test(expected = java.lang.IllegalArgumentException::class)
+ @Test
fun runBlockingTestBuilder_throwsOnBadDispatcher() {
- runBlockingTest(newSingleThreadContext("name")) {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest(Dispatchers.Default) {
+ }
}
}
- @Test(expected = java.lang.IllegalArgumentException::class)
+ @Test
fun runBlockingTestBuilder_throwsOnBadHandler() {
- runBlockingTest(CoroutineExceptionHandler { _, _ -> Unit} ) {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest(CoroutineExceptionHandler { _, _ -> }) {
+ }
}
}
@@ -338,36 +361,48 @@ class TestRunBlockingTest {
}
- @Test(expected = IllegalAccessError::class)
- fun testWithTestContextThrowingAnAssertionError() = runBlockingTest {
- val expectedError = IllegalAccessError("hello")
+ @Test
+ fun testWithTestContextThrowingAnAssertionError() {
+ assertFailsWith<TestException> {
+ runBlockingTest {
+ val expectedError = TestException("hello")
- val job = launch {
- throw expectedError
- }
+ launch {
+ throw expectedError
+ }
- // don't rethrow or handle the exception
+ // don't rethrow or handle the exception
+ }
+ }
}
- @Test(expected = IllegalAccessError::class)
- fun testExceptionHandlingWithLaunch() = runBlockingTest {
- val expectedError = IllegalAccessError("hello")
+ @Test
+ fun testExceptionHandlingWithLaunch() {
+ assertFailsWith<TestException> {
+ runBlockingTest {
+ val expectedError = TestException("hello")
- launch {
- throw expectedError
+ launch {
+ throw expectedError
+ }
+ }
}
}
- @Test(expected = IllegalAccessError::class)
- fun testExceptions_notThrownImmediately() = runBlockingTest {
- val expectedException = IllegalAccessError("hello")
- val result = runCatching {
- launch {
- throw expectedException
+ @Test
+ fun testExceptions_notThrownImmediately() {
+ assertFailsWith<TestException> {
+ runBlockingTest {
+ val expectedException = TestException("hello")
+ val result = runCatching {
+ launch {
+ throw expectedException
+ }
+ }
+ runCurrent()
+ assertEquals(true, result.isSuccess)
}
}
- runCurrent()
- assertEquals(true, result.isSuccess)
}
@@ -380,9 +415,13 @@ class TestRunBlockingTest {
assertNotSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
}
- @Test(expected = IllegalArgumentException::class)
- fun testPartialDispatcherOverride() = runBlockingTest(Dispatchers.Unconfined) {
- fail("Unreached")
+ @Test
+ fun testPartialDispatcherOverride() {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest(Dispatchers.Unconfined) {
+ fail("Unreached")
+ }
+ }
}
@Test
@@ -390,8 +429,12 @@ class TestRunBlockingTest {
assertSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
}
- @Test(expected = IllegalArgumentException::class)
- fun testOverrideExceptionHandlerError() = runBlockingTest(CoroutineExceptionHandler { _, _ -> }) {
- fail("Unreached")
+ @Test
+ fun testOverrideExceptionHandlerError() {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTest(CoroutineExceptionHandler { _, _ -> }) {
+ fail("Unreached")
+ }
+ }
}
-}
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/native/src/TestBuilders.kt b/kotlinx-coroutines-test/native/src/TestBuilders.kt
new file mode 100644
index 00000000..a9599019
--- /dev/null
+++ b/kotlinx-coroutines-test/native/src/TestBuilders.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+import kotlinx.coroutines.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+public actual typealias TestResult = Unit
+
+internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit) {
+ runBlocking {
+ testProcedure()
+ }
+}
diff --git a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
new file mode 100644
index 00000000..4d865f83
--- /dev/null
+++ b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test.internal
+import kotlinx.coroutines.*
+
+@Suppress("INVISIBLE_MEMBER")
+internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher =
+ when (val mainDispatcher = Main) {
+ is TestMainDispatcher -> mainDispatcher
+ else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) }
+ }
diff --git a/kotlinx-coroutines-test/native/test/FailingTests.kt b/kotlinx-coroutines-test/native/test/FailingTests.kt
new file mode 100644
index 00000000..9fb77ce7
--- /dev/null
+++ b/kotlinx-coroutines-test/native/test/FailingTests.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+/** These are tests that we want to fail. They are here so that, when the issue is fixed, their failure indicates that
+ * everything is better now. */
+class FailingTests {
+ @Test
+ fun testRunTestLoopShutdownOnTimeout() = testResultMap({ fn ->
+ assertFailsWith<IllegalStateException> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 1) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/native/test/Helpers.kt b/kotlinx-coroutines-test/native/test/Helpers.kt
new file mode 100644
index 00000000..ef478b7e
--- /dev/null
+++ b/kotlinx-coroutines-test/native/test/Helpers.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlin.test.*
+
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) {
+ block {
+ test()
+ }
+}
+
+actual typealias NoNative = Ignore
diff --git a/kotlinx-coroutines-test/npm/README.md b/kotlinx-coroutines-test/npm/README.md
new file mode 100644
index 00000000..4df4825d
--- /dev/null
+++ b/kotlinx-coroutines-test/npm/README.md
@@ -0,0 +1,4 @@
+# kotlinx-coroutines-test
+
+Testing support for `kotlinx-coroutines` in
+[Kotlin/JS](https://kotlinlang.org/docs/js-overview.html).
diff --git a/kotlinx-coroutines-test/npm/package.json b/kotlinx-coroutines-test/npm/package.json
new file mode 100644
index 00000000..b59d92fe
--- /dev/null
+++ b/kotlinx-coroutines-test/npm/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "kotlinx-coroutines-test",
+ "version" : "$version",
+ "description" : "Test utilities for kotlinx-coroutines",
+ "main" : "kotlinx-coroutines-test.js",
+ "author": "JetBrains",
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/Kotlin/kotlinx.coroutines",
+ "bugs": {
+ "url": "https://github.com/Kotlin/kotlinx.coroutines/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Kotlin/kotlinx.coroutines.git"
+ },
+ "keywords": [
+ "Kotlin",
+ "async",
+ "coroutines",
+ "JetBrains",
+ "test"
+ ]
+}
diff --git a/kotlinx-coroutines-test/src/DelayController.kt b/kotlinx-coroutines-test/src/DelayController.kt
deleted file mode 100644
index 6e722227..00000000
--- a/kotlinx-coroutines-test/src/DelayController.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/**
- * Control the virtual clock time of a [CoroutineDispatcher].
- *
- * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public interface DelayController {
- /**
- * Returns the current virtual clock-time as it is known to this Dispatcher.
- *
- * @return The virtual clock-time
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public val currentTime: Long
-
- /**
- * Moves the Dispatcher's virtual clock forward by a specified amount of time.
- *
- * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
- * blocking coroutines.
- *
- * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
- * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
- * calls to delay.
- *
- * ```
- * @Test
- * fun advanceTimeTest() = runBlockingTest {
- * foo()
- * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
- * // virtual time is 2_000, next resume is at 2_001
- * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
- * // virtual time is 2_0002
- * }
- *
- * fun CoroutineScope.foo() {
- * launch {
- * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
- * // virtual time is 1_000
- * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
- * // virtual time is 1_500
- * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
- * // virtual time is 2_001
- * }
- * }
- * ```
- *
- * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
- * @return The amount of delay-time that this Dispatcher's clock has been forwarded.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun advanceTimeBy(delayTimeMillis: Long): Long
-
- /**
- * Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
- *
- * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
- * returns.
- *
- * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun advanceUntilIdle(): Long
-
- /**
- * Run any tasks that are pending at or before the current virtual clock-time.
- *
- * Calling this function will never advance the clock.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun runCurrent()
-
- /**
- * Call after test code completes to ensure that the dispatcher is properly cleaned up.
- *
- * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
- * coroutines.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- @Throws(UncompletedCoroutinesError::class)
- public fun cleanupTestCoroutines()
-
- /**
- * Run a block of code in a paused dispatcher.
- *
- * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
- * will resume auto-advancing.
- *
- * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
- * setup may be done between the time the coroutine is created and started.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public suspend fun pauseDispatcher(block: suspend () -> Unit)
-
- /**
- * Pause the dispatcher.
- *
- * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
- * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun pauseDispatcher()
-
- /**
- * Resume the dispatcher from a paused state.
- *
- * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
- * time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
- * or [advanceUntilIdle].
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun resumeDispatcher()
-}
-
-/**
- * Thrown when a test has completed and there are tasks that are not completed or cancelled.
- */
-// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)
diff --git a/kotlinx-coroutines-test/src/TestBuilders.kt b/kotlinx-coroutines-test/src/TestBuilders.kt
deleted file mode 100644
index b40769ee..00000000
--- a/kotlinx-coroutines-test/src/TestBuilders.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlin.coroutines.*
-
-/**
- * Executes a [testBody] inside an immediate execution dispatcher.
- *
- * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
- * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
- * extra time.
- *
- * ```
- * @Test
- * fun exampleTest() = runBlockingTest {
- * val deferred = async {
- * delay(1_000)
- * async {
- * delay(1_000)
- * }.await()
- * }
- *
- * deferred.await() // result available immediately
- * }
- *
- * ```
- *
- * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
- * conditions.
- *
- * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
- *
- * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
- * (including coroutines suspended on join/await).
- *
- * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
- * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
- * @param testBody The code of the unit-test.
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
- val (safeContext, dispatcher) = context.checkArguments()
- val startingJobs = safeContext.activeJobs()
- val scope = TestCoroutineScope(safeContext)
- val deferred = scope.async {
- scope.testBody()
- }
- dispatcher.advanceUntilIdle()
- deferred.getCompletionExceptionOrNull()?.let {
- throw it
- }
- scope.cleanupTestCoroutines()
- val endingJobs = safeContext.activeJobs()
- if ((endingJobs - startingJobs).isNotEmpty()) {
- throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs")
- }
-}
-
-private fun CoroutineContext.activeJobs(): Set<Job> {
- return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
-}
-
-/**
- * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
- */
-// todo: need documentation on how this extension is supposed to be used
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
- runBlockingTest(coroutineContext, block)
-
-/**
- * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
- runBlockingTest(this, block)
-
-private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
- // TODO optimize it
- val dispatcher = get(ContinuationInterceptor).run {
- this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } }
- this ?: TestCoroutineDispatcher()
- }
-
- val exceptionHandler = get(CoroutineExceptionHandler).run {
- this?.let {
- require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" }
- }
- this ?: TestCoroutineExceptionHandler()
- }
-
- val job = get(Job) ?: SupervisorJob()
- return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController)
-}
diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt
deleted file mode 100644
index f6464789..00000000
--- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.atomicfu.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.internal.*
-import kotlin.coroutines.*
-import kotlin.math.*
-
-/**
- * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
- * and implements [DelayController] to control its virtual clock.
- *
- * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are
- * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the
- * methods on [DelayController].
- *
- * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will
- * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the
- * methods on [DelayController].
- *
- * @see DelayController
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayController {
- private var dispatchImmediately = true
- set(value) {
- field = value
- if (value) {
- // there may already be tasks from setup code we need to run
- advanceUntilIdle()
- }
- }
-
- // The ordered queue for the runnable tasks.
- private val queue = ThreadSafeHeap<TimedRunnable>()
-
- // The per-scheduler global order counter.
- private val _counter = atomic(0L)
-
- // Storing time in nanoseconds internally.
- private val _time = atomic(0L)
-
- /** @suppress */
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- if (dispatchImmediately) {
- block.run()
- } else {
- post(block)
- }
- }
-
- /** @suppress */
- @InternalCoroutinesApi
- override fun dispatchYield(context: CoroutineContext, block: Runnable) {
- post(block)
- }
-
- /** @suppress */
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- postDelayed(CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }, timeMillis)
- }
-
- /** @suppress */
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val node = postDelayed(block, timeMillis)
- return object : DisposableHandle {
- override fun dispose() {
- queue.remove(node)
- }
- }
- }
-
- /** @suppress */
- override fun toString(): String {
- return "TestCoroutineDispatcher[currentTime=${currentTime}ms, queued=${queue.size}]"
- }
-
- private fun post(block: Runnable) =
- queue.addLast(TimedRunnable(block, _counter.getAndIncrement()))
-
- private fun postDelayed(block: Runnable, delayTime: Long) =
- TimedRunnable(block, _counter.getAndIncrement(), safePlus(currentTime, delayTime))
- .also {
- queue.addLast(it)
- }
-
- private fun safePlus(currentTime: Long, delayTime: Long): Long {
- check(delayTime >= 0)
- val result = currentTime + delayTime
- if (result < currentTime) return Long.MAX_VALUE // clam on overflow
- return result
- }
-
- private fun doActionsUntil(targetTime: Long) {
- while (true) {
- val current = queue.removeFirstIf { it.time <= targetTime } ?: break
- // If the scheduled time is 0 (immediate) use current virtual time
- if (current.time != 0L) _time.value = current.time
- current.run()
- }
- }
-
- /** @suppress */
- override val currentTime: Long get() = _time.value
-
- /** @suppress */
- override fun advanceTimeBy(delayTimeMillis: Long): Long {
- val oldTime = currentTime
- advanceUntilTime(oldTime + delayTimeMillis)
- return currentTime - oldTime
- }
-
- /**
- * Moves the CoroutineContext's clock-time to a particular moment in time.
- *
- * @param targetTime The point in time to which to move the CoroutineContext's clock (milliseconds).
- */
- private fun advanceUntilTime(targetTime: Long) {
- doActionsUntil(targetTime)
- _time.update { currentValue -> max(currentValue, targetTime) }
- }
-
- /** @suppress */
- override fun advanceUntilIdle(): Long {
- val oldTime = currentTime
- while(!queue.isEmpty) {
- runCurrent()
- val next = queue.peek() ?: break
- advanceUntilTime(next.time)
- }
- return currentTime - oldTime
- }
-
- /** @suppress */
- override fun runCurrent(): Unit = doActionsUntil(currentTime)
-
- /** @suppress */
- override suspend fun pauseDispatcher(block: suspend () -> Unit) {
- val previous = dispatchImmediately
- dispatchImmediately = false
- try {
- block()
- } finally {
- dispatchImmediately = previous
- }
- }
-
- /** @suppress */
- override fun pauseDispatcher() {
- dispatchImmediately = false
- }
-
- /** @suppress */
- override fun resumeDispatcher() {
- dispatchImmediately = true
- }
-
- /** @suppress */
- override fun cleanupTestCoroutines() {
- // process any pending cancellations or completions, but don't advance time
- doActionsUntil(currentTime)
-
- // run through all pending tasks, ignore any submitted coroutines that are not active
- val pendingTasks = mutableListOf<TimedRunnable>()
- while (true) {
- pendingTasks += queue.removeFirstOrNull() ?: break
- }
- val activeDelays = pendingTasks
- .mapNotNull { it.runnable as? CancellableContinuationRunnable<*> }
- .filter { it.continuation.isActive }
-
- val activeTimeouts = pendingTasks.filter { it.runnable !is CancellableContinuationRunnable<*> }
- if (activeDelays.isNotEmpty() || activeTimeouts.isNotEmpty()) {
- throw UncompletedCoroutinesError(
- "Unfinished coroutines during teardown. Ensure all coroutines are" +
- " completed or cancelled by your test."
- )
- }
- }
-}
-
-/**
- * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled
- * in the future.
- */
-private class CancellableContinuationRunnable<T>(
- @JvmField val continuation: CancellableContinuation<T>,
- private val block: CancellableContinuation<T>.() -> Unit
-) : Runnable {
- override fun run() = continuation.block()
-}
-
-/**
- * A Runnable for our event loop that represents a task to perform at a time.
- */
-private class TimedRunnable(
- @JvmField val runnable: Runnable,
- private val count: Long = 0,
- @JvmField val time: Long = 0
-) : Comparable<TimedRunnable>, Runnable by runnable, ThreadSafeHeapNode {
- override var heap: ThreadSafeHeap<*>? = null
- override var index: Int = 0
-
- override fun compareTo(other: TimedRunnable) = if (time == other.time) {
- count.compareTo(other.count)
- } else {
- time.compareTo(other.time)
- }
-
- override fun toString() = "TimedRunnable(time=$time, run=$runnable)"
-}
diff --git a/kotlinx-coroutines-test/src/TestCoroutineScope.kt b/kotlinx-coroutines-test/src/TestCoroutineScope.kt
deleted file mode 100644
index 7c1ff872..00000000
--- a/kotlinx-coroutines-test/src/TestCoroutineScope.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlin.coroutines.*
-
-/**
- * A scope which provides detailed control over the execution of coroutines for tests.
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public interface TestCoroutineScope: CoroutineScope, UncaughtExceptionCaptor, DelayController {
- /**
- * Call after the test completes.
- * Calls [UncaughtExceptionCaptor.cleanupTestCoroutines] and [DelayController.cleanupTestCoroutines].
- *
- * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
- * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
- * coroutines.
- */
- public override fun cleanupTestCoroutines()
-}
-
-private class TestCoroutineScopeImpl (
- override val coroutineContext: CoroutineContext
-):
- TestCoroutineScope,
- UncaughtExceptionCaptor by coroutineContext.uncaughtExceptionCaptor,
- DelayController by coroutineContext.delayController
-{
- override fun cleanupTestCoroutines() {
- coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutines()
- coroutineContext.delayController.cleanupTestCoroutines()
- }
-}
-
-/**
- * A scope which provides detailed control over the execution of coroutines for tests.
- *
- * If the provided context does not provide a [ContinuationInterceptor] (Dispatcher) or [CoroutineExceptionHandler], the
- * scope adds [TestCoroutineDispatcher] and [TestCoroutineExceptionHandler] automatically.
- *
- * @param context an optional context that MAY provide [UncaughtExceptionCaptor] and/or [DelayController]
- */
-@Suppress("FunctionName")
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
- var safeContext = context
- if (context[ContinuationInterceptor] == null) safeContext += TestCoroutineDispatcher()
- if (context[CoroutineExceptionHandler] == null) safeContext += TestCoroutineExceptionHandler()
- return TestCoroutineScopeImpl(safeContext)
-}
-
-private inline val CoroutineContext.uncaughtExceptionCaptor: UncaughtExceptionCaptor
- get() {
- val handler = this[CoroutineExceptionHandler]
- return handler as? UncaughtExceptionCaptor ?: throw IllegalArgumentException(
- "TestCoroutineScope requires a UncaughtExceptionCaptor such as " +
- "TestCoroutineExceptionHandler as the CoroutineExceptionHandler"
- )
- }
-
-private inline val CoroutineContext.delayController: DelayController
- get() {
- val handler = this[ContinuationInterceptor]
- return handler as? DelayController ?: throw IllegalArgumentException(
- "TestCoroutineScope requires a DelayController such as TestCoroutineDispatcher as " +
- "the ContinuationInterceptor (Dispatcher)"
- )
- }
diff --git a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt
deleted file mode 100644
index c85d27ea..00000000
--- a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test.internal
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.internal.*
-import kotlin.coroutines.*
-
-/**
- * The testable main dispatcher used by kotlinx-coroutines-test.
- * It is a [MainCoroutineDispatcher] which delegates all actions to a settable delegate.
- */
-internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory) : MainCoroutineDispatcher(), Delay {
- private var _delegate: CoroutineDispatcher? = null
- private val delegate: CoroutineDispatcher get() {
- _delegate?.let { return it }
- mainFactory.tryCreateDispatcher(emptyList()).let {
- // If we've failed to create a dispatcher, do no set _delegate
- if (!isMissing()) {
- _delegate = it
- }
- return it
- }
- }
-
- @Suppress("INVISIBLE_MEMBER")
- private val delay: Delay get() = delegate as? Delay ?: DefaultDelay
-
- override val immediate: MainCoroutineDispatcher
- get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- delegate.dispatch(context, block)
- }
-
- override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
-
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- delay.scheduleResumeAfterDelay(timeMillis, continuation)
- }
-
- override suspend fun delay(time: Long) {
- delay.delay(time)
- }
-
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- return delay.invokeOnTimeout(timeMillis, block, context)
- }
-
- fun setDispatcher(dispatcher: CoroutineDispatcher) {
- _delegate = dispatcher
- }
-
- fun resetDispatcher() {
- _delegate = null
- }
-}
-
-internal class TestMainDispatcherFactory : MainDispatcherFactory {
-
- override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
- val originalFactory = allFactories.asSequence()
- .filter { it !== this }
- .maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory
- return TestMainDispatcher(originalFactory)
- }
-
- /**
- * [Int.MAX_VALUE] -- test dispatcher always wins no matter what factories are present in the classpath.
- * By default all actions are delegated to the second-priority dispatcher, so that it won't be the issue.
- */
- override val loadPriority: Int
- get() = Int.MAX_VALUE
-}
diff --git a/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt
deleted file mode 100644
index 260edf9d..00000000
--- a/kotlinx-coroutines-test/test/TestCoroutineDispatcherTest.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import org.junit.Test
-import kotlin.test.*
-
-class TestCoroutineDispatcherTest {
- @Test
- fun whenStringCalled_itReturnsString() {
- val subject = TestCoroutineDispatcher()
- assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenStringCalled_itReturnsCurrentTime() {
- val subject = TestCoroutineDispatcher()
- subject.advanceTimeBy(1000)
- assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenStringCalled_itShowsQueuedJobs() {
- val subject = TestCoroutineDispatcher()
- val scope = TestCoroutineScope(subject)
- scope.pauseDispatcher()
- scope.launch {
- delay(1_000)
- }
- assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=1]", subject.toString())
- scope.advanceTimeBy(50)
- assertEquals("TestCoroutineDispatcher[currentTime=50ms, queued=1]", subject.toString())
- scope.advanceUntilIdle()
- assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenDispatcherPaused_doesntAutoProgressCurrent() {
- val subject = TestCoroutineDispatcher()
- subject.pauseDispatcher()
- val scope = CoroutineScope(subject)
- var executed = 0
- scope.launch {
- executed++
- }
- assertEquals(0, executed)
- }
-
- @Test
- fun whenDispatcherResumed_doesAutoProgressCurrent() {
- val subject = TestCoroutineDispatcher()
- val scope = CoroutineScope(subject)
- var executed = 0
- scope.launch {
- executed++
- }
-
- assertEquals(1, executed)
- }
-
- @Test
- fun whenDispatcherResumed_doesNotAutoProgressTime() {
- val subject = TestCoroutineDispatcher()
- val scope = CoroutineScope(subject)
- var executed = 0
- scope.launch {
- delay(1_000)
- executed++
- }
-
- assertEquals(0, executed)
- subject.advanceUntilIdle()
- assertEquals(1, executed)
- }
-
- @Test
- fun whenDispatcherPaused_thenResume_itDoesDispatchCurrent() {
- val subject = TestCoroutineDispatcher()
- subject.pauseDispatcher()
- val scope = CoroutineScope(subject)
- var executed = 0
- scope.launch {
- executed++
- }
-
- assertEquals(0, executed)
- subject.resumeDispatcher()
- assertEquals(1, executed)
- }
-
- @Test(expected = UncompletedCoroutinesError::class)
- fun whenDispatcherHasUncompletedCoroutines_itThrowsErrorInCleanup() {
- val subject = TestCoroutineDispatcher()
- subject.pauseDispatcher()
- val scope = CoroutineScope(subject)
- scope.launch {
- delay(1_000)
- }
- subject.cleanupTestCoroutines()
- }
-
- @Test
- fun whenDispatchCalled_runsOnCurrentThread() {
- val currentThread = Thread.currentThread()
- val subject = TestCoroutineDispatcher()
- val scope = TestCoroutineScope(subject)
-
- val deferred = scope.async(Dispatchers.Default) {
- withContext(subject) {
- assertNotSame(currentThread, Thread.currentThread())
- 3
- }
- }
-
- runBlocking {
- // just to ensure the above code terminates
- assertEquals(3, deferred.await())
- }
- }
-
- @Test
- fun whenAllDispatchersMocked_runsOnSameThread() {
- val currentThread = Thread.currentThread()
- val subject = TestCoroutineDispatcher()
- val scope = TestCoroutineScope(subject)
-
- val deferred = scope.async(subject) {
- withContext(subject) {
- assertSame(currentThread, Thread.currentThread())
- 3
- }
- }
-
- runBlocking {
- // just to ensure the above code terminates
- assertEquals(3, deferred.await())
- }
- }
-} \ No newline at end of file
diff --git a/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt
deleted file mode 100644
index fa14c384..00000000
--- a/kotlinx-coroutines-test/test/TestCoroutineScopeTest.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import org.junit.Test
-import kotlin.test.*
-
-class TestCoroutineScopeTest {
- @Test
- fun whenGivenInvalidExceptionHandler_throwsException() {
- val handler = CoroutineExceptionHandler { _, _ -> Unit }
- assertFails {
- TestCoroutineScope(handler)
- }
- }
-
- @Test
- fun whenGivenInvalidDispatcher_throwsException() {
- assertFails {
- TestCoroutineScope(newSingleThreadContext("incorrect call"))
- }
- }
-}
diff --git a/kotlinx-coroutines-test/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/test/TestDispatchersTest.kt
deleted file mode 100644
index 98d97053..00000000
--- a/kotlinx-coroutines-test/test/TestDispatchersTest.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import org.junit.*
-import org.junit.Test
-import kotlin.coroutines.*
-import kotlin.test.*
-
-class TestDispatchersTest : TestBase() {
-
- @Before
- fun setUp() {
- Dispatchers.resetMain()
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun testSelfSet() = runTest {
- Dispatchers.setMain(Dispatchers.Main)
- }
-
- @Test
- fun testSingleThreadExecutor() = runTest {
- val mainThread = Thread.currentThread()
- Dispatchers.setMain(Dispatchers.Unconfined)
- newSingleThreadContext("testSingleThread").use { threadPool ->
- withContext(Dispatchers.Main) {
- assertSame(mainThread, Thread.currentThread())
- }
-
- Dispatchers.setMain(threadPool)
- withContext(Dispatchers.Main) {
- assertNotSame(mainThread, Thread.currentThread())
- }
- assertSame(mainThread, Thread.currentThread())
-
- withContext(Dispatchers.Main.immediate) {
- assertNotSame(mainThread, Thread.currentThread())
- }
- assertSame(mainThread, Thread.currentThread())
-
- Dispatchers.setMain(Dispatchers.Unconfined)
- withContext(Dispatchers.Main.immediate) {
- assertSame(mainThread, Thread.currentThread())
- }
- assertSame(mainThread, Thread.currentThread())
- }
- }
-
- @Test
- fun testImmediateDispatcher() = runTest {
- Dispatchers.setMain(ImmediateDispatcher())
- expect(1)
- withContext(Dispatchers.Main) {
- expect(3)
- }
-
- Dispatchers.setMain(RegularDispatcher())
- withContext(Dispatchers.Main) {
- expect(6)
- }
-
- finish(7)
- }
-
- private inner class ImmediateDispatcher : CoroutineDispatcher() {
- override fun isDispatchNeeded(context: CoroutineContext): Boolean {
- expect(2)
- return false
- }
-
- override fun dispatch(context: CoroutineContext, block: Runnable) = expectUnreached()
- }
-
- private inner class RegularDispatcher : CoroutineDispatcher() {
- override fun isDispatchNeeded(context: CoroutineContext): Boolean {
- expect(4)
- return true
- }
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- expect(5)
- block.run()
- }
- }
-}
diff --git a/kotlinx-coroutines-test/test/TestModuleHelpers.kt b/kotlinx-coroutines-test/test/TestModuleHelpers.kt
deleted file mode 100644
index 12541bd9..00000000
--- a/kotlinx-coroutines-test/test/TestModuleHelpers.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import org.junit.*
-import java.time.*
-
-const val SLOW = 10_000L
-
-/**
- * Assert a block completes within a second or fail the suite
- */
-suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) {
- val start = Instant.now().toEpochMilli()
- // don't need to be fancy with timeouts here since anything longer than a few ms is an error
- block()
- val duration = Instant.now().minusMillis(start).toEpochMilli()
- Assert.assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)", duration < 2_000)
-}
diff --git a/license/NOTICE.txt b/license/NOTICE.txt
index d1d00c1a..8d1100a3 100644
--- a/license/NOTICE.txt
+++ b/license/NOTICE.txt
@@ -5,4 +5,4 @@
=========================================================================
kotlinx.coroutines library.
-Copyright 2016-2019 JetBrains s.r.o and respective authors and developers \ No newline at end of file
+Copyright 2016-2021 JetBrains s.r.o and respective authors and developers
diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt
index 3682d5e3..0479028d 100644
--- a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt
+++ b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt
@@ -278,10 +278,8 @@ class PublishTest : TestBase() {
val publisher = flowPublish {
assertFailsWith<NullPointerException> { send(null) }
assertFailsWith<NullPointerException> { trySend(null) }
- @Suppress("DEPRECATION")
- assertFailsWith<NullPointerException> { offer(null) }
send("OK")
}
assertEquals("OK", publisher.awaitFirstOrNull())
}
-} \ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md
index ec59d3d9..cd27b27d 100644
--- a/reactive/kotlinx-coroutines-reactive/README.md
+++ b/reactive/kotlinx-coroutines-reactive/README.md
@@ -33,23 +33,23 @@ Suspending extension functions and suspending iteration:
<!--- INDEX kotlinx.coroutines -->
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
<!--- INDEX kotlinx.coroutines.channels -->
-[ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
+[ProducerScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
<!--- MODULE kotlinx-coroutines-reactive -->
<!--- INDEX kotlinx.coroutines.reactive -->
-[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html
-[Publisher.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/as-flow.html
-[Flow.asPublisher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/as-publisher.html
-[org.reactivestreams.Publisher.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first.html
-[org.reactivestreams.Publisher.awaitFirstOrDefault]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-default.html
-[org.reactivestreams.Publisher.awaitFirstOrElse]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-else.html
-[org.reactivestreams.Publisher.awaitFirstOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-null.html
-[org.reactivestreams.Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-single.html
+[kotlinx.coroutines.reactive.publish]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html
+[Publisher.asFlow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/as-flow.html
+[Flow.asPublisher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/as-publisher.html
+[org.reactivestreams.Publisher.awaitFirst]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first.html
+[org.reactivestreams.Publisher.awaitFirstOrDefault]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-default.html
+[org.reactivestreams.Publisher.awaitFirstOrElse]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-else.html
+[org.reactivestreams.Publisher.awaitFirstOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-first-or-null.html
+[org.reactivestreams.Publisher.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/await-single.html
<!--- END -->
diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api
index 75f1b306..b52df185 100644
--- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api
+++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api
@@ -12,9 +12,10 @@ public final class kotlinx/coroutines/reactive/AwaitKt {
public final class kotlinx/coroutines/reactive/ChannelKt {
public static final fun collect (Lorg/reactivestreams/Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun openSubscription (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel;
+ public static final synthetic fun openSubscription (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel;
public static synthetic fun openSubscription$default (Lorg/reactivestreams/Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel;
public static final fun toChannel (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel;
+ public static synthetic fun toChannel$default (Lorg/reactivestreams/Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel;
}
public abstract interface class kotlinx/coroutines/reactive/ContextInjector {
@@ -22,7 +23,7 @@ public abstract interface class kotlinx/coroutines/reactive/ContextInjector {
}
public final class kotlinx/coroutines/reactive/ConvertKt {
- public static final fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher;
+ public static final synthetic fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher;
public static synthetic fun asPublisher$default (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher;
}
diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts
index 128d4d86..c2e4b5c9 100644
--- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts
+++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts
@@ -5,8 +5,8 @@
val reactiveStreamsVersion = property("reactive_streams_version")
dependencies {
- compile("org.reactivestreams:reactive-streams:$reactiveStreamsVersion")
- testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion")
+ api("org.reactivestreams:reactive-streams:$reactiveStreamsVersion")
+ testImplementation("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion")
}
val testNG by tasks.registering(Test::class) {
@@ -34,3 +34,17 @@ tasks.check {
externalDocumentationLink(
url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/"
)
+
+val commonKoverExcludes = listOf(
+ "kotlinx.coroutines.reactive.FlowKt", // Deprecated
+ "kotlinx.coroutines.reactive.FlowKt__MigrationKt", // Deprecated
+ "kotlinx.coroutines.reactive.ConvertKt" // Deprecated
+)
+
+tasks.koverHtmlReport {
+ excludes = commonKoverExcludes
+}
+
+tasks.koverVerify {
+ excludes = commonKoverExcludes
+}
diff --git a/reactive/kotlinx-coroutines-reactive/src/Await.kt b/reactive/kotlinx-coroutines-reactive/src/Await.kt
index fef1205a..3d9a0f85 100644
--- a/reactive/kotlinx-coroutines-reactive/src/Await.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/Await.kt
@@ -106,7 +106,7 @@ public suspend fun <T> Publisher<T>.awaitSingle(): T = awaitOne(Mode.SINGLE)
@Deprecated(
message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " +
"Please consider using awaitFirstOrDefault().",
- level = DeprecationLevel.WARNING
+ level = DeprecationLevel.ERROR
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> Publisher<T>.awaitSingleOrDefault(default: T): T = awaitOne(Mode.SINGLE_OR_DEFAULT, default)
@@ -135,7 +135,7 @@ public suspend fun <T> Publisher<T>.awaitSingleOrDefault(default: T): T = awaitO
message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " +
"There is a specialized version for Reactor's Mono, please use that where applicable. " +
"Alternatively, please consider using awaitFirstOrNull().",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull()", "kotlinx.coroutines.reactor")
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> Publisher<T>.awaitSingleOrNull(): T? = awaitOne(Mode.SINGLE_OR_DEFAULT)
@@ -164,7 +164,7 @@ public suspend fun <T> Publisher<T>.awaitSingleOrNull(): T? = awaitOne(Mode.SING
@Deprecated(
message = "Deprecated without a replacement due to its name incorrectly conveying the behavior. " +
"Please consider using awaitFirstOrElse().",
- level = DeprecationLevel.WARNING
+ level = DeprecationLevel.ERROR
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> Publisher<T>.awaitSingleOrElse(defaultValue: () -> T): T =
awaitOne(Mode.SINGLE_OR_DEFAULT) ?: defaultValue()
@@ -198,12 +198,20 @@ private suspend fun <T> Publisher<T>.awaitOne(
/** cancelling the new subscription due to rule 2.5, though the publisher would either have to
* subscribe more than once, which would break 2.12, or leak this [Subscriber]. */
if (subscription != null) {
- sub.cancel()
+ withSubscriptionLock {
+ sub.cancel()
+ }
return
}
subscription = sub
- cont.invokeOnCancellation { sub.cancel() }
- sub.request(if (mode == Mode.FIRST || mode == Mode.FIRST_OR_DEFAULT) 1 else Long.MAX_VALUE)
+ cont.invokeOnCancellation {
+ withSubscriptionLock {
+ sub.cancel()
+ }
+ }
+ withSubscriptionLock {
+ sub.request(if (mode == Mode.FIRST || mode == Mode.FIRST_OR_DEFAULT) 1 else Long.MAX_VALUE)
+ }
}
override fun onNext(t: T) {
@@ -228,12 +236,16 @@ private suspend fun <T> Publisher<T>.awaitOne(
return
}
seenValue = true
- sub.cancel()
+ withSubscriptionLock {
+ sub.cancel()
+ }
cont.resume(t)
}
Mode.LAST, Mode.SINGLE, Mode.SINGLE_OR_DEFAULT -> {
if ((mode == Mode.SINGLE || mode == Mode.SINGLE_OR_DEFAULT) && seenValue) {
- sub.cancel()
+ withSubscriptionLock {
+ sub.cancel()
+ }
/* the check for `cont.isActive` is needed in case `sub.cancel() above calls `onComplete` or
`onError` on its own. */
if (cont.isActive) {
@@ -289,6 +301,14 @@ private suspend fun <T> Publisher<T>.awaitOne(
inTerminalState = true
return true
}
+
+ /**
+ * Enforce rule 2.7: [Subscription.request] and [Subscription.cancel] must be executed serially
+ */
+ @Synchronized
+ private fun withSubscriptionLock(block: () -> Unit) {
+ block()
+ }
})
}
diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt
index b7fbf134..a8db2171 100644
--- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt
@@ -11,29 +11,6 @@ import kotlinx.coroutines.internal.*
import org.reactivestreams.*
/**
- * Subscribes to this [Publisher] and returns a channel to receive the elements emitted by it.
- * The resulting channel needs to be [cancelled][ReceiveChannel.cancel] in order to unsubscribe from this publisher.
-
- * @param request how many items to request from the publisher in advance (optional, a single element by default).
- *
- * This method is deprecated in the favor of [Flow].
- * Instead of iterating over the resulting channel please use [collect][Flow.collect]:
- * ```
- * asFlow().collect { value ->
- * // process value
- * }
- * ```
- */
-@Deprecated(
- message = "Transforming publisher to channel is deprecated, use asFlow() instead",
- level = DeprecationLevel.ERROR) // Will be error in 1.4
-public fun <T> Publisher<T>.openSubscription(request: Int = 1): ReceiveChannel<T> {
- val channel = SubscriptionChannel<T>(request)
- subscribe(channel)
- return channel
-}
-
-/**
* Subscribes to this [Publisher] and performs the specified action for each received element.
*
* If [action] throws an exception at some point, the subscription is cancelled, and the exception is rethrown from
@@ -123,3 +100,12 @@ private class SubscriptionChannel<T>(
}
}
+/** @suppress */
+@Deprecated(
+ message = "Transforming publisher to channel is deprecated, use asFlow() instead",
+ level = DeprecationLevel.HIDDEN) // ERROR in 1.4, HIDDEN in 1.6.0
+public fun <T> Publisher<T>.openSubscription(request: Int = 1): ReceiveChannel<T> {
+ val channel = SubscriptionChannel<T>(request)
+ subscribe(channel)
+ return channel
+}
diff --git a/reactive/kotlinx-coroutines-reactive/src/Convert.kt b/reactive/kotlinx-coroutines-reactive/src/Convert.kt
index 3cb05b60..9492b498 100644
--- a/reactive/kotlinx-coroutines-reactive/src/Convert.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/Convert.kt
@@ -8,15 +8,9 @@ import kotlinx.coroutines.channels.*
import org.reactivestreams.*
import kotlin.coroutines.*
-/**
- * Converts a stream of elements received from the channel to the hot reactive publisher.
- *
- * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers,
- * they'll receive values in round-robin way.
- * @param context -- the coroutine context from which the resulting observable is going to be signalled
- */
+/** @suppress */
@Deprecated(message = "Deprecated in the favour of consumeAsFlow()",
- level = DeprecationLevel.ERROR, // Error in 1.4
+ level = DeprecationLevel.HIDDEN, // Error in 1.4, HIDDEN in 1.6.0
replaceWith = ReplaceWith("this.consumeAsFlow().asPublisher(context)", imports = ["kotlinx.coroutines.flow.consumeAsFlow"]))
public fun <T> ReceiveChannel<T>.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher<T> = publish(context) {
for (t in this@asPublisher)
diff --git a/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt
index 4928a743..1b8683ce 100644
--- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt
+++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt
@@ -6,6 +6,7 @@ package kotlinx.coroutines.reactive
import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
import kotlinx.coroutines.sync.*
import org.reactivestreams.*
@@ -104,10 +105,21 @@ public class PublisherCoroutine<in T>(
// registerSelectSend
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override fun <R> registerSelectClause2(select: SelectInstance<R>, element: T, block: suspend (SendChannel<T>) -> R) {
- mutex.onLock.registerSelectClause2(select, null) {
+ val clause = suspend {
doLockedNext(element)?.let { throw it }
block(this)
}
+
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.lock()
+ // Already selected -- bail out
+ if (!select.trySelect()) {
+ mutex.unlock()
+ return@launch
+ }
+
+ clause.startCoroutineCancellable(select.completion)
+ }
}
/*
diff --git a/reactive/kotlinx-coroutines-reactive/test/AwaitCancellationStressTest.kt b/reactive/kotlinx-coroutines-reactive/test/AwaitCancellationStressTest.kt
new file mode 100644
index 00000000..aaf8df6e
--- /dev/null
+++ b/reactive/kotlinx-coroutines-reactive/test/AwaitCancellationStressTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.reactive
+
+import kotlinx.coroutines.*
+import org.junit.*
+import org.reactivestreams.*
+import java.util.concurrent.locks.*
+
+/**
+ * This test checks implementation of rule 2.7 for await methods - serial execution of subscription methods
+ */
+class AwaitCancellationStressTest : TestBase() {
+ private val iterations = 10_000 * stressTestMultiplier
+
+ @Test
+ fun testAwaitCancellationOrder() = runTest {
+ repeat(iterations) {
+ val job = launch(Dispatchers.Default) {
+ testPublisher().awaitFirst()
+ }
+ job.cancelAndJoin()
+ }
+ }
+
+ private fun testPublisher() = Publisher<Int> { s ->
+ val lock = ReentrantLock()
+ s.onSubscribe(object : Subscription {
+ override fun request(n: Long) {
+ check(lock.tryLock())
+ s.onNext(42)
+ lock.unlock()
+ }
+
+ override fun cancel() {
+ check(lock.tryLock())
+ lock.unlock()
+ }
+ })
+ }
+}
diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt
index efe7ec7e..fa039897 100644
--- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt
+++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt
@@ -52,9 +52,6 @@ class IntegrationTest(
assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" })
assertFailsWith<NoSuchElementException> { pub.awaitLast() }
assertFailsWith<NoSuchElementException> { pub.awaitSingle() }
- assertEquals("OK", pub.awaitSingleOrDefault("OK"))
- assertNull(pub.awaitSingleOrNull())
- assertEquals("ELSE", pub.awaitSingleOrElse { "ELSE" })
var cnt = 0
pub.collect { cnt++ }
assertEquals(0, cnt)
@@ -72,9 +69,6 @@ class IntegrationTest(
assertEquals("OK", pub.awaitFirstOrElse { "ELSE" })
assertEquals("OK", pub.awaitLast())
assertEquals("OK", pub.awaitSingle())
- assertEquals("OK", pub.awaitSingleOrDefault("!"))
- assertEquals("OK", pub.awaitSingleOrNull())
- assertEquals("OK", pub.awaitSingleOrElse { "ELSE" })
var cnt = 0
pub.collect {
assertEquals("OK", it)
@@ -189,10 +183,6 @@ class IntegrationTest(
onError(dummyThrowable)
onComplete()
}
- assertDetectsBadPublisher<Int>({ awaitSingleOrDefault(2) }, "terminal state") {
- onComplete()
- onError(dummyThrowable)
- }
assertDetectsBadPublisher<Int>({ awaitFirst() }, "terminal state") {
onNext(0)
onComplete()
@@ -251,4 +241,4 @@ internal suspend inline fun <reified E: Throwable> assertCallsExceptionHandlerWi
it
}
}
-} \ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt
index 095b724d..d92a8883 100644
--- a/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt
+++ b/reactive/kotlinx-coroutines-reactive/test/PublishTest.kt
@@ -5,9 +5,13 @@
package kotlinx.coroutines.reactive
import kotlinx.coroutines.*
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.sync.*
import org.junit.Test
import org.reactivestreams.*
+import java.util.concurrent.*
import kotlin.test.*
class PublishTest : TestBase() {
@@ -278,10 +282,40 @@ class PublishTest : TestBase() {
val publisher = publish {
assertFailsWith<NullPointerException> { send(null) }
assertFailsWith<NullPointerException> { trySend(null) }
- @Suppress("DEPRECATION")
- assertFailsWith<NullPointerException> { offer(null) }
send("OK")
}
assertEquals("OK", publisher.awaitFirstOrNull())
}
-} \ No newline at end of file
+
+ @Test
+ fun testOnSendCancelled() = runTest {
+ val latch = CountDownLatch(1)
+ val published = publish(Dispatchers.Default) {
+ expect(2)
+ // Collector is ready
+ send(1)
+ try {
+ send(2)
+ expectUnreached()
+ } catch (e: CancellationException) {
+ // publisher cancellation is async
+ latch.countDown()
+ throw e
+ }
+ }
+
+ expect(1)
+ val collectorLatch = Mutex(true)
+ val job = launch {
+ published.asFlow().buffer(0).collect {
+ collectorLatch.unlock()
+ hang { expect(4) }
+ }
+ }
+ collectorLatch.lock()
+ expect(3)
+ job.cancelAndJoin()
+ latch.await()
+ finish(5)
+ }
+}
diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt
index e3b1d3b3..4a552b5f 100644
--- a/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt
+++ b/reactive/kotlinx-coroutines-reactive/test/PublisherMultiTest.kt
@@ -5,6 +5,7 @@
package kotlinx.coroutines.reactive
import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
import org.junit.Test
import kotlin.test.*
@@ -16,7 +17,7 @@ class PublisherMultiTest : TestBase() {
// concurrent emitters (many coroutines)
val jobs = List(n) {
// launch
- launch {
+ launch(Dispatchers.Default) {
send(it)
}
}
@@ -28,4 +29,26 @@ class PublisherMultiTest : TestBase() {
}
assertEquals(n, resultSet.size)
}
+
+ @Test
+ fun testConcurrentStressOnSend() = runBlocking {
+ val n = 10_000 * stressTestMultiplier
+ val observable = publish<Int> {
+ // concurrent emitters (many coroutines)
+ val jobs = List(n) {
+ // launch
+ launch(Dispatchers.Default) {
+ select<Unit> {
+ onSend(it) {}
+ }
+ }
+ }
+ jobs.forEach { it.join() }
+ }
+ val resultSet = mutableSetOf<Int>()
+ observable.collect {
+ assertTrue(resultSet.add(it))
+ }
+ assertEquals(n, resultSet.size)
+ }
}
diff --git a/reactive/kotlinx-coroutines-reactor/README.md b/reactive/kotlinx-coroutines-reactor/README.md
index 69157afc..577385a0 100644
--- a/reactive/kotlinx-coroutines-reactor/README.md
+++ b/reactive/kotlinx-coroutines-reactor/README.md
@@ -32,24 +32,24 @@ Conversion functions:
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
<!--- INDEX kotlinx.coroutines.channels -->
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
<!--- MODULE kotlinx-coroutines-reactor -->
<!--- INDEX kotlinx.coroutines.reactor -->
-[mono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html
-[flux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html
-[Flow.asFlux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-flux.html
-[ReactorContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/-reactor-context/index.html
-[kotlinx.coroutines.Job.asMono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-mono.html
-[kotlinx.coroutines.Deferred.asMono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-mono.html
-[reactor.core.scheduler.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-coroutine-dispatcher.html
+[mono]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html
+[flux]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html
+[Flow.asFlux]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-flux.html
+[ReactorContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/-reactor-context/index.html
+[kotlinx.coroutines.Job.asMono]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-mono.html
+[kotlinx.coroutines.Deferred.asMono]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-mono.html
+[reactor.core.scheduler.Scheduler.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/as-coroutine-dispatcher.html
<!--- END -->
diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts
index 03af7f4f..d4bb135f 100644
--- a/reactive/kotlinx-coroutines-reactor/build.gradle.kts
+++ b/reactive/kotlinx-coroutines-reactor/build.gradle.kts
@@ -5,8 +5,8 @@
val reactorVersion = version("reactor")
dependencies {
- compile("io.projectreactor:reactor-core:$reactorVersion")
- compile(project(":kotlinx-coroutines-reactive"))
+ api("io.projectreactor:reactor-core:$reactorVersion")
+ api(project(":kotlinx-coroutines-reactive"))
}
java {
@@ -27,3 +27,16 @@ tasks {
externalDocumentationLink(
url = "https://projectreactor.io/docs/core/$reactorVersion/api/"
)
+
+val commonKoverExcludes = listOf(
+ "kotlinx.coroutines.reactor.FlowKt", // Deprecated
+ "kotlinx.coroutines.reactor.ConvertKt\$asFlux$1" // Deprecated
+)
+
+tasks.koverHtmlReport {
+ excludes = commonKoverExcludes
+}
+
+tasks.koverVerify {
+ excludes = commonKoverExcludes
+}
diff --git a/reactive/kotlinx-coroutines-reactor/src/Mono.kt b/reactive/kotlinx-coroutines-reactor/src/Mono.kt
index e86d51c6..f31004b6 100644
--- a/reactive/kotlinx-coroutines-reactor/src/Mono.kt
+++ b/reactive/kotlinx-coroutines-reactor/src/Mono.kt
@@ -157,7 +157,7 @@ public fun <T> CoroutineScope.mono(
@Deprecated(
message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " +
"Please use awaitSingle() instead.",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingle()")
) // Warning since 1.5, error in 1.6
public suspend fun <T> Mono<T>.awaitFirst(): T = awaitSingle()
@@ -181,7 +181,7 @@ public suspend fun <T> Mono<T>.awaitFirst(): T = awaitSingle()
@Deprecated(
message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " +
"Please use awaitSingleOrNull() instead.",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default")
) // Warning since 1.5, error in 1.6
public suspend fun <T> Mono<T>.awaitFirstOrDefault(default: T): T = awaitSingleOrNull() ?: default
@@ -205,7 +205,7 @@ public suspend fun <T> Mono<T>.awaitFirstOrDefault(default: T): T = awaitSingleO
@Deprecated(
message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " +
"Please use awaitSingleOrNull() instead.",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull()")
) // Warning since 1.5, error in 1.6
public suspend fun <T> Mono<T>.awaitFirstOrNull(): T? = awaitSingleOrNull()
@@ -229,7 +229,7 @@ public suspend fun <T> Mono<T>.awaitFirstOrNull(): T? = awaitSingleOrNull()
@Deprecated(
message = "Mono produces at most one value, so the semantics of dropping the remaining elements are not useful. " +
"Please use awaitSingleOrNull() instead.",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: defaultValue()")
) // Warning since 1.5, error in 1.6
public suspend fun <T> Mono<T>.awaitFirstOrElse(defaultValue: () -> T): T = awaitSingleOrNull() ?: defaultValue()
@@ -253,7 +253,7 @@ public suspend fun <T> Mono<T>.awaitFirstOrElse(defaultValue: () -> T): T = awai
@Deprecated(
message = "Mono produces at most one value, so the last element is the same as the first. " +
"Please use awaitSingle() instead.",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingle()")
) // Warning since 1.5, error in 1.6
public suspend fun <T> Mono<T>.awaitLast(): T = awaitSingle()
diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt
index d9228409..912fb6e5 100644
--- a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt
+++ b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt
@@ -4,7 +4,6 @@
package kotlinx.coroutines.reactor
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlin.coroutines.*
import kotlinx.coroutines.reactive.*
import reactor.util.context.*
@@ -65,11 +64,7 @@ public class ReactorContext(public val context: Context) : AbstractCoroutineCont
*/
public fun ContextView.asCoroutineContext(): ReactorContext = ReactorContext(this)
-/**
- * Wraps the given [Context] into [ReactorContext], so it can be added to the coroutine's context
- * and later used via `coroutineContext[ReactorContext]`.
- * @suppress
- */
+/** @suppress */
@Deprecated("The more general version for ContextView should be used instead", level = DeprecationLevel.HIDDEN)
public fun Context.asCoroutineContext(): ReactorContext = readOnly().asCoroutineContext() // `readOnly()` is zero-cost.
diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt
index cc336ba6..3879c62c 100644
--- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt
+++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt
@@ -69,72 +69,6 @@ class FluxSingleTest : TestBase() {
}
@Test
- fun testAwaitSingleOrDefault() {
- val flux = flux {
- send(Flux.empty<String>().awaitSingleOrDefault("O") + "K")
- }
-
- checkSingleValue(flux) {
- assertEquals("OK", it)
- }
- }
-
- @Test
- fun testAwaitSingleOrDefaultException() {
- val flux = flux {
- send(Flux.just("O", "#").awaitSingleOrDefault("!") + "K")
- }
-
- checkErroneous(flux) {
- assert(it is IllegalArgumentException)
- }
- }
-
- @Test
- fun testAwaitSingleOrNull() {
- val flux = flux<String?> {
- send(Flux.empty<String>().awaitSingleOrNull() ?: "OK")
- }
-
- checkSingleValue(flux) {
- assertEquals("OK", it)
- }
- }
-
- @Test
- fun testAwaitSingleOrNullException() {
- val flux = flux {
- send((Flux.just("O", "#").awaitSingleOrNull() ?: "!") + "K")
- }
-
- checkErroneous(flux) {
- assert(it is IllegalArgumentException)
- }
- }
-
- @Test
- fun testAwaitSingleOrElse() {
- val flux = flux {
- send(Flux.empty<String>().awaitSingleOrElse { "O" } + "K")
- }
-
- checkSingleValue(flux) {
- assertEquals("OK", it)
- }
- }
-
- @Test
- fun testAwaitSingleOrElseException() {
- val flux = flux {
- send(Flux.just("O", "#").awaitSingleOrElse { "!" } + "K")
- }
-
- checkErroneous(flux) {
- assert(it is IllegalArgumentException)
- }
- }
-
- @Test
fun testAwaitFirst() {
val flux = flux {
send(Flux.just("O", "#").awaitFirst() + "K")
diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt
index d059eb66..f575af41 100644
--- a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt
+++ b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt
@@ -170,10 +170,8 @@ class FluxTest : TestBase() {
val flux = flux {
assertFailsWith<NullPointerException> { send(null) }
assertFailsWith<NullPointerException> { trySend(null) }
- @Suppress("DEPRECATION")
- assertFailsWith<NullPointerException> { offer(null) }
send("OK")
}
assertEquals("OK", flux.awaitFirstOrNull())
}
-} \ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt
index 421295d1..2a5e5dc1 100644
--- a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt
+++ b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt
@@ -119,29 +119,6 @@ class MonoTest : TestBase() {
assertNull(Mono.empty<Int>().awaitSingleOrNull())
}
- /** Tests that the versions of the await methods specialized for Mono for deprecation behave correctly and we don't
- * break any code by introducing them. */
- @Test
- @Suppress("DEPRECATION")
- fun testDeprecatedAwaitMethods() = runBlocking {
- val filledMono = mono<String> { "OK" }
- assertEquals("OK", filledMono.awaitFirst())
- assertEquals("OK", filledMono.awaitFirstOrDefault("!"))
- assertEquals("OK", filledMono.awaitFirstOrNull())
- assertEquals("OK", filledMono.awaitFirstOrElse { "ELSE" })
- assertEquals("OK", filledMono.awaitLast())
- assertEquals("OK", filledMono.awaitSingleOrDefault("!"))
- assertEquals("OK", filledMono.awaitSingleOrElse { "ELSE" })
- val emptyMono = mono<String> { null }
- assertFailsWith<NoSuchElementException> { emptyMono.awaitFirst() }
- assertEquals("OK", emptyMono.awaitFirstOrDefault("OK"))
- assertNull(emptyMono.awaitFirstOrNull())
- assertEquals("ELSE", emptyMono.awaitFirstOrElse { "ELSE" })
- assertFailsWith<NoSuchElementException> { emptyMono.awaitLast() }
- assertEquals("OK", emptyMono.awaitSingleOrDefault("OK"))
- assertEquals("ELSE", emptyMono.awaitSingleOrElse { "ELSE" })
- }
-
/** Tests that calls to [awaitSingleOrNull] (and, thus, to the rest of such functions) throw [CancellationException]
* and unsubscribe from the publisher when their [Job] is cancelled. */
@Test
diff --git a/reactive/kotlinx-coroutines-rx2/README.md b/reactive/kotlinx-coroutines-rx2/README.md
index d93f569a..41317165 100644
--- a/reactive/kotlinx-coroutines-rx2/README.md
+++ b/reactive/kotlinx-coroutines-rx2/README.md
@@ -50,40 +50,40 @@ Conversion functions:
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
<!--- INDEX kotlinx.coroutines.channels -->
-[ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
+[ProducerScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
<!--- MODULE kotlinx-coroutines-rx2 -->
<!--- INDEX kotlinx.coroutines.rx2 -->
-[rxCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-completable.html
-[rxMaybe]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-maybe.html
-[rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html
-[rxObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-observable.html
-[rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html
-[Flow.asFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-flowable.html
-[Flow.asObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-observable.html
-[ObservableSource.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-flow.html
-[io.reactivex.CompletableSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await.html
-[io.reactivex.MaybeSource.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single.html
-[io.reactivex.MaybeSource.awaitSingleOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single-or-null.html
-[io.reactivex.SingleSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await.html
-[io.reactivex.ObservableSource.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first.html
-[io.reactivex.ObservableSource.awaitFirstOrDefault]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-default.html
-[io.reactivex.ObservableSource.awaitFirstOrElse]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-else.html
-[io.reactivex.ObservableSource.awaitFirstOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-null.html
-[io.reactivex.ObservableSource.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single.html
-[kotlinx.coroutines.Job.asCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-completable.html
-[kotlinx.coroutines.Deferred.asSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-single.html
-[io.reactivex.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-coroutine-dispatcher.html
+[rxCompletable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-completable.html
+[rxMaybe]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-maybe.html
+[rxSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html
+[rxObservable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-observable.html
+[rxFlowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html
+[Flow.asFlowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-flowable.html
+[Flow.asObservable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-observable.html
+[ObservableSource.asFlow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-flow.html
+[io.reactivex.CompletableSource.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await.html
+[io.reactivex.MaybeSource.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single.html
+[io.reactivex.MaybeSource.awaitSingleOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single-or-null.html
+[io.reactivex.SingleSource.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await.html
+[io.reactivex.ObservableSource.awaitFirst]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first.html
+[io.reactivex.ObservableSource.awaitFirstOrDefault]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-default.html
+[io.reactivex.ObservableSource.awaitFirstOrElse]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-else.html
+[io.reactivex.ObservableSource.awaitFirstOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-first-or-null.html
+[io.reactivex.ObservableSource.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/await-single.html
+[kotlinx.coroutines.Job.asCompletable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-completable.html
+[kotlinx.coroutines.Deferred.asSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-single.html
+[io.reactivex.Scheduler.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/as-coroutine-dispatcher.html
<!--- END -->
diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api
index c27ef4d7..c2d1c4bf 100644
--- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api
+++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api
@@ -16,8 +16,8 @@ public final class kotlinx/coroutines/rx2/RxAwaitKt {
public final class kotlinx/coroutines/rx2/RxChannelKt {
public static final fun collect (Lio/reactivex/MaybeSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun collect (Lio/reactivex/ObservableSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun openSubscription (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
- public static final fun openSubscription (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
+ public static final synthetic fun openSubscription (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
+ public static final synthetic fun openSubscription (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
public static final fun toChannel (Lio/reactivex/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
public static final fun toChannel (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel;
}
@@ -69,7 +69,9 @@ public final class kotlinx/coroutines/rx2/RxObservableKt {
}
public final class kotlinx/coroutines/rx2/RxSchedulerKt {
- public static final fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/rx2/SchedulerCoroutineDispatcher;
+ public static final fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/CoroutineDispatcher;
+ public static final synthetic fun asCoroutineDispatcher (Lio/reactivex/Scheduler;)Lkotlinx/coroutines/rx2/SchedulerCoroutineDispatcher;
+ public static final fun asScheduler (Lkotlinx/coroutines/CoroutineDispatcher;)Lio/reactivex/Scheduler;
}
public final class kotlinx/coroutines/rx2/RxSingleKt {
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt b/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt
index 0e0b47eb..da9809c9 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxAwait.kt
@@ -80,7 +80,7 @@ public suspend fun <T> MaybeSource<T>.awaitSingle(): T = awaitSingleOrNull() ?:
*/
@Deprecated(
message = "Deprecated in favor of awaitSingleOrNull()",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull()")
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> MaybeSource<T>.await(): T? = awaitSingleOrNull()
@@ -102,7 +102,7 @@ public suspend fun <T> MaybeSource<T>.await(): T? = awaitSingleOrNull()
*/
@Deprecated(
message = "Deprecated in favor of awaitSingleOrNull()",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default")
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> MaybeSource<T>.awaitOrDefault(default: T): T = awaitSingleOrNull() ?: default
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt
index bb093b07..fc09bf9e 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt
@@ -13,36 +13,6 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.reactive.*
/**
- * Subscribes to this [MaybeSource] and returns a channel to receive elements emitted by it.
- * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source.
- *
- * This API is deprecated in the favour of [Flow].
- * [MaybeSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first.
- * @suppress
- */
-@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.ERROR) // Will be hidden in 1.5
-public fun <T> MaybeSource<T>.openSubscription(): ReceiveChannel<T> {
- val channel = SubscriptionChannel<T>()
- subscribe(channel)
- return channel
-}
-
-/**
- * Subscribes to this [ObservableSource] and returns a channel to receive elements emitted by it.
- * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source.
- *
- * This API is deprecated in the favour of [Flow].
- * [ObservableSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first.
- * @suppress
- */
-@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.ERROR) // Will be hidden in 1.5
-public fun <T> ObservableSource<T>.openSubscription(): ReceiveChannel<T> {
- val channel = SubscriptionChannel<T>()
- subscribe(channel)
- return channel
-}
-
-/**
* Subscribes to this [MaybeSource] and performs the specified action for each received element.
*
* If [action] throws an exception at some point or if the [MaybeSource] raises an error, the exception is rethrown from
@@ -107,3 +77,19 @@ private class SubscriptionChannel<T> :
close(cause = e)
}
}
+
+/** @suppress */
+@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.HIDDEN) // ERROR in 1.4.0, HIDDEN in 1.6.0
+public fun <T> ObservableSource<T>.openSubscription(): ReceiveChannel<T> {
+ val channel = SubscriptionChannel<T>()
+ subscribe(channel)
+ return channel
+}
+
+/** @suppress */
+@Deprecated(message = "Deprecated in the favour of Flow", level = DeprecationLevel.HIDDEN) // ERROR in 1.4.0, HIDDEN in 1.6.0
+public fun <T> MaybeSource<T>.openSubscription(): ReceiveChannel<T> {
+ val channel = SubscriptionChannel<T>()
+ subscribe(channel)
+ return channel
+}
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt
index 3f915382..e4670f35 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt
@@ -20,7 +20,7 @@ public fun rxCompletable(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
): Completable {
- require(context[Job] === null) { "Completable context cannot contain job in it." +
+ require(context[Job] === null) { "Completable context cannot contain job in it. " +
"Its lifecycle should be managed via Disposable handle. Had $context" }
return rxCompletableInternal(GlobalScope, context, block)
}
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
index 5f409815..90e770bb 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt
@@ -10,6 +10,7 @@ import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
import kotlinx.coroutines.sync.*
import kotlin.coroutines.*
@@ -95,10 +96,22 @@ private class RxObservableCoroutine<T : Any>(
element: T,
block: suspend (SendChannel<T>) -> R
) {
- mutex.onLock.registerSelectClause2(select, null) {
+ val clause = suspend {
doLockedNext(element)?.let { throw it }
block(this)
}
+
+ // This is the default replacement proposed in onLock replacement
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.lock()
+ // Already selected -- bail out
+ if (!select.trySelect()) {
+ mutex.unlock()
+ return@launch
+ }
+
+ clause.startCoroutineCancellable(select.completion)
+ }
}
// assert: mutex.isLocked()
diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
index 0262fc12..d7d5f6cf 100644
--- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
+++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt
@@ -4,16 +4,143 @@
package kotlinx.coroutines.rx2
-import io.reactivex.Scheduler
+import io.reactivex.*
+import io.reactivex.disposables.*
+import io.reactivex.plugins.*
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import java.util.concurrent.TimeUnit
-import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.*
+import java.util.concurrent.*
+import kotlin.coroutines.*
/**
* Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher]
* and provides native support of [delay] and [withTimeout].
*/
-public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this)
+public fun Scheduler.asCoroutineDispatcher(): CoroutineDispatcher =
+ if (this is DispatcherScheduler) {
+ dispatcher
+ } else {
+ SchedulerCoroutineDispatcher(this)
+ }
+
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.2, binary compatibility with earlier versions")
+@JvmName("asCoroutineDispatcher")
+public fun Scheduler.asCoroutineDispatcher0(): SchedulerCoroutineDispatcher =
+ SchedulerCoroutineDispatcher(this)
+
+/**
+ * Converts an instance of [CoroutineDispatcher] to an implementation of [Scheduler].
+ */
+public fun CoroutineDispatcher.asScheduler(): Scheduler =
+ if (this is SchedulerCoroutineDispatcher) {
+ scheduler
+ } else {
+ DispatcherScheduler(this)
+ }
+
+private class DispatcherScheduler(@JvmField val dispatcher: CoroutineDispatcher) : Scheduler() {
+
+ private val schedulerJob = SupervisorJob()
+
+ /**
+ * The scope for everything happening in this [DispatcherScheduler].
+ *
+ * Running tasks, too, get launched under this scope, because [shutdown] should cancel the running tasks as well.
+ */
+ private val scope = CoroutineScope(schedulerJob + dispatcher)
+
+ /**
+ * The counter of created workers, for their pretty-printing.
+ */
+ private val workerCounter = atomic(1L)
+
+ override fun scheduleDirect(block: Runnable, delay: Long, unit: TimeUnit): Disposable =
+ scope.scheduleTask(block, unit.toMillis(delay)) { task ->
+ Runnable { scope.launch { task() } }
+ }
+
+ override fun createWorker(): Worker = DispatcherWorker(workerCounter.getAndIncrement(), dispatcher, schedulerJob)
+
+ override fun shutdown() {
+ schedulerJob.cancel()
+ }
+
+ private class DispatcherWorker(
+ private val counter: Long,
+ private val dispatcher: CoroutineDispatcher,
+ parentJob: Job
+ ) : Worker() {
+
+ private val workerJob = SupervisorJob(parentJob)
+ private val workerScope = CoroutineScope(workerJob + dispatcher)
+ private val blockChannel = Channel<suspend () -> Unit>(Channel.UNLIMITED)
+
+ init {
+ workerScope.launch {
+ blockChannel.consumeEach {
+ it()
+ }
+ }
+ }
+
+ override fun schedule(block: Runnable, delay: Long, unit: TimeUnit): Disposable =
+ workerScope.scheduleTask(block, unit.toMillis(delay)) { task ->
+ Runnable { blockChannel.trySend(task) }
+ }
+
+ override fun isDisposed(): Boolean = !workerScope.isActive
+
+ override fun dispose() {
+ blockChannel.close()
+ workerJob.cancel()
+ }
+
+ override fun toString(): String = "$dispatcher (worker $counter, ${if (isDisposed) "disposed" else "active"})"
+ }
+
+ override fun toString(): String = dispatcher.toString()
+}
+
+private typealias Task = suspend () -> Unit
+
+/**
+ * Schedule [block] so that an adapted version of it, wrapped in [adaptForScheduling], executes after [delayMillis]
+ * milliseconds.
+ */
+private fun CoroutineScope.scheduleTask(
+ block: Runnable,
+ delayMillis: Long,
+ adaptForScheduling: (Task) -> Runnable
+): Disposable {
+ val ctx = coroutineContext
+ var handle: DisposableHandle? = null
+ val disposable = Disposables.fromRunnable {
+ // null if delay <= 0
+ handle?.dispose()
+ }
+ val decoratedBlock = RxJavaPlugins.onSchedule(block)
+ suspend fun task() {
+ if (disposable.isDisposed) return
+ try {
+ runInterruptible {
+ decoratedBlock.run()
+ }
+ } catch (e: Throwable) {
+ handleUndeliverableException(e, ctx)
+ }
+ }
+
+ val toSchedule = adaptForScheduling(::task)
+ if (!isActive) return Disposables.disposed()
+ if (delayMillis <= 0) {
+ toSchedule.run()
+ } else {
+ @Suppress("INVISIBLE_MEMBER")
+ ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it }
+ }
+ return disposable
+}
/**
* Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler].
@@ -45,8 +172,10 @@ public class SchedulerCoroutineDispatcher(
/** @suppress */
override fun toString(): String = scheduler.toString()
+
/** @suppress */
override fun equals(other: Any?): Boolean = other is SchedulerCoroutineDispatcher && other.scheduler === scheduler
+
/** @suppress */
override fun hashCode(): Int = System.identityHashCode(scheduler)
-}
+} \ No newline at end of file
diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt
index 30266e3e..7e1d3350 100644
--- a/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt
+++ b/reactive/kotlinx-coroutines-rx2/test/ObservableCompletionStressTest.kt
@@ -12,7 +12,7 @@ import kotlin.coroutines.*
class ObservableCompletionStressTest : TestBase() {
private val N_REPEATS = 10_000 * stressTestMultiplier
- private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = rxObservable(context) {
+ private fun range(context: CoroutineContext, start: Int, count: Int) = rxObservable(context) {
for (x in start until start + count) send(x)
}
@@ -33,4 +33,4 @@ class ObservableCompletionStressTest : TestBase() {
}
}
}
-} \ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt
index 074fcf49..70232114 100644
--- a/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt
+++ b/reactive/kotlinx-coroutines-rx2/test/ObservableMultiTest.kt
@@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2
import io.reactivex.*
import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
import org.junit.Test
import java.io.*
import kotlin.test.*
@@ -48,6 +49,29 @@ class ObservableMultiTest : TestBase() {
}
@Test
+ fun testConcurrentStressOnSend() {
+ val n = 10_000 * stressTestMultiplier
+ val observable = rxObservable<Int> {
+ newCoroutineContext(coroutineContext)
+ // concurrent emitters (many coroutines)
+ val jobs = List(n) {
+ // launch
+ launch(Dispatchers.Default) {
+ val i = it
+ select<Unit> {
+ onSend(i) {}
+ }
+ }
+ }
+ jobs.forEach { it.join() }
+ }
+ checkSingleValue(observable.toList()) { list ->
+ assertEquals(n, list.size)
+ assertEquals((0 until n).toList(), list.sorted())
+ }
+ }
+
+ @Test
fun testIteratorResendUnconfined() {
val n = 10_000 * stressTestMultiplier
val observable = rxObservable(Dispatchers.Unconfined) {
@@ -88,4 +112,4 @@ class ObservableMultiTest : TestBase() {
assertEquals("OK", it)
}
}
-} \ No newline at end of file
+}
diff --git a/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt b/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt
new file mode 100644
index 00000000..ea33a9fa
--- /dev/null
+++ b/reactive/kotlinx-coroutines-rx2/test/SchedulerStressTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.rx2
+
+import kotlinx.coroutines.*
+import org.junit.*
+import java.util.concurrent.*
+
+class SchedulerStressTest : TestBase() {
+ @Before
+ fun setup() {
+ ignoreLostThreads("RxCachedThreadScheduler-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-")
+ }
+
+ /**
+ * Test that we don't get an OOM if we schedule many jobs at once.
+ * It's expected that if you don't dispose you'd see an OOM error.
+ */
+ @Test
+ fun testSchedulerDisposed(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableDisposed(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerDisposed(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableDisposed(worker::schedule)
+ }
+
+ private suspend fun testRunnableDisposed(block: RxSchedulerBlockNoDelay) {
+ val n = 2000 * stressTestMultiplier
+ repeat(n) {
+ val a = ByteArray(1000000) //1MB
+ val disposable = block(Runnable {
+ keepMe(a)
+ expectUnreached()
+ })
+ disposable.dispose()
+ yield() // allow the scheduled task to observe that it was disposed
+ }
+ }
+
+ /**
+ * Test function that holds a reference. Used for testing OOM situations
+ */
+ private fun keepMe(a: ByteArray) {
+ Thread.sleep(a.size / (a.size + 1) + 10L)
+ }
+
+ /**
+ * Test that we don't get an OOM if we schedule many delayed jobs at once. It's expected that if you don't dispose that you'd
+ * see a OOM error.
+ */
+ @Test
+ fun testSchedulerDisposedDuringDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableDisposedDuringDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerDisposedDuringDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableDisposedDuringDelay(worker::schedule)
+ }
+
+ private fun testRunnableDisposedDuringDelay(block: RxSchedulerBlockWithDelay) {
+ val n = 2000 * stressTestMultiplier
+ repeat(n) {
+ val a = ByteArray(1000000) //1MB
+ val delayMillis: Long = 10
+ val disposable = block(Runnable {
+ keepMe(a)
+ expectUnreached()
+ }, delayMillis, TimeUnit.MILLISECONDS)
+ disposable.dispose()
+ }
+ }
+} \ No newline at end of file
diff --git a/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt b/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt
index 26dbe8f4..19418671 100644
--- a/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt
+++ b/reactive/kotlinx-coroutines-rx2/test/SchedulerTest.kt
@@ -4,10 +4,18 @@
package kotlinx.coroutines.rx2
-import io.reactivex.schedulers.Schedulers
+import io.reactivex.*
+import io.reactivex.disposables.*
+import io.reactivex.plugins.*
+import io.reactivex.schedulers.*
import kotlinx.coroutines.*
-import org.junit.Before
+import kotlinx.coroutines.sync.*
+import org.junit.*
import org.junit.Test
+import java.lang.Runnable
+import java.util.concurrent.*
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.*
import kotlin.test.*
class SchedulerTest : TestBase() {
@@ -17,7 +25,7 @@ class SchedulerTest : TestBase() {
}
@Test
- fun testIoScheduler(): Unit = runBlocking {
+ fun testIoScheduler(): Unit = runTest {
expect(1)
val mainThread = Thread.currentThread()
withContext(Schedulers.io().asCoroutineDispatcher()) {
@@ -31,4 +39,458 @@ class SchedulerTest : TestBase() {
}
finish(4)
}
-} \ No newline at end of file
+
+ /** Tests [toString] implementations of [CoroutineDispatcher.asScheduler] and its [Scheduler.Worker]. */
+ @Test
+ fun testSchedulerToString() {
+ val name = "Dispatchers.Default"
+ val scheduler = Dispatchers.Default.asScheduler()
+ assertContains(scheduler.toString(), name)
+ val worker = scheduler.createWorker()
+ val activeWorkerName = worker.toString()
+ assertContains(worker.toString(), name)
+ worker.dispose()
+ val disposedWorkerName = worker.toString()
+ assertNotEquals(activeWorkerName, disposedWorkerName)
+ }
+
+ private fun runSchedulerTest(nThreads: Int = 1, action: (Scheduler) -> Unit) {
+ val future = CompletableFuture<Unit>()
+ try {
+ newFixedThreadPoolContext(nThreads, "test").use { dispatcher ->
+ RxJavaPlugins.setErrorHandler {
+ if (!future.completeExceptionally(it)) {
+ handleUndeliverableException(it, dispatcher)
+ }
+ }
+ action(dispatcher.asScheduler())
+ }
+ } finally {
+ RxJavaPlugins.setErrorHandler(null)
+ }
+ future.complete(Unit)
+ future.getNow(Unit) // rethrow any encountered errors
+ }
+
+ private fun ensureSeparateThread(schedule: (Runnable, Long, TimeUnit) -> Unit, scheduleNoDelay: (Runnable) -> Unit) {
+ val mainThread = Thread.currentThread()
+ val cdl1 = CountDownLatch(1)
+ val cdl2 = CountDownLatch(1)
+ expect(1)
+ val thread = AtomicReference<Thread?>(null)
+ fun checkThread() {
+ val current = Thread.currentThread()
+ thread.getAndSet(current)?.let { assertEquals(it, current) }
+ }
+ schedule({
+ assertNotSame(mainThread, Thread.currentThread())
+ checkThread()
+ cdl2.countDown()
+ }, 300, TimeUnit.MILLISECONDS)
+ scheduleNoDelay {
+ expect(2)
+ checkThread()
+ assertNotSame(mainThread, Thread.currentThread())
+ cdl1.countDown()
+ }
+ cdl1.await()
+ cdl2.await()
+ finish(3)
+ }
+
+ /**
+ * Tests [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler] on a single-threaded dispatcher.
+ */
+ @Test
+ fun testSingleThreadedDispatcherDirect(): Unit = runSchedulerTest(1) {
+ ensureSeparateThread(it::scheduleDirect, it::scheduleDirect)
+ }
+
+ /**
+ * Tests [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler] running its tasks on the correct thread.
+ */
+ @Test
+ fun testSingleThreadedWorker(): Unit = runSchedulerTest(1) {
+ val worker = it.createWorker()
+ ensureSeparateThread(worker::schedule, worker::schedule)
+ }
+
+ private fun checkCancelling(schedule: (Runnable, Long, TimeUnit) -> Disposable) {
+ // cancel the task before it has a chance to run.
+ val handle1 = schedule({
+ throw IllegalStateException("should have been successfully cancelled")
+ }, 10_000, TimeUnit.MILLISECONDS)
+ handle1.dispose()
+ // cancel the task after it started running.
+ val cdl1 = CountDownLatch(1)
+ val cdl2 = CountDownLatch(1)
+ val handle2 = schedule({
+ cdl1.countDown()
+ cdl2.await()
+ if (Thread.interrupted())
+ throw IllegalStateException("cancelling the task should not interrupt the thread")
+ }, 100, TimeUnit.MILLISECONDS)
+ cdl1.await()
+ handle2.dispose()
+ cdl2.countDown()
+ }
+
+ /**
+ * Test cancelling [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testCancellingDirect(): Unit = runSchedulerTest {
+ checkCancelling(it::scheduleDirect)
+ }
+
+ /**
+ * Test cancelling [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testCancellingWorker(): Unit = runSchedulerTest {
+ val worker = it.createWorker()
+ checkCancelling(worker::schedule)
+ }
+
+ /**
+ * Test shutting down [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testShuttingDown() {
+ val n = 5
+ runSchedulerTest(nThreads = n) { scheduler ->
+ val cdl1 = CountDownLatch(n)
+ val cdl2 = CountDownLatch(1)
+ val cdl3 = CountDownLatch(n)
+ repeat(n) {
+ scheduler.scheduleDirect {
+ cdl1.countDown()
+ try {
+ cdl2.await()
+ } catch (e: InterruptedException) {
+ // this is the expected outcome
+ cdl3.countDown()
+ }
+ }
+ }
+ cdl1.await()
+ scheduler.shutdown()
+ if (!cdl3.await(1, TimeUnit.SECONDS)) {
+ cdl2.countDown()
+ error("the tasks were not cancelled when the scheduler was shut down")
+ }
+ }
+ }
+
+ /** Tests that there are no uncaught exceptions if [Disposable.dispose] on a worker happens when tasks are present. */
+ @Test
+ fun testDisposingWorker() = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ yield() // so that the worker starts waiting on the channel
+ assertFalse(worker.isDisposed)
+ worker.dispose()
+ assertTrue(worker.isDisposed)
+ }
+
+ /** Tests trying to use a [Scheduler.Worker]/[Scheduler] after [Scheduler.Worker.dispose]/[Scheduler.shutdown]. */
+ @Test
+ fun testSchedulingAfterDisposing() = runSchedulerTest {
+ expect(1)
+ val worker = it.createWorker()
+ // use CDL to ensure that the worker has properly initialized
+ val cdl1 = CountDownLatch(1)
+ setScheduler(2, 3)
+ val disposable1 = worker.schedule {
+ cdl1.countDown()
+ }
+ cdl1.await()
+ expect(4)
+ assertFalse(disposable1.isDisposed)
+ setScheduler(6, -1)
+ // check that the worker automatically disposes of the tasks after being disposed
+ assertFalse(worker.isDisposed)
+ worker.dispose()
+ assertTrue(worker.isDisposed)
+ expect(5)
+ val disposable2 = worker.schedule {
+ expectUnreached()
+ }
+ assertTrue(disposable2.isDisposed)
+ setScheduler(7, 8)
+ // ensure that the scheduler still works
+ val cdl2 = CountDownLatch(1)
+ val disposable3 = it.scheduleDirect {
+ cdl2.countDown()
+ }
+ cdl2.await()
+ expect(9)
+ assertFalse(disposable3.isDisposed)
+ // check that the scheduler automatically disposes of the tasks after being shut down
+ it.shutdown()
+ setScheduler(10, -1)
+ val disposable4 = it.scheduleDirect {
+ expectUnreached()
+ }
+ assertTrue(disposable4.isDisposed)
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(11)
+ }
+
+ @Test
+ fun testSchedulerWithNoDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithNoDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithNoDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithNoDelay(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableWithNoDelay(block: RxSchedulerBlockNoDelay) {
+ expect(1)
+ suspendCancellableCoroutine<Unit> {
+ block(Runnable {
+ expect(2)
+ it.resume(Unit)
+ })
+ }
+ yield()
+ finish(3)
+ }
+
+ @Test
+ fun testSchedulerWithDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect, 300)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule, 300)
+ }
+
+ @Test
+ fun testSchedulerWithZeroDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithZeroDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableWithDelay(block: RxSchedulerBlockWithDelay, delayMillis: Long = 0) {
+ expect(1)
+ suspendCancellableCoroutine<Unit> {
+ block({
+ expect(2)
+ it.resume(Unit)
+ }, delayMillis, TimeUnit.MILLISECONDS)
+ }
+ finish(3)
+ }
+
+ @Test
+ fun testAsSchedulerWithNegativeDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect, -1)
+ }
+
+ @Test
+ fun testAsSchedulerWorkerWithNegativeDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule, -1)
+ }
+
+ @Test
+ fun testSchedulerImmediateDispose(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableImmediateDispose(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerImmediateDispose(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableImmediateDispose(scheduler.createWorker()::schedule)
+ }
+
+ private fun testRunnableImmediateDispose(block: RxSchedulerBlockNoDelay) {
+ val disposable = block {
+ expectUnreached()
+ }
+ disposable.dispose()
+ }
+
+ @Test
+ fun testConvertDispatcherToOriginalScheduler(): Unit = runTest {
+ val originalScheduler = Schedulers.io()
+ val dispatcher = originalScheduler.asCoroutineDispatcher()
+ val scheduler = dispatcher.asScheduler()
+ assertSame(originalScheduler, scheduler)
+ }
+
+ @Test
+ fun testConvertSchedulerToOriginalDispatcher(): Unit = runTest {
+ val originalDispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = originalDispatcher.asScheduler()
+ val dispatcher = scheduler.asCoroutineDispatcher()
+ assertSame(originalDispatcher, dispatcher)
+ }
+
+ @Test
+ fun testSchedulerExpectRxPluginsCall(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCall(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerExpectRxPluginsCall(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCall(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableExpectRxPluginsCall(block: RxSchedulerBlockNoDelay) {
+ expect(1)
+ setScheduler(2, 4)
+ suspendCancellableCoroutine<Unit> {
+ block(Runnable {
+ expect(5)
+ it.resume(Unit)
+ })
+ expect(3)
+ }
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(6)
+ }
+
+ @Test
+ fun testSchedulerExpectRxPluginsCallWithDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCallDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerExpectRxPluginsCallWithDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableExpectRxPluginsCallDelay(worker::schedule)
+ }
+
+ private suspend fun testRunnableExpectRxPluginsCallDelay(block: RxSchedulerBlockWithDelay) {
+ expect(1)
+ setScheduler(2, 4)
+ suspendCancellableCoroutine<Unit> {
+ block({
+ expect(5)
+ it.resume(Unit)
+ }, 10, TimeUnit.MILLISECONDS)
+ expect(3)
+ }
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(6)
+ }
+
+ private fun setScheduler(expectedCountOnSchedule: Int, expectCountOnRun: Int) {
+ RxJavaPlugins.setScheduleHandler {
+ expect(expectedCountOnSchedule)
+ Runnable {
+ expect(expectCountOnRun)
+ it.run()
+ }
+ }
+ }
+
+ /**
+ * Tests that [Scheduler.Worker] runs all work sequentially.
+ */
+ @Test
+ fun testWorkerSequentialOrdering() = runTest {
+ expect(1)
+ val scheduler = Dispatchers.Default.asScheduler()
+ val worker = scheduler.createWorker()
+ val iterations = 100
+ for (i in 0..iterations) {
+ worker.schedule {
+ expect(2 + i)
+ }
+ }
+ suspendCoroutine<Unit> {
+ worker.schedule {
+ it.resume(Unit)
+ }
+ }
+ finish((iterations + 2) + 1)
+ }
+
+ /**
+ * Test that ensures that delays are actually respected (tasks scheduled sooner in the future run before tasks scheduled later,
+ * even when the later task is submitted before the earlier one)
+ */
+ @Test
+ fun testSchedulerRespectsDelays(): Unit = runTest {
+ val scheduler = Dispatchers.Default.asScheduler()
+ testRunnableRespectsDelays(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerRespectsDelays(): Unit = runTest {
+ val scheduler = Dispatchers.Default.asScheduler()
+ testRunnableRespectsDelays(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableRespectsDelays(block: RxSchedulerBlockWithDelay) {
+ expect(1)
+ val semaphore = Semaphore(2, 2)
+ block({
+ expect(3)
+ semaphore.release()
+ }, 100, TimeUnit.MILLISECONDS)
+ block({
+ expect(2)
+ semaphore.release()
+ }, 1, TimeUnit.MILLISECONDS)
+ semaphore.acquire()
+ semaphore.acquire()
+ finish(4)
+ }
+
+ /**
+ * Tests that cancelling a runnable in one worker doesn't affect work in another scheduler.
+ *
+ * This is part of expected behavior documented.
+ */
+ @Test
+ fun testMultipleWorkerCancellation(): Unit = runTest {
+ expect(1)
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ suspendCancellableCoroutine<Unit> {
+ val workerOne = scheduler.createWorker()
+ workerOne.schedule({
+ expect(3)
+ it.resume(Unit)
+ }, 50, TimeUnit.MILLISECONDS)
+ val workerTwo = scheduler.createWorker()
+ workerTwo.schedule({
+ expectUnreached()
+ }, 1000, TimeUnit.MILLISECONDS)
+ workerTwo.dispose()
+ expect(2)
+ }
+ finish(4)
+ }
+}
+
+typealias RxSchedulerBlockNoDelay = (Runnable) -> Disposable
+typealias RxSchedulerBlockWithDelay = (Runnable, Long, TimeUnit) -> Disposable \ No newline at end of file
diff --git a/reactive/kotlinx-coroutines-rx3/README.md b/reactive/kotlinx-coroutines-rx3/README.md
index 1530558c..753df277 100644
--- a/reactive/kotlinx-coroutines-rx3/README.md
+++ b/reactive/kotlinx-coroutines-rx3/README.md
@@ -50,40 +50,40 @@ Conversion functions:
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
<!--- INDEX kotlinx.coroutines.channels -->
-[ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
+[ProducerScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html
<!--- INDEX kotlinx.coroutines.flow -->
-[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
+[Flow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
<!--- MODULE kotlinx-coroutines-rx3 -->
<!--- INDEX kotlinx.coroutines.rx3 -->
-[rxCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-completable.html
-[rxMaybe]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-maybe.html
-[rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-single.html
-[rxObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-observable.html
-[rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-flowable.html
-[Flow.asFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-flowable.html
-[Flow.asObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-observable.html
-[ObservableSource.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-flow.html
-[io.reactivex.rxjava3.core.CompletableSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await.html
-[io.reactivex.rxjava3.core.MaybeSource.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single.html
-[io.reactivex.rxjava3.core.MaybeSource.awaitSingleOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single-or-null.html
-[io.reactivex.rxjava3.core.SingleSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await.html
-[io.reactivex.rxjava3.core.ObservableSource.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first.html
-[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrDefault]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-default.html
-[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrElse]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-else.html
-[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-null.html
-[io.reactivex.rxjava3.core.ObservableSource.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single.html
-[kotlinx.coroutines.Job.asCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-completable.html
-[kotlinx.coroutines.Deferred.asSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-single.html
-[io.reactivex.rxjava3.core.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-coroutine-dispatcher.html
+[rxCompletable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-completable.html
+[rxMaybe]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-maybe.html
+[rxSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-single.html
+[rxObservable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-observable.html
+[rxFlowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-flowable.html
+[Flow.asFlowable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-flowable.html
+[Flow.asObservable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-observable.html
+[ObservableSource.asFlow]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-flow.html
+[io.reactivex.rxjava3.core.CompletableSource.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await.html
+[io.reactivex.rxjava3.core.MaybeSource.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single.html
+[io.reactivex.rxjava3.core.MaybeSource.awaitSingleOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single-or-null.html
+[io.reactivex.rxjava3.core.SingleSource.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await.html
+[io.reactivex.rxjava3.core.ObservableSource.awaitFirst]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first.html
+[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrDefault]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-default.html
+[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrElse]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-else.html
+[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-first-or-null.html
+[io.reactivex.rxjava3.core.ObservableSource.awaitSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/await-single.html
+[kotlinx.coroutines.Job.asCompletable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-completable.html
+[kotlinx.coroutines.Deferred.asSingle]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-single.html
+[io.reactivex.rxjava3.core.Scheduler.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/as-coroutine-dispatcher.html
<!--- END -->
diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api
index f6f3f1d0..5776214b 100644
--- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api
+++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api
@@ -58,7 +58,9 @@ public final class kotlinx/coroutines/rx3/RxObservableKt {
}
public final class kotlinx/coroutines/rx3/RxSchedulerKt {
- public static final fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/rx3/SchedulerCoroutineDispatcher;
+ public static final fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/CoroutineDispatcher;
+ public static final synthetic fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/rx3/SchedulerCoroutineDispatcher;
+ public static final fun asScheduler (Lkotlinx/coroutines/CoroutineDispatcher;)Lio/reactivex/rxjava3/core/Scheduler;
}
public final class kotlinx/coroutines/rx3/RxSingleKt {
diff --git a/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt
index 2a14cf7c..754dd794 100644
--- a/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt
+++ b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt
@@ -81,7 +81,7 @@ public suspend fun <T> MaybeSource<T>.awaitSingle(): T = awaitSingleOrNull() ?:
*/
@Deprecated(
message = "Deprecated in favor of awaitSingleOrNull()",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull()")
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> MaybeSource<T>.await(): T? = awaitSingleOrNull()
@@ -104,7 +104,7 @@ public suspend fun <T> MaybeSource<T>.await(): T? = awaitSingleOrNull()
*/
@Deprecated(
message = "Deprecated in favor of awaitSingleOrNull()",
- level = DeprecationLevel.WARNING,
+ level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("this.awaitSingleOrNull() ?: default")
) // Warning since 1.5, error in 1.6, hidden in 1.7
public suspend fun <T> MaybeSource<T>.awaitOrDefault(default: T): T = awaitSingleOrNull() ?: default
diff --git a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
index 57007bbd..1c5f7c0a 100644
--- a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
+++ b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.selects.*
import kotlinx.coroutines.sync.*
import kotlin.coroutines.*
import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.intrinsics.*
/**
* Creates cold [observable][Observable] that will run a given [block] in a coroutine.
@@ -95,10 +96,22 @@ private class RxObservableCoroutine<T : Any>(
element: T,
block: suspend (SendChannel<T>) -> R
) {
- mutex.onLock.registerSelectClause2(select, null) {
+ val clause = suspend {
doLockedNext(element)?.let { throw it }
block(this)
}
+
+ // This is the default replacement proposed in onLock replacement
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.lock()
+ // Already selected -- bail out
+ if (!select.trySelect()) {
+ mutex.unlock()
+ return@launch
+ }
+
+ clause.startCoroutineCancellable(select.completion)
+ }
}
// assert: mutex.isLocked()
diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
index 24c3f118..abaf0245 100644
--- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
+++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt
@@ -4,16 +4,144 @@
package kotlinx.coroutines.rx3
-import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.core.*
+import io.reactivex.rxjava3.disposables.*
+import io.reactivex.rxjava3.plugins.*
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import java.util.concurrent.TimeUnit
-import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.channels.*
+import java.util.concurrent.*
+import kotlin.coroutines.*
/**
* Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher]
* and provides native support of [delay] and [withTimeout].
*/
-public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this)
+public fun Scheduler.asCoroutineDispatcher(): CoroutineDispatcher =
+ if (this is DispatcherScheduler) {
+ dispatcher
+ } else {
+ SchedulerCoroutineDispatcher(this)
+ }
+
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.2, binary compatibility with earlier versions")
+@JvmName("asCoroutineDispatcher")
+public fun Scheduler.asCoroutineDispatcher0(): SchedulerCoroutineDispatcher =
+ SchedulerCoroutineDispatcher(this)
+
+/**
+ * Converts an instance of [CoroutineDispatcher] to an implementation of [Scheduler].
+ */
+public fun CoroutineDispatcher.asScheduler(): Scheduler =
+ if (this is SchedulerCoroutineDispatcher) {
+ scheduler
+ } else {
+ DispatcherScheduler(this)
+ }
+
+private class DispatcherScheduler(@JvmField val dispatcher: CoroutineDispatcher) : Scheduler() {
+
+ private val schedulerJob = SupervisorJob()
+
+ /**
+ * The scope for everything happening in this [DispatcherScheduler].
+ *
+ * Running tasks, too, get launched under this scope, because [shutdown] should cancel the running tasks as well.
+ */
+ private val scope = CoroutineScope(schedulerJob + dispatcher)
+
+ /**
+ * The counter of created workers, for their pretty-printing.
+ */
+ private val workerCounter = atomic(1L)
+
+ override fun scheduleDirect(block: Runnable, delay: Long, unit: TimeUnit): Disposable =
+ scope.scheduleTask(block, unit.toMillis(delay)) { task ->
+ Runnable { scope.launch { task() } }
+ }
+
+ override fun createWorker(): Worker = DispatcherWorker(workerCounter.getAndIncrement(), dispatcher, schedulerJob)
+
+ override fun shutdown() {
+ schedulerJob.cancel()
+ }
+
+ private class DispatcherWorker(
+ private val counter: Long,
+ private val dispatcher: CoroutineDispatcher,
+ parentJob: Job
+ ) : Worker() {
+
+ private val workerJob = SupervisorJob(parentJob)
+ private val workerScope = CoroutineScope(workerJob + dispatcher)
+ private val blockChannel = Channel<suspend () -> Unit>(Channel.UNLIMITED)
+
+ init {
+ workerScope.launch {
+ blockChannel.consumeEach {
+ it()
+ }
+ }
+ }
+
+ override fun schedule(block: Runnable, delay: Long, unit: TimeUnit): Disposable =
+ workerScope.scheduleTask(block, unit.toMillis(delay)) { task ->
+ Runnable { blockChannel.trySend(task) }
+ }
+
+ override fun isDisposed(): Boolean = !workerScope.isActive
+
+ override fun dispose() {
+ blockChannel.close()
+ workerJob.cancel()
+ }
+
+ override fun toString(): String = "$dispatcher (worker $counter, ${if (isDisposed) "disposed" else "active"})"
+ }
+
+ override fun toString(): String = dispatcher.toString()
+}
+
+private typealias Task = suspend () -> Unit
+
+/**
+ * Schedule [block] so that an adapted version of it, wrapped in [adaptForScheduling], executes after [delayMillis]
+ * milliseconds.
+ */
+private fun CoroutineScope.scheduleTask(
+ block: Runnable,
+ delayMillis: Long,
+ adaptForScheduling: (Task) -> Runnable
+): Disposable {
+ val ctx = coroutineContext
+ var handle: DisposableHandle? = null
+ val disposable = Disposable.fromRunnable {
+ // null if delay <= 0
+ handle?.dispose()
+ }
+ val decoratedBlock = RxJavaPlugins.onSchedule(block)
+ suspend fun task() {
+ if (disposable.isDisposed) return
+ try {
+ runInterruptible {
+ decoratedBlock.run()
+ }
+ } catch (e: Throwable) {
+ handleUndeliverableException(e, ctx)
+ }
+ }
+
+ val toSchedule = adaptForScheduling(::task)
+ if (!isActive) return Disposable.disposed()
+ if (delayMillis <= 0) {
+ toSchedule.run()
+ } else {
+ @Suppress("INVISIBLE_MEMBER")
+ ctx.delay.invokeOnTimeout(delayMillis, toSchedule, ctx).let { handle = it }
+ }
+ return disposable
+}
/**
* Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler].
@@ -45,8 +173,10 @@ public class SchedulerCoroutineDispatcher(
/** @suppress */
override fun toString(): String = scheduler.toString()
+
/** @suppress */
override fun equals(other: Any?): Boolean = other is SchedulerCoroutineDispatcher && other.scheduler === scheduler
+
/** @suppress */
override fun hashCode(): Int = System.identityHashCode(scheduler)
}
diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt
index b4adf7af..d7c799db 100644
--- a/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt
+++ b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt
@@ -6,6 +6,7 @@ package kotlinx.coroutines.rx3
import io.reactivex.rxjava3.core.*
import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
import org.junit.Test
import java.io.*
import kotlin.test.*
@@ -34,7 +35,7 @@ class ObservableMultiTest : TestBase() {
// concurrent emitters (many coroutines)
val jobs = List(n) {
// launch
- launch {
+ launch(Dispatchers.Default) {
val i = it
send(i)
}
@@ -48,6 +49,29 @@ class ObservableMultiTest : TestBase() {
}
@Test
+ fun testConcurrentStressOnSend() {
+ val n = 10_000 * stressTestMultiplier
+ val observable = rxObservable<Int> {
+ newCoroutineContext(coroutineContext)
+ // concurrent emitters (many coroutines)
+ val jobs = List(n) {
+ // launch
+ launch(Dispatchers.Default) {
+ val i = it
+ select<Unit> {
+ onSend(i) {}
+ }
+ }
+ }
+ jobs.forEach { it.join() }
+ }
+ checkSingleValue(observable.toList()) { list ->
+ assertEquals(n, list.size)
+ assertEquals((0 until n).toList(), list.sorted())
+ }
+ }
+
+ @Test
fun testIteratorResendUnconfined() {
val n = 10_000 * stressTestMultiplier
val observable = rxObservable(Dispatchers.Unconfined) {
diff --git a/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt b/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt
new file mode 100644
index 00000000..5abb511d
--- /dev/null
+++ b/reactive/kotlinx-coroutines-rx3/test/SchedulerStressTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.rx3
+
+import kotlinx.coroutines.*
+import org.junit.*
+import java.util.concurrent.*
+
+class SchedulerStressTest : TestBase() {
+ @Before
+ fun setup() {
+ ignoreLostThreads("RxCachedThreadScheduler-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-")
+ }
+
+ /**
+ * Test that we don't get an OOM if we schedule many jobs at once.
+ * It's expected that if you don't dispose you'd see an OOM error.
+ */
+ @Test
+ fun testSchedulerDisposed(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableDisposed(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerDisposed(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableDisposed(worker::schedule)
+ }
+
+ private suspend fun testRunnableDisposed(block: RxSchedulerBlockNoDelay) {
+ val n = 2000 * stressTestMultiplier
+ repeat(n) {
+ val a = ByteArray(1000000) //1MB
+ val disposable = block(Runnable {
+ keepMe(a)
+ expectUnreached()
+ })
+ disposable.dispose()
+ yield() // allow the scheduled task to observe that it was disposed
+ }
+ }
+
+ /**
+ * Test function that holds a reference. Used for testing OOM situations
+ */
+ private fun keepMe(a: ByteArray) {
+ Thread.sleep(a.size / (a.size + 1) + 10L)
+ }
+
+ /**
+ * Test that we don't get an OOM if we schedule many delayed jobs at once. It's expected that if you don't dispose that you'd
+ * see a OOM error.
+ */
+ @Test
+ fun testSchedulerDisposedDuringDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableDisposedDuringDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerDisposedDuringDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableDisposedDuringDelay(worker::schedule)
+ }
+
+ private fun testRunnableDisposedDuringDelay(block: RxSchedulerBlockWithDelay) {
+ val n = 2000 * stressTestMultiplier
+ repeat(n) {
+ val a = ByteArray(1000000) //1MB
+ val delayMillis: Long = 10
+ val disposable = block(Runnable {
+ keepMe(a)
+ expectUnreached()
+ }, delayMillis, TimeUnit.MILLISECONDS)
+ disposable.dispose()
+ }
+ }
+}
diff --git a/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt
index 9e95c213..c966cdfd 100644
--- a/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt
+++ b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt
@@ -4,10 +4,18 @@
package kotlinx.coroutines.rx3
-import io.reactivex.rxjava3.schedulers.Schedulers
+import io.reactivex.rxjava3.core.*
+import io.reactivex.rxjava3.disposables.*
+import io.reactivex.rxjava3.plugins.*
+import io.reactivex.rxjava3.schedulers.*
import kotlinx.coroutines.*
-import org.junit.Before
+import kotlinx.coroutines.sync.*
+import org.junit.*
import org.junit.Test
+import java.lang.Runnable
+import java.util.concurrent.*
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.*
import kotlin.test.*
class SchedulerTest : TestBase() {
@@ -17,7 +25,7 @@ class SchedulerTest : TestBase() {
}
@Test
- fun testIoScheduler(): Unit = runBlocking {
+ fun testIoScheduler(): Unit = runTest {
expect(1)
val mainThread = Thread.currentThread()
withContext(Schedulers.io().asCoroutineDispatcher()) {
@@ -31,4 +39,458 @@ class SchedulerTest : TestBase() {
}
finish(4)
}
+
+ /** Tests [toString] implementations of [CoroutineDispatcher.asScheduler] and its [Scheduler.Worker]. */
+ @Test
+ fun testSchedulerToString() {
+ val name = "Dispatchers.Default"
+ val scheduler = Dispatchers.Default.asScheduler()
+ assertContains(scheduler.toString(), name)
+ val worker = scheduler.createWorker()
+ val activeWorkerName = worker.toString()
+ assertContains(worker.toString(), name)
+ worker.dispose()
+ val disposedWorkerName = worker.toString()
+ assertNotEquals(activeWorkerName, disposedWorkerName)
+ }
+
+ private fun runSchedulerTest(nThreads: Int = 1, action: (Scheduler) -> Unit) {
+ val future = CompletableFuture<Unit>()
+ try {
+ newFixedThreadPoolContext(nThreads, "test").use { dispatcher ->
+ RxJavaPlugins.setErrorHandler {
+ if (!future.completeExceptionally(it)) {
+ handleUndeliverableException(it, dispatcher)
+ }
+ }
+ action(dispatcher.asScheduler())
+ }
+ } finally {
+ RxJavaPlugins.setErrorHandler(null)
+ }
+ future.complete(Unit)
+ future.getNow(Unit) // rethrow any encountered errors
+ }
+
+ private fun ensureSeparateThread(schedule: (Runnable, Long, TimeUnit) -> Unit, scheduleNoDelay: (Runnable) -> Unit) {
+ val mainThread = Thread.currentThread()
+ val cdl1 = CountDownLatch(1)
+ val cdl2 = CountDownLatch(1)
+ expect(1)
+ val thread = AtomicReference<Thread?>(null)
+ fun checkThread() {
+ val current = Thread.currentThread()
+ thread.getAndSet(current)?.let { assertEquals(it, current) }
+ }
+ schedule({
+ assertNotSame(mainThread, Thread.currentThread())
+ checkThread()
+ cdl2.countDown()
+ }, 300, TimeUnit.MILLISECONDS)
+ scheduleNoDelay {
+ expect(2)
+ checkThread()
+ assertNotSame(mainThread, Thread.currentThread())
+ cdl1.countDown()
+ }
+ cdl1.await()
+ cdl2.await()
+ finish(3)
+ }
+
+ /**
+ * Tests [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler] on a single-threaded dispatcher.
+ */
+ @Test
+ fun testSingleThreadedDispatcherDirect(): Unit = runSchedulerTest(1) {
+ ensureSeparateThread(it::scheduleDirect, it::scheduleDirect)
+ }
+
+ /**
+ * Tests [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler] running its tasks on the correct thread.
+ */
+ @Test
+ fun testSingleThreadedWorker(): Unit = runSchedulerTest(1) {
+ val worker = it.createWorker()
+ ensureSeparateThread(worker::schedule, worker::schedule)
+ }
+
+ private fun checkCancelling(schedule: (Runnable, Long, TimeUnit) -> Disposable) {
+ // cancel the task before it has a chance to run.
+ val handle1 = schedule({
+ throw IllegalStateException("should have been successfully cancelled")
+ }, 10_000, TimeUnit.MILLISECONDS)
+ handle1.dispose()
+ // cancel the task after it started running.
+ val cdl1 = CountDownLatch(1)
+ val cdl2 = CountDownLatch(1)
+ val handle2 = schedule({
+ cdl1.countDown()
+ cdl2.await()
+ if (Thread.interrupted())
+ throw IllegalStateException("cancelling the task should not interrupt the thread")
+ }, 100, TimeUnit.MILLISECONDS)
+ cdl1.await()
+ handle2.dispose()
+ cdl2.countDown()
+ }
+
+ /**
+ * Test cancelling [Scheduler.scheduleDirect] for [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testCancellingDirect(): Unit = runSchedulerTest {
+ checkCancelling(it::scheduleDirect)
+ }
+
+ /**
+ * Test cancelling [Scheduler.Worker.schedule] for [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testCancellingWorker(): Unit = runSchedulerTest {
+ val worker = it.createWorker()
+ checkCancelling(worker::schedule)
+ }
+
+ /**
+ * Test shutting down [CoroutineDispatcher.asScheduler].
+ */
+ @Test
+ fun testShuttingDown() {
+ val n = 5
+ runSchedulerTest(nThreads = n) { scheduler ->
+ val cdl1 = CountDownLatch(n)
+ val cdl2 = CountDownLatch(1)
+ val cdl3 = CountDownLatch(n)
+ repeat(n) {
+ scheduler.scheduleDirect {
+ cdl1.countDown()
+ try {
+ cdl2.await()
+ } catch (e: InterruptedException) {
+ // this is the expected outcome
+ cdl3.countDown()
+ }
+ }
+ }
+ cdl1.await()
+ scheduler.shutdown()
+ if (!cdl3.await(1, TimeUnit.SECONDS)) {
+ cdl2.countDown()
+ error("the tasks were not cancelled when the scheduler was shut down")
+ }
+ }
+ }
+
+ /** Tests that there are no uncaught exceptions if [Disposable.dispose] on a worker happens when tasks are present. */
+ @Test
+ fun testDisposingWorker() = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ yield() // so that the worker starts waiting on the channel
+ assertFalse(worker.isDisposed)
+ worker.dispose()
+ assertTrue(worker.isDisposed)
+ }
+
+ /** Tests trying to use a [Scheduler.Worker]/[Scheduler] after [Scheduler.Worker.dispose]/[Scheduler.shutdown]. */
+ @Test
+ fun testSchedulingAfterDisposing() = runSchedulerTest {
+ expect(1)
+ val worker = it.createWorker()
+ // use CDL to ensure that the worker has properly initialized
+ val cdl1 = CountDownLatch(1)
+ setScheduler(2, 3)
+ val disposable1 = worker.schedule {
+ cdl1.countDown()
+ }
+ cdl1.await()
+ expect(4)
+ assertFalse(disposable1.isDisposed)
+ setScheduler(6, -1)
+ // check that the worker automatically disposes of the tasks after being disposed
+ assertFalse(worker.isDisposed)
+ worker.dispose()
+ assertTrue(worker.isDisposed)
+ expect(5)
+ val disposable2 = worker.schedule {
+ expectUnreached()
+ }
+ assertTrue(disposable2.isDisposed)
+ setScheduler(7, 8)
+ // ensure that the scheduler still works
+ val cdl2 = CountDownLatch(1)
+ val disposable3 = it.scheduleDirect {
+ cdl2.countDown()
+ }
+ cdl2.await()
+ expect(9)
+ assertFalse(disposable3.isDisposed)
+ // check that the scheduler automatically disposes of the tasks after being shut down
+ it.shutdown()
+ setScheduler(10, -1)
+ val disposable4 = it.scheduleDirect {
+ expectUnreached()
+ }
+ assertTrue(disposable4.isDisposed)
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(11)
+ }
+
+ @Test
+ fun testSchedulerWithNoDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithNoDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithNoDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithNoDelay(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableWithNoDelay(block: RxSchedulerBlockNoDelay) {
+ expect(1)
+ suspendCancellableCoroutine<Unit> {
+ block(Runnable {
+ expect(2)
+ it.resume(Unit)
+ })
+ }
+ yield()
+ finish(3)
+ }
+
+ @Test
+ fun testSchedulerWithDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect, 300)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule, 300)
+ }
+
+ @Test
+ fun testSchedulerWithZeroDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerWithZeroDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableWithDelay(block: RxSchedulerBlockWithDelay, delayMillis: Long = 0) {
+ expect(1)
+ suspendCancellableCoroutine<Unit> {
+ block({
+ expect(2)
+ it.resume(Unit)
+ }, delayMillis, TimeUnit.MILLISECONDS)
+ }
+ finish(3)
+ }
+
+ @Test
+ fun testAsSchedulerWithNegativeDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler::scheduleDirect, -1)
+ }
+
+ @Test
+ fun testAsSchedulerWorkerWithNegativeDelay(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableWithDelay(scheduler.createWorker()::schedule, -1)
+ }
+
+ @Test
+ fun testSchedulerImmediateDispose(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableImmediateDispose(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerImmediateDispose(): Unit = runTest {
+ val scheduler = (currentDispatcher() as CoroutineDispatcher).asScheduler()
+ testRunnableImmediateDispose(scheduler.createWorker()::schedule)
+ }
+
+ private fun testRunnableImmediateDispose(block: RxSchedulerBlockNoDelay) {
+ val disposable = block {
+ expectUnreached()
+ }
+ disposable.dispose()
+ }
+
+ @Test
+ fun testConvertDispatcherToOriginalScheduler(): Unit = runTest {
+ val originalScheduler = Schedulers.io()
+ val dispatcher = originalScheduler.asCoroutineDispatcher()
+ val scheduler = dispatcher.asScheduler()
+ assertSame(originalScheduler, scheduler)
+ }
+
+ @Test
+ fun testConvertSchedulerToOriginalDispatcher(): Unit = runTest {
+ val originalDispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = originalDispatcher.asScheduler()
+ val dispatcher = scheduler.asCoroutineDispatcher()
+ assertSame(originalDispatcher, dispatcher)
+ }
+
+ @Test
+ fun testSchedulerExpectRxPluginsCall(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCall(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerExpectRxPluginsCall(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCall(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableExpectRxPluginsCall(block: RxSchedulerBlockNoDelay) {
+ expect(1)
+ setScheduler(2, 4)
+ suspendCancellableCoroutine<Unit> {
+ block(Runnable {
+ expect(5)
+ it.resume(Unit)
+ })
+ expect(3)
+ }
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(6)
+ }
+
+ @Test
+ fun testSchedulerExpectRxPluginsCallWithDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ testRunnableExpectRxPluginsCallDelay(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerExpectRxPluginsCallWithDelay(): Unit = runTest {
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ val worker = scheduler.createWorker()
+ testRunnableExpectRxPluginsCallDelay(worker::schedule)
+ }
+
+ private suspend fun testRunnableExpectRxPluginsCallDelay(block: RxSchedulerBlockWithDelay) {
+ expect(1)
+ setScheduler(2, 4)
+ suspendCancellableCoroutine<Unit> {
+ block({
+ expect(5)
+ it.resume(Unit)
+ }, 10, TimeUnit.MILLISECONDS)
+ expect(3)
+ }
+ RxJavaPlugins.setScheduleHandler(null)
+ finish(6)
+ }
+
+ private fun setScheduler(expectedCountOnSchedule: Int, expectCountOnRun: Int) {
+ RxJavaPlugins.setScheduleHandler {
+ expect(expectedCountOnSchedule)
+ Runnable {
+ expect(expectCountOnRun)
+ it.run()
+ }
+ }
+ }
+
+ /**
+ * Tests that [Scheduler.Worker] runs all work sequentially.
+ */
+ @Test
+ fun testWorkerSequentialOrdering() = runTest {
+ expect(1)
+ val scheduler = Dispatchers.Default.asScheduler()
+ val worker = scheduler.createWorker()
+ val iterations = 100
+ for (i in 0..iterations) {
+ worker.schedule {
+ expect(2 + i)
+ }
+ }
+ suspendCoroutine<Unit> {
+ worker.schedule {
+ it.resume(Unit)
+ }
+ }
+ finish((iterations + 2) + 1)
+ }
+
+ /**
+ * Test that ensures that delays are actually respected (tasks scheduled sooner in the future run before tasks scheduled later,
+ * even when the later task is submitted before the earlier one)
+ */
+ @Test
+ fun testSchedulerRespectsDelays(): Unit = runTest {
+ val scheduler = Dispatchers.Default.asScheduler()
+ testRunnableRespectsDelays(scheduler::scheduleDirect)
+ }
+
+ @Test
+ fun testSchedulerWorkerRespectsDelays(): Unit = runTest {
+ val scheduler = Dispatchers.Default.asScheduler()
+ testRunnableRespectsDelays(scheduler.createWorker()::schedule)
+ }
+
+ private suspend fun testRunnableRespectsDelays(block: RxSchedulerBlockWithDelay) {
+ expect(1)
+ val semaphore = Semaphore(2, 2)
+ block({
+ expect(3)
+ semaphore.release()
+ }, 100, TimeUnit.MILLISECONDS)
+ block({
+ expect(2)
+ semaphore.release()
+ }, 1, TimeUnit.MILLISECONDS)
+ semaphore.acquire()
+ semaphore.acquire()
+ finish(4)
+ }
+
+ /**
+ * Tests that cancelling a runnable in one worker doesn't affect work in another scheduler.
+ *
+ * This is part of expected behavior documented.
+ */
+ @Test
+ fun testMultipleWorkerCancellation(): Unit = runTest {
+ expect(1)
+ val dispatcher = currentDispatcher() as CoroutineDispatcher
+ val scheduler = dispatcher.asScheduler()
+ suspendCancellableCoroutine<Unit> {
+ val workerOne = scheduler.createWorker()
+ workerOne.schedule({
+ expect(3)
+ it.resume(Unit)
+ }, 50, TimeUnit.MILLISECONDS)
+ val workerTwo = scheduler.createWorker()
+ workerTwo.schedule({
+ expectUnreached()
+ }, 1000, TimeUnit.MILLISECONDS)
+ workerTwo.dispose()
+ expect(2)
+ }
+ finish(4)
+ }
}
+
+typealias RxSchedulerBlockNoDelay = (Runnable) -> Disposable
+typealias RxSchedulerBlockWithDelay = (Runnable, Long, TimeUnit) -> Disposable \ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 44effa7c..f0a76489 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -8,7 +8,7 @@ pluginManagement {
// JMH
id "net.ltgt.apt" version "0.21"
- id "me.champeau.gradle.jmh" version "0.5.2"
+ id "me.champeau.gradle.jmh" version "0.5.3"
}
repositories {
@@ -18,7 +18,6 @@ pluginManagement {
}
rootProject.name = 'kotlinx.coroutines'
-enableFeaturePreview('GRADLE_METADATA')
def module(String path) {
int i = path.lastIndexOf('/')
@@ -59,5 +58,3 @@ module('ui/kotlinx-coroutines-swing')
if (!build_snapshot_train) {
module('js/example-frontend-js')
}
-
-module('integration-testing')
diff --git a/site/deploy.sh b/site/deploy.sh
deleted file mode 100755
index a04e4925..00000000
--- a/site/deploy.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env bash
-
-#
-# Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-#
-
-# Abort on first error
-set -e
-
-# Directories
-SITE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-ROOT_DIR="$SITE_DIR/.."
-BUILD_DIR="$ROOT_DIR/build"
-DIST_DIR="$BUILD_DIR/dokka/htmlMultiModule"
-PAGES_DIR="$BUILD_DIR/pages"
-
-# Init options
-GRADLE_OPT=
-PUSH_OPT=
-
-# Set dry run if needed
-if [ "$2" == "push" ] ; then
- echo "--- Doing LIVE site deployment, so do clean build"
- GRADLE_OPT=clean
-else
- echo "--- Doing dry-run. To commit do 'deploy.sh <version> push'"
- PUSH_OPT=--dry-run
-fi
-
-# Makes sure that site is built
-"$ROOT_DIR/gradlew" $GRADLE_OPT dokkaHtmlMultiModule
-
-# Cleanup dist directory (and ignore errors)
-rm -rf "$PAGES_DIR" || true
-
-# Prune worktrees to avoid errors from previous attempts
-git --work-tree "$ROOT_DIR" worktree prune
-
-# Create git worktree for gh-pages branch
-git --work-tree "$ROOT_DIR" worktree add -B gh-pages "$PAGES_DIR" origin/gh-pages
-
-# Now work in newly created workspace
-cd "$PAGES_DIR"
-
-# Fixup all the old documentation files
-# Remove non-.html files
-git rm `find . -type f -not -name '*.html' -not -name '.git'` > /dev/null
-
-# Remove all the old documentation
-git rm -r * > /dev/null
-
-# Copy new documentation from dist
-cp -r "$DIST_DIR"/* "$PAGES_DIR"
-
-# Add it all to git
-git add *
-
-# Commit docs for the new version
-if [ -z "$1" ] ; then
- echo "No argument with version supplied -- skipping commit"
-else
- git commit -m "Version $1 docs"
- git push $PUSH_OPT origin gh-pages:gh-pages
-fi
diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md
index 71b2d69c..4ee898e2 100644
--- a/ui/coroutines-guide-ui.md
+++ b/ui/coroutines-guide-ui.md
@@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { .
`app/build.gradle` file:
```groovy
-implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
+implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
```
You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your
@@ -608,30 +608,30 @@ After delay
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
-[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
-[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
-[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
-[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
-[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
-[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[CoroutineStart.UNDISPATCHED]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-u-n-d-i-s-p-a-t-c-h-e-d/index.html
+[launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
+[delay]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
+[Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Job.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html
+[CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[MainScope()]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
+[withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
+[Dispatchers.Default]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+[CoroutineStart]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
+[async]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
+[CoroutineStart.UNDISPATCHED]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-u-n-d-i-s-p-a-t-c-h-e-d/index.html
<!--- INDEX kotlinx.coroutines.channels -->
-[actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
-[SendChannel.trySend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
-[SendChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
-[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
-[Channel.Factory.CONFLATED]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/-factory/-c-o-n-f-l-a-t-e-d.html
+[actor]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
+[SendChannel.trySend]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/try-send.html
+[SendChannel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html
+[Channel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
+[Channel.Factory.CONFLATED]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/-factory/-c-o-n-f-l-a-t-e-d.html
<!--- MODULE kotlinx-coroutines-javafx -->
<!--- INDEX kotlinx.coroutines.javafx -->
-[kotlinx.coroutines.Dispatchers.JavaFx]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-javafx/kotlinx.coroutines.javafx/-java-fx.html
+[kotlinx.coroutines.Dispatchers.JavaFx]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-javafx/kotlinx.coroutines.javafx/-java-fx.html
<!--- MODULE kotlinx-coroutines-android -->
<!--- INDEX kotlinx.coroutines.android -->
diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts
index 2fd0b814..625ce728 100644
--- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts
+++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts
@@ -2,10 +2,17 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+project.configureAar()
+
dependencies {
- kotlinCompilerPluginClasspathMain(project(":kotlinx-coroutines-core"))
+ configureAarUnpacking()
+
testImplementation("com.google.android:android:${version("android")}")
testImplementation("org.robolectric:robolectric:${version("robolectric")}")
+ // Required by robolectric
+ testImplementation("androidx.test:core:1.2.0")
+ testImplementation("androidx.test:monitor:1.2.0")
+
testImplementation(project(":kotlinx-coroutines-test"))
testImplementation(project(":kotlinx-coroutines-android"))
}
diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt
index bcc12d54..676ee431 100644
--- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt
+++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt
@@ -26,6 +26,7 @@ class InitMainDispatcherBeforeRobolectricTestRunner(testClass: Class<*>) : Robol
@Config(manifest = Config.NONE, sdk = [28])
@RunWith(InitMainDispatcherBeforeRobolectricTestRunner::class)
+@LooperMode(LooperMode.Mode.LEGACY)
class CustomizedRobolectricTest : TestBase() {
@Test
fun testComponent() {
@@ -52,4 +53,4 @@ class CustomizedRobolectricTest : TestBase() {
mainLooper.unPause()
assertTrue(component.launchCompleted)
}
-} \ No newline at end of file
+}
diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt
index eab6fc17..99744f89 100644
--- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt
+++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstRobolectricTest.kt
@@ -15,6 +15,7 @@ import kotlin.test.*
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
+@LooperMode(LooperMode.Mode.LEGACY)
open class FirstRobolectricTest {
@Test
fun testComponent() {
diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts
index 9cec1dc9..7618c529 100644
--- a/ui/kotlinx-coroutines-android/build.gradle.kts
+++ b/ui/kotlinx-coroutines-android/build.gradle.kts
@@ -2,22 +2,32 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+import kotlinx.kover.api.*
+
configurations {
create("r8")
}
repositories {
mavenCentral()
- jcenter() // https://youtrack.jetbrains.com/issue/IDEA-261387
}
+
+project.configureAar()
+
dependencies {
+ configureAarUnpacking()
+
compileOnly("com.google.android:android:${version("android")}")
compileOnly("androidx.annotation:annotation:${version("androidx_annotation")}")
testImplementation("com.google.android:android:${version("android")}")
testImplementation("org.robolectric:robolectric:${version("robolectric")}")
- testImplementation("org.smali:baksmali:${version("baksmali")}")
+ // Required by robolectric
+ testImplementation("androidx.test:core:1.2.0")
+ testImplementation("androidx.test:monitor:1.2.0")
+
+ testImplementation("org.smali:baksmali:${version("baksmali")}")
"r8"("com.android.tools.build:builder:7.1.0-alpha01")
}
@@ -103,3 +113,8 @@ open class RunR8 : JavaExec() {
}
}
+tasks.withType<Test> {
+ extensions.configure<KoverTaskExtension> {
+ excludes = excludes + listOf("com.android.*", "android.*") // Exclude robolectric-generated classes
+ }
+}
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro
index c7cd15fe..ef42483f 100644
--- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/coroutines.pro
@@ -1,5 +1,6 @@
# When editing this file, update the following files as well:
-# - META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro
+# - META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro
# - META-INF/proguard/coroutines.pro
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
+-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
index 0d04990a..cf317c41 100644
--- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
@@ -9,8 +9,6 @@
boolean ANDROID_DETECTED return true;
}
--keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
-
# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
boolean SUPPORT_MISSING return false;
@@ -21,4 +19,4 @@
boolean getASSERTIONS_ENABLED() return false;
boolean getDEBUG() return false;
boolean getRECOVER_STACK_TRACES() return false;
-} \ No newline at end of file
+}
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro
deleted file mode 100644
index 549d0e85..00000000
--- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro
+++ /dev/null
@@ -1,9 +0,0 @@
-# When editing this file, update the following files as well:
-# - META-INF/com.android.tools/proguard/coroutines.pro
-# - META-INF/proguard/coroutines.pro
-
--keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
-
--assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
- boolean ANDROID_DETECTED return true;
-} \ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro
new file mode 100644
index 00000000..1aa2b114
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro
@@ -0,0 +1,10 @@
+# When editing this file, update the following files as well for AGP 3.6.0+:
+# - META-INF/com.android.tools/proguard/coroutines.pro
+# - META-INF/proguard/coroutines.pro
+
+# After R8 3.0.0 (or probably sometime before that), R8 learned how to optimize
+# classes mentioned in META-INF/services files, and explicitly -keeping them
+# disables these optimizations.
+# https://github.com/Kotlin/kotlinx.coroutines/issues/3111
+-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
+-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro
index 6c918d49..087f1ce8 100644
--- a/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro
@@ -2,6 +2,7 @@
# When editing this file, update the following files as well for AGP 3.6.0+:
# - META-INF/com.android.tools/proguard/coroutines.pro
-# - META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro
+# - META-INF/com.android.tools/r8-upto-3.0.0/coroutines.pro
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
+-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
diff --git a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
index 7d06752c..0bc603ea 100644
--- a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
+++ b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
@@ -5,12 +5,10 @@
package kotlinx.coroutines.android
import android.os.*
-import androidx.annotation.*
import kotlinx.coroutines.*
import java.lang.reflect.*
import kotlin.coroutines.*
-@Keep
internal class AndroidExceptionPreHandler :
AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler
{
@@ -34,20 +32,21 @@ internal class AndroidExceptionPreHandler :
override fun handleException(context: CoroutineContext, exception: Throwable) {
/*
- * If we are on old SDK, then use Android's `Thread.getUncaughtExceptionPreHandler()` that ensures that
- * an exception is logged before crashing the application.
+ * Android Oreo introduced private API for a global pre-handler for uncaught exceptions, to ensure that the
+ * exceptions are logged even if the default uncaught exception handler is replaced by the app. The pre-handler
+ * is invoked from the Thread's private dispatchUncaughtException() method, so our manual invocation of the
+ * Thread's uncaught exception handler bypasses the pre-handler in Android Oreo, and uncaught coroutine
+ * exceptions are not logged. This issue was addressed in Android Pie, which added a check in the default
+ * uncaught exception handler to invoke the pre-handler if it was not invoked already (see
+ * https://android-review.googlesource.com/c/platform/frameworks/base/+/654578/). So the issue is present only
+ * in Android Oreo.
*
- * Since Android Pie default uncaught exception handler always ensures that exception is logged without interfering with
- * pre-handler, so reflection hack is no longer needed.
- *
- * See https://android-review.googlesource.com/c/platform/frameworks/base/+/654578/
+ * We're fixing this by manually invoking the pre-handler using reflection, if running on an Android Oreo SDK
+ * version (26 and 27).
*/
- val thread = Thread.currentThread()
- if (Build.VERSION.SDK_INT >= 28) {
- thread.uncaughtExceptionHandler.uncaughtException(thread, exception)
- } else {
+ if (Build.VERSION.SDK_INT in 26..27) {
(preHandler()?.invoke(null) as? Thread.UncaughtExceptionHandler)
- ?.uncaughtException(thread, exception)
+ ?.uncaughtException(Thread.currentThread(), exception)
}
}
}
diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
index ca8dd0d0..5e33169d 100644
--- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
+++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt
@@ -51,8 +51,10 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
internal class AndroidDispatcherFactory : MainDispatcherFactory {
- override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
- HandlerContext(Looper.getMainLooper().asHandler(async = true))
+ override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
+ val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
+ return HandlerContext(mainLooper.asHandler(async = true))
+ }
override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
@@ -188,7 +190,7 @@ public suspend fun awaitFrame(): Long {
postFrameCallback(choreographer, cont)
}
}
- // post into looper thread thread to figure it out
+ // post into looper thread to figure it out
return suspendCancellableCoroutine { cont ->
Dispatchers.Main.dispatch(EmptyCoroutineContext, Runnable {
updateChoreographerAndPostFrameCallback(cont)
diff --git a/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt
new file mode 100644
index 00000000..12207970
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/test/AndroidExceptionPreHandlerTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.android
+
+import kotlinx.coroutines.*
+import org.junit.Test
+import org.junit.runner.*
+import org.robolectric.*
+import org.robolectric.annotation.*
+import kotlin.test.*
+
+@RunWith(RobolectricTestRunner::class)
+@Config(manifest = Config.NONE, sdk = [27])
+@LooperMode(LooperMode.Mode.LEGACY)
+class AndroidExceptionPreHandlerTest : TestBase() {
+ @Test
+ fun testUnhandledException() = runTest {
+ val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
+ try {
+ Thread.setDefaultUncaughtExceptionHandler { _, e ->
+ expect(3)
+ assertIs<TestException>(e)
+ }
+ expect(1)
+ GlobalScope.launch(Dispatchers.Main) {
+ expect(2)
+ throw TestException()
+ }.join()
+ finish(4)
+ } finally {
+ Thread.setDefaultUncaughtExceptionHandler(previousHandler)
+ }
+ }
+}
diff --git a/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt b/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt
index a1f0a03d..a5b5ec95 100644
--- a/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt
+++ b/ui/kotlinx-coroutines-android/test/DisabledHandlerTest.kt
@@ -13,6 +13,7 @@ import org.robolectric.annotation.*
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
+@LooperMode(LooperMode.Mode.LEGACY)
class DisabledHandlerTest : TestBase() {
private var delegateToSuper = false
diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt
new file mode 100644
index 00000000..c2091f33
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherAsyncTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.android
+
+import android.os.*
+import kotlinx.coroutines.*
+import org.junit.Test
+import org.junit.runner.*
+import org.robolectric.*
+import org.robolectric.Shadows.*
+import org.robolectric.annotation.*
+import org.robolectric.shadows.*
+import org.robolectric.util.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+@RunWith(RobolectricTestRunner::class)
+@Config(manifest = Config.NONE, sdk = [28])
+@LooperMode(LooperMode.Mode.LEGACY)
+class HandlerDispatcherAsyncTest : TestBase() {
+
+ /**
+ * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a
+ * result we only test its behavior on the newest API level and assert that it uses async
+ * messages. We rely on the other tests to exercise the variance of the mechanism that the main
+ * dispatcher uses to ensure it has correct behavior on all API levels.
+ */
+ @Test
+ fun mainIsAsync() = runTest {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
+
+ val mainLooper = shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+
+ val job = launch(Dispatchers.Main) {
+ expect(2)
+ }
+
+ val message = mainMessageQueue.head
+ assertTrue(message.isAsynchronous)
+ job.join(mainLooper)
+ }
+
+ @Test
+ fun asyncMessagesApi14() = runTest {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14)
+
+ val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
+
+ val mainLooper = shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+
+ val job = launch(main) {
+ expect(2)
+ }
+
+ val message = mainMessageQueue.head
+ assertFalse(message.isAsynchronous)
+ job.join(mainLooper)
+ }
+
+ @Test
+ fun asyncMessagesApi16() = runTest {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16)
+
+ val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
+
+ val mainLooper = shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+
+ val job = launch(main) {
+ expect(2)
+ }
+
+ val message = mainMessageQueue.head
+ assertTrue(message.isAsynchronous)
+ job.join(mainLooper)
+ }
+
+ @Test
+ fun asyncMessagesApi28() = runTest {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
+
+ val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
+
+ val mainLooper = shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+
+ val job = launch(main) {
+ expect(2)
+ }
+
+ val message = mainMessageQueue.head
+ assertTrue(message.isAsynchronous)
+ job.join(mainLooper)
+ }
+
+ @Test
+ fun noAsyncMessagesIfNotRequested() = runTest {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
+
+ val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher()
+
+ val mainLooper = shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+
+ val job = launch(main) {
+ expect(2)
+ }
+
+ val message = mainMessageQueue.head
+ assertFalse(message.isAsynchronous)
+ job.join(mainLooper)
+ }
+
+ @Test
+ fun testToString() {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
+ val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName")
+ assertEquals("testName", main.toString())
+ assertEquals("testName.immediate", main.immediate.toString())
+ assertEquals("testName.immediate", main.immediate.immediate.toString())
+ }
+
+ private suspend fun Job.join(mainLooper: ShadowLooper) {
+ expect(1)
+ mainLooper.unPause()
+ join()
+ finish(3)
+ }
+
+ // TODO compile against API 23+ so this can be invoked without reflection.
+ private val Looper.queue: MessageQueue
+ get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue
+
+ // TODO compile against API 22+ so this can be invoked without reflection.
+ private val Message.isAsynchronous: Boolean
+ get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean
+}
diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
index 5128a74c..fe97ae8d 100644
--- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
+++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.android
@@ -9,156 +9,131 @@ import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.*
import org.robolectric.*
-import org.robolectric.Shadows.*
import org.robolectric.annotation.*
import org.robolectric.shadows.*
-import org.robolectric.util.*
+import java.util.concurrent.*
import kotlin.test.*
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
+@LooperMode(LooperMode.Mode.LEGACY)
class HandlerDispatcherTest : TestBase() {
-
- /**
- * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a
- * result we only test its behavior on the newest API level and assert that it uses async
- * messages. We rely on the other tests to exercise the variance of the mechanism that the main
- * dispatcher uses to ensure it has correct behavior on all API levels.
- */
@Test
- fun mainIsAsync() = runTest {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
-
- val mainLooper = shadowOf(Looper.getMainLooper())
- mainLooper.pause()
- val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
-
- val job = launch(Dispatchers.Main) {
+ fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) {
+ expect(1)
+ // launch in the immediate dispatcher
+ launch(Dispatchers.Main.immediate) {
expect(2)
+ yield()
+ expect(4)
}
-
- val message = mainMessageQueue.head
- assertTrue(message.isAsynchronous)
- job.join(mainLooper)
+ expect(3) // after yield
+ yield() // yield back
+ finish(5)
}
@Test
- fun asyncMessagesApi14() = runTest {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14)
-
- val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
-
- val mainLooper = shadowOf(Looper.getMainLooper())
- mainLooper.pause()
- val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
-
- val job = launch(main) {
- expect(2)
- }
-
- val message = mainMessageQueue.head
- assertFalse(message.isAsynchronous)
- job.join(mainLooper)
+ fun testMainDispatcherToString() {
+ assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
+ assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
}
@Test
- fun asyncMessagesApi16() = runTest {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16)
-
- val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
-
- val mainLooper = shadowOf(Looper.getMainLooper())
+ fun testDefaultDelayIsNotDelegatedToMain() = runTest {
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
mainLooper.pause()
- val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
+ assertFalse { mainLooper.scheduler.areAnyRunnable() }
- val job = launch(main) {
- expect(2)
+ val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) {
+ expect(1)
+ delay(Long.MAX_VALUE)
+ expectUnreached()
}
-
- val message = mainMessageQueue.head
- assertTrue(message.isAsynchronous)
- job.join(mainLooper)
+ expect(2)
+ assertEquals(0, mainLooper.scheduler.size())
+ job.cancelAndJoin()
+ finish(3)
}
@Test
- fun asyncMessagesApi28() = runTest {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
-
- val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
-
- val mainLooper = shadowOf(Looper.getMainLooper())
+ fun testWithTimeoutIsDelegatedToMain() = runTest {
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
mainLooper.pause()
- val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
-
- val job = launch(main) {
- expect(2)
+ assertFalse { mainLooper.scheduler.areAnyRunnable() }
+ val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
+ withTimeout(1) {
+ expect(1)
+ hang { expect(3) }
+ }
+ expectUnreached()
}
-
- val message = mainMessageQueue.head
- assertTrue(message.isAsynchronous)
- job.join(mainLooper)
+ expect(2)
+ assertEquals(1, mainLooper.scheduler.size())
+ // Schedule cancellation
+ mainLooper.runToEndOfTasks()
+ job.join()
+ finish(4)
}
@Test
- fun noAsyncMessagesIfNotRequested() = runTest {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
-
- val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher()
-
- val mainLooper = shadowOf(Looper.getMainLooper())
+ fun testDelayDelegatedToMain() = runTest {
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
mainLooper.pause()
- val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
-
- val job = launch(main) {
- expect(2)
+ val job = launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
+ expect(1)
+ delay(1)
+ expect(3)
}
-
- val message = mainMessageQueue.head
- assertFalse(message.isAsynchronous)
- job.join(mainLooper)
+ expect(2)
+ assertEquals(1, mainLooper.scheduler.size())
+ // Schedule cancellation
+ mainLooper.runToEndOfTasks()
+ job.join()
+ finish(4)
}
@Test
- fun testToString() {
- ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
- val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName")
- assertEquals("testName", main.toString())
- assertEquals("testName.immediate", main.immediate.toString())
- assertEquals("testName.immediate", main.immediate.immediate.toString())
- }
+ fun testAwaitFrame() = runTest {
+ doTestAwaitFrame()
- private suspend fun Job.join(mainLooper: ShadowLooper) {
- expect(1)
- mainLooper.unPause()
- join()
- finish(3)
- }
+ reset()
- // TODO compile against API 23+ so this can be invoked without reflection.
- private val Looper.queue: MessageQueue
- get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue
+ // Now the second test: we cannot test it separately because we're caching choreographer in HandlerDispatcher
+ doTestAwaitWithDetectedChoreographer()
+ }
- // TODO compile against API 22+ so this can be invoked without reflection.
- private val Message.isAsynchronous: Boolean
- get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean
+ private fun CoroutineScope.doTestAwaitFrame() {
+ ShadowChoreographer.setPostFrameCallbackDelay(100)
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
+ mainLooper.pause()
+ launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
+ expect(1)
+ awaitFrame()
+ expect(5)
+ }
+ expect(2)
+ // Run choreographer detection
+ mainLooper.runOneTask()
+ expect(3)
+ mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS)
+ expect(4)
+ mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS)
+ finish(6)
+ }
- @Test
- fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) {
- expect(1)
- // launch in the immediate dispatcher
- launch(Dispatchers.Main.immediate) {
- expect(2)
- yield()
+ private fun CoroutineScope.doTestAwaitWithDetectedChoreographer() {
+ ShadowChoreographer.setPostFrameCallbackDelay(100)
+ val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
+ launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
+ expect(1)
+ awaitFrame()
expect(4)
}
- expect(3) // after yield
- yield() // yield back
+ // Run choreographer detection
+ expect(2)
+ mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS)
+ expect(3)
+ mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS)
finish(5)
}
-
- @Test
- fun testMainDispatcherToString() {
- assertEquals("Dispatchers.Main", Dispatchers.Main.toString())
- assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString())
- }
}
diff --git a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt
index 5d60d641..47beb85b 100644
--- a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt
+++ b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt
@@ -11,7 +11,6 @@ import java.io.*
import java.util.stream.*
import kotlin.test.*
-@Ignore
class R8ServiceLoaderOptimizationTest : TestBase() {
private val r8Dex = File(System.getProperty("dexPath")!!).asDexFile()
private val r8DexNoOptim = File(System.getProperty("noOptimDexPath")!!).asDexFile()
diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts
index 9e30590c..f9f66249 100644
--- a/ui/kotlinx-coroutines-javafx/build.gradle.kts
+++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts
@@ -6,17 +6,20 @@ plugins {
id("org.openjfx.javafxplugin") version "0.0.9"
}
+configurations {
+ register("javafx")
+ named("compileOnly") {
+ extendsFrom(configurations["javafx"])
+ }
+ named("testImplementation") {
+ extendsFrom(configurations["javafx"])
+ }
+}
+
javafx {
version = version("javafx")
modules = listOf("javafx.controls")
- configuration = "compileOnly"
-}
-
-sourceSets {
- test.configure {
- compileClasspath += configurations.compileOnly
- runtimeClasspath += configurations.compileOnly
- }
+ configuration = "javafx"
}
val JDK_18: String? by lazy {
diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt
index 0a35cbf2..d158fb74 100644
--- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt
+++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt
@@ -10,7 +10,6 @@ import javafx.event.*
import javafx.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
-import kotlinx.coroutines.javafx.JavaFx.delay
import java.lang.UnsupportedOperationException
import java.lang.reflect.*
import java.util.concurrent.*
@@ -35,22 +34,18 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay {
/** @suppress */
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler {
+ val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) {
with(continuation) { resumeUndispatched(Unit) }
- })
+ }
continuation.invokeOnCancellation { timeline.stop() }
}
/** @suppress */
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler {
+ val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) {
block.run()
- })
- return object : DisposableHandle {
- override fun dispose() {
- timeline.stop()
- }
}
+ return DisposableHandle { timeline.stop() }
}
private fun schedule(time: Long, unit: TimeUnit, handler: EventHandler<ActionEvent>): Timeline =
diff --git a/ui/kotlinx-coroutines-swing/build.gradle.kts b/ui/kotlinx-coroutines-swing/build.gradle.kts
index b8ca906a..157ce401 100644
--- a/ui/kotlinx-coroutines-swing/build.gradle.kts
+++ b/ui/kotlinx-coroutines-swing/build.gradle.kts
@@ -3,5 +3,5 @@
*/
dependencies {
- testCompile(project(":kotlinx-coroutines-jdk8"))
+ testImplementation(project(":kotlinx-coroutines-jdk8"))
}
diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt
index d2d9b786..3b43483d 100644
--- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt
+++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt
@@ -74,6 +74,16 @@ private object ImmediateSwingDispatcher : SwingDispatcher() {
* Dispatches execution onto Swing event dispatching thread and provides native [delay] support.
*/
internal object Swing : SwingDispatcher() {
+
+ /* A workaround so that the dispatcher's initialization crashes with an exception if running in a headless
+ environment. This is needed so that this broken dispatcher is not used as the source of delays. */
+ init {
+ Timer(1) { }.apply {
+ isRepeats = false
+ start()
+ }
+ }
+
override val immediate: MainCoroutineDispatcher
get() = ImmediateSwingDispatcher