diff --git a/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/CoroutineLogger.kt b/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/CoroutineLogger.kt new file mode 100644 index 00000000..a4796280 --- /dev/null +++ b/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/CoroutineLogger.kt @@ -0,0 +1,5 @@ +package com.raxdenstudios.commons.coroutines + +interface CoroutineLogger { + fun logError(tag: String, message: String?, throwable: Throwable) +} \ No newline at end of file diff --git a/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtension.kt b/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtension.kt index 473a2858..b2230da6 100644 --- a/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtension.kt +++ b/libraries/coroutines/src/main/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtension.kt @@ -1,13 +1,14 @@ package com.raxdenstudios.commons.coroutines.ext -import android.util.Log +import com.raxdenstudios.commons.coroutines.CoroutineLogger import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +var defaultCoroutineLogger: CoroutineLogger? = null val defaultExceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.e("CoroutineException", throwable.message, throwable) + defaultCoroutineLogger?.logError("CoroutineException", throwable.message, throwable) } fun CoroutineScope.safeLaunch( @@ -15,16 +16,16 @@ fun CoroutineScope.safeLaunch( block: suspend CoroutineScope.() -> Unit, ): Job = launch( context = exceptionHandler, - block = { block.invoke(this) } + block = block ) fun CoroutineScope.launch( - onError: (throwable: Throwable) -> Unit = { _ -> }, + onError: (Throwable) -> Unit = {}, block: suspend CoroutineScope.() -> Unit, ): Job = launch( context = CoroutineExceptionHandler { _, throwable -> - Log.e("CoroutineException", throwable.message, throwable) + defaultCoroutineLogger?.logError("CoroutineException", throwable.message, throwable) onError(throwable) }, - block = { block.invoke(this) } + block = block ) diff --git a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/CoroutineScopeProviderTest.kt b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/CoroutineScopeProviderTest.kt new file mode 100644 index 00000000..c72da830 --- /dev/null +++ b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/CoroutineScopeProviderTest.kt @@ -0,0 +1,109 @@ +package com.raxdenstudios.commons.coroutines + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +internal class CoroutineScopeProviderTest { + + @Test + fun `verify default implementation provides correct scopes`() { + val scopeProvider = object : CoroutineScopeProvider { + override val main: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + override val io: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + override val default: CoroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + } + + assertThat(scopeProvider.main).isNotNull() + assertThat(scopeProvider.io).isNotNull() + assertThat(scopeProvider.default).isNotNull() + + scopeProvider.main.cancel() + scopeProvider.io.cancel() + scopeProvider.default.cancel() + } + + @Test + fun `verify test implementation can use test scopes`() = runTest { + val testDispatcher = kotlinx.coroutines.test.UnconfinedTestDispatcher() + + val scopeProvider = object : CoroutineScopeProvider { + override val main: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val io: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val default: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + } + + assertThat(scopeProvider.main).isNotNull() + assertThat(scopeProvider.io).isNotNull() + assertThat(scopeProvider.default).isNotNull() + + scopeProvider.main.cancel() + scopeProvider.io.cancel() + scopeProvider.default.cancel() + } + + @Test + fun `verify scopes can be used for launching coroutines`() = runTest { + val testDispatcher = kotlinx.coroutines.test.UnconfinedTestDispatcher() + var executionCount = 0 + + val scopeProvider = object : CoroutineScopeProvider { + override val main: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val io: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val default: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + } + + scopeProvider.main.launch { + executionCount++ + } + + scopeProvider.io.launch { + executionCount++ + } + + scopeProvider.default.launch { + executionCount++ + } + + testScheduler.advanceUntilIdle() + + assertThat(executionCount).isEqualTo(3) + + scopeProvider.main.cancel() + scopeProvider.io.cancel() + scopeProvider.default.cancel() + } + + @Test + fun `verify scope cancellation stops coroutines`() = runTest { + val testDispatcher = kotlinx.coroutines.test.StandardTestDispatcher(testScheduler) + var executionCompleted = false + + val scopeProvider = object : CoroutineScopeProvider { + override val main: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val io: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + override val default: CoroutineScope = CoroutineScope(testDispatcher + SupervisorJob()) + } + + scopeProvider.main.launch { + delay(1000) + executionCompleted = true + } + + scopeProvider.main.cancel() + testScheduler.advanceUntilIdle() + + assertThat(executionCompleted).isFalse() + + scopeProvider.io.cancel() + scopeProvider.default.cancel() + } +} diff --git a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/DispatcherProviderTest.kt b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/DispatcherProviderTest.kt new file mode 100644 index 00000000..033b023a --- /dev/null +++ b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/DispatcherProviderTest.kt @@ -0,0 +1,56 @@ +package com.raxdenstudios.commons.coroutines + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test + +@ExperimentalCoroutinesApi +internal class DispatcherProviderTest { + + @Test + fun `verify default implementation provides correct dispatchers`() { + val dispatcherProvider = object : DispatcherProvider { + override val main: CoroutineDispatcher = Dispatchers.Main + override val io: CoroutineDispatcher = Dispatchers.IO + override val default: CoroutineDispatcher = Dispatchers.Default + } + + assertThat(dispatcherProvider.main).isEqualTo(Dispatchers.Main) + assertThat(dispatcherProvider.io).isEqualTo(Dispatchers.IO) + assertThat(dispatcherProvider.default).isEqualTo(Dispatchers.Default) + } + + @Test + fun `verify test implementation can use test dispatchers`() { + val testDispatcher = kotlinx.coroutines.test.UnconfinedTestDispatcher() + + val dispatcherProvider = object : DispatcherProvider { + override val main: CoroutineDispatcher = testDispatcher + override val io: CoroutineDispatcher = testDispatcher + override val default: CoroutineDispatcher = testDispatcher + } + + assertThat(dispatcherProvider.main).isEqualTo(testDispatcher) + assertThat(dispatcherProvider.io).isEqualTo(testDispatcher) + assertThat(dispatcherProvider.default).isEqualTo(testDispatcher) + } + + @Test + fun `verify custom implementation can provide different dispatchers`() { + val customMainDispatcher = kotlinx.coroutines.test.UnconfinedTestDispatcher() + val customIoDispatcher = kotlinx.coroutines.test.StandardTestDispatcher() + val customDefaultDispatcher = Dispatchers.Default + + val dispatcherProvider = object : DispatcherProvider { + override val main: CoroutineDispatcher = customMainDispatcher + override val io: CoroutineDispatcher = customIoDispatcher + override val default: CoroutineDispatcher = customDefaultDispatcher + } + + assertThat(dispatcherProvider.main).isEqualTo(customMainDispatcher) + assertThat(dispatcherProvider.io).isEqualTo(customIoDispatcher) + assertThat(dispatcherProvider.default).isEqualTo(customDefaultDispatcher) + } +} diff --git a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/AnswerExtensionTest.kt b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/AnswerExtensionTest.kt index e99fffe5..1930c79f 100644 --- a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/AnswerExtensionTest.kt +++ b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/AnswerExtensionTest.kt @@ -4,6 +4,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.raxdenstudios.commons.core.Answer import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue @@ -167,4 +168,60 @@ internal class AnswerExtensionTest { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `use thenFailure when result is success`() = runTest { + val flowData = flowOf(Answer.Success("originalValue")) + + flowData.thenFailure { "otherValue" }.test { + val result = awaitItem() + assertThat(result.isSuccess).isTrue() + assertThat(result).isEqualTo(Answer.Success("originalValue")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `use thenFailure when result is failure`() = runTest { + val flowData = flowOf(Answer.Failure("originalValue")) + + flowData.thenFailure { "otherValue" }.test { + val result = awaitItem() + assertThat(result.isFailure).isTrue() + assertThat(result).isEqualTo(Answer.Failure("otherValue")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `use toAnswer with successful flow`() = runTest { + val flowData = flowOf("value1", "value2") + + flowData.toAnswer().test { + val result1 = awaitItem() + assertThat(result1.isSuccess).isTrue() + assertThat(result1).isEqualTo(Answer.Success("value1")) + + val result2 = awaitItem() + assertThat(result2.isSuccess).isTrue() + assertThat(result2).isEqualTo(Answer.Success("value2")) + + awaitComplete() + } + } + + @Test + fun `use toAnswer with flow that throws exception`() = runTest { + val flowData = flow { + emit("value") + error("Test error") + } + + flowData.toAnswer().test { + val result1 = awaitItem() + assertThat(result1.isSuccess).isTrue() + assertThat(result1).isEqualTo(Answer.Success("value")) + awaitComplete() + } + } } diff --git a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtensionTest.kt b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtensionTest.kt index 47625b65..cac4d202 100644 --- a/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtensionTest.kt +++ b/libraries/coroutines/src/test/java/com/raxdenstudios/commons/coroutines/ext/CoroutinesExtensionTest.kt @@ -51,4 +51,65 @@ internal class CoroutinesExtensionTest { assertThat(job).isNotNull() assertThat(job).isInstanceOf(Job::class.java) } + + @Test + fun `use launch with onError callback`() = runTest { + var errorCaught = false + var errorMessage: String? = null + + val job = scope.launch( + onError = { throwable -> + errorCaught = true + errorMessage = throwable.message + } + ) { + error("Test error") + } + + job.join() + + assertThat(job).isNotNull() + assertThat(job).isInstanceOf(Job::class.java) + assertThat(errorCaught).isTrue() + assertThat(errorMessage).isEqualTo("Test error") + } + + @Test + fun `use launch without exception`() = runTest { + var errorCaught = false + var executionCompleted = false + + val job = scope.launch( + onError = { errorCaught = true } + ) { + executionCompleted = true + } + + job.join() + + assertThat(job).isNotNull() + assertThat(job).isInstanceOf(Job::class.java) + assertThat(errorCaught).isFalse() + assertThat(executionCompleted).isTrue() + } + + @Test + fun `use safeLaunch with custom exception handler`() = runTest { + var customHandlerCalled = false + val customHandler = kotlinx.coroutines.CoroutineExceptionHandler { _, _ -> + customHandlerCalled = true + } + + val job = scope.safeLaunch( + exceptionHandler = customHandler + ) { + error("Custom handler test") + } + + job.join() + + assertThat(job).isNotNull() + assertThat(job).isInstanceOf(Job::class.java) + assertThat(customHandlerCalled).isTrue() + } }