Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.raxdenstudios.commons.coroutines

interface CoroutineLogger {
fun logError(tag: String, message: String?, throwable: Throwable)
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
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(
exceptionHandler: CoroutineExceptionHandler = defaultExceptionHandler,
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
)
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Loading