diff --git a/mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreTest.kt b/mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreGenericTests.kt similarity index 91% rename from mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreTest.kt rename to mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreGenericTests.kt index 4269157c..9fa64367 100644 --- a/mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreTest.kt +++ b/mvikotlin-main/src/commonTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreGenericTests.kt @@ -5,7 +5,7 @@ import com.arkivanov.mvikotlin.core.utils.isAssertOnMainThreadEnabled import kotlin.test.AfterTest import kotlin.test.BeforeTest -class DefaultStoreTest : StoreGenericTests by StoreGenericTests( +class DefaultStoreGenericTests : StoreGenericTests( storeFactory = { initialState, bootstrapper, executorFactory, reducer -> DefaultStore( initialState = initialState, diff --git a/mvikotlin-main/src/darwinTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreThreadingTests.kt b/mvikotlin-main/src/darwinTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreThreadingTests.kt new file mode 100644 index 00000000..17835738 --- /dev/null +++ b/mvikotlin-main/src/darwinTest/kotlin/com/arkivanov/mvikotlin/main/store/DefaultStoreThreadingTests.kt @@ -0,0 +1,17 @@ +package com.arkivanov.mvikotlin.main.store + +import com.arkivanov.mvikotlin.core.test.internal.StoreThreadingTests +import com.arkivanov.mvikotlin.core.utils.isAssertOnMainThreadEnabled +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +class DefaultStoreThreadingTests : StoreThreadingTests( + storeFactory = { initialState, bootstrapper, executorFactory, reducer -> + DefaultStore( + initialState = initialState, + bootstrapper = bootstrapper, + executor = executorFactory(), + reducer = reducer + ) + } +) diff --git a/mvikotlin-test-internal/build.gradle.kts b/mvikotlin-test-internal/build.gradle.kts index e3980530..eecb6da0 100644 --- a/mvikotlin-test-internal/build.gradle.kts +++ b/mvikotlin-test-internal/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { dependencies { implementation(project(":mvikotlin")) implementation(project(":rx")) + implementation(project(":rx-internal")) implementation(project(":utils-internal")) implementation(deps.kotlin.kotlinTestCommon) implementation(deps.kotlin.kotlinTestAnnotationsCommon) diff --git a/mvikotlin-test-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreGenericTests.kt b/mvikotlin-test-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreGenericTests.kt index c1a3c1a6..30347a7b 100644 --- a/mvikotlin-test-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreGenericTests.kt +++ b/mvikotlin-test-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreGenericTests.kt @@ -15,464 +15,402 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("FunctionName") -interface StoreGenericTests { - - @Test - fun state_val_returns_initial_state_WHEN_created() +@Suppress("FunctionName", "UnnecessaryAbstractClass") +abstract class StoreGenericTests( + private val storeFactory: ( + initialState: String, + bootstrapper: Bootstrapper?, + executorFactory: () -> Executor, + reducer: Reducer + ) -> Store +) { @Test - fun initializes_bootstrapper_WHEN_created() + fun state_val_returns_initial_state_WHEN_created() { + val store = store(initialState = "initial") - @Test - fun calls_bootstrapper_after_initialization_WHEN_created() + val state = store.state - @Test - fun initializes_executor_WHEN_with_bootstrapper_and_created() + assertEquals("initial", state) + } @Test - fun initializes_executor_WHEN_without_bootstrapper_and_created() + fun initializes_bootstrapper_WHEN_created() { + val bootstrapper = TestBootstrapper() - @Test - fun initializes_executor_before_bootstrapper_call_WHEN_with_bootstrapper_and_created() + store(bootstrapper = bootstrapper) - @Test - fun delivers_actions_from_bootstrapper_to_executor_after_bootstrap() + assertTrue(bootstrapper.isInitialized) + } @Test - fun delivers_actions_from_bootstrapper_to_executor_during_bootstrap() + fun calls_bootstrapper_after_initialization_WHEN_created() { + var events by atomic(emptyList()) - @Test - fun does_not_deliver_actions_from_bootstrapper_to_executor_WHEN_disposed_and_bootstrapper_produced_actions() + store(bootstrapper = TestBootstrapper(init = { events = events + "init" }, invoke = { events = events + "invoke" })) - @Test - fun produces_labels_from_executor() + assertEquals(listOf("init", "invoke"), events) + } @Test - fun does_not_produce_labels_from_executor_to_unsubscribed_observer() + fun initializes_executor_WHEN_with_bootstrapper_and_created() { + val executor = TestExecutor() - @Test - fun delivers_intents_to_executor() + store(bootstrapper = TestBootstrapper(), executorFactory = { executor }) - @Test - fun does_not_deliver_intents_to_executor_WHEN_disposed_and_new_intents() + assertTrue(executor.isInitialized) + } @Test - fun executor_can_read_initial_state() + fun initializes_executor_WHEN_without_bootstrapper_and_created() { + val executor = TestExecutor() - @Test - fun executor_can_read_new_state_WHEN_state_changed() + store(executorFactory = { executor }) - @Test - fun delivers_messages_from_executor_to_reducer() + assertTrue(executor.isInitialized) + } @Test - fun state_val_returns_new_state_WHEN_new_state_returned_from_reducer() + fun initializes_executor_before_bootstrapper_call_WHEN_with_bootstrapper_and_created() { + var events by atomic(emptyList()) - @Test - fun executor_can_read_new_state_WHEN_new_state_returned_from_reducer() + store( + bootstrapper = TestBootstrapper(init = { events = events + "bootstrapper" }), + executorFactory = { TestExecutor(init = { events = events + "executor" }) } + ) - @Test - fun bootstrapper_disposed_WHEN_store_disposed() + assertEquals(listOf("executor", "bootstrapper"), events) + } @Test - fun executor_disposed_WHEN_store_disposed() + fun delivers_actions_from_bootstrapper_to_executor_after_bootstrap() { + var actions by atomic(emptyList()) + val bootstrapper = TestBootstrapper() - @Test - fun states_observers_completed_WHEN_store_disposed() + store( + bootstrapper = bootstrapper, + executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } + ) - @Test - fun labels_observers_completed_WHEN_store_disposed() + bootstrapper.dispatch("action1") + bootstrapper.dispatch("action2") - @Test - fun states_observers_disposables_disposed_WHEN_store_disposed() + assertEquals(listOf("action1", "action2"), actions) + } @Test - fun labels_observers_disposables_disposed_WHEN_store_disposed() + fun delivers_actions_from_bootstrapper_to_executor_during_bootstrap() { + var actions by atomic(emptyList()) + + store( + bootstrapper = TestBootstrapper { + dispatch("action1") + dispatch("action2") + }, + executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } + ) + + assertEquals(listOf("action1", "action2"), actions) + } @Test - fun store_isDisposed_returns_true_WHEN_store_disposed() + fun does_not_deliver_actions_from_bootstrapper_to_executor_WHEN_disposed_and_bootstrapper_produced_actions() { + var actions by atomic(emptyList()) + val bootstrapper = TestBootstrapper() - @Test - fun states_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() + val store = + store( + bootstrapper = bootstrapper, + executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } + ) - @Test - fun labels_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() + store.dispose() + bootstrapper.dispatch("action1") + bootstrapper.dispatch("action2") - @Test - fun executor_not_called_WHEN_recursive_intent_on_label() + assertEquals(emptyList(), actions) + } @Test - fun executor_called_WHEN_recursive_intent_on_label_and_first_intent_processed() -} + fun produces_labels_from_executor() { + var labels by atomic(emptyList()) + val executor = TestExecutor() -@Suppress("FunctionName") -fun StoreGenericTests( - storeFactory: ( - initialState: String, - bootstrapper: Bootstrapper?, - executorFactory: () -> Executor, - reducer: Reducer - ) -> Store -): StoreGenericTests = - object : StoreGenericTests { - override fun state_val_returns_initial_state_WHEN_created() { - val store = store(initialState = "initial") + val store = store(executorFactory = { executor }) + store.labels(observer(onNext = { labels = labels + it })) - val state = store.state + executor.publish("label1") + executor.publish("label2") - assertEquals("initial", state) - } + assertEquals(listOf("label1", "label2"), labels) + } - override fun initializes_bootstrapper_WHEN_created() { - val bootstrapper = TestBootstrapper() + @Test + fun does_not_produce_labels_from_executor_to_unsubscribed_observer() { + var labels by atomic(emptyList()) + val executor = TestExecutor() - store(bootstrapper = bootstrapper) + val store = store(executorFactory = { executor }) + store.labels(observer(onNext = { labels = labels + it })).dispose() - assertTrue(bootstrapper.isInitialized) - } + executor.publish("label1") + executor.publish("label2") - override fun calls_bootstrapper_after_initialization_WHEN_created() { - var events by atomic(emptyList()) + assertEquals(emptyList(), labels) + } - store(bootstrapper = TestBootstrapper(init = { events = events + "init" }, invoke = { events = events + "invoke" })) + @Test + fun delivers_intents_to_executor() { + var intents by atomic(emptyList()) + val store = store(executorFactory = { TestExecutor(executeIntent = { intents = intents + it }) }) - assertEquals(listOf("init", "invoke"), events) - } + store.accept("intent1") + store.accept("intent2") - override fun initializes_executor_WHEN_with_bootstrapper_and_created() { - val executor = TestExecutor() + assertEquals(listOf("intent1", "intent2"), intents) + } - store(bootstrapper = TestBootstrapper(), executorFactory = { executor }) + @Test + fun does_not_deliver_intents_to_executor_WHEN_disposed_and_new_intents() { + var intents by atomic(emptyList()) + val store = store(executorFactory = { TestExecutor(executeIntent = { intents = intents + it }) }) - assertTrue(executor.isInitialized) - } + store.dispose() + store.accept("intent1") + store.accept("intent2") - override fun initializes_executor_WHEN_without_bootstrapper_and_created() { - val executor = TestExecutor() + assertEquals(emptyList(), intents) + } - store(executorFactory = { executor }) + @Test + fun executor_can_read_initial_state() { + val executor = TestExecutor() + store(initialState = "initial", executorFactory = { executor }) - assertTrue(executor.isInitialized) - } + assertEquals("initial", executor.state) + } - override fun initializes_executor_before_bootstrapper_call_WHEN_with_bootstrapper_and_created() { - var events by atomic(emptyList()) + @Test + fun executor_can_read_new_state_WHEN_state_changed() { + val executor = TestExecutor() + store(executorFactory = { executor }, reducer = reducer { it }) - store( - bootstrapper = TestBootstrapper(init = { events = events + "bootstrapper" }), - executorFactory = { TestExecutor(init = { events = events + "executor" }) } - ) + executor.dispatch("message") - assertEquals(listOf("executor", "bootstrapper"), events) - } + assertEquals("message", executor.state) + } - override fun delivers_actions_from_bootstrapper_to_executor_after_bootstrap() { - var actions by atomic(emptyList()) - val bootstrapper = TestBootstrapper() + @Test + fun delivers_messages_from_executor_to_reducer() { + var messages by atomic(emptyList()) + val executor = TestExecutor() + store( + executorFactory = { executor }, + reducer = reducer { + messages = messages + it + this + } + ) + + executor.dispatch("message1") + executor.dispatch("message2") + + assertEquals(listOf("message1", "message2"), messages) + } + @Test + fun state_val_returns_new_state_WHEN_new_state_returned_from_reducer() { + val executor = TestExecutor() + val store = store( - bootstrapper = bootstrapper, - executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } + executorFactory = { executor }, + reducer = reducer { it } ) - bootstrapper.dispatch("action1") - bootstrapper.dispatch("action2") - - assertEquals(listOf("action1", "action2"), actions) - } + executor.dispatch("message") - override fun delivers_actions_from_bootstrapper_to_executor_during_bootstrap() { - var actions by atomic(emptyList()) + assertEquals("message", store.state) + } - store( - bootstrapper = TestBootstrapper { - dispatch("action1") - dispatch("action2") - }, - executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } - ) + @Test + fun executor_can_read_new_state_WHEN_new_state_returned_from_reducer() { + val executor = TestExecutor() + store( + executorFactory = { executor }, + reducer = reducer { it } + ) - assertEquals(listOf("action1", "action2"), actions) - } + executor.dispatch("message") - override fun does_not_deliver_actions_from_bootstrapper_to_executor_WHEN_disposed_and_bootstrapper_produced_actions() { - var actions by atomic(emptyList()) - val bootstrapper = TestBootstrapper() + assertEquals("message", executor.state) + } - val store = - store( - bootstrapper = bootstrapper, - executorFactory = { TestExecutor(executeAction = { actions = actions + it }) } - ) + @Test + fun bootstrapper_disposed_WHEN_store_disposed() { + val bootstrapper = TestBootstrapper() + val store = store(bootstrapper = bootstrapper) - store.dispose() - bootstrapper.dispatch("action1") - bootstrapper.dispatch("action2") + store.dispose() - assertEquals(emptyList(), actions) - } + assertTrue(bootstrapper.isDisposed) + } - override fun produces_labels_from_executor() { - var labels by atomic(emptyList()) - val executor = TestExecutor() + @Test + fun executor_disposed_WHEN_store_disposed() { + val executor = TestExecutor() + val store = store(executorFactory = { executor }) - val store = store(executorFactory = { executor }) - store.labels(observer(onNext = { labels = labels + it })) + store.dispose() - executor.publish("label1") - executor.publish("label2") + assertTrue(executor.isDisposed) + } - assertEquals(listOf("label1", "label2"), labels) - } + @Test + fun states_observers_completed_WHEN_store_disposed() { + var isCompleted1 by atomic(false) + var isCompleted2 by atomic(false) + val store = store() + store.states(observer(onComplete = { isCompleted1 = true })) + store.states(observer(onComplete = { isCompleted2 = true })) - override fun does_not_produce_labels_from_executor_to_unsubscribed_observer() { - var labels by atomic(emptyList()) - val executor = TestExecutor() + store.dispose() - val store = store(executorFactory = { executor }) - store.labels(observer(onNext = { labels = labels + it })).dispose() + assertTrue(isCompleted1) + assertTrue(isCompleted2) + } - executor.publish("label1") - executor.publish("label2") + @Test + fun labels_observers_completed_WHEN_store_disposed() { + var isCompleted1 by atomic(false) + var isCompleted2 by atomic(false) + val store = store() + store.labels(observer(onComplete = { isCompleted1 = true })) + store.labels(observer(onComplete = { isCompleted2 = true })) - assertEquals(emptyList(), labels) - } + store.dispose() - override fun delivers_intents_to_executor() { - var intents by atomic(emptyList()) - val store = store(executorFactory = { TestExecutor(executeIntent = { intents = intents + it }) }) + assertTrue(isCompleted1) + assertTrue(isCompleted2) + } - store.accept("intent1") - store.accept("intent2") + @Test + fun states_observers_disposables_disposed_WHEN_store_disposed() { + val store = store() + val disposable1 = store.states(observer()) + val disposable2 = store.states(observer()) - assertEquals(listOf("intent1", "intent2"), intents) - } + store.dispose() - override fun does_not_deliver_intents_to_executor_WHEN_disposed_and_new_intents() { - var intents by atomic(emptyList()) - val store = store(executorFactory = { TestExecutor(executeIntent = { intents = intents + it }) }) + assertTrue(disposable1.isDisposed) + assertTrue(disposable2.isDisposed) + } - store.dispose() - store.accept("intent1") - store.accept("intent2") + @Test + fun labels_observers_disposables_disposed_WHEN_store_disposed() { + val store = store() + val disposable1 = store.labels(observer()) + val disposable2 = store.labels(observer()) - assertEquals(emptyList(), intents) - } + store.dispose() - override fun executor_can_read_initial_state() { - val executor = TestExecutor() - store(initialState = "initial", executorFactory = { executor }) + assertTrue(disposable1.isDisposed) + assertTrue(disposable2.isDisposed) + } - assertEquals("initial", executor.state) - } + @Test + fun store_isDisposed_returns_true_WHEN_store_disposed() { + val store = store() - override fun executor_can_read_new_state_WHEN_state_changed() { - val executor = TestExecutor() - store(executorFactory = { executor }, reducer = reducer { it }) + store.dispose() - executor.dispatch("message") + assertTrue(store.isDisposed) + } - assertEquals("message", executor.state) - } + @Test + fun states_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() { + val store = store() - override fun delivers_messages_from_executor_to_reducer() { - var messages by atomic(emptyList()) - val executor = TestExecutor() - store( - executorFactory = { executor }, - reducer = reducer { - messages = messages + it - this - } - ) + val list = ArrayList() + store.states(observer { list += it }) - executor.dispatch("message1") - executor.dispatch("message2") + assertFalse(list.isFrozen) + } - assertEquals(listOf("message1", "message2"), messages) - } + @Test + fun labels_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() { + val store = store() - override fun state_val_returns_new_state_WHEN_new_state_returned_from_reducer() { - val executor = TestExecutor() - val store = - store( - executorFactory = { executor }, - reducer = reducer { it } - ) + val list = ArrayList() + store.labels(observer { list += it }) - executor.dispatch("message") + assertFalse(list.isFrozen) + } - assertEquals("message", store.state) - } + @Test + fun executor_not_called_WHEN_recursive_intent_on_label() { + var isProcessingIntent by atomic(false) + var isCalledRecursively by atomic(false) - override fun executor_can_read_new_state_WHEN_new_state_returned_from_reducer() { - val executor = TestExecutor() + val store = store( - executorFactory = { executor }, + executorFactory = { + TestExecutor( + executeIntent = { + if (it == "intent1") { + isProcessingIntent = true + publish("label") + isProcessingIntent = false + } else { + isCalledRecursively = isProcessingIntent + } + } + ) + }, reducer = reducer { it } ) - executor.dispatch("message") - - assertEquals("message", executor.state) - } - - override fun bootstrapper_disposed_WHEN_store_disposed() { - val bootstrapper = TestBootstrapper() - val store = store(bootstrapper = bootstrapper) - - store.dispose() + store.labels(observer { store.accept("intent2") }) + store.accept("intent1") - assertTrue(bootstrapper.isDisposed) - } - - override fun executor_disposed_WHEN_store_disposed() { - val executor = TestExecutor() - val store = store(executorFactory = { executor }) - - store.dispose() - - assertTrue(executor.isDisposed) - } - - override fun states_observers_completed_WHEN_store_disposed() { - var isCompleted1 by atomic(false) - var isCompleted2 by atomic(false) - val store = store() - store.states(observer(onComplete = { isCompleted1 = true })) - store.states(observer(onComplete = { isCompleted2 = true })) - - store.dispose() - - assertTrue(isCompleted1) - assertTrue(isCompleted2) - } - - override fun labels_observers_completed_WHEN_store_disposed() { - var isCompleted1 by atomic(false) - var isCompleted2 by atomic(false) - val store = store() - store.labels(observer(onComplete = { isCompleted1 = true })) - store.labels(observer(onComplete = { isCompleted2 = true })) - - store.dispose() - - assertTrue(isCompleted1) - assertTrue(isCompleted2) - } - - override fun states_observers_disposables_disposed_WHEN_store_disposed() { - val store = store() - val disposable1 = store.states(observer()) - val disposable2 = store.states(observer()) - - store.dispose() - - assertTrue(disposable1.isDisposed) - assertTrue(disposable2.isDisposed) - } - - override fun labels_observers_disposables_disposed_WHEN_store_disposed() { - val store = store() - val disposable1 = store.labels(observer()) - val disposable2 = store.labels(observer()) - - store.dispose() - - assertTrue(disposable1.isDisposed) - assertTrue(disposable2.isDisposed) - } - - override fun store_isDisposed_returns_true_WHEN_store_disposed() { - val store = store() - - store.dispose() - - assertTrue(store.isDisposed) - } - - override fun states_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() { - val store = store() - - val list = ArrayList() - store.states(observer { list += it }) - - assertFalse(list.isFrozen) - } - - override fun labels_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() { - val store = store() + assertFalse(isCalledRecursively) + } - val list = ArrayList() - store.labels(observer { list += it }) + @Test + fun executor_called_WHEN_recursive_intent_on_label_and_first_intent_processed() { + var isProcessingIntent by atomic(false) + var isCalledAfter by atomic(false) - assertFalse(list.isFrozen) - } + val store = + store( + executorFactory = { + TestExecutor( + executeIntent = { + if (it == "intent1") { + isProcessingIntent = true + publish("label") + isProcessingIntent = false + } else { + isCalledAfter = !isProcessingIntent + } + } + ) + }, + reducer = reducer { it } + ) - override fun executor_not_called_WHEN_recursive_intent_on_label() { - var isProcessingIntent by atomic(false) - var isCalledRecursively by atomic(false) + store.labels(observer { store.accept("intent2") }) + store.accept("intent1") - val store = - store( - executorFactory = { - TestExecutor( - executeIntent = { - if (it == "intent1") { - isProcessingIntent = true - publish("label") - isProcessingIntent = false - } else { - isCalledRecursively = isProcessingIntent - } - } - ) - }, - reducer = reducer { it } - ) - - store.labels(observer { store.accept("intent2") }) - store.accept("intent1") - - assertFalse(isCalledRecursively) - } - - override fun executor_called_WHEN_recursive_intent_on_label_and_first_intent_processed() { - var isProcessingIntent by atomic(false) - var isCalledAfter by atomic(false) - - val store = - store( - executorFactory = { - TestExecutor( - executeIntent = { - if (it == "intent1") { - isProcessingIntent = true - publish("label") - isProcessingIntent = false - } else { - isCalledAfter = !isProcessingIntent - } - } - ) - }, - reducer = reducer { it } - ) - - store.labels(observer { store.accept("intent2") }) - store.accept("intent1") - - assertTrue(isCalledAfter) - } - - private fun store( - initialState: String = "initial_state", - bootstrapper: Bootstrapper? = null, - executorFactory: () -> Executor = { TestExecutor() }, - reducer: Reducer = reducer() - ): Store = - storeFactory(initialState, bootstrapper, executorFactory, reducer) - .freeze() - .apply { init() } + assertTrue(isCalledAfter) } + + private fun store( + initialState: String = "initial_state", + bootstrapper: Bootstrapper? = null, + executorFactory: () -> Executor = { TestExecutor() }, + reducer: Reducer = reducer() + ): Store = + storeFactory(initialState, bootstrapper, executorFactory, reducer) + .freeze() + .apply { init() } +} diff --git a/mvikotlin-test-internal/src/darwinMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreThreadingTests.kt b/mvikotlin-test-internal/src/darwinMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreThreadingTests.kt new file mode 100644 index 00000000..dd54abba --- /dev/null +++ b/mvikotlin-test-internal/src/darwinMain/kotlin/com/arkivanov/mvikotlin/core/test/internal/StoreThreadingTests.kt @@ -0,0 +1,38 @@ +package com.arkivanov.mvikotlin.core.test.internal + +import com.arkivanov.mvikotlin.core.store.Bootstrapper +import com.arkivanov.mvikotlin.core.store.Executor +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.utils.internal.assertOnMainThread +import com.arkivanov.mvikotlin.utils.internal.freeze +import com.arkivanov.mvikotlin.utils.internal.runOnBackgroundBlocking +import kotlin.test.Test + +@Suppress("FunctionName", "UnnecessaryAbstractClass") +abstract class StoreThreadingTests( + private val storeFactory: ( + initialState: String, + bootstrapper: Bootstrapper?, + executorFactory: () -> Executor, + reducer: Reducer + ) -> Store +) { + + @Test + fun GIVEN_store_created_on_background_thread_WHEN_init_on_main_thread_THEN_no_crash() { + assertOnMainThread() + + val store = runOnBackgroundBlocking { store() } + store.init() + } + + private fun store( + initialState: String = "initial_state", + bootstrapper: Bootstrapper? = null, + executorFactory: () -> Executor = { TestExecutor() }, + reducer: Reducer = reducer() + ): Store = + storeFactory(initialState, bootstrapper, executorFactory, reducer) + .freeze() +} diff --git a/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreFactory.kt b/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreFactory.kt index 43ef28db..adb30a08 100644 --- a/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreFactory.kt +++ b/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreFactory.kt @@ -24,9 +24,9 @@ class TimeTravelStoreFactory : StoreFactory { initialState = initialState, bootstrapper = bootstrapper, executorFactory = executorFactory, - reducer = reducer + reducer = reducer, + onInit = { TimeTravelControllerHolder.impl.attachStore(store = it, name = name) }, ).also { store -> - TimeTravelControllerHolder.impl.attachStore(store = store, name = name) if (autoInit) { store.init() } diff --git a/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreImpl.kt b/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreImpl.kt index 4f144880..a533145a 100644 --- a/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreImpl.kt +++ b/mvikotlin-timetravel/src/commonMain/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreImpl.kt @@ -19,7 +19,8 @@ internal class TimeTravelStoreImpl?, private val executorFactory: () -> Executor, - private val reducer: Reducer + private val reducer: Reducer, + private val onInit: (TimeTravelStore) -> Unit = {}, ) : TimeTravelStore { private val executor = executorFactory() @@ -69,6 +70,8 @@ internal class TimeTravelStoreImpl { override val state: State get() = internalState diff --git a/mvikotlin-timetravel/src/commonTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreGenericTests.kt b/mvikotlin-timetravel/src/commonTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreGenericTests.kt index 562de391..7f123867 100644 --- a/mvikotlin-timetravel/src/commonTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreGenericTests.kt +++ b/mvikotlin-timetravel/src/commonTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreGenericTests.kt @@ -6,14 +6,19 @@ import com.arkivanov.mvikotlin.core.test.internal.TestExecutor import com.arkivanov.mvikotlin.core.test.internal.reducer import com.arkivanov.mvikotlin.core.utils.isAssertOnMainThreadEnabled import com.arkivanov.mvikotlin.rx.observer +import com.arkivanov.mvikotlin.utils.internal.atomic import com.arkivanov.mvikotlin.utils.internal.freeze +import com.arkivanov.mvikotlin.utils.internal.getValue import com.arkivanov.mvikotlin.utils.internal.isFrozen +import com.arkivanov.mvikotlin.utils.internal.setValue import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertFalse +import kotlin.test.assertTrue -class TimeTravelStoreGenericTests : StoreGenericTests by StoreGenericTests( +@Suppress("TestFunctionName") +class TimeTravelStoreGenericTests : StoreGenericTests( storeFactory = { initialState, bootstrapper, executorFactory, reducer -> TimeTravelStoreImpl( initialState = initialState, @@ -38,21 +43,37 @@ class TimeTravelStoreGenericTests : StoreGenericTests by StoreGenericTests( @Test fun events_subscriber_not_frozen_WHEN_store_frozen_and_subscribed() { - val store = TimeTravelStoreImpl( - initialState = "initialState", - bootstrapper = TestBootstrapper(), - executorFactory = { TestExecutor() }, - reducer = reducer { it } - ) - .apply { - events(observer { process(it.type, it.value) }) - init() - } - .freeze() + val store = + TimeTravelStoreImpl( + initialState = "initialState", + bootstrapper = TestBootstrapper(), + executorFactory = { TestExecutor() }, + reducer = reducer { it } + ) + .apply { init() } + .freeze() val list = ArrayList() store.events(observer { list += it }) assertFalse(list.isFrozen) } + + @Test + fun WHEN_init_THEN_onInit_called() { + var isCalled by atomic(false) + + val store = + TimeTravelStoreImpl( + initialState = "initialState", + bootstrapper = TestBootstrapper(), + executorFactory = { TestExecutor() }, + reducer = reducer { it }, + onInit = { isCalled = true }, + ) + + store.init() + + assertTrue(isCalled) + } } diff --git a/mvikotlin-timetravel/src/darwinTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreThreadingTests.kt b/mvikotlin-timetravel/src/darwinTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreThreadingTests.kt new file mode 100644 index 00000000..43d696bb --- /dev/null +++ b/mvikotlin-timetravel/src/darwinTest/kotlin/com/arkivanov/mvikotlin/timetravel/store/TimeTravelStoreThreadingTests.kt @@ -0,0 +1,19 @@ +package com.arkivanov.mvikotlin.timetravel.store + +import com.arkivanov.mvikotlin.core.test.internal.StoreThreadingTests +import com.arkivanov.mvikotlin.rx.observer +import kotlin.test.Test +import kotlin.test.fail + +class TimeTravelStoreThreadingTests : StoreThreadingTests( + storeFactory = { initialState, bootstrapper, executorFactory, reducer -> + TimeTravelStoreImpl( + initialState = initialState, + bootstrapper = bootstrapper, + executorFactory = executorFactory, + reducer = reducer + ).apply { + events(observer { process(it.type, it.value) }) + } + } +) diff --git a/rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/ThreadLocalSubjectTestNative.kt b/rx-internal/src/nativeTest/kotlin/com/arkivanov/mvikotlin/rx/internal/BaseSubjectTestNative.kt similarity index 84% rename from rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/ThreadLocalSubjectTestNative.kt rename to rx-internal/src/nativeTest/kotlin/com/arkivanov/mvikotlin/rx/internal/BaseSubjectTestNative.kt index 4dee27a5..dcd6ebaf 100644 --- a/rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/ThreadLocalSubjectTestNative.kt +++ b/rx-internal/src/nativeTest/kotlin/com/arkivanov/mvikotlin/rx/internal/BaseSubjectTestNative.kt @@ -6,6 +6,7 @@ import com.arkivanov.mvikotlin.utils.internal.freeze import com.arkivanov.mvikotlin.utils.internal.getValue import com.arkivanov.mvikotlin.utils.internal.isAssertOnMainThreadEnabled import com.arkivanov.mvikotlin.utils.internal.isFrozen +import com.arkivanov.mvikotlin.utils.internal.runOnBackgroundBlocking import com.arkivanov.mvikotlin.utils.internal.setValue import platform.posix.pthread_self import kotlin.test.AfterTest @@ -15,9 +16,9 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class ThreadLocalSubjectTestNative { +class BaseSubjectTestNative { - private val subject = ThreadLocalSubject().freeze() + private val subject = BaseSubject().freeze() @BeforeTest fun before() { @@ -42,7 +43,7 @@ class ThreadLocalSubjectTestNative { fun produces_values_WHEN_subscribed_on_background_thread_and_onNext_called_on_main_thread() { var values by atomic>(emptyList()) val mainThreadId = pthread_self() - val subject = ThreadLocalSubject(isOnMainThread = { pthread_self() == mainThreadId }) + val subject = BaseSubject(isOnMainThread = { pthread_self() == mainThreadId }) runOnBackgroundBlocking { subject.subscribe(observer(onNext = { values += it })) @@ -59,7 +60,7 @@ class ThreadLocalSubjectTestNative { fun completes_WHEN_subscribed_on_background_thread_and_onComplete_called_on_main_thread() { var isCompleted by atomic(false) val mainThreadId = pthread_self() - val subject = ThreadLocalSubject(isOnMainThread = { pthread_self() == mainThreadId }) + val subject = BaseSubject(isOnMainThread = { pthread_self() == mainThreadId }) runOnBackgroundBlocking { subject.subscribe(observer(onComplete = { isCompleted = true })) diff --git a/utils-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/utils/internal/MainThreadAssert.kt b/utils-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/utils/internal/MainThreadAssert.kt index e558f34b..1ac2ed92 100644 --- a/utils-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/utils/internal/MainThreadAssert.kt +++ b/utils-internal/src/commonMain/kotlin/com/arkivanov/mvikotlin/utils/internal/MainThreadAssert.kt @@ -1,7 +1,10 @@ package com.arkivanov.mvikotlin.utils.internal +import kotlin.native.concurrent.SharedImmutable + var isAssertOnMainThreadEnabled: Boolean by atomic(true) +@SharedImmutable private val mainThreadIdRef = atomic(null) fun assertOnMainThread() { diff --git a/rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/Utils.kt b/utils-internal/src/nativeMain/kotlin/com/arkivanov/mvikotlin/utils/internal/Utils.kt similarity index 81% rename from rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/Utils.kt rename to utils-internal/src/nativeMain/kotlin/com/arkivanov/mvikotlin/utils/internal/Utils.kt index 9287616f..fcde08fd 100644 --- a/rx-internal/src/nativeCommonTest/kotlin/com/arkivanov/mvikotlin/rx/internal/Utils.kt +++ b/utils-internal/src/nativeMain/kotlin/com/arkivanov/mvikotlin/utils/internal/Utils.kt @@ -1,8 +1,5 @@ -package com.arkivanov.mvikotlin.rx.internal +package com.arkivanov.mvikotlin.utils.internal -import com.arkivanov.mvikotlin.utils.internal.atomic -import com.arkivanov.mvikotlin.utils.internal.getValue -import com.arkivanov.mvikotlin.utils.internal.setValue import kotlin.native.concurrent.TransferMode import kotlin.native.concurrent.Worker import kotlin.native.concurrent.freeze