From 15b6345be0820f53dddc307aaf289f12ad8419bd Mon Sep 17 00:00:00 2001 From: sokolova Date: Mon, 28 Jan 2019 11:43:44 +0300 Subject: [PATCH 1/5] Custom ServiceLoader without jar checksum verification Fixes #878 --- build.gradle | 1 - gradle/wrapper/gradle-wrapper.properties | 3 +- kotlinx-coroutines-core/build.gradle | 2 + .../kotlinx.coroutines.CoroutineScope | 13 +++ .../jvm/src/internal/FastServiceLoader.kt | 87 +++++++++++++++++++ .../jvm/src/internal/MainDispatchers.kt | 9 +- .../jvm/test/internal/ServiceLoaderTest.kt | 23 +++++ .../services/kotlinx.coroutines.Delay | 1 + .../android-unit-tests/src/DelayImpl.kt | 10 +++ .../src/EmptyCoroutineScopeImpl.kt | 20 +++++ ....coroutines.internal.MainDispatcherFactory | 2 +- 11 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/resources/META-INF/services/kotlinx.coroutines.CoroutineScope create mode 100644 kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt create mode 100644 ui/kotlinx-coroutines-android/android-unit-tests/resources/META-INF/services/kotlinx.coroutines.Delay create mode 100644 ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt create mode 100644 ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt diff --git a/build.gradle b/build.gradle index 72ea1514fa..a9781d4647 100644 --- a/build.gradle +++ b/build.gradle @@ -133,7 +133,6 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "ben test.kotlin.srcDirs = ['test'] main.resources.srcDirs = ['resources'] test.resources.srcDirs = ['test-resources'] - } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 115e6ac0aa..603c4e1143 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Mar 15 12:06:46 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index e963466ceb..63f4ac27d4 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -46,6 +46,8 @@ kotlin.sourceSets { jvmTest.dependencies { api "com.devexperts.lincheck:lincheck:$lincheck_version" api "com.esotericsoftware:kryo:4.0.0" + + implementation project (":android-unit-tests") } } diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/services/kotlinx.coroutines.CoroutineScope b/kotlinx-coroutines-core/jvm/resources/META-INF/services/kotlinx.coroutines.CoroutineScope new file mode 100644 index 0000000000..2b6308a78d --- /dev/null +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/services/kotlinx.coroutines.CoroutineScope @@ -0,0 +1,13 @@ +kotlinx.coroutines.android.EmptyCoroutineScopeImpl1 +kotlinx.coroutines.android.EmptyCoroutineScopeImpl2 +# testing configuration file parsing # kotlinx.coroutines.service.loader.LocalEmptyCoroutineScope2 + +kotlinx.coroutines.android.EmptyCoroutineScopeImpl2 + +kotlinx.coroutines.android.EmptyCoroutineScopeImpl1 + + +kotlinx.coroutines.android.EmptyCoroutineScopeImpl1 + + +kotlinx.coroutines.android.EmptyCoroutineScopeImpl3#comment \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt new file mode 100644 index 0000000000..d8e917800d --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt @@ -0,0 +1,87 @@ +package kotlinx.coroutines.internal + +import java.util.* +import java.io.* +import java.net.* +import java.util.jar.* +import java.util.zip.* + +/** + * Name of the boolean property that enables using of [FastServiceLoader]. + */ +private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.verify.service.loader" + +/** + * A simplified version of [ServiceLoader]. + * FastServiceLoader locates and instantiates all service providers named in configuration + * files placed in the resource directory META-INF/services. + * + * The main difference between this class and classic service loader is in skipping + * verification JARs. A verification requires reading the whole JAR (and it causes problems and ANRs on Android devices) + * and prevents only trivial checksum issues. See #878. + * + * If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues. + */ + +internal object FastServiceLoader { + private const val PREFIX: String = "META-INF/services/" + + @JvmField + internal val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) + + internal fun load(service: Class, loader: ClassLoader): List { + if (!FAST_SERVICE_LOADER_ENABLED) { + return ServiceLoader.load(service, loader).toList() + } + return try { + loadProviders(service, loader) + } catch (e: Throwable) { + // Fallback to default service loader + ServiceLoader.load(service, loader).toList() + } + } + + internal fun loadProviders(service: Class, loader: ClassLoader): List { + val fullServiceName = PREFIX + service.name + val urls = loader.getResources(fullServiceName).toList() + val providers = mutableListOf() + urls.forEach { + val providerNames = parse(it) + providers.addAll(providerNames.map { getProviderInstance(it, loader, service) }) + } + require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" } + return providers + } + + private fun getProviderInstance(name: String, loader: ClassLoader, service: Class): S { + val clazz = Class.forName(name, false, loader) + require(service.isAssignableFrom(clazz)) { "Expected service of class $service, but found $clazz" } + return service.cast(clazz.getDeclaredConstructor().newInstance()) + } + + private fun parse(url: URL): List { + val string = url.toString() + return if (string.startsWith("jar")) { + val pathToJar = string.substringAfter("jar:file:").substringBefore('!') + val entry = string.substringAfter("!/") + (JarFile(pathToJar, false) as Closeable).use { file -> + BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)),"UTF-8")).use { r -> + parseFile(r) + } + } + } else emptyList() + } + + private fun parseFile(r: BufferedReader): List { + val names = mutableSetOf() + while (true) { + val line = r.readLine() ?: break + val serviceName = line.substringBefore("#").trim() + require(serviceName.all { it == '.' || Character.isJavaIdentifierPart(it) }) { "Illegal service provider class name: $serviceName" } + if (serviceName.isNotEmpty()) { + names.add(serviceName) + } + } + return names.toList() + } +} diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 951053d377..f2f0af7aa8 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -12,9 +12,8 @@ internal object MainDispatcherLoader { private fun loadMainDispatcher(): MainCoroutineDispatcher { return try { val factories = MainDispatcherFactory::class.java.let { clz -> - ServiceLoader.load(clz, clz.classLoader).toList() + FastServiceLoader.load(clz, clz.classLoader) } - factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories) ?: MissingMainCoroutineDispatcher(null) } catch (e: Throwable) { @@ -72,7 +71,7 @@ private class MissingMainCoroutineDispatcher( if (cause == null) { throw IllegalStateException( "Module with the Main dispatcher is missing. " + - "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'" + "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'" ) } else { val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "") @@ -92,6 +91,6 @@ public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory { get() = -1 override fun createDispatcher(allFactories: List): MainCoroutineDispatcher { - return MissingMainCoroutineDispatcher(null) + return MissingMainCoroutineDispatcher(null) } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt new file mode 100644 index 0000000000..92a167b072 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt @@ -0,0 +1,23 @@ +package kotlinx.coroutines.internal + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Delay +import kotlin.test.Test + +class ServiceLoaderTest { + @Test + fun testLoadingSameModuleService() { + val providers = Delay::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) } + assert(providers.size == 1 && providers[0].javaClass.name == "kotlinx.coroutines.android.DelayImpl") + } + + @Test + fun testCrossModuleService() { + val providers = CoroutineScope::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) } + assert(providers.size == 3) + val className = "kotlinx.coroutines.android.EmptyCoroutineScopeImpl" + for (i in 1 .. 3) { + assert(providers[i - 1].javaClass.name == "$className$i") + } + } +} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/resources/META-INF/services/kotlinx.coroutines.Delay b/ui/kotlinx-coroutines-android/android-unit-tests/resources/META-INF/services/kotlinx.coroutines.Delay new file mode 100644 index 0000000000..3535451166 --- /dev/null +++ b/ui/kotlinx-coroutines-android/android-unit-tests/resources/META-INF/services/kotlinx.coroutines.Delay @@ -0,0 +1 @@ +kotlinx.coroutines.android.DelayImpl \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt b/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt new file mode 100644 index 0000000000..51225a24bd --- /dev/null +++ b/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt @@ -0,0 +1,10 @@ +package kotlinx.coroutines.android + +import kotlinx.coroutines.* + +@InternalCoroutinesApi +class DelayImpl : Delay { + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + continuation.cancel() + } +} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt b/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt new file mode 100644 index 0000000000..9d4c0e554b --- /dev/null +++ b/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt @@ -0,0 +1,20 @@ +package kotlinx.coroutines.android + +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal class EmptyCoroutineScopeImpl1 : CoroutineScope { + override val coroutineContext: CoroutineContext + get() = EmptyCoroutineContext +} + +internal class EmptyCoroutineScopeImpl2 : CoroutineScope { + override val coroutineContext: CoroutineContext + get() = EmptyCoroutineContext +} + +internal class EmptyCoroutineScopeImpl3 : CoroutineScope { + override val coroutineContext: CoroutineContext + get() = EmptyCoroutineContext +} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/ui/kotlinx-coroutines-android/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory index d700530b21..387be938b4 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory +++ b/ui/kotlinx-coroutines-android/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory @@ -1 +1 @@ -kotlinx.coroutines.android.AndroidDispatcherFactory +kotlinx.coroutines.android.AndroidDispatcherFactory \ No newline at end of file From 5514edb1df5cc9f7c73a6d65abd1378254d4b285 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 22 Mar 2019 19:33:33 +0300 Subject: [PATCH 2/5] Load services both from regular jars and files, filter duplicates --- kotlinx-coroutines-core/build.gradle | 1 - .../jvm/src/internal/FastServiceLoader.kt | 40 ++++++++++--------- .../jvm/test/CommonPoolTest.kt | 1 - ...LoaderTest.kt => FastServiceLoaderTest.kt} | 9 +++-- .../android-unit-tests/src/DelayImpl.kt | 4 +- .../src/EmptyCoroutineScopeImpl.kt | 1 + 6 files changed, 29 insertions(+), 27 deletions(-) rename kotlinx-coroutines-core/jvm/test/internal/{ServiceLoaderTest.kt => FastServiceLoaderTest.kt} (73%) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 63f4ac27d4..a6db106ff5 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -46,7 +46,6 @@ kotlin.sourceSets { jvmTest.dependencies { api "com.devexperts.lincheck:lincheck:$lincheck_version" api "com.esotericsoftware:kryo:4.0.0" - implementation project (":android-unit-tests") } } diff --git a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt index d8e917800d..6cf1b4750b 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt @@ -1,15 +1,15 @@ package kotlinx.coroutines.internal -import java.util.* import java.io.* import java.net.* +import java.util.* import java.util.jar.* import java.util.zip.* /** * Name of the boolean property that enables using of [FastServiceLoader]. */ -private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.verify.service.loader" +private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.fast.service.loader" /** * A simplified version of [ServiceLoader]. @@ -22,12 +22,10 @@ private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.verify * * If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues. */ - internal object FastServiceLoader { private const val PREFIX: String = "META-INF/services/" - @JvmField - internal val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) + private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) internal fun load(service: Class, loader: ClassLoader): List { if (!FAST_SERVICE_LOADER_ENABLED) { @@ -41,16 +39,14 @@ internal object FastServiceLoader { } } + // Visible for tests internal fun loadProviders(service: Class, loader: ClassLoader): List { val fullServiceName = PREFIX + service.name - val urls = loader.getResources(fullServiceName).toList() - val providers = mutableListOf() - urls.forEach { - val providerNames = parse(it) - providers.addAll(providerNames.map { getProviderInstance(it, loader, service) }) - } + // Filter out situations when both JAR and regular files are in the classpath (e.g. IDEA) + val urls = loader.getResources(fullServiceName) + val providers = urls.toList().flatMap { parse(it) }.toSet() require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" } - return providers + return providers.map { getProviderInstance(it, loader, service) } } private fun getProviderInstance(name: String, loader: ClassLoader, service: Class): S { @@ -60,16 +56,22 @@ internal object FastServiceLoader { } private fun parse(url: URL): List { - val string = url.toString() - return if (string.startsWith("jar")) { - val pathToJar = string.substringAfter("jar:file:").substringBefore('!') - val entry = string.substringAfter("!/") + val path = url.toString() + // Fast-path for JARs + if (path.startsWith("jar")) { + val pathToJar = path.substringAfter("jar:file:").substringBefore('!') + val entry = path.substringAfter("!/") + // mind the verify = false flag! (JarFile(pathToJar, false) as Closeable).use { file -> - BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)),"UTF-8")).use { r -> - parseFile(r) + BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)), "UTF-8")).use { r -> + return parseFile(r) } } - } else emptyList() + } + // Regular path for everything elese + return BufferedReader(InputStreamReader(url.openStream())).use { reader -> + parseFile(reader) + } } private fun parseFile(r: BufferedReader): List { diff --git a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt b/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt index 147fe38a3b..9af8c28555 100644 --- a/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt +++ b/kotlinx-coroutines-core/jvm/test/CommonPoolTest.kt @@ -39,7 +39,6 @@ class CommonPoolTest { val fjp1: ExecutorService = createFJP(1, fjpCtor, dwtfCtor) ?: return assertTrue(CommonPool.isGoodCommonPool(fjpClass, fjp1)) fjp1.shutdown() - println("CommonPool.isGoodCommonPool test passed") } private fun createFJP( diff --git a/kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt b/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt similarity index 73% rename from kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt rename to kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt index 92a167b072..67793d1861 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt @@ -2,19 +2,20 @@ package kotlinx.coroutines.internal import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Delay -import kotlin.test.Test +import kotlin.test.* -class ServiceLoaderTest { +class FastServiceLoaderTest { @Test fun testLoadingSameModuleService() { val providers = Delay::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) } - assert(providers.size == 1 && providers[0].javaClass.name == "kotlinx.coroutines.android.DelayImpl") + assertEquals(1, providers.size) + assertEquals("kotlinx.coroutines.android.DelayImpl", providers[0].javaClass.name) } @Test fun testCrossModuleService() { val providers = CoroutineScope::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) } - assert(providers.size == 3) + assertEquals(3, providers.size) val className = "kotlinx.coroutines.android.EmptyCoroutineScopeImpl" for (i in 1 .. 3) { assert(providers[i - 1].javaClass.name == "$className$i") diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt b/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt index 51225a24bd..fc63198c7b 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt @@ -2,9 +2,9 @@ package kotlinx.coroutines.android import kotlinx.coroutines.* -@InternalCoroutinesApi +// Class for testing service loader class DelayImpl : Delay { override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { continuation.cancel() } -} \ No newline at end of file +} diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt b/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt index 9d4c0e554b..06779bf5a7 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/src/EmptyCoroutineScopeImpl.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +// Classes for testing service loader internal class EmptyCoroutineScopeImpl1 : CoroutineScope { override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext From 3b8267b1d073e3515a561862106fa6c83fd78822 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 27 Mar 2019 12:42:44 +0300 Subject: [PATCH 3/5] Update docs index --- site/docs/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/docs/index.md b/site/docs/index.md index 59f33697de..3e6bb93494 100644 --- a/site/docs/index.md +++ b/site/docs/index.md @@ -13,12 +13,14 @@ Library support for Kotlin coroutines. This reference is a companion to | Name | Description | | ---------------------------------------------------------- | ------------------------------------------------ | | [kotlinx-coroutines-core](kotlinx-coroutines-core) | Core primitives to work with coroutines | +| [kotlinx-coroutines-debug](kotlinx-coroutines-debug) | Debugging utilities for coroutines | +| [kotlinx-coroutines-test](kotlinx-coroutines-test) | Test primitives for coroutines, `Main` dispatcher injection | | [kotlinx-coroutines-reactive](kotlinx-coroutines-reactive) | Utilities for [Reactive Streams](https://www.reactive-streams.org) | | [kotlinx-coroutines-reactor](kotlinx-coroutines-reactor) | Utilities for [Reactor](https://projectreactor.io) | | [kotlinx-coroutines-rx2](kotlinx-coroutines-rx2) | Utilities for [RxJava 2.x](https://github.com/ReactiveX/RxJava) | -| [kotlinx-coroutines-android](kotlinx-coroutines-android) | `UI` context for Android applications | -| [kotlinx-coroutines-javafx](kotlinx-coroutines-javafx) | `JavaFx` context for JavaFX UI applications | -| [kotlinx-coroutines-swing](kotlinx-coroutines-swing) | `Swing` context for Swing UI applications | +| [kotlinx-coroutines-android](kotlinx-coroutines-android) | `Main` dispatcher for Android applications | +| [kotlinx-coroutines-javafx](kotlinx-coroutines-javafx) | `JavaFx` dispatcher for JavaFX UI applications | +| [kotlinx-coroutines-swing](kotlinx-coroutines-swing) | `Swing` dispatcher for Swing UI applications | | [kotlinx-coroutines-jdk8](kotlinx-coroutines-jdk8) | Integration with JDK8 `CompletableFuture` (Android API level 24) | | [kotlinx-coroutines-guava](kotlinx-coroutines-guava) | Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained) | | [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j) | Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html) | From d57bfa2e3d8a622d6a988a9f0a6bcf2b825c5e17 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 4 Apr 2019 14:25:13 +0300 Subject: [PATCH 4/5] Flow preview implementation Preview feature for #254 --- .../kotlinx-coroutines-core.txt | 99 +++++++ .../kotlinx-coroutines-reactive.txt | 10 + docs/compatibility.md | 16 ++ gradle/experimental.gradle | 3 +- .../common/src/Annotations.kt | 16 ++ .../common/src/flow/Builders.kt | 221 +++++++++++++++ .../common/src/flow/ChannelFlow.kt | 62 +++++ .../common/src/flow/Flow.kt | 99 +++++++ .../common/src/flow/FlowCollector.kt | 23 ++ .../common/src/flow/Migration.kt | 81 ++++++ .../common/src/flow/internal/NullSurrogate.kt | 7 + .../common/src/flow/internal/SafeCollector.kt | 26 ++ .../common/src/flow/operators/Context.kt | 121 +++++++++ .../common/src/flow/operators/Delay.kt | 34 +++ .../common/src/flow/operators/Distinct.kt | 36 +++ .../common/src/flow/operators/Errors.kt | 94 +++++++ .../common/src/flow/operators/Limit.kt | 85 ++++++ .../common/src/flow/operators/Merge.kt | 115 ++++++++ .../common/src/flow/operators/Transform.kt | 96 +++++++ .../common/src/flow/terminal/Collect.kt | 34 +++ .../common/src/flow/terminal/Collection.kt | 34 +++ .../common/src/flow/terminal/Count.kt | 39 +++ .../common/src/flow/terminal/Reduce.kt | 83 ++++++ .../common/test/NamedDispatchers.kt | 63 +++++ .../common/test/TestBase.common.kt | 41 ++- .../common/test/flow/FlowInvariantsTest.kt | 54 ++++ .../test/flow/channels/ChannelFlowTest.kt | 142 ++++++++++ .../test/flow/operators/ConcatenateMapTest.kt | 137 ++++++++++ .../test/flow/operators/ConcatenateTest.kt | 59 ++++ .../operators/DistinctUntilChangedTest.kt | 57 ++++ .../common/test/flow/operators/DropTest.kt | 54 ++++ .../test/flow/operators/DropWhileTest.kt | 51 ++++ .../common/test/flow/operators/FilterTest.kt | 83 ++++++ .../test/flow/operators/FilterTrivialTest.kt | 43 +++ .../common/test/flow/operators/FlatMapTest.kt | 188 +++++++++++++ .../operators/FlowContextOptimizationsTest.kt | 104 +++++++ .../test/flow/operators/FlowContextTest.kt | 145 ++++++++++ .../common/test/flow/operators/FlowOnTest.kt | 254 ++++++++++++++++++ .../test/flow/operators/FlowWithTest.kt | 201 ++++++++++++++ .../test/flow/operators/MapNotNullTest.kt | 52 ++++ .../common/test/flow/operators/MapTest.kt | 50 ++++ .../common/test/flow/operators/OnEachTest.kt | 52 ++++ .../common/test/flow/operators/OnErrorTest.kt | 113 ++++++++ .../common/test/flow/operators/TakeTest.kt | 66 +++++ .../test/flow/operators/TakeWhileTest.kt | 68 +++++ .../common/test/flow/terminal/CountTest.kt | 47 ++++ .../common/test/flow/terminal/FoldTest.kt | 55 ++++ .../common/test/flow/terminal/LaunchFlow.kt | 98 +++++++ .../common/test/flow/terminal/ReduceTest.kt | 75 ++++++ .../common/test/flow/terminal/SingleTest.kt | 66 +++++ .../test/flow/terminal/ToCollectionTest.kt | 31 +++ .../jvm/test/flow/FlowFromChannelTest.kt | 109 ++++++++ .../jvm/test/guide/example-compose-05.kt | 2 +- .../src/flow/FlowAsPublisher.kt | 115 ++++++++ .../src/flow/PublisherAsFlow.kt | 72 +++++ .../test/flow/IterableFlowTckTest.kt | 132 +++++++++ .../test/flow/PublisherAsFlowTest.kt | 45 ++++ .../test/flow/RangePublisherTest.kt | 48 ++++ .../UnboundedIntegerIncrementPublisherTest.kt | 57 ++++ 59 files changed, 4460 insertions(+), 3 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/flow/Builders.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/Flow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/FlowCollector.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/Migration.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Context.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Delay.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Errors.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Limit.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Merge.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Transform.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/terminal/Count.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt create mode 100644 kotlinx-coroutines-core/common/test/NamedDispatchers.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/ConcatenateMapTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/ConcatenateTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/DropWhileTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FilterTrivialTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FlatMapTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/OnErrorTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/TakeWhileTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/CountTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/LaunchFlow.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/ToCollectionTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/FlowFromChannelTest.kt create mode 100644 reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt create mode 100644 reactive/kotlinx-coroutines-reactive/src/flow/PublisherAsFlow.kt create mode 100644 reactive/kotlinx-coroutines-reactive/test/flow/IterableFlowTckTest.kt create mode 100644 reactive/kotlinx-coroutines-reactive/test/flow/PublisherAsFlowTest.kt create mode 100644 reactive/kotlinx-coroutines-reactive/test/flow/RangePublisherTest.kt create mode 100644 reactive/kotlinx-coroutines-reactive/test/flow/UnboundedIntegerIncrementPublisherTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 54aa9cfc68..f35ef54158 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -295,6 +295,9 @@ public final class kotlinx/coroutines/ExecutorsKt { public abstract interface annotation class kotlinx/coroutines/ExperimentalCoroutinesApi : java/lang/annotation/Annotation { } +public abstract interface annotation class kotlinx/coroutines/FlowPreview : java/lang/annotation/Annotation { +} + public final class kotlinx/coroutines/GlobalScope : kotlinx/coroutines/CoroutineScope { public static final field INSTANCE Lkotlinx/coroutines/GlobalScope; public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; @@ -768,6 +771,102 @@ public final class kotlinx/coroutines/channels/TickerMode : java/lang/Enum { public static fun values ()[Lkotlinx/coroutines/channels/TickerMode; } +public abstract interface class kotlinx/coroutines/flow/Flow { + public abstract fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class kotlinx/coroutines/flow/FlowCollector { + public abstract fun emit (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class kotlinx/coroutines/flow/FlowKt { + public static final fun asFlow (Ljava/lang/Iterable;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Ljava/util/Iterator;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlin/jvm/functions/Function0;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlin/ranges/IntRange;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlin/ranges/LongRange;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlin/sequences/Sequence;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Lkotlinx/coroutines/channels/BroadcastChannel;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow ([I)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow ([J)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; + public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; + public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun concatenate (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun concatenate (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun delayEach (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; + public static final fun delayFlow (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; + public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun distinctUntilChangedBy (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun drop (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final fun dropWhile (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun emptyFlow ()Lkotlinx/coroutines/flow/Flow; + public static final fun filter (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun filterNot (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun filterNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun flatMap (Lkotlinx/coroutines/flow/Flow;IILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flatMap$default (Lkotlinx/coroutines/flow/Flow;IILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun flow (Lkotlin/jvm/functions/Function2;)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;I)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flowOn$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)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 map (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun merge (Ljava/lang/Iterable;II)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun merge$default (Ljava/lang/Iterable;IIILjava/lang/Object;)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 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 onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun onErrorReturn$default (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;I)Lkotlinx/coroutines/channels/ReceiveChannel; + public static synthetic fun produceIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)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 final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun take (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final fun takeWhile (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun toCollection (Lkotlinx/coroutines/flow/Flow;Ljava/util/Collection;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun toList (Lkotlinx/coroutines/flow/Flow;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun toList$default (Lkotlinx/coroutines/flow/Flow;Ljava/util/List;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun toSet (Lkotlinx/coroutines/flow/Flow;Ljava/util/Set;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun toSet$default (Lkotlinx/coroutines/flow/Flow;Ljava/util/Set;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun transform (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun unsafeFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; +} + +public final class kotlinx/coroutines/flow/MigrationKt { + public static final fun BehaviourSubject ()Ljava/lang/Object; + public static final fun PublishSubject ()Ljava/lang/Object; + public static final fun ReplaySubject ()Ljava/lang/Object; + public static final fun concat (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun concatMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun observeOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)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 publishOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V + public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)V + public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static final fun subscribeOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun withContext (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V +} + +public final class kotlinx/coroutines/flow/internal/SafeCollector : kotlinx/coroutines/flow/FlowCollector { + public fun (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/ContinuationInterceptor;)V + public fun emit (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public class kotlinx/coroutines/scheduling/ExperimentalCoroutineDispatcher : kotlinx/coroutines/ExecutorCoroutineDispatcher { public synthetic fun (II)V public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt index 6c62079890..26821f638d 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt @@ -23,3 +23,13 @@ public final class kotlinx/coroutines/reactive/PublishKt { public static synthetic fun publish$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; } +public final class kotlinx/coroutines/reactive/flow/FlowAsPublisherKt { + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher; +} + +public final class kotlinx/coroutines/reactive/flow/PublisherAsFlowKt { + public static final fun from (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow; + public static final fun from (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun from$default (Lorg/reactivestreams/Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + diff --git a/docs/compatibility.md b/docs/compatibility.md index 3dfe73a40c..e56fc1be15 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -9,6 +9,7 @@ * [Compatibility](#compatibility) * [Public API types](#public-api-types) * [Experimental API](#experimental-api) + * [Flow preview API](#flow-preview-api) * [Obsolete API](#obsolete-api) * [Internal API](#internal-api) * [Stable API](#stable-api) @@ -42,6 +43,18 @@ It may lead to undesired consequences when end users of your library update thei has slightly different semantics. * You want to build core infrastructure of the application around experimental API. +### Flow preview API +All [Flow]-related API is marked with [@FlowPreview][FlowPreview] annotation. +This annotation indicates that Flow API is in preview status. +We provide no compatibility guarantees between releases for preview features, including binary, source and semantics compatibility. + +When using preview API may be dangerous: +* You are writing a library/framework and want to use [Flow] API in a stable release or in a stable API. +* You want to use [Flow] in the core infrastructure of your application. +* You want to use [Flow] as "write-and-forget" solution and cannot afford additional maintenance cost when + it comes to `kotlinx.coroutines` updates. + + ### Obsolete API Obsolete API is marked with [@ObsoleteCoroutinesApi][ObsoleteCoroutinesApi] annotation. Obsolete API is similar to experimental, but already known to have serious design flaws and its potential replacement, @@ -108,8 +121,11 @@ For the Maven project, a warning can be disabled by passing a compiler flag in y + +[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html [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 diff --git a/gradle/experimental.gradle b/gradle/experimental.gradle index 45d4daa5bf..51bda6c595 100644 --- a/gradle/experimental.gradle +++ b/gradle/experimental.gradle @@ -9,4 +9,5 @@ ext.experimentalAnnotations = [ "kotlin.ExperimentalMultiplatform", "kotlinx.coroutines.ExperimentalCoroutinesApi", "kotlinx.coroutines.ObsoleteCoroutinesApi", - "kotlinx.coroutines.InternalCoroutinesApi"] + "kotlinx.coroutines.InternalCoroutinesApi", + "kotlinx.coroutines.FlowPreview"] diff --git a/kotlinx-coroutines-core/common/src/Annotations.kt b/kotlinx-coroutines-core/common/src/Annotations.kt index 2b81146d29..9f52a1a57b 100644 --- a/kotlinx-coroutines-core/common/src/Annotations.kt +++ b/kotlinx-coroutines-core/common/src/Annotations.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines +import kotlinx.coroutines.flow.Flow + /** * Marks declarations that are still **experimental** in coroutines API, which means that the design of the * corresponding declarations has open issues which may (or may not) lead to their changes in the future. @@ -15,6 +17,20 @@ package kotlinx.coroutines @Experimental(level = Experimental.Level.WARNING) public annotation class ExperimentalCoroutinesApi +/** + * Marks all [Flow] declarations as a feature preview to indicate that [Flow] is still experimental and has a 'preview' status. + * + * Flow preview has **no** backward compatibility guarantees, including both binary and source compatibility. + * Its API and semantics can and will be changed in next releases. + * + * Feature preview can be used to evaluate its real-world strengths and weaknesses, gather and provide feedback. + * According to the feedback, [Flow] will be refined on its road to stabilization and promotion to stable API. + */ +@MustBeDocumented +@Retention(value = AnnotationRetention.BINARY) +@Experimental(level = Experimental.Level.WARNING) +public annotation class FlowPreview + /** * Marks declarations that are **obsolete** in coroutines API, which means that the design of the corresponding * declarations has serious known flaws and they will be redesigned in the future. diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt new file mode 100644 index 0000000000..c495d6e671 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* + +/** + * Creates flow from the given suspendable [block]. + * + * Example of usage: + * ``` + * fun fibonacci(): Flow = flow { + * emit(1L) + * var f1 = 1L + * var f2 = 1L + * repeat(100) { + * var tmp = f1 + * f1 = f2 + * f2 += tmp + * emit(f1) + * } + * } + * ``` + * + * `emit` should happen strictly in the dispatchers of the [block] in order to preserve flow purity. + * For example, the following code will produce [IllegalStateException]: + * ``` + * flow { + * emit(1) // Ok + * withContext(Dispatcher.IO) { + * emit(2) // Will fail with ISE + * } + * } + * ``` + * If you want to switch the context where this flow is executed use [flowOn] operator. + */ +@FlowPreview +public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow { + return object : Flow { + override suspend fun collect(collector: FlowCollector) { + SafeCollector(collector, coroutineContext[ContinuationInterceptor]).block() + } + } +} + +/** + * Analogue of [flow] builder that does not check a context of flow execution. + * Used in our own operators where we trust the context of the invocation. + */ +@FlowPreview +@PublishedApi +internal fun unsafeFlow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow { + return object : Flow { + override suspend fun collect(collector: FlowCollector) { + collector.block() + } + } +} + +/** + * Creates flow that produces single value from the given functional type. + */ +@FlowPreview +public fun (() -> T).asFlow(): Flow = unsafeFlow { + emit(invoke()) +} + +/** + * Creates flow that produces single value from the given functional type. + */ +@FlowPreview +public fun (suspend () -> T).asFlow(): Flow = unsafeFlow { + emit(invoke()) +} + +/** + * Creates flow that produces values from the given iterable. + */ +@FlowPreview +public fun Iterable.asFlow(): Flow = unsafeFlow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given iterable. + */ +@FlowPreview +public fun Iterator.asFlow(): Flow = unsafeFlow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given sequence. + */ +@FlowPreview +public fun Sequence.asFlow(): Flow = unsafeFlow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given array of elements. + */ +@FlowPreview +public fun flowOf(vararg elements: T): Flow = unsafeFlow { + for (element in elements) { + emit(element) + } +} + +/** + * Returns an empty flow. + */ +@FlowPreview +public fun emptyFlow(): Flow = EmptyFlow + +private object EmptyFlow : Flow { + override suspend fun collect(collector: FlowCollector) = Unit +} + +/** + * Creates flow that produces values from the given array. + */ +@FlowPreview +public fun Array.asFlow(): Flow = flow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given array. + */ +@FlowPreview +public fun IntArray.asFlow(): Flow = flow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given array. + */ +@FlowPreview +public fun LongArray.asFlow(): Flow = flow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given range. + */ +@FlowPreview +public fun IntRange.asFlow(): Flow = flow { + forEach { value -> + emit(value) + } +} + +/** + * Creates flow that produces values from the given range. + */ +@FlowPreview +public fun LongRange.asFlow(): Flow = flow { + forEach { value -> + emit(value) + } +} + +/** + * Creates an instance of the cold [Flow] from a supplied [SendChannel]. + * + * To control backpressure, [bufferSize] is used and matches directly the `capacity` parameter of [Channel] factory. + * The provided channel can later be used by any external service to communicate with flow and its buffer determines + * backpressure buffer size or its behaviour (e.g. in case when [Channel.CONFLATED] was used). + * + * Example of usage: + * ``` + * fun flowFrom(api: CallbackBasedApi): Flow = flowViaChannel { channel -> + * val adapter = FlowSinkAdapter(channel) // implementation of callback interface + * api.register(adapter) + * channel.invokeOnClose { + * api.unregister(adapter) + * } + * } + * ``` + */ +@FlowPreview +public fun flowViaChannel( + bufferSize: Int = 16, + @BuilderInference block: suspend (SendChannel) -> Unit +): Flow { + require(bufferSize >= 0) { "Buffer size should be positive, but was $bufferSize" } + return flow { + coroutineScope { + val channel = Channel(bufferSize) + launch { + block(channel) + } + + channel.consumeEach { value -> + emit(value) + } + } + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt new file mode 100644 index 0000000000..9034aec4c8 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.jvm.* + +/** + * Represents the given broadcast channel as a hot flow. + * Every flow collector will trigger a new broadcast channel subscription. + * + * ### Cancellation semantics + * 1) Flow consumer is cancelled when the original channel is cancelled. + * 2) Flow consumer completes normally when the original channel completes (~is closed) normally. + * 3) If the flow consumer fails with an exception, subscription is cancelled. + */ +@FlowPreview +public fun BroadcastChannel.asFlow(): Flow = flow { + val subscription = openSubscription() + subscription.consumeEach { value -> + emit(value) + } +} + +/** + * Creates a [broadcast] coroutine that collects the given flow. + * + * This transformation is **stateful**, it launches a [broadcast] coroutine + * that collects the given flow and thus resulting channel should be properly closed or cancelled. + */ +@FlowPreview +public fun Flow.broadcastIn( + scope: CoroutineScope, capacity: Int = 1, + start: CoroutineStart = CoroutineStart.LAZY +): BroadcastChannel = scope.broadcast(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. + */ +@FlowPreview +public fun Flow.produceIn( + scope: CoroutineScope, + capacity: Int = 1 +): ReceiveChannel = scope.produce(capacity = capacity) { + // TODO it would be nice to have it with start = lazy as well + collect { value -> + send(value) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt new file mode 100644 index 0000000000..dacdba31d1 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* + +/** + * A cold asynchronous stream of the data, that emits from zero to N (where N can be unbounded) + * values and completes normally or with an exception. + * + * All transformations on the flow, such as [map] and [filter] do not trigger flow collection or execution, only + * terminal operators (e.g. [single])do trigger it. + * + * Flow can be collected in a suspending manner, without actual blocking using [collect] extension that will complete normally or exceptionally: + * ``` + * try { + * flow.collect { value -> + * println("Received $value") + * } + * } catch (e: Exception) { + * println("Flow has thrown an exception: $e") + * } + * ``` + * Additionally, the library provides a rich set of terminal operators in `kotlinx.coroutines.flow.terminal`, such as + * [single], [reduce] and others. + * + * Flow also can be collected asynchronously using launch-like coroutine: + * ``` + * flow.launchIn(uiScope) { + * onEach { value -> + * println("Received $value") + * } + * + * catch { + * println("Flow has failed") + * } + * + * finally { + * println("Doing cleanup) + * } + * } + * ``` + * + * Flow does not carry information whether it is a cold stream (that can be collected multiple times and + * triggers its evaluation every time collection is executed) or hot one, but conventionally flow represents a cold stream. + * Transitions between hot and cold streams are support via channels and corresponding API: [flowViaChannel], [broadcastIn], [produceIn]. + * + * Flow is a **pure** concept: it encapsulates its own execution context and never propagates it to the downstream, thus making + * reasoning about execution context of particular transformations or terminal operations trivial. + * There are two ways of changing the flow's context: [flowOn][Flow.flowOn] and [flowWith][Flow.flowWith]. + * The former changes the upstream context ("everything above the flowOn operator") while the latter + * changes the context of the flow within [flowWith] body. For additional information refer to these operators documentation. + * + * This reasoning can be demonstrated in the practice: + * ``` + * val flow = flowOf(1, 2, 3) + * .map { it + 1 } // Will be executed in ctx_1 + * .flowOn(ctx_1) // Changes upstream context: flowOf and map + * + * // Now we have flow that is pure: it is executed somewhere but this information is encapsulated in the flow itself + * + * val filtered = flow + * .filter { it == 3 } // Pure operator without a context + * + * withContext(Dispatchers.Main) { + * // All not encapsulated operators will be executed in Main: filter and single + * val result = filtered.single() + * myUi.text = result + * } + * ``` + * + * Flow is [Reactive Streams](http://www.reactive-streams.org/) compliant, you can safely interop it with reactive streams using [Flow.asPublisher] and [Publisher.asFlow] from + * kotlinx-coroutines-reactive module. + */ +@FlowPreview +public interface Flow { + + /** + * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. + * + * A valid implementation of this method has the following constraints: + * 1) It should not change the coroutine context (e.g. with `withContext(Dispatchers.IO)`) when emitting values. + * The emission should happen in the context of the [collect] call. + * + * Only coroutine builders that inherit the context are allowed, for example the following code is fine: + * ``` + * coroutineScope { // Context is inherited + * launch { // Dispatcher is not overridden, fine as well + * collector.emit(someValue) + * } + * } + * ``` + * + * 2) It should serialize calls to [emit][FlowCollector.emit] as [FlowCollector] implementations are not thread safe by default. + */ + public suspend fun collect(collector: FlowCollector) +} diff --git a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt new file mode 100644 index 0000000000..b7f231fbd5 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* + +/** + * [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 usually should not be implemented directly, but rather used as a receiver in [flow] builder when implementing a custom operator. + * Implementations of this interface are not thread-safe. + */ +@FlowPreview +public interface FlowCollector { + + /** + * Collects the value emitted by the upstream. + */ + public suspend fun emit(value: T) +} diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt new file mode 100644 index 0000000000..5ab2dcf738 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused", "DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER") +package kotlinx.coroutines.flow + +import kotlin.coroutines.* + +/** + * These deprecations are added to improve user experience when they will start to + * search for their favourite operators and/or patterns that are missing or renamed in Flow. + */ + +/** @suppress **/ +@Deprecated(message = "Use flowWith or flowOn instead", level = DeprecationLevel.ERROR) +public fun Flow.subscribeOn(context: CoroutineContext): Flow = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "Use flowWith or flowOn instead", level = DeprecationLevel.ERROR) +public fun Flow.observeOn(context: CoroutineContext): Flow = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "Use flowWith or flowOn instead", level = DeprecationLevel.ERROR) +public fun Flow.publishOn(context: CoroutineContext): Flow = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "Use BroadcastChannel.asFlow()", level = DeprecationLevel.ERROR) +public fun BehaviourSubject(): Any = error("Should not be called") + +/** @suppress **/ +@Deprecated( + message = "ReplaySubject is not supported. The closest analogue is buffered broadcast channel", + level = DeprecationLevel.ERROR) +public fun ReplaySubject(): Any = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "PublishSubject is not supported", level = DeprecationLevel.ERROR) +public fun PublishSubject(): Any = error("Should not be called") + +/** @suppress **/ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue is named onErrorCollect", + replaceWith = ReplaceWith("onErrorCollect(fallback)") +) +public fun Flow.onErrorResume(fallback: Flow): Flow = error("Should not be called") + + +/** @suppress **/ +@Suppress("UNUSED_PARAMETER", "UNUSED", "DeprecatedCallableAddReplaceWith") +@Deprecated(message = "withContext in flow body is deprecated, use flowOn instead", level = DeprecationLevel.ERROR) +public fun FlowCollector.withContext(context: CoroutineContext, block: suspend () -> R): Unit = error("Should not be called") + + +/** @suppress **/ +@Deprecated(message = "Use launch + collect instead", level = DeprecationLevel.ERROR) +public fun Flow.subscribe(): Unit = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "Use launch + collect instead", level = DeprecationLevel.ERROR) +public fun Flow.subscribe(onEach: (T) -> Unit): Unit = error("Should not be called") + +/** @suppress **/ +@Deprecated(message = "Use launch + collect instead", level = DeprecationLevel.ERROR) +public fun Flow.subscribe(onEach: (T) -> Unit, onError: (Throwable) -> Unit): Unit = error("Should not be called") + +/** @suppress **/ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue is named concatenate", + replaceWith = ReplaceWith("concatenate()") +) +public fun Flow.concat(): Flow = error("Should not be called") + +/** @suppress **/ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue is named concatenate", + replaceWith = ReplaceWith("concatenate(mapper)") +) +public fun Flow.concatMap(mapper: (T) -> Flow): Flow = error("Should not be called") diff --git a/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt b/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt new file mode 100644 index 0000000000..dc728f98d7 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +internal object NullSurrogate diff --git a/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.kt new file mode 100644 index 0000000000..d723dcdec9 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/SafeCollector.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +@PublishedApi +internal class SafeCollector( + private val collector: FlowCollector, + private val interceptor: ContinuationInterceptor? +) : FlowCollector, SynchronizedObject() { + + override suspend fun emit(value: T) { + if (interceptor != coroutineContext[ContinuationInterceptor]) { + error( + "Flow invariant is violated: flow was collected in $interceptor, but emission happened in ${coroutineContext[ContinuationInterceptor]}. " + + "Please refer to 'flow' documentation or use 'flowOn' instead" + ) + } + collector.emit(value) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt new file mode 100644 index 0000000000..13e7db7cef --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlinx.coroutines.flow.unsafeFlow as flow + +/** + * The operator that changes the context where this flow is executed to the given [flowContext]. + * This operator is composable and affects only precedent operators that do not have its own context. + * This operator is pure: [flowContext] **does not** leak into the downstream flow. + * + * For example: + * ``` + * val singleValue = intFlow // will be executed on IO if context wasn't specified before + * .map { ... } // Will be executed in IO + * .flowOn(Dispatchers.IO) + * .filter { ... } // Will be executed in Default + * .flowOn(Dispatchers.Default) + * .single() // Will be executed in the context of the caller + * ``` + * For more explanation of purity concept please refer to [Flow] documentation. + * + * This operator uses a channel of the specific [bufferSize] in order to switch between contexts, + * but it is not guaranteed that the channel will be created, implementation is free to optimize it away in case of fusing. + * + * @throws [IllegalStateException] if provided context contains [Job] instance. + */ +@FlowPreview +public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 16): Flow { + check(flowContext, bufferSize) + return flow { + // Fast-path, context is not changed, so we can just fallback to plain collect + val currentContext = coroutineContext.minusKey(Job) // Jobs are ignored + if (flowContext == currentContext) { + collect { value -> emit(value) } + return@flow + } + + coroutineScope { + val channel = produce(flowContext, capacity = bufferSize) { + collect { value -> + send(value) + } + } + + // TODO semantics doesn't play well here and we pay for that with additional object + (channel as Job).invokeOnCompletion { if (it is CancellationException && it.cause == null) cancel() } + for (element in channel) { + emit(element) + } + + val producer = channel as Job + if (producer.isCancelled) { + producer.join() + throw producer.getCancellationException() + } + } + } +} + +/** + * The operator that changes the context where all transformations applied to the given flow within a [builder] are executed. + * This operator is pure and does not affect the context of the precedent 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 purity concept please refer to [Flow] documentation. + * + * This operator uses channel of the specific [bufferSize] in order to switch between contexts, + * but it is not guaranteed that channel will be created, implementation is free to optimize it away in case of fusing. + * + * @throws [IllegalStateException] if provided context contains [Job] instance. + */ +@FlowPreview +public fun Flow.flowWith( + flowContext: CoroutineContext, + bufferSize: Int = 16, + builder: Flow.() -> Flow +): Flow { + check(flowContext, bufferSize) + val source = this + return flow { + /** + * Here we should subtract 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 cancellations if multiple flowWith are used. + */ + val originalContext = coroutineContext.minusKey(Job) + val prepared = source.flowOn(originalContext, bufferSize) + builder(prepared).flowOn(flowContext, bufferSize).collect { value -> + emit(value) + } + } +} + +private fun check(flowContext: CoroutineContext, bufferSize: Int) { + require(flowContext[Job] == null) { + "Flow context cannot contain job in it. Had $flowContext" + } + + require(bufferSize >= 0) { + "Buffer size should be positive, but was $bufferSize" + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt new file mode 100644 index 0000000000..bc407f015f --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.unsafeFlow as flow +import kotlin.jvm.* + +/** + * Delays the emission of values from this flow for the given [timeMillis]. + */ +@FlowPreview +public fun Flow.delayFlow(timeMillis: Long): Flow = flow { + delay(timeMillis) + collect { value -> + emit(value) + } +} + +/** + * Delays each element emitted by the given flow for the given [timeMillis]. + */ +@FlowPreview +public fun Flow.delayEach(timeMillis: Long): Flow = flow { + collect { value -> + delay(timeMillis) + emit(value) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt new file mode 100644 index 0000000000..e3a794553a --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* +import kotlin.jvm.* +import kotlinx.coroutines.flow.unsafeFlow as flow + +/** + * Returns flow where all subsequent repetitions of the same value are filtered out. + */ +@FlowPreview +public fun Flow.distinctUntilChanged(): Flow = distinctUntilChangedBy { it } + +/** + * Returns flow where all subsequent repetitions of the same key are filtered out, where + * key is extracted with [keySelector] function. + */ +@FlowPreview +public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = + flow { + var previousKey: Any? = NullSurrogate + collect { value -> + val key = keySelector(value) + if (previousKey != key) { + previousKey = key + emit(value) + } + } + } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt new file mode 100644 index 0000000000..a08eabbe14 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.jvm.* +import kotlinx.coroutines.flow.unsafeFlow as flow + +// TODO discuss usability from IDE with lambda as last parameter +public typealias ExceptionPredicate = (Throwable) -> Boolean + +private val ALWAYS_TRUE: ExceptionPredicate = { true } + +/** + * Switches to the [fallback] flow if the original flow throws an exception that matches the [predicate]. + */ +@FlowPreview +public fun Flow.onErrorCollect( + fallback: Flow, + predicate: ExceptionPredicate = ALWAYS_TRUE +): Flow = collectSafely { e -> + if (!predicate(e)) throw e + fallback.collect { value -> + emit(value) + } +} + +/** + * Emits the [fallback] value and finishes successfully if the original flow throws exception that matches the given [predicate]; + */ +@FlowPreview +public fun Flow.onErrorReturn(fallback: T, predicate: ExceptionPredicate = ALWAYS_TRUE): Flow = + collectSafely { e -> + if (!predicate(e)) throw e + emit(fallback) + } + +/** + * Operator that retries [n][retries] times to collect the given flow in an exception that matches the given [predicate] occurs + * in the given flow. Exceptions from collectors of this flow are not retried. + */ +@FlowPreview +public fun Flow.retry( + retries: Int = Int.MAX_VALUE, + predicate: ExceptionPredicate = ALWAYS_TRUE +): Flow { + require(retries > 0) { "Expected positive amount of retries, but had $retries" } + return flow { + @Suppress("NAME_SHADOWING") + var retries = retries + // Note that exception may come from the downstream operators, we should not switch on that + while (true) { + var fromDownstream = false + try { + collect { value -> + try { + emit(value) + } catch (e: Throwable) { + fromDownstream = predicate(e) + throw e + } + } + break + } catch (e: Throwable) { + if (fromDownstream) throw e + if (!predicate(e) || retries-- == 0) throw e + } + } + } +} + +private fun Flow.collectSafely(onException: suspend FlowCollector.(Throwable) -> Unit): Flow = + flow { + // Note that exception may come from the downstream operators, we should not switch on that + var fromDownstream = false + try { + collect { + try { + emit(it) + } catch (e: Throwable) { + fromDownstream = true + throw e + } + } + } catch (e: Throwable) { + if (fromDownstream) throw e + onException(e) + } + } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt new file mode 100644 index 0000000000..7a2e3146cb --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.flow.unsafeFlow as flow +import kotlinx.coroutines.* +import kotlin.jvm.* + +/** + * Returns a flow that ignores first [count] elements. + */ +@FlowPreview +public fun Flow.drop(count: Int): Flow { + require(count >= 0) { "Drop count should be non-negative, but had $count" } + return flow { + var skipped = 0 + collect { value -> + if (++skipped > count) emit(value) + } + } +} + +/** + * Returns a flow containing all elements except first elements that satisfy the given predicate. + */ +@FlowPreview +public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = flow { + var matched = false + collect { value -> + if (matched) { + emit(value) + } else if (!predicate(value)) { + matched = true + emit(value) + } + } +} + +/** + * Returns a flow that contains first [count] elements. + * When [count] elements are consumed, the original flow is cancelled. + */ +@FlowPreview +public fun Flow.take(count: Int): Flow { + require(count > 0) { "Take count should be positive, but had $count" } + return flow { + var consumed = 0 + try { + collect { value -> + emit(value) + if (++consumed == count) { + throw TakeLimitException() + } + } + } catch (e: TakeLimitException) { + // Nothing, bail out + } + } +} + +/** + * Returns a flow that contains first elements satisfying the given [predicate]. + */ +@FlowPreview +public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = flow { + try { + collect { value -> + if (predicate(value)) emit(value) + else throw TakeLimitException() + } + } catch (e: TakeLimitException) { + // Nothing, bail out + } +} + +private class TakeLimitException : CancellationException("Flow limit is reached, cancelling") { + // TODO expect/actual + // override fun fillInStackTrace(): Throwable = this +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt new file mode 100644 index 0000000000..34b89aa7e2 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") +@file:Suppress("unused") + +package kotlinx.coroutines.flow +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.flow.unsafeFlow as flow +import kotlin.jvm.* + +/** + * Transforms elements emitted by the original flow by applying [mapper], that returns another flow, and then merging and flattening these flows. + * + * Note that even though this operator looks very familiar, we discourage its usage in a regular application-specific flows. + * Most likely, suspending operation in [map] operator will be sufficient and linear transformations are much easier to reason about. + * + * [bufferSize] parameter controls the size of backpressure aka the amount of queued in-flight elements. + * [concurrency] parameter controls the size of in-flight flows. + */ +@FlowPreview +public fun Flow.flatMap(concurrency: Int = 16, bufferSize: Int = 16, mapper: suspend (value: T) -> Flow): Flow { + return flow { + val semaphore = Channel(concurrency) + val flatMap = SerializingFlatMapCollector(this, bufferSize) + coroutineScope { + collect { outerValue -> + semaphore.send(Unit) // Acquire concurrency permit + val inner = mapper(outerValue) + launch { + inner.collect { value -> + flatMap.push(value) + } + semaphore.receive() // Release concurrency permit + } + } + } + } +} + +/** + * Merges given sequence of flows into a single flow with no guarantees on the order. + * + * [bufferSize] parameter controls the size of backpressure aka the amount of queued in-flight elements. + * [concurrency] parameter controls the size of in-flight flows. + */ +@FlowPreview +public fun Iterable>.merge(concurrency: Int = 16, bufferSize: Int = 16): Flow = asFlow().flatMap(concurrency, bufferSize) { it } + +/** + * Concatenates values of each flow sequentially, without interleaving them. + */ +@FlowPreview +public fun Flow>.concatenate(): Flow = flow { + collect { + val inner = it + inner.collect { value -> + emit(value) + } + } +} + +/** + * Transforms each value of the given flow into flow of another type and then flattens these flows + * sequentially, without interleaving them. + */ +@FlowPreview +public fun Flow.concatenate(mapper: suspend (T) -> Flow): Flow = flow { + collect { value -> + mapper(value).collect { innerValue -> + emit(innerValue) + } + } +} + +// Effectively serializes access to downstream collector from flatMap +private class SerializingFlatMapCollector( + private val downstream: FlowCollector, + private val bufferSize: Int +) { + + // Let's try to leverage the fact that flatMap is never contended + private val channel: Channel by lazy { Channel(bufferSize) } + private val inProgress = atomic(false) + + public suspend fun push(value: T) { + if (!inProgress.compareAndSet(false, true)) { + channel.send(value ?: NullSurrogate) + if (inProgress.compareAndSet(false, true)) { + helpPush() + } + return + } + + downstream.emit(value) + helpPush() + } + + @Suppress("UNCHECKED_CAST") + private suspend fun helpPush() { + var element = channel.poll() + while (element != null) { // TODO receive or closed + if (element === NullSurrogate) downstream.emit(null as T) + else downstream.emit(element as T) + element = channel.poll() + } + + inProgress.value = false + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt new file mode 100644 index 0000000000..6f39e3b732 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.jvm.* +import kotlinx.coroutines.flow.unsafeFlow as flow + +/** + * Applies [transformer] function to each value of the given flow. + * [transformer] is a generic function that may transform emitted element, skip it or emit it multiple times. + * + * This operator is useless by itself, but can be used as a building block of user-specific operators: + * ``` + * fun Flow.skipOddAndDuplicateEven(): Flow = transform { value -> + * if (value % 2 == 0) { // Emit only even values, but twice + * emit(value) + * emit(value) + * } // Do nothing if odd + * } + * ``` + */ +@FlowPreview +public fun Flow.transform(@BuilderInference transformer: suspend FlowCollector.(value: T) -> Unit): Flow { + return flow { + collect { value -> + transformer(value) + } + } +} + +/** + * Returns a flow containing only values of the original flow that matches the given [predicate]. + */ +@FlowPreview +public fun Flow.filter(predicate: suspend (T) -> Boolean): Flow = flow { + collect { value -> + if (predicate(value)) emit(value) + } +} + +/** + * Returns a flow containing only values of the original flow that do not match the given [predicate]. + */ +@FlowPreview +public fun Flow.filterNot(predicate: suspend (T) -> Boolean): Flow = flow { + collect { value -> + if (!predicate(value)) emit(value) + } +} + +/** + * Returns a flow containing only values that are instances of specified type [R]. + */ +@FlowPreview +@Suppress("UNCHECKED_CAST") +public inline fun Flow<*>.filterIsInstance(): Flow = filter { it is R } as Flow + +/** + * Returns a flow containing only values of the original flow that are not null. + */ +@FlowPreview +public fun Flow.filterNotNull(): Flow = flow { + collect { value -> if (value != null) emit(value) } +} + +/** + * Returns a flow containing the results of applying the given [transformer] function to each value of the original flow. + */ +@FlowPreview +public fun Flow.map(transformer: suspend (value: T) -> R): Flow = transform { value -> emit(transformer(value)) } + +/** + * Returns a flow that contains only non-null results of applying the given [transformer] function to each value of the original flow. + */ +@FlowPreview +public fun Flow.mapNotNull(transformer: suspend (value: T) -> R?): Flow = transform { value -> + val transformed = transformer(value) ?: return@transform + emit(transformed) +} + +/** + * Returns a flow which performs the given [action] on each value of the original flow. + */ +@FlowPreview +public fun Flow.onEach(action: suspend (T) -> Unit): Flow = flow { + collect { value -> + action(value) + emit(value) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt new file mode 100644 index 0000000000..47333933f7 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.jvm.* + +/** + * 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") + * } + * ``` + */ +@FlowPreview +public suspend fun Flow.collect(action: suspend (value: T) -> Unit): Unit = + collect(object : FlowCollector { + override suspend fun emit(value: T) = action(value) + }) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt new file mode 100644 index 0000000000..ebeaee4dcd --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.jvm.* + +/** + * Collects given flow into a [destination] + */ +@FlowPreview +public suspend fun Flow.toList(destination: MutableList = ArrayList()): List = toCollection(destination) + +/** + * Collects given flow into a [destination] + */ +@FlowPreview +public suspend fun Flow.toSet(destination: MutableSet = LinkedHashSet()): Set = toCollection(destination) + +/** + * Collects given flow into a [destination] + */ +@FlowPreview +public suspend fun > Flow.toCollection(destination: C): C { + collect { value -> + destination.add(value) + } + return destination +} diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt new file mode 100644 index 0000000000..b3f75fa693 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.jvm.* + +/** + * Returns the number of elements in this flow. + */ +@FlowPreview +public suspend fun Flow.count(): Int { + var i = 0 + collect { + ++i + } + + return i +} + +/** + * Returns the number of elements matching the given predicate. + */ +@FlowPreview +public suspend fun Flow.count(predicate: suspend (T) -> Boolean): Int { + var i = 0 + collect { value -> + if (predicate(value)) { + ++i + } + } + + return i +} diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt new file mode 100644 index 0000000000..ac3c93cc0d --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.flow.unsafeFlow as flow +import kotlin.jvm.* + +/** + * Accumulates value starting with the first element and applying [operation] to current accumulator value and each element. + * Throws [UnsupportedOperationException] if flow was empty. + */ +@FlowPreview +public suspend fun Flow.reduce(operation: suspend (accumulator: S, value: T) -> S): S { + var accumulator: Any? = NullSurrogate + + collect { value -> + accumulator = if (accumulator !== NullSurrogate) { + @Suppress("UNCHECKED_CAST") + operation(accumulator as S, value) + } else { + value + } + } + + if (accumulator === NullSurrogate) throw UnsupportedOperationException("Empty flow can't be reduced") + @Suppress("UNCHECKED_CAST") + return accumulator as S +} + +/** + * Accumulates value starting with [initial] value and applying [operation] current accumulator value and each element + */ +@FlowPreview +public suspend fun Flow.fold( + initial: R, + operation: suspend (acc: R, value: T) -> R +): R { + var accumulator = initial + collect { value -> + accumulator = operation(accumulator, value) + } + return accumulator +} + +/** + * Terminal operator, that awaits for one and only one value to be published. + * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow + * that contains more than one element. + */ +@FlowPreview +public suspend fun Flow.single(): T { + var result: Any? = NullSurrogate + collect { value -> + if (result !== NullSurrogate) error("Expected only one element") + result = value + } + + if (result === NullSurrogate) throw NoSuchElementException("Expected at least one element") + @Suppress("UNCHECKED_CAST") + return result as T +} + +/** + * Terminal operator, that awaits for one and only one value to be published. + * Throws [IllegalStateException] for flow that contains more than one element. + */ +@FlowPreview +public suspend fun Flow.singleOrNull(): T? { + var result: T? = null + collect { value -> + if (result != null) error("Expected only one element") + result = value + } + + return result +} diff --git a/kotlinx-coroutines-core/common/test/NamedDispatchers.kt b/kotlinx-coroutines-core/common/test/NamedDispatchers.kt new file mode 100644 index 0000000000..5e48bb68fc --- /dev/null +++ b/kotlinx-coroutines-core/common/test/NamedDispatchers.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* + +/** + * Test dispatchers that emulate multiplatform context tracking. + */ +@ThreadLocal +public object NamedDispatchers { + + private val stack = ArrayStack() + + public fun name(): String = stack.peek() ?: error("No names on stack") + + public fun nameOr(defaultValue: String): String = stack.peek() ?: defaultValue + + public operator fun invoke(name: String) = named(name) + + private fun named(name: String): CoroutineDispatcher = object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + stack.push(name) + try { + block.run() + } finally { + val last = stack.pop() ?: error("No names on stack") + require(last == name) { "Inconsistent stack: expected $name, but had $last" } + } + } + } +} + +private class ArrayStack { + private var elements = arrayOfNulls(16) + private var head = 0 + + public fun push(value: String) { + if (elements.size == head - 1) ensureCapacity() + elements[head++] = value + } + + public fun peek(): String? = elements.getOrNull(head - 1) + + public fun pop(): String? { + if (head == 0) return null + return elements[--head] + } + + private fun ensureCapacity() { + val currentSize = elements.size + val newCapacity = currentSize shl 1 + val newElements = arrayOfNulls(newCapacity) + val remaining = elements.size - head + arraycopy(elements, head, newElements, 0, remaining) + arraycopy(elements, 0, newElements, remaining, head) + elements = newElements + } +} diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 9c162ff54f..04e75e3264 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -4,8 +4,10 @@ package kotlinx.coroutines +import kotlinx.coroutines.flow.* import kotlin.coroutines.* import kotlinx.coroutines.internal.* +import kotlin.test.* public expect open class TestBase constructor() { public val isStressTest: Boolean @@ -24,6 +26,42 @@ public expect open class TestBase constructor() { ) } +public suspend inline fun hang(onCancellation: () -> Unit) { + try { + suspendCancellableCoroutine { } + } finally { + onCancellation() + } +} + +public inline fun assertFailsWith(block: () -> Unit) { + try { + block() + error("Should not be reached") + } catch (e: Throwable) { + assertTrue(e is T) + } +} + +public suspend inline fun assertFailsWith(flow: Flow<*>) { + var e: Throwable? = null + var completed = false + flow.launchIn(CoroutineScope(Dispatchers.Unconfined)) { + onEach {} + catch { + e = it + } + finally { + completed = true + assertTrue(it is T) + } + }.join() + assertTrue(e is T) + assertTrue(completed) +} + +public suspend fun Flow.sum() = fold(0) { acc, value -> acc + value } + public class TestException(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestException1(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable @@ -40,4 +78,5 @@ public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { dispatcher.dispatch(context, block) } } -} \ No newline at end of file +} + diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt new file mode 100644 index 0000000000..86f685181c --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2019 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.test.* + +class FlowInvariantsTest : TestBase() { + + @Test + fun testWithContextContract() = runTest { + flow { + kotlinx.coroutines.withContext(NonCancellable) { + // This one cannot be prevented :( + emit(1) + } + }.collect { + assertEquals(1, it) + } + } + + @Test + fun testWithContextContractViolated() = runTest({ it is IllegalStateException }) { + flow { + kotlinx.coroutines.withContext(NamedDispatchers("foo")) { + emit(1) + } + }.collect { + fail() + } + } + + @Test + fun testWithContextDoesNotChangeExecution() = runTest { + val flow = flow { + emit(NamedDispatchers.name()) + }.flowOn(NamedDispatchers("original")) + + var result = "unknown" + withContext(NamedDispatchers("misc")) { + flow + .flowOn(NamedDispatchers("upstream")) + .launchIn(this + NamedDispatchers("consumer")) { + onEach { + result = it + } + }.join() + } + + assertEquals("original", result) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt new file mode 100644 index 0000000000..438b7c8313 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class ChannelFlowTest : TestBase() { + @Test + fun testBroadcastChannelAsFlow() = runTest { + val channel = broadcast { + repeat(10) { + send(it + 1) + } + } + + val sum = channel.asFlow().sum() + assertEquals(55, sum) + } + + @Test + fun testExceptionInBroadcast() = runTest { + val channel = broadcast(NonCancellable) { // otherwise failure will cancel scope as well + repeat(10) { + send(it + 1) + } + throw TestException() + } + assertEquals(15, channel.asFlow().take(5).sum()) + assertFailsWith(channel.asFlow()) + } + + @Test + fun testBroadcastChannelAsFlowLimits() = runTest { + val channel = BroadcastChannel(1) + val flow = channel.asFlow().map { it * it }.drop(1).take(2) + + var expected = 0 + launch { + assertTrue(channel.offer(1)) // Handed to the coroutine + assertTrue(channel.offer(2)) // Buffered + assertFalse(channel.offer(3)) // Failed to offer + channel.send(3) + yield() + assertEquals(1, expected) + assertTrue(channel.offer(4)) // Handed to the coroutine + assertTrue(channel.offer(5)) // Buffered + assertFalse(channel.offer(6)) // Failed to offer + channel.send(6) + assertEquals(2, expected) + } + + val sum = flow.sum() + assertEquals(13, sum) + ++expected + val sum2 = flow.sum() + assertEquals(61, sum2) + ++expected + } + + @Test + fun flowAsBroadcast() = runTest { + val flow = flow { + repeat(10) { + emit(it) + } + } + + val channel = flow.broadcastIn(this) + assertEquals((0..9).toList(), channel.openSubscription().toList()) + } + + @Test + fun flowAsBroadcastMultipleSubscription() = runTest { + val flow = flow { + repeat(10) { + emit(it) + } + } + + val broadcast = flow.broadcastIn(this) + val channel = broadcast.openSubscription() + val channel2 = broadcast.openSubscription() + + assertEquals(0, channel.receive()) + assertEquals(0, channel2.receive()) + yield() + assertEquals(1, channel.receive()) + assertEquals(1, channel2.receive()) + + channel.cancel() + channel2.cancel() + yield() + ensureActive() + } + + @Test + fun flowAsBroadcastException() = runTest { + val flow = flow { + repeat(10) { + emit(it) + } + + throw TestException() + } + + val channel = flow.broadcastIn(this + NonCancellable) + assertFailsWith { channel.openSubscription().toList() } + assertTrue(channel.isClosedForSend) // Failure in the flow fails the channel + } + + // Semantics of these tests puzzle me, we should figure out the way to prohibit such chains + @Test + fun testFlowAsBroadcastAsFlow() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + }.broadcastIn(this).asFlow() + + assertEquals(6, flow.sum()) + assertEquals(0, flow.sum()) // Well suddenly flow is no longer idempotent and cold + } + + @Test + fun testBroadcastAsFlowAsBroadcast() = runTest { + val channel = broadcast { + send(1) + }.asFlow().broadcastIn(this) + + channel.openSubscription().consumeEach { + assertEquals(1, it) + } + + channel.openSubscription().consumeEach { + fail() + } + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateMapTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateMapTest.kt new file mode 100644 index 0000000000..d4e15a8667 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateMapTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class ConcatenateMapTest : TestBase() { + @Test + fun testConcatenate() = runTest { + val n = 100 + val sum = flow { + repeat(n) { + emit(it + 1) // 1..100 + } + }.concatenate { value -> + // 1 + (1 + 2) + (1 + 2 + 3) + ... (1 + .. + n) + flow { + repeat(value) { + emit(it + 1) + } + } + }.sum() + + assertEquals(n * (n + 1) * (n + 2) / 6, sum) + } + + @Test + fun testSingle() = runTest { + val flow = flow { + repeat(100) { + emit(it) + } + }.concatenate { value -> + if (value == 99) flowOf(42) + else flowOf() + } + + val value = flow.single() + assertEquals(42, value) + } + + @Test + fun testFailure() = runTest { + var finally = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { finally = true } + } + + emit(1) + } + }.concatenate { + flow { + latch.receive() + throw TestException() + } + } + + assertFailsWith { flow.count() } + assertTrue(finally) + } + + @Test + fun testFailureInMapOperation() = runTest { + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { expect(3) } + } + + expect(1) + emit(1) + } + }.concatenate { + latch.receive() + expect(2) + throw TestException() + flowOf() // Workaround for KT-30642, return type should not be Nothing + } + + assertFailsWith { flow.count() } + finish(4) + } + + @Test + fun testContext() = runTest { + val captured = ArrayList() + val flow = flowOf(1) + .flowOn(NamedDispatchers("irrelevant")) + .concatenate { + flow { + captured += NamedDispatchers.name() + emit(it) + } + } + + flow.flowOn(NamedDispatchers("1")).sum() + flow.flowOn(NamedDispatchers("2")).sum() + assertEquals(listOf("1", "2"), captured) + } + + + @Test + fun testIsolatedContext() = runTest { + val flow = flowOf(1) + .flowOn(NamedDispatchers("irrelevant")) + .flowWith(NamedDispatchers("inner")) { + concatenate { + flow { + expect(2) + assertEquals("inner", NamedDispatchers.name()) + emit(it) + } + } + }.flowOn(NamedDispatchers("irrelevant")) + .concatenate { + flow { + expect(3) + assertEquals("outer", NamedDispatchers.name()) + emit(it) + } + }.flowOn(NamedDispatchers("outer")) + + expect(1) + assertEquals(1, flow.single()) + finish(4) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateTest.kt new file mode 100644 index 0000000000..6a50bc92dc --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/ConcatenateTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2019 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.test.* + +class ConcatenateTest : TestBase() { + @Test + fun testConcatenate() = runTest { + val n = 100 + val sum = (1..n).asFlow() + .map { value -> + flow { + repeat(value) { + emit(it + 1) + } + } + }.concatenate().sum() + assertEquals(n * (n + 1) * (n + 2) / 6, sum) + } + + @Test + fun testSingle() = runTest { + val flows = flow { + repeat(100) { + if (it == 99) emit(flowOf(42)) + else emit(flowOf()) + } + } + + val value = flows.concatenate().single() + assertEquals(42, value) + } + + + @Test + fun testContext() = runTest { + val flow = flow { + emit(flow { + expect(2) + assertEquals("first", NamedDispatchers.name()) + emit(1) + }.flowOn(NamedDispatchers("first"))) + + emit(flow { + expect(3) + assertEquals("second", NamedDispatchers.name()) + emit(1) + }.flowOn(NamedDispatchers("second"))) + }.concatenate().flowOn(NamedDispatchers("first")) + + expect(1) + assertEquals(2, flow.sum()) + finish(4) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt new file mode 100644 index 0000000000..cb4fb836db --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2019 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.test.* + +class DistinctUntilChangedTest : TestBase() { + + private class Box(val i: Int) + + @Test + fun testDistinctUntilChanged() = runTest { + val flow = flowOf(1, 1, 2, 2, 1).distinctUntilChanged() + assertEquals(4, flow.sum()) + } + + @Test + fun testDistinctUntilChangedKeySelector() = runTest { + val flow = flow { + emit(Box(1)) + emit(Box(1)) + emit(Box(2)) + emit(Box(1)) + } + + val sum1 = flow.distinctUntilChanged().map { it.i }.sum() + val sum2 = flow.distinctUntilChangedBy(Box::i).map { it.i }.sum() + assertEquals(5, sum1) + assertEquals(4, sum2) + } + + @Test + fun testThrowingKeySelector() = runTest { + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { expect(3) } + } + expect(2) + emit(1) + } + }.distinctUntilChangedBy { throw TestException() } + + expect(1) + assertFailsWith(flow) + finish(4) + } + + @Test + fun testDistinctUntilChangedNull() = runTest{ + val flow = flowOf(null, 1, null).distinctUntilChanged() + assertEquals(listOf(null, 1, null), flow.toList()) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt new file mode 100644 index 0000000000..5ad4bbc4c9 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/DropTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2019 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.test.* + +class DropTest : TestBase() { + @Test + fun testDrop() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + } + + assertEquals(5, flow.drop(1).sum()) + assertEquals(0, flow.drop(Int.MAX_VALUE).sum()) + assertNull(flow.drop(Int.MAX_VALUE).singleOrNull()) + assertEquals(3, flow.drop(1).take(2).drop(1).single()) + } + + @Test + fun testEmptyFlow() = runTest { + assertEquals(0, flowOf().drop(1).sum()) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { expect(5) } + } + expect(2) + emit(1) + expect(3) + emit(2) + expectUnreached() + } + }.drop(1) + .map { + expect(4) + throw TestException() + 42 + }.onErrorReturn(42) + + expect(1) + assertEquals(42, flow.single()) + finish(6) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DropWhileTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DropWhileTest.kt new file mode 100644 index 0000000000..088954bc6b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/DropWhileTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2019 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.test.* + +class DropWhileTest : TestBase() { + @Test + fun testDropWhile() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + } + + assertEquals(6, flow.dropWhile { false }.sum()) + assertNull(flow.dropWhile { true }.singleOrNull()) + assertEquals(5, flow.dropWhile { it < 2 }.sum()) + assertEquals(1, flow.take(1).dropWhile { it > 1 }.single()) + } + + @Test + fun testEmptyFlow() = runTest { + assertEquals(0, flowOf().dropWhile { true }.sum()) + assertEquals(0, flowOf().dropWhile { false }.sum()) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { expect(4) } + } + expect(2) + emit(1) + expectUnreached() + } + }.dropWhile { + expect(3) + throw TestException() + } + + expect(1) + assertFailsWith(flow) + finish(5) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt new file mode 100644 index 0000000000..ee84d405f3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FilterTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FilterTest : TestBase() { + @Test + fun testFilter() = runTest { + val flow = flowOf(1, 2) + assertEquals(2, flow.filter { it % 2 == 0 }.sum()) + assertEquals(3, flow.filter { true }.sum()) + assertEquals(0, flow.filter { false }.sum()) + } + + @Test + fun testEmptyFlow() = runTest { + val sum = emptyFlow().filter { true }.sum() + assertEquals(0, sum) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang {cancelled = true} + } + emit(1) + } + }.filter { + latch.receive() + throw TestException() + true + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } + + + @Test + fun testFilterNot() = runTest { + val flow = flowOf(1, 2) + assertEquals(0, flow.filterNot { true }.sum()) + assertEquals(3, flow.filterNot { false }.sum()) + } + + @Test + fun testEmptyFlowFilterNot() = runTest { + val sum = emptyFlow().filterNot { true }.sum() + assertEquals(0, sum) + } + + @Test + fun testErrorCancelsUpstreamwFilterNot() = runTest { + var cancelled = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang {cancelled = true} + } + emit(1) + } + }.filterNot { + latch.receive() + throw TestException() + true + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FilterTrivialTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FilterTrivialTest.kt new file mode 100644 index 0000000000..1d3c69bc7e --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FilterTrivialTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FilterTrivialTest : TestBase() { + + @Test + fun testFilterNotNull() = runTest { + val flow = flowOf(1, 2, null) + assertEquals(3, flow.filterNotNull().sum()) + } + + @Test + fun testEmptyFlowNotNull() = runTest { + val sum = emptyFlow().filterNotNull().sum() + assertEquals(0, sum) + } + + @Test + fun testFilterIsInstance() = runTest { + val flow = flowOf("value", 2.0) + assertEquals(2.0, flow.filterIsInstance().single()) + assertEquals("value", flow.filterIsInstance().single()) + } + + @Test + fun testFilterIsInstanceNullable() = runTest { + val flow = flowOf(1, 2, null) + assertEquals(2, flow.filterIsInstance().count()) + assertEquals(3, flow.filterIsInstance().count()) + } + + @Test + fun testEmptyFlowIsInstance() = runTest { + val sum = emptyFlow().filterIsInstance().sum() + assertEquals(0, sum) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapTest.kt new file mode 100644 index 0000000000..ad5d46be69 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FlatMapTest : TestBase() { + + @Test + fun testFlatMap() = runTest { + val n = 100 + val sum = (1..100).asFlow() + .flatMap { value -> + // 1 + (1 + 2) + (1 + 2 + 3) + ... (1 + .. + n) + flow { + repeat(value) { + emit(it + 1) + } + } + }.sum() + + assertEquals(n * (n + 1) * (n + 2) / 6, sum) + } + + @Test + fun testSingle() = runTest { + val flow = flow { + repeat(100) { + emit(it) + } + }.flatMap { value -> + if (value == 99) flowOf(42) + else flowOf() + } + + val value = flow.single() + assertEquals(42, value) + } + + @Test + fun testFailureCancellation() = runTest { + val flow = flow { + expect(2) + emit(1) + expect(3) + emit(2) + expect(4) + }.flatMap { + if (it == 1) flow { + hang { expect(6) } + } else flow { + expect(5) + throw TestException() + } + } + + expect(1) + assertFailsWith { flow.singleOrNull() } + finish(7) + } + + @Test + fun testFailureInMapOperationCancellation() = runTest { + val latch = Channel() + val flow = flow { + expect(2) + emit(1) + expect(3) + emit(2) + expectUnreached() + }.flatMap { + if (it == 1) flow { + expect(5) + latch.send(Unit) + hang { expect(7) } + } else { + expect(4) + latch.receive() + expect(6) + throw TestException() + } + } + + expect(1) + assertFailsWith { flow.count() } + finish(8) + } + + @Test + fun testConcurrentFailure() = runTest { + val latch = Channel() + val flow = flow { + expect(2) + emit(1) + expect(3) + emit(2) + }.flatMap { + if (it == 1) flow { + expect(5) + latch.send(Unit) + hang { + expect(7) + throw TestException2() + + } + } else { + expect(4) + latch.receive() + expect(6) + throw TestException() + } + } + + expect(1) + assertFailsWith(flow) + finish(8) + } + + @Test + fun testContext() = runTest { + val captured = ArrayList() + val flow = flowOf(1) + .flowOn(NamedDispatchers("irrelevant")) + .flatMap { + captured += NamedDispatchers.name() + flow { + captured += NamedDispatchers.name() + emit(it) + } + } + + flow.flowOn(NamedDispatchers("1")).sum() + flow.flowOn(NamedDispatchers("2")).sum() + assertEquals(listOf("1", "1", "2", "2"), captured) + } + + @Test + fun testIsolatedContext() = runTest { + val flow = flowOf(1) + .flowOn(NamedDispatchers("irrelevant")) + .flowWith(NamedDispatchers("inner")) { + flatMap { + flow { + assertEquals("inner", NamedDispatchers.name()) + emit(it) + } + } + }.flowOn(NamedDispatchers("irrelevant")) + .flatMap { + flow { + assertEquals("outer", NamedDispatchers.name()) + emit(it) + } + }.flowOn(NamedDispatchers("outer")) + + assertEquals(1, flow.singleOrNull()) + } + + @Test + fun testFlatMapConcurrency() = runTest { + var concurrentRequests = 0 + val flow = (1..100).asFlow().flatMap(concurrency = 2) { value -> + flow { + ++concurrentRequests + emit(value) + delay(Long.MAX_VALUE) + } + } + + val consumer = launch { + flow.collect { value -> + expect(value) + } + } + + repeat(4) { + yield() + } + + assertEquals(2, concurrentRequests) + consumer.cancelAndJoin() + finish(3) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt new file mode 100644 index 0000000000..c1afbc2feb --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.coroutines.* +import kotlin.test.* + +class FlowContextOptimizationsTest : TestBase() { + + @Test + fun testBaseline() = runTest { + val flowDispatcher = wrapperDispatcher(coroutineContext) + val collectContext = coroutineContext + flow { + assertSame(flowDispatcher, kotlin.coroutines.coroutineContext[ContinuationInterceptor] as CoroutineContext) + expect(1) + emit(1) + expect(2) + emit(2) + expect(3) + }.flowOn(flowDispatcher) + .collect { value -> + assertEquals(collectContext, coroutineContext) + if (value == 1) expect(4) + else expect(5) + } + + finish(6) + } + + @Test + fun testFusable() = runTest { + flow { + expect(1) + emit(1) + expect(3) + emit(2) + expect(5) + }.flowOn(coroutineContext.minusKey(Job)) + .collect { value -> + if (value == 1) expect(2) + else expect(4) + } + + finish(6) + } + + @Test + fun testFusableWithIntermediateOperators() = runTest { + flow { + expect(1) + emit(1) + expect(3) + emit(2) + expect(5) + }.flowOn(coroutineContext.minusKey(Job)) + .map { it } + .flowOn(coroutineContext.minusKey(Job)) + .collect { value -> + if (value == 1) expect(2) + else expect(4) + } + + finish(6) + } + + @Test + fun testNotFusableWithContext() = runTest { + flow { + expect(1) + emit(1) + expect(2) + emit(2) + expect(3) + }.flowOn(coroutineContext.minusKey(Job) + CoroutineName("Name")) + .collect { value -> + if (value == 1) expect(4) + else expect(5) + } + + finish(6) + } + + @Test + fun testFusableFlowWith() = runTest { + flow { + expect(1) + emit(1) + expect(4) + }.flowWith(coroutineContext.minusKey(Job)) { + onEach { value -> + expect(2) + } + }.collect { + expect(3) + } + + finish(5) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt new file mode 100644 index 0000000000..141e8e9cf6 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FlowContextTest : TestBase() { + + private val captured = ArrayList() + + @Test + fun testMixedContext() = runTest { + val flow = flow { + captured += NamedDispatchers.nameOr("main") + emit(314) + } + + val mapper: suspend (Int) -> Int = { + captured += NamedDispatchers.nameOr("main") + it + } + + val value = flow // upstream + .map(mapper) // upstream + .flowOn(NamedDispatchers("upstream")) + .map(mapper) // upstream 2 + .flowWith(NamedDispatchers("downstream")) { + map(mapper) // downstream + } + .flowOn(NamedDispatchers("upstream 2")) + .map(mapper) // main + .single() + + assertEquals(314, value) + assertEquals(listOf("upstream", "upstream", "upstream 2", "downstream", "main"), captured) + } + + @Test + fun testException() = runTest { + val flow = flow { + emit(314) + delay(Long.MAX_VALUE) + }.flowOn(NamedDispatchers("upstream")) + .map { + throw TestException() + } + + assertFailsWith { flow.single() } + assertFailsWith(flow) + ensureActive() + } + + @Test + fun testMixedContextsAndException() = runTest { + val baseFlow = flow { + emit(314) + hang { } + } + + var state = 0 + var needle = 1 + val mapper: suspend (Int) -> Int = { + if (++state == needle) throw TestException() + it + } + + val flow = baseFlow.map(mapper) // 1 + .flowOn(NamedDispatchers("ctx 1")) + .map(mapper) // 2 + .flowWith(NamedDispatchers("ctx 2")) { + map(mapper) // 3 + } + .map(mapper) // 4 + .flowOn(NamedDispatchers("ctx 3")) + .map(mapper) // 5 + + repeat(5) { // Will hang for 6 + state = 0 + needle = it + 1 + assertFailsWith { flow.single() } + + state = 0 + assertFailsWith(flow) + } + + ensureActive() + } + + @Test + fun testNestedContexts() = runTest { + val mapper: suspend (Int) -> Int = { captured += NamedDispatchers.nameOr("main"); it } + val value = flow { + captured += NamedDispatchers.nameOr("main") + emit(1) + }.flowWith(NamedDispatchers("outer")) { + map(mapper) + .flowOn(NamedDispatchers("nested first")) + .flowWith(NamedDispatchers("nested second")) { + map(mapper) + .flowOn(NamedDispatchers("inner first")) + .map(mapper) + } + .map(mapper) + }.map(mapper) + .single() + + val expected = listOf("main", "nested first", "inner first", "nested second", "outer", "main") + assertEquals(expected, captured) + assertEquals(1, value) + } + + + @Test + fun testFlowContextCancellation() = runTest { + val latch = Channel() + val flow = flow { + assertEquals("delayed", NamedDispatchers.name()) + expect(2) + emit(1) + }.flowWith(NamedDispatchers("outer")) { + map { expect(3); it + 1 }.flowOn(NamedDispatchers("inner")) + }.map { + expect(4) + assertEquals("delayed", NamedDispatchers.name()) + latch.send(Unit) + hang { expect(6) } + }.flowOn(NamedDispatchers("delayed")) + + + val job = launch(NamedDispatchers("launch")) { + expect(1) + flow.single() + } + + latch.receive() + expect(5) + job.cancelAndJoin() + finish(7) + ensureActive() + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt new file mode 100644 index 0000000000..49df21d576 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FlowOnTest : TestBase() { + + @Test + fun testFlowOn() = runTest { + val source = Source(42) + val consumer = Consumer(42) + + val flow = source::produce.asFlow() + flow.flowOn(NamedDispatchers("ctx1")).launchIn(this) { + onEach { consumer.consume(it) } + }.join() + + assertEquals("ctx1", source.contextName) + assertEquals("main", consumer.contextName) + + flow.flowOn(NamedDispatchers("ctx2")).launchIn(this) { + onEach { consumer.consume(it) } + }.join() + + assertEquals("ctx2", source.contextName) + assertEquals("main", consumer.contextName) + } + + @Test + fun testFlowOnAndOperators() = runTest { + val source = Source(42) + val consumer = Consumer(42) + val captured = ArrayList() + val mapper: suspend (Int) -> Int = { + captured += NamedDispatchers.nameOr("main") + it + } + + val flow = source::produce.asFlow() + flow.map(mapper) + .flowOn(NamedDispatchers("ctx1")) + .map(mapper) + .flowOn(NamedDispatchers("ctx2")) + .map(mapper) + .launchIn(this) { + onEach { consumer.consume(it) } + }.join() + + assertEquals(listOf("ctx1", "ctx2", "main"), captured) + assertEquals("ctx1", source.contextName) + assertEquals("main", consumer.contextName) + } + + @Test + public fun testFlowOnThrowingSource() = runTest { + val flow = flow { + expect(1) + emit(NamedDispatchers.name()) + expect(3) + throw TestException() + }.map { + expect(2) + assertEquals("throwing", it) + it + }.flowOn(NamedDispatchers("throwing")) + + assertFailsWith { flow.single() } + ensureActive() + finish(4) + } + + @Test + public fun testFlowOnThrowingOperator() = runTest { + val flow = flow { + expect(1) + emit(NamedDispatchers.name()) + expectUnreached() + }.map { + expect(2) + assertEquals("throwing", it) + throw TestException(); it + }.flowOn(NamedDispatchers("throwing")) + + assertFailsWith(flow) + ensureActive() + finish(3) + } + + @Test + public fun testFlowOnDownstreamOperator() = runTest() { + val flow = flow { + expect(2) + emit(NamedDispatchers.name()) + hang { expect(5) } + delay(Long.MAX_VALUE) + }.map { + expect(3) + it + }.flowOn(NamedDispatchers("throwing")) + .map { + expect(4); + throw TestException() + } + + expect(1) + assertFailsWith { flow.single() } + ensureActive() + finish(6) + } + + @Test + public fun testFlowOnThrowingConsumer() = runTest { + val flow = flow { + expect(2) + emit(NamedDispatchers.name()) + hang { expect(4) } + } + + expect(1) + flow.flowOn(NamedDispatchers("...")).launchIn(this + NamedDispatchers("launch")) { + onEach { + expect(3) + throw TestException() + } + catch { expect(5) } + }.join() + + ensureActive() + finish(6) + } + + @Test + fun testFlowOnWithJob() = runTest({ it is IllegalArgumentException }) { + flow { + emit(1) + }.flowOn(NamedDispatchers("foo") + Job()) + } + + @Test + fun testFlowOnCancellation() = runTest { + val latch = Channel() + expect(1) + val job = launch(NamedDispatchers("launch")) { + flow { + expect(2) + latch.send(Unit) + expect(3) + hang { + assertEquals("cancelled", NamedDispatchers.name()) + expect(5) + } + }.flowOn(NamedDispatchers("cancelled")).single() + } + + latch.receive() + expect(4) + job.cancel() + job.join() + ensureActive() + finish(6) + } + + @Test + fun testFlowOnCancellationHappensBefore() = runTest { + launch { + try { + flow { + expect(1) + val flowJob = kotlin.coroutines.coroutineContext[Job]!! + launch { + expect(2) + flowJob.cancel() + } + hang { expect(3) } + }.flowOn(NamedDispatchers("upstream")).single() + } catch (e: CancellationException) { + expect(4) + } + }.join() + ensureActive() + finish(5) + } + + @Test + fun testIndependentOperatorContext() = runTest { + val value = flow { + assertEquals("base", NamedDispatchers.nameOr("main")) + expect(1) + emit(-239) + }.map { + assertEquals("base", NamedDispatchers.nameOr("main")) + expect(2) + it + }.flowOn(NamedDispatchers("base")) + .map { + assertEquals("main", NamedDispatchers.nameOr("main")) + expect(3) + it + }.single() + + assertEquals(-239, value) + finish(4) + } + + @Test + fun testMultipleFlowOn() = runTest { + flow { + assertEquals("ctx1", NamedDispatchers.nameOr("main")) + expect(1) + emit(1) + }.map { + assertEquals("ctx1", NamedDispatchers.nameOr("main")) + expect(2) + }.flowOn(NamedDispatchers("ctx1")) + .map { + assertEquals("ctx2", NamedDispatchers.nameOr("main")) + expect(3) + }.flowOn(NamedDispatchers("ctx2")) + .map { + assertEquals("ctx3", NamedDispatchers.nameOr("main")) + expect(4) + }.flowOn(NamedDispatchers("ctx3")) + .map { + assertEquals("main", NamedDispatchers.nameOr("main")) + expect(5) + } + .single() + + finish(6) + } + + private inner class Source(private val value: Int) { + public var contextName: String = "unknown" + + fun produce(): Int { + contextName = NamedDispatchers.nameOr("main") + return value + } + } + + private inner class Consumer(private val expected: Int) { + public var contextName: String = "unknown" + + fun consume(value: Int) { + contextName = NamedDispatchers.nameOr("main") + assertEquals(expected, value) + } + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt new file mode 100644 index 0000000000..e31a1db66f --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FlowWithTest : TestBase() { + + private fun mapper(name: String, index: Int): suspend (Int) -> Int = { + assertEquals(name, NamedDispatchers.nameOr("main")) + expect(index) + it + } + + @Test + fun testFlowWith() = runTest { + val flow = flow { + assertEquals("main", NamedDispatchers.nameOr("main")) + expect(1) + emit(314) + } + + val result = flow.flowWith(NamedDispatchers("ctx1")) { + map(mapper("ctx1", 2)) + }.flowWith(NamedDispatchers("ctx2")) { + map(mapper("ctx2", 3)) + }.map(mapper("main", 4)).single() + assertEquals(314, result) + finish(5) + } + + @Test + public fun testFlowWithThrowingSource() = runTest { + val flow = flow { + emit(NamedDispatchers.nameOr("main")) + throw TestException() + }.flowWith(NamedDispatchers("throwing")) { + map { + assertEquals("main", it) + it + } + } + + assertFailsWith { flow.single() } + assertFailsWith(flow) + ensureActive() + } + + @Test + public fun testFlowWithThrowingOperator() = runTest { + val flow = flow { + emit(NamedDispatchers.nameOr("main")) + hang {} + }.flowWith(NamedDispatchers("throwing")) { + map { + assertEquals("main", it) + throw TestException() + } + } + + assertFailsWith { flow.single() } + assertFailsWith(flow) + ensureActive() + } + + @Test + public fun testFlowWithThrowingDownstreamOperator() = runTest { + val flow = flow { + emit(42) + hang {} + }.flowWith(NamedDispatchers("throwing")) { + map { it } + }.map { throw TestException() } + + assertFailsWith { flow.single() } + assertFailsWith(flow) + ensureActive() + } + + @Test + fun testMultipleFlowWith() = runTest() { + flow { + expect(1) + emit(1) + }.map(mapper("main", 2)) + .flowWith(NamedDispatchers("downstream")) { + map(mapper("downstream", 3)) + } + .flowWith(NamedDispatchers("downstream 2")) { + map(mapper("downstream 2", 4)) + } + .flowWith(NamedDispatchers("downstream 3")) { + map(mapper("downstream 3", 5)) + } + .map(mapper("main", 6)) + .flowWith(NamedDispatchers("downstream 4")) { + map(mapper("downstream 4", 7)) + }.flowWith(NamedDispatchers("ignored")) { this } + .single() + + finish(8) + } + + @Test + fun testFlowWithCancellation() = runTest() { + val latch = Channel() + expect(1) + val job = launch(NamedDispatchers("launch")) { + flow { + expect(2) + latch.send(Unit) + expect(3) + hang { + assertEquals("launch", NamedDispatchers.nameOr("main")) + expect(5) + } + }.flowWith(NamedDispatchers("cancelled")) { + map { + expectUnreached() + it + } + }.single() + } + + latch.receive() + expect(4) + job.cancel() + job.join() + ensureActive() + finish(6) + } + + @Test + fun testFlowWithCancellationHappensBefore() = runTest { + launch { + try { + flow { + expect(1) + val flowJob = kotlin.coroutines.coroutineContext[Job]!! + launch { + expect(2) + flowJob.cancel() + } + hang { expect(3) } + }.flowWith(NamedDispatchers("downstream")) { + map { it } + }.single() + } catch (e: CancellationException) { + expect(4) + } + }.join() + finish(5) + } + + @Test + fun testMultipleFlowWithException() = runTest() { + var switch = 0 + val flow = flow { + emit(Unit) + if (switch == 0) throw TestException() + }.map { if (switch == 1) throw TestException() else Unit } + .flowWith(NamedDispatchers("downstream")) { + map { if (switch == 2) throw TestException() else Unit } + } + repeat(3) { + switch = it + assertFailsWith { flow.single() } + assertFailsWith(flow) + } + } + + @Test + fun testMultipleFlowWithJobsCancellation() = runTest() { + val latch = Channel() + val flow = flow { + expect(1) + emit(Unit) + latch.send(Unit) + hang { expect(4) } + }.flowWith(NamedDispatchers("downstream")) { + map { + expect(2) + Unit + } + } + + val job = launch { + flow.single() + } + + latch.receive() + expect(3) + job.cancelAndJoin() + ensureActive() + finish(5) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt new file mode 100644 index 0000000000..13f1dad991 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/MapNotNullTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class MapNotNullTest : TestBase() { + @Test + fun testMap() = runTest { + val flow = flow { + emit(1) + emit(null) + emit(2) + } + + val result = flow.mapNotNull { it }.sum() + assertEquals(3, result) + } + + @Test + fun testEmptyFlow() = runTest { + val sum = emptyFlow().mapNotNull { expectUnreached(); it }.sum() + assertEquals(0, sum) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { cancelled = true } + } + emit(1) + } + }.mapNotNull { + latch.receive() + throw TestException() + it + 1 + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt new file mode 100644 index 0000000000..a7f1088fad --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class MapTest : TestBase() { + @Test + fun testMap() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + val result = flow.map { it + 1 }.sum() + assertEquals(5, result) + } + + @Test + fun testEmptyFlow() = runTest { + val sum = emptyFlow().map { expectUnreached(); it }.sum() + assertEquals(0, sum) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { cancelled = true } + } + emit(1) + } + }.map { + latch.receive() + throw TestException() + it + 1 + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt new file mode 100644 index 0000000000..8766078894 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnEachTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class OnEachTest : TestBase() { + @Test + fun testOnEach() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + val result = flow.onEach { expect(it) }.sum() + assertEquals(3, result) + finish(3) + } + + @Test + fun testEmptyFlow() = runTest { + val value = emptyFlow().onEach { fail() }.singleOrNull() + assertNull(value) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { cancelled = true } + } + emit(1) + } + }.onEach { + latch.receive() + throw TestException() + it + 1 + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnErrorTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnErrorTest.kt new file mode 100644 index 0000000000..a515a78380 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnErrorTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2016-2019 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.test.* + +class OnErrorTest : TestBase() { + @Test + fun testRetry() = runTest { + var counter = 0 + val flow = flow { + emit(1) + if (++counter < 4) throw TestException() + } + + assertEquals(4, flow.retry(4).sum()) + counter = 0 + assertFailsWith(flow) + counter = 0 + assertFailsWith(flow.retry(2)) + } + + @Test + fun testRetryPredicate() = runTest { + var counter = 0 + val flow = flow { + emit(1); + if (++counter == 1) throw TestException() + } + + assertEquals(2, flow.retry(1) { it is TestException }.sum()) + counter = 0 + assertFailsWith(flow.retry(1) { it !is TestException }) + } + + @Test + fun testRetryExceptionFromDownstream() = runTest { + var executed = 0 + val flow = flow { + emit(1) + }.retry(42).map { + ++executed + throw TestException() + } + + assertFailsWith(flow) + assertEquals(1, executed) + } + + @Test + fun testOnErrorReturn() = runTest { + val flow = flow { + emit(1) + throw TestException() + } + + assertEquals(42, flow.onErrorReturn(41).sum()) + assertFailsWith(flow) + } + + @Test + fun testOnErrorReturnPredicate() = runTest { + val flow = flow { emit(1); throw TestException() } + assertFailsWith(flow.onErrorReturn(42) { it !is TestException }) + } + + @Test + fun testOnErrorReturnExceptionFromDownstream() = runTest { + var executed = 0 + val flow = flow { + emit(1) + }.onErrorReturn(42).map { + ++executed + throw TestException() + } + + assertFailsWith(flow) + assertEquals(1, executed) + } + + @Test + fun testOnErrorCollect() = runTest { + val flow = flow { + emit(1) + throw TestException() + }.onErrorCollect(flowOf(2)) + + assertEquals(3, flow.sum()) + } + + @Test + fun testOnErrorCollectPredicate() = runTest { + val flow = flow { emit(1); throw TestException() } + assertFailsWith(flow.onErrorCollect(flowOf(2)) { it !is TestException }) + } + + @Test + fun testOnErrorCollectExceptionFromDownstream() = runTest { + var executed = 0 + val flow = flow { + emit(1) + }.onErrorCollect(flowOf(1, 2, 3)).map { + ++executed + throw TestException() + } + + assertFailsWith(flow) + assertEquals(1, executed) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt new file mode 100644 index 0000000000..100b96d82b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2019 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.test.* + +class TakeTest : TestBase() { + @Test + fun testTake() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + assertEquals(3, flow.take(2).sum()) + assertEquals(3, flow.take(Int.MAX_VALUE).sum()) + assertEquals(1, flow.take(1).single()) + assertEquals(2, flow.drop(1).take(1).single()) + } + + @Test + fun testEmptyFlow() = runTest { + val sum = emptyFlow().take(10).sum() + assertEquals(0, sum) + } + + @Test + fun testCancelUpstream() = runTest { + var cancelled = false + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { cancelled = true } + } + + emit(1) + } + } + + assertEquals(1, flow.take(1).single()) + assertTrue(cancelled) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { cancelled = true } + } + emit(1) + } + }.take(2) + .map { + throw TestException() + 42 + }.onErrorReturn(42) + + assertEquals(42, flow.single()) + assertTrue(cancelled) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TakeWhileTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TakeWhileTest.kt new file mode 100644 index 0000000000..63f1bff3c2 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/TakeWhileTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2019 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.test.* + +class TakeWhileTest : TestBase() { + + @Test + fun testTakeWhile() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + assertEquals(3, flow.takeWhile { true }.sum()) + assertEquals(1, flow.takeWhile { it < 2 }.single()) + assertEquals(2, flow.drop(1).takeWhile { it < 3 }.single()) + assertNull(flow.drop(1).takeWhile { it < 2 }.singleOrNull()) + } + + @Test + fun testEmptyFlow() = runTest { + assertEquals(0, emptyFlow().takeWhile { true }.sum()) + assertEquals(0, emptyFlow().takeWhile { false }.sum()) + } + + @Test + fun testCancelUpstream() = runTest { + var cancelled = false + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { cancelled = true } + } + + emit(1) + emit(2) + } + } + + assertEquals(1, flow.takeWhile { it < 2 }.single()) + assertTrue(cancelled) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + var cancelled = false + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { cancelled = true } + } + emit(1) + } + }.takeWhile { + throw TestException() + } + + assertFailsWith(flow) + assertTrue(cancelled) + assertEquals(42, flow.onErrorReturn(42).single()) + assertEquals(42, flow.onErrorCollect(flowOf(42)).single()) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/CountTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/CountTest.kt new file mode 100644 index 0000000000..4a6f5ae673 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/CountTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2019 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.test.* + +class CountTest : TestBase() { + @Test + fun testCount() = runTest { + val flow = flowOf(239, 240) + assertEquals(2, flow.count()) + assertEquals(2, flow.count { true }) + assertEquals(1, flow.count { it % 2 == 0}) + assertEquals(0, flow.count { false }) + } + + @Test + fun testNoValues() = runTest { + assertEquals(0, flowOf().count()) + assertEquals(0, flowOf().count { false }) + assertEquals(0, flowOf().count { true }) + } + + @Test + fun testException() = runTest { + val flow = flow { + throw TestException() + } + + assertFailsWith { flow.count() } + assertFailsWith { flow.count { false } } + } + + @Test + fun testExceptionAfterValue() = runTest { + val flow = flow { + emit(1) + throw TestException() + } + + assertFailsWith { flow.count() } + assertFailsWith { flow.count { false } } + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt new file mode 100644 index 0000000000..3c88571378 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FoldTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class FoldTest : TestBase() { + @Test + fun testFold() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + val result = flow.fold(3) { value, acc -> value + acc } + assertEquals(6, result) + } + + @Test + fun testEmptyFold() = runTest { + val flow = flowOf() + assertEquals(42, flow.fold(42) { value, acc -> value + acc }) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + expect(3) + hang { expect(5) } + } + expect(2) + emit(1) + } + } + + expect(1) + assertFailsWith { + flow.fold(42) { _, _ -> + latch.receive() + expect(4) + throw TestException() + 42 // Workaround for KT-30642, return type should not be Nothing + } + } + finish(6) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/LaunchFlow.kt b/kotlinx-coroutines-core/common/test/flow/terminal/LaunchFlow.kt new file mode 100644 index 0000000000..ceefa73925 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/LaunchFlow.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2019 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.jvm.* +import kotlin.reflect.* + +public typealias Handler = suspend CoroutineScope.(T) -> Unit + +/* + * Design of this builder is not yet stable, so leaving it as is. + */ +public class LaunchFlowBuilder { + /* + * NB: this implementation is a temporary ad-hoc (and slightly incorrect) + * solution until coroutine-builders are ready + * + * NB 2: this internal stuff is required to workaround KT-30795 + */ + @PublishedApi + internal var onEach: Handler? = null + @PublishedApi + internal var finally: Handler? = null + @PublishedApi + internal var exceptionHandlers = LinkedHashMap, Handler>() + + public fun onEach(action: suspend CoroutineScope.(value: T) -> Unit) { + check(onEach == null) { "onEach block is already registered" } + check(exceptionHandlers.isEmpty()) { "onEach block should be registered before exceptionHandlers block" } + check(finally == null) { "onEach block should be registered before finally block" } + onEach = action + } + + public inline fun catch(noinline action: suspend CoroutineScope.(T) -> Unit) { + check(onEach != null) { "onEach block should be registered first" } + check(finally == null) { "exceptionHandlers block should be registered before finally block" } + @Suppress("UNCHECKED_CAST") + exceptionHandlers[T::class] = action as Handler + } + + public fun finally(action: suspend CoroutineScope.(cause: Throwable?) -> Unit) { + check(finally == null) { "Finally block is already registered" } + check(onEach != null) { "onEach block should be registered before finally block" } + if (finally == null) finally = action + } + + internal fun build(): Handlers = + Handlers(onEach ?: error("onEach is not registered"), exceptionHandlers, finally) +} + +internal class Handlers( + @JvmField + internal var onEach: Handler, + @JvmField + internal var exceptionHandlers: Map, Handler>, + @JvmField + internal var finally: Handler? +) + +private fun CoroutineScope.launchFlow( + flow: Flow, + builder: LaunchFlowBuilder.() -> Unit +): Job { + val handlers = LaunchFlowBuilder().apply(builder).build() + return launch { + var caught: Throwable? = null + try { + coroutineScope { + flow.collect { value -> + handlers.onEach(this, value) + } + } + } catch (e: Throwable) { + handlers.exceptionHandlers.forEach { (key, value) -> + if (key.isInstance(e)) { + caught = e + value.invoke(this, e) + return@forEach + } + } + if (caught == null) { + caught = e + throw e + } + } finally { + cancel() // TODO discuss + handlers.finally?.invoke(CoroutineScope(coroutineContext + NonCancellable), caught) + } + } +} + +public fun Flow.launchIn( + scope: CoroutineScope, + builder: LaunchFlowBuilder.() -> Unit +): Job = scope.launchFlow(this, builder) diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt new file mode 100644 index 0000000000..a84c02785a --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2019 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 kotlin.test.* + +class ReduceTest : TestBase() { + @Test + fun testReduce() = runTest { + val flow = flow { + emit(1) + emit(2) + } + + val result = flow.reduce { value, acc -> value + acc } + assertEquals(3, result) + } + + @Test + fun testEmptyReduce() = runTest { + val flow = emptyFlow() + assertFailsWith { flow.reduce { acc, value -> value + acc } } + } + + @Test + fun testNullableReduce() = runTest { + val flow = flowOf(1, null, null, 2) + var invocations = 0 + val sum = flow.reduce { acc, value -> + ++invocations + value + } + assertEquals(2, sum) + assertEquals(3, invocations) + } + + @Test + fun testReduceNulls() = runTest { + assertNull(flowOf(null).reduce { _, value -> value }) + assertNull(flowOf(null, null).reduce { _, value -> value }) + assertFailsWith { flowOf().reduce { _, value -> value } } + } + + @Test + fun testErrorCancelsUpstream() = runTest { + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + expect(3) + hang { expect(5) } + } + expect(2) + emit(1) + emit(2) + } + } + + expect(1) + assertFailsWith { + flow.reduce { _, _ -> + latch.receive() + expect(4) + throw TestException() + 42 // Workaround for KT-30642, return type should not be Nothing + } + } + finish(6) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt new file mode 100644 index 0000000000..5ce6d47b69 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2019 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.test.* + +class SingleTest : TestBase() { + + @Test + fun testSingle() = runTest { + val flow = flow { + emit(239L) + } + + assertEquals(239L, flow.single()) + assertEquals(239L, flow.singleOrNull()) + + } + + @Test + fun testMultipleValues() = runTest { + val flow = flow { + emit(239L) + emit(240L) + } + assertFailsWith { flow.single() } + assertFailsWith { flow.singleOrNull() } + } + + @Test + fun testNoValues() = runTest { + assertFailsWith { flow {}.single() } + assertNull(flow {}.singleOrNull()) + } + + @Test + fun testException() = runTest { + val flow = flow { + throw TestException() + } + + assertFailsWith { flow.single() } + assertFailsWith { flow.singleOrNull() } + } + + @Test + fun testExceptionAfterValue() = runTest { + val flow = flow { + emit(1) + throw TestException() + } + + assertFailsWith { flow.single() } + assertFailsWith { flow.singleOrNull() } + } + + @Test + fun testNullableSingle() = runTest { + assertEquals(1, flowOf(1).single()) + assertNull(flowOf(null).single()) + assertFailsWith { flowOf().single() } + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/ToCollectionTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/ToCollectionTest.kt new file mode 100644 index 0000000000..cfcbb5218b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/ToCollectionTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2019 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.test.* + +class ToCollectionTest : TestBase() { + + private val flow = flow { + repeat(10) { + emit(42) + } + } + + private val emptyFlow = flowOf() + + @Test + fun testToList() = runTest { + assertEquals(List(10) { 42 }, flow.toList()) + assertEquals(emptyList(), emptyFlow.toList()) + } + + @Test + fun testToSet() = runTest { + assertEquals(setOf(42), flow.toSet()) + assertEquals(emptySet(), emptyFlow.toSet()) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/flow/FlowFromChannelTest.kt b/kotlinx-coroutines-core/jvm/test/flow/FlowFromChannelTest.kt new file mode 100644 index 0000000000..9d7799c9d8 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/FlowFromChannelTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2016-2019 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 org.junit.Test +import kotlin.concurrent.* +import kotlin.test.* + +class FlowFromChannelTest : TestBase() { + + private class CallbackApi(val block: (SendChannel) -> Unit) { + var started = false + @Volatile + var stopped = false + lateinit var thread: Thread + + fun start(sink: SendChannel) { + started = true + thread = thread { + while (!stopped) { + block(sink) + } + } + } + + fun stop() { + stopped = true + } + } + + @Test(timeout = 5_000L) + fun testThrowingConsumer() = runTest { + var i = 0 + val api = CallbackApi { + runCatching { it.offer(++i) } + } + + val flow = flowViaChannel { channel -> + api.start(channel) + channel.invokeOnClose { + api.stop() + } + } + + var receivedConsensus = 0 + var isDone = false + var exception: Throwable? = null + val job = flow + .filter { it > 10 } + .launchIn(this) { + onEach { + if (it == 11) { + ++receivedConsensus + } else { + receivedConsensus = 42 + } + throw RuntimeException() + } + catch { exception = it } + finally { isDone = true } + } + job.join() + assertEquals(1, receivedConsensus) + assertTrue(isDone) + assertTrue { exception is RuntimeException } + api.thread.join() + assertTrue(api.started) + assertTrue(api.stopped) + } + + @Test(timeout = 5_000L) + fun testThrowingSource() = runBlocking { + var i = 0 + val api = CallbackApi { + if (i < 5) { + it.offer(++i) + } else { + it.close(RuntimeException()) + } + } + + val flow = flowViaChannel { channel -> + api.start(channel) + channel.invokeOnClose { + api.stop() + } + } + + var received = 0 + var isDone = false + var exception: Throwable? = null + val job = flow.launchIn(this) { + onEach { ++received } + catch { exception = it } + finally { isDone = true } + } + + job.join() + assertTrue(isDone) + assertTrue { exception is RuntimeException } + api.thread.join() + assertTrue(api.started) + assertTrue(api.stopped) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt index 3e73d2c6e6..81e75a816f 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt @@ -14,7 +14,7 @@ fun main() = runBlocking { println("The answer is ${concurrentSum()}") } println("Completed in $time ms") -//sampleEnd +//sampleEnd } suspend fun concurrentSum(): Int = coroutineScope { diff --git a/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt b/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt new file mode 100644 index 0000000000..b8b87f1ed1 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.reactivestreams.* +import java.util.concurrent.atomic.* + +/** + * Transforms the given flow to a spec-compliant [Publisher] + */ +@JvmName("from") +@FlowPreview +public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) + +/** + * Adapter that transforms [Flow] into TCK-complaint [Publisher]. + * Any calls to [cancel] cancels the original flow. + */ +@Suppress("PublisherImplementation") +private class FlowAsPublisher(private val flow: Flow) : Publisher { + + override fun subscribe(subscriber: Subscriber?) { + if (subscriber == null) throw NullPointerException() + subscriber.onSubscribe( + FlowSubscription( + flow, + subscriber + ) + ) + } + + private class FlowSubscription(val flow: Flow, val subscriber: Subscriber) : Subscription { + @Volatile + internal var canceled: Boolean = false + private val requested = AtomicLong(0L) + private val producer: AtomicReference?> = AtomicReference() + + // This is actually optimizable + private var job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.LAZY) { + try { + consumeFlow() + subscriber.onComplete() + } catch (e: Throwable) { + // Failed with real exception + if (!coroutineContext[Job]!!.isCancelled) { + subscriber.onError(e) + } + } + } + + private suspend fun CoroutineScope.consumeFlow() { + flow.collect { value -> + if (!isActive) { + subscriber.onComplete() + yield() // Force cancellation + } + + if (requested.get() == 0L) { + suspendCancellableCoroutine { + producer.set(it) + if (requested.get() != 0L) it.resumeSafely() + } + } + + requested.decrementAndGet() + val result = kotlin.runCatching { + subscriber.onNext(value) + } + + if (result.isFailure) { + subscriber.onError(result.exceptionOrNull()) + } + } + } + + override fun cancel() { + canceled = true + job.cancel() + } + + override fun request(n: Long) { + if (n <= 0) { + return + } + + if (canceled) return + + job.start() + var snapshot: Long + var newValue: Long + do { + snapshot = requested.get() + newValue = snapshot + n + if (newValue <= 0L) newValue = Long.MAX_VALUE + + } while (!requested.compareAndSet(snapshot, newValue)) + + val prev = producer.get() + if (prev == null || !producer.compareAndSet(prev, null)) return + + prev.resumeSafely() + } + + private fun CancellableContinuation.resumeSafely() { + val token = tryResume(Unit) + if (token != null) { + completeResume(token) + } + } + } +} diff --git a/reactive/kotlinx-coroutines-reactive/src/flow/PublisherAsFlow.kt b/reactive/kotlinx-coroutines-reactive/src/flow/PublisherAsFlow.kt new file mode 100644 index 0000000000..e0490235c3 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/src/flow/PublisherAsFlow.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import org.reactivestreams.* + +/** + * Transforms the given reactive [Publisher] into [Flow]. + * Backpressure is controlled by [batchSize] parameter that controls the size of in-flight elements + * and [Subscription.request] size. + * + * If any of the resulting flow transformations fails, subscription is immediately cancelled and all in-flights elements + * are discarded. + */ +@FlowPreview +@JvmOverloads // For nice Java API +@JvmName("from") +public fun Publisher.asFlow(batchSize: Int = 1): Flow = + PublisherAsFlow(this, batchSize) + +private class PublisherAsFlow(private val publisher: Publisher, private val batchSize: Int) : Flow { + + override suspend fun collect(collector: FlowCollector) { + val channel = Channel(batchSize) + val subscriber = ReactiveSubscriber(channel, batchSize) + publisher.subscribe(subscriber) + try { + var consumed = 0 + for (i in channel) { + collector.emit(i) + if (++consumed == batchSize) { + consumed = 0 + subscriber.subscription.request(batchSize.toLong()) + } + } + } finally { + subscriber.subscription.cancel() + } + } + + @Suppress("SubscriberImplementation") + private class ReactiveSubscriber( + private val channel: Channel, + private val batchSize: Int + ) : Subscriber { + + lateinit var subscription: Subscription + + override fun onComplete() { + channel.close() + } + + override fun onSubscribe(s: Subscription) { + subscription = s + s.request(batchSize.toLong()) + } + + override fun onNext(t: T) { + // Controlled by batch-size + require(channel.offer(t)) { "Element $t was not added to channel because it was full, $channel" } + } + + override fun onError(t: Throwable?) { + channel.close(t) + } + } +} diff --git a/reactive/kotlinx-coroutines-reactive/test/flow/IterableFlowTckTest.kt b/reactive/kotlinx-coroutines-reactive/test/flow/IterableFlowTckTest.kt new file mode 100644 index 0000000000..31c5a3c489 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/test/flow/IterableFlowTckTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UNCHECKED_CAST") + +package kotlinx.coroutines.reactive.flow + +import kotlinx.coroutines.flow.* +import org.junit.* +import org.reactivestreams.* +import org.reactivestreams.tck.* + +import org.junit.Assert.* +import org.reactivestreams.Subscription +import org.reactivestreams.Subscriber +import java.util.ArrayList +import java.util.concurrent.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ForkJoinPool.commonPool + +class IterableFlowTckTest : PublisherVerification(TestEnvironment()) { + + private fun generate(num: Long): Array { + return Array(if (num >= Integer.MAX_VALUE) 1000000 else num.toInt()) { it.toLong() } + } + + override fun createPublisher(elements: Long): Publisher { + return generate(elements).asIterable().asFlow().asPublisher() + } + + @Suppress("SubscriberImplementation") + override fun createFailedPublisher(): Publisher? { + /* + * This is a hack for our adapter structure: + * Tests assume that calling "collect" is enough for publisher to fail and it is not + * true for our implementation + */ + val pub = { error(42) }.asFlow().asPublisher() + return Publisher { subscriber -> + pub.subscribe(object : Subscriber by subscriber as Subscriber { + override fun onSubscribe(s: Subscription) { + subscriber.onSubscribe(s) + s.request(1) + } + }) + } + } + + @Test + fun testStackOverflowTrampoline() { + val latch = CountDownLatch(1) + val collected = ArrayList() + val toRequest = 1000L + val array = generate(toRequest) + val publisher = array.asIterable().asFlow().asPublisher() + + publisher.subscribe(object : Subscriber { + private lateinit var s: Subscription + + override fun onSubscribe(s: Subscription) { + this.s = s + s.request(1) + } + + override fun onNext(aLong: Long) { + collected.add(aLong) + + s.request(1) + } + + override fun onError(t: Throwable) { + + } + + override fun onComplete() { + latch.countDown() + } + }) + + latch.await(5, TimeUnit.SECONDS) + assertEquals(collected, array.toList()) + } + + @Test + fun testConcurrentRequest() { + val latch = CountDownLatch(1) + val collected = ArrayList() + val n = 50000L + val array = generate(n) + val publisher = array.asIterable().asFlow().asPublisher() + + publisher.subscribe(object : Subscriber { + private var s: Subscription? = null + + override fun onSubscribe(s: Subscription) { + this.s = s + for (i in 0 until n) { + commonPool().execute { s.request(1) } + } + } + + override fun onNext(aLong: Long) { + collected.add(aLong) + } + + override fun onError(t: Throwable) { + + } + + override fun onComplete() { + latch.countDown() + } + }) + + latch.await(50, TimeUnit.SECONDS) + assertEquals(array.toList(), collected) + } + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() { + // This test has a bug in it + } +} diff --git a/reactive/kotlinx-coroutines-reactive/test/flow/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-reactive/test/flow/PublisherAsFlowTest.kt new file mode 100644 index 0000000000..6c3501df1c --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/test/flow/PublisherAsFlowTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import kotlin.test.* + +class PublisherAsFlowTest : TestBase() { + + @Test + fun testCancellation() = runTest { + var onNext = 0 + var onCancelled = 0 + var onError = 0 + + val publisher = publish { + coroutineContext[Job]?.invokeOnCompletion { + if (it is CancellationException) ++onCancelled + } + + repeat(100) { + send(it) + } + } + + publisher.asFlow().launchIn(CoroutineScope(Dispatchers.Unconfined)) { + onEach { + ++onNext + throw RuntimeException() + } + catch { + ++onError + } + }.join() + + + assertEquals(1, onNext) + assertEquals(1, onError) + assertEquals(1, onCancelled) + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactive/test/flow/RangePublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/flow/RangePublisherTest.kt new file mode 100644 index 0000000000..1b37ee9974 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/test/flow/RangePublisherTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive.flow + +import org.junit.* +import org.reactivestreams.* +import org.reactivestreams.example.unicast.* +import org.reactivestreams.tck.* + +class RangePublisherTest : PublisherVerification(TestEnvironment(50, 50)) { + + override fun createPublisher(elements: Long): Publisher { + return RangePublisher(1, elements.toInt()).asFlow().asPublisher() + } + + override fun createFailedPublisher(): Publisher? { + return null + } + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } +} + +class RangePublisherWrappedTwiceTest : PublisherVerification(TestEnvironment(50, 50)) { + + override fun createPublisher(elements: Long): Publisher { + return RangePublisher(1, elements.toInt()).asFlow().asPublisher().asFlow().asPublisher() + } + + override fun createFailedPublisher(): Publisher? { + return null + } + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactive/test/flow/UnboundedIntegerIncrementPublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/flow/UnboundedIntegerIncrementPublisherTest.kt new file mode 100644 index 0000000000..9e611008c2 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/test/flow/UnboundedIntegerIncrementPublisherTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive.flow + +import org.junit.* +import org.reactivestreams.example.unicast.AsyncIterablePublisher +import org.reactivestreams.Publisher +import org.reactivestreams.example.unicast.InfiniteIncrementNumberPublisher +import org.reactivestreams.tck.TestEnvironment +import java.util.concurrent.Executors +import java.util.concurrent.ExecutorService +import org.reactivestreams.tck.PublisherVerification +import org.testng.annotations.AfterClass +import org.testng.annotations.BeforeClass +import org.testng.annotations.Test + +@Test +class UnboundedIntegerIncrementPublisherTest : PublisherVerification(TestEnvironment()) { + + private var e: ExecutorService? = null + + @BeforeClass + internal fun before() { + e = Executors.newFixedThreadPool(4) + } + + @AfterClass + internal fun after() { + if (e != null) e!!.shutdown() + } + + override fun createPublisher(elements: Long): Publisher { + return InfiniteIncrementNumberPublisher(e!!).asFlow().asPublisher() + } + + override fun createFailedPublisher(): Publisher { + return AsyncIterablePublisher(object : Iterable { + override fun iterator(): Iterator { + throw RuntimeException("Error state signal!") + } + }, e!!) + } + + override fun maxElementsFromPublisher(): Long { + return super.publisherUnableToSignalOnComplete() + } + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } +} From 2cbcd05a9d8958604691d29e17b26b06eccc2675 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 4 Apr 2019 14:27:11 +0300 Subject: [PATCH 5/5] Version 1.2.0-alpha-2 --- CHANGES.md | 8 +++++++- README.md | 10 +++++----- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 4 ++-- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3205844b74..4d9509982f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Change log for kotlinx.coroutines +## Version 1.2.0-alpha-2 + +This release contains major [feature preview](/docs/compatibility.md#flow-preview-api): cold streams aka `Flow` (#254). + +Performance: +* Performance of `Dispatcher.Main` initialization is significantly improved (#878). + ## Version 1.2.0-alpha * Major debug agent improvements. Real stacktraces are merged with coroutine stacktraces for running coroutines, merging heuristic is improved, API is cleaned up and is on its road to stabilization (#997). @@ -14,7 +21,6 @@ * `withContext` checks cancellation on entering (#962). * Operator `invoke` on `CoroutineDispatcher` (#428). * Java 8 extensions for `delay` and `withTimeout` now properly handle too large values (#428). -* Performance of `Dispatcher.Main` initialization is significantly improved (#878). * A global exception handler for fatal exceptions in coroutines is introduced (#808, #773). * Major improvements in cancellation machinery and exceptions delivery consistency. Cancel with custom exception is completely removed. * Kotlin version is updated to 1.3.21. diff --git a/README.md b/README.md index e454e09340..d07ab3446d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.2.0-alpha) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.2.0-alpha) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.2.0-alpha-2) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.2.0-alpha-2) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. This is a companion version for Kotlin `1.3.21` release. @@ -75,7 +75,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.2.0-alpha + 1.2.0-alpha-2 ``` @@ -93,7 +93,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha-2' } ``` @@ -119,7 +119,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha-2") } ``` @@ -147,7 +147,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha-2' ``` This gives you access to Android [Dispatchers.Main](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-android/kotlinx.coroutines.android/kotlinx.coroutines.-dispatchers/index.html) coroutine dispatcher and also makes sure that in case of crashed coroutine with unhandled exception this diff --git a/gradle.properties b/gradle.properties index 69198219fe..745fb5befc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kotlin -version=1.2.0-alpha-SNAPSHOT +version=1.2.0-alpha-2-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.3.21 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index b8475f19f4..01f1cde0aa 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -18,7 +18,7 @@ of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances usin Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.0-alpha' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.0-alpha-2' } ``` @@ -57,7 +57,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent It is possible to use this module 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.2.0-alpha.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.2.0-alpha-2.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 8259e891ef..45f86c5572 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -7,7 +7,7 @@ Test utilities for `kotlinx.coroutines`. Provides `Dispatchers.setMain` to overr Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.0-alpha' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.0-alpha-2' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 296ccc8779..5ae8a5a999 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -165,7 +165,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha-2" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 320e02744f..aff1936f07 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -19,5 +19,5 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable kotlin_version=1.3.21 -coroutines_version=1.2.0-alpha +coroutines_version=1.2.0-alpha-2 diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 320e02744f..aff1936f07 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -19,5 +19,5 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable kotlin_version=1.3.21 -coroutines_version=1.2.0-alpha +coroutines_version=1.2.0-alpha-2