mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Fix copy paste regressions
This commit is contained in:
@@ -25,7 +25,8 @@ class DynamicStateFlow<T>(
|
||||
loggingTag: String? = null,
|
||||
parentScope: CoroutineScope,
|
||||
coroutineContext: CoroutineContext = parentScope.coroutineContext,
|
||||
private val startValueProvider: suspend CoroutineScope.() -> T
|
||||
private val onRelease: CoroutineScope.(T) -> Unit = {},
|
||||
private val startValueProvider: suspend CoroutineScope.() -> T,
|
||||
) {
|
||||
private val lTag = loggingTag?.let { "$it:DSFlow" }
|
||||
|
||||
@@ -47,6 +48,12 @@ class DynamicStateFlow<T>(
|
||||
}
|
||||
}
|
||||
|
||||
invokeOnClose {
|
||||
lTag?.let { log(it, VERBOSE) { "invokeOnClose executing..." } }
|
||||
onRelease(currentValue)
|
||||
lTag?.let { log(it, VERBOSE) { "internal channelFlow finished." } }
|
||||
}
|
||||
|
||||
updateActions.collect { update ->
|
||||
currentValue = valueGuard.withLock {
|
||||
try {
|
||||
@@ -68,12 +75,11 @@ class DynamicStateFlow<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lTag?.let { log(it, VERBOSE) { "internal channelFlow finished." } }
|
||||
}
|
||||
|
||||
private val internalFlow = producer
|
||||
.onStart { lTag?.let { log(it, VERBOSE) { "Internal onStart" } } }
|
||||
// .onEach { value -> lTag?.let { log(it, VERBOSE) { "New value: $value" } } }
|
||||
.onCompletion { err ->
|
||||
when {
|
||||
err is CancellationException -> {
|
||||
|
||||
@@ -11,16 +11,15 @@ import io.mockk.mockk
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.test.createTestCoroutineScope
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
import testhelper.coroutine.runBlockingTest2
|
||||
import testhelper.coroutine.runTest2
|
||||
import testhelper.flow.test
|
||||
import java.io.IOException
|
||||
import java.lang.Thread.sleep
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
@@ -62,7 +61,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
)
|
||||
|
||||
testScope.apply {
|
||||
runBlockingTest2(allowUncompleted = true) {
|
||||
runTest2(autoCancel = true) {
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
}
|
||||
@@ -83,7 +82,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..16).forEach { _ ->
|
||||
@@ -123,7 +122,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
@@ -159,7 +158,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
startValueProvider = { "1" },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
hotData.updateAsync { "1" }
|
||||
@@ -174,40 +173,39 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple subscribers share the flow`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
fun `multiple subscribers share the flow`() = runTest2(autoCancel = true) {
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
parentScope = this,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
testScope.runBlockingTest2(allowUncompleted = true) {
|
||||
val sub1 = hotData.flow.test(tag = "sub1", startOnScope = this)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", startOnScope = this)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", startOnScope = this)
|
||||
val sub1 = hotData.flow.test(tag = "sub1", scope = this)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", scope = this)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", scope = this)
|
||||
|
||||
hotData.updateAsync { "A" }
|
||||
hotData.updateAsync { "B" }
|
||||
hotData.updateAsync { "C" }
|
||||
hotData.updateAsync { "A" }
|
||||
hotData.updateAsync { "B" }
|
||||
hotData.updateAsync { "C" }
|
||||
|
||||
listOf(sub1, sub2, sub3).forEach {
|
||||
it.await { list, _ -> list.size == 4 }
|
||||
it.latestValues shouldBe listOf("Test", "A", "B", "C")
|
||||
it.cancel()
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
hotData.flow.first() shouldBe "C"
|
||||
listOf(sub1, sub2, sub3).forEach {
|
||||
it.await { list, _ -> list.size == 4 }
|
||||
it.latestValues shouldBe listOf("Test", "A", "B", "C")
|
||||
it.cancelAndJoin()
|
||||
}
|
||||
|
||||
hotData.flow.first() shouldBe "C"
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value is persisted between unsubscribes`() = runBlockingTest2(allowUncompleted = true) {
|
||||
fun `value is persisted between unsubscribes`() = runTest2(autoCancel = true) {
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
@@ -218,7 +216,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", startOnScope = this)
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", scope = this)
|
||||
testCollector1.silent = false
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
@@ -232,16 +230,14 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
testCollector1.await { list, _ -> list.size == 11 }
|
||||
testCollector1.latestValues shouldBe (1L..11L).toList()
|
||||
|
||||
testCollector1.cancel()
|
||||
testCollector1.awaitFinal()
|
||||
testCollector1.cancelAndJoin()
|
||||
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", startOnScope = this)
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", scope = this)
|
||||
testCollector2.silent = false
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector2.cancel()
|
||||
testCollector2.awaitFinal()
|
||||
testCollector2.cancelAndJoin()
|
||||
|
||||
testCollector2.latestValues shouldBe listOf(11L)
|
||||
|
||||
@@ -267,7 +263,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
this + 1
|
||||
}
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
@@ -276,7 +272,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
testCollector.await { _, i -> i == 3 }
|
||||
testCollector.latestValues shouldBe listOf(2, 3, 0)
|
||||
|
||||
testCollector.cancel()
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -293,7 +289,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
},
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
@@ -307,30 +303,23 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
|
||||
testCollector.cancel()
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates error handler`() {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
|
||||
fun `async updates error handler`() = runTest2(expectedError = IOException::class) {
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
parentScope = this,
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testScope.advanceUntilIdle()
|
||||
val testCollector = hotData.flow.test(scope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
hotData.updateAsync { throw IOException("Surprise") }
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class)
|
||||
|
||||
testCollector.cancel()
|
||||
advanceUntilIdle()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -344,7 +333,7 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
var thrownError: Exception? = null
|
||||
@@ -358,6 +347,30 @@ class DynamicStateFlowTest : BaseTest() {
|
||||
thrownError!!.shouldBeInstanceOf<IOException>()
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
|
||||
testCollector.cancel()
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean up function is called when parent scope is cancelled`() = runTest {
|
||||
val testScope =
|
||||
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
|
||||
var onReleaseValue: String? = null
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { "Test" },
|
||||
onRelease = {
|
||||
onReleaseValue = it
|
||||
}
|
||||
)
|
||||
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
|
||||
testScope.cancel()
|
||||
|
||||
onReleaseValue shouldBe "Test"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,38 @@
|
||||
package testhelper.coroutine
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.UncompletedCoroutinesError
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* If you have a test that uses a coroutine that never stops, you may use this.
|
||||
*/
|
||||
|
||||
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
|
||||
fun TestCoroutineScope.runBlockingTest2(
|
||||
allowUncompleted: Boolean = false,
|
||||
block: suspend TestCoroutineScope.() -> Unit
|
||||
): Unit = runBlockingTest2(
|
||||
allowUncompleted = allowUncompleted,
|
||||
context = coroutineContext,
|
||||
testBody = block
|
||||
)
|
||||
|
||||
fun runBlockingTest2(
|
||||
allowUncompleted: Boolean = false,
|
||||
fun runTest2(
|
||||
autoCancel: Boolean = false,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
testBody: suspend TestCoroutineScope.() -> Unit
|
||||
expectedError: KClass<out Throwable>? = null,
|
||||
testBody: suspend TestScope.() -> Unit
|
||||
) {
|
||||
try {
|
||||
runBlocking {
|
||||
try {
|
||||
runBlockingTest(
|
||||
context = context,
|
||||
testBody = testBody
|
||||
)
|
||||
} catch (e: UncompletedCoroutinesError) {
|
||||
if (!allowUncompleted) throw e
|
||||
else log { "Ignoring active job." }
|
||||
val scope = TestScope(context = context)
|
||||
try {
|
||||
scope.runTest {
|
||||
testBody()
|
||||
if (autoCancel) scope.cancel("autoCancel")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val isExpected = expectedError?.isInstance(e) ?: false
|
||||
if (!isExpected) throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (!allowUncompleted || (e.message != "This job has not completed yet")) {
|
||||
} catch (e: CancellationException) {
|
||||
if (e.message == "autoCancel" && autoCancel) {
|
||||
io.kotest.mpp.log { "Test was auto-cancelled ${e.asLog()}" }
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,14 +8,11 @@ import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.test.createTestCoroutineScope
|
||||
|
||||
fun <T> Flow<T>.test(
|
||||
tag: String? = null,
|
||||
startOnScope: CoroutineScope = createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
|
||||
): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = startOnScope)
|
||||
scope: CoroutineScope
|
||||
): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = scope)
|
||||
|
||||
fun <T> Flow<T>.createTest(
|
||||
tag: String? = null
|
||||
@@ -80,7 +77,7 @@ class TestCollector<T>(
|
||||
}
|
||||
|
||||
suspend fun awaitFinal(cancel: Boolean = false) = apply {
|
||||
if (cancel) cancel()
|
||||
if (cancel) job.cancel()
|
||||
try {
|
||||
job.join()
|
||||
} catch (e: Exception) {
|
||||
@@ -93,11 +90,9 @@ class TestCollector<T>(
|
||||
require(error == null) { "Error was not null: $error" }
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
suspend fun cancelAndJoin() {
|
||||
if (job.isCompleted) throw IllegalStateException("Flow is already canceled.")
|
||||
|
||||
runBlocking {
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:${Versions.Gradle.buildTools}")
|
||||
classpath("com.android.tools.build:gradle:7.2.2")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
||||
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
||||
|
||||
Reference in New Issue
Block a user