diff --git a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt index 6f99c547..81e835d1 100644 --- a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt +++ b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt @@ -11,10 +11,10 @@ import io.reactivex.rxjava3.disposables.FormulaDisposableHelper object RxJavaRuntime { - private var defaultErrorHandler: ((Throwable) -> Unit)? = null + private var defaultErrorHandler: RxJavaRuntimeErrorHandler? = null - fun setDefaultErrorHandler(handler: ((Throwable) -> Unit)?) { - defaultErrorHandler = handler + fun setDefaultErrorHandler(errorHandler: RxJavaRuntimeErrorHandler?) { + this.defaultErrorHandler = errorHandler } fun start( @@ -29,12 +29,20 @@ object RxJavaRuntime { type = formula.type(), local = inspector, ) + + val onError = { error: Throwable -> + val handled = defaultErrorHandler?.onError(error) ?: false + if (!handled) { + emitter.onError(error) + } + } + val runtimeFactory = { FormulaRuntime( threadChecker = threadChecker, formula = formula, onOutput = emitter::onNext, - onError = defaultErrorHandler ?: emitter::onError, + onError = onError, inspector = mergedInspector, isValidationEnabled = isValidationEnabled, ) @@ -52,7 +60,7 @@ object RxJavaRuntime { runtime = runtimeFactory() } runtime.onInput(input) - }, defaultErrorHandler ?: emitter::onError)) + }, onError)) val runnable = Runnable { threadChecker.check("Need to unsubscribe on the main thread.") diff --git a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt new file mode 100644 index 00000000..aa5fb1ed --- /dev/null +++ b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt @@ -0,0 +1,10 @@ +package com.instacart.formula.rxjava3 + +interface RxJavaRuntimeErrorHandler { + /** + * @param error [Throwable] that occurred + * + * @return true if error was handled, false otherwise + */ + fun onError(error: Throwable): Boolean +} diff --git a/formula/src/test/java/com/instacart/formula/RxJavaRuntimeTest.kt b/formula/src/test/java/com/instacart/formula/RxJavaRuntimeTest.kt deleted file mode 100644 index b98c9e55..00000000 --- a/formula/src/test/java/com/instacart/formula/RxJavaRuntimeTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.instacart.formula - -import com.google.common.truth.Truth.assertThat -import com.instacart.formula.internal.ClearPluginsRule -import com.instacart.formula.internal.Try -import com.instacart.formula.rxjava3.RxJavaRuntime -import com.instacart.formula.subjects.DynamicParentFormula -import com.instacart.formula.subjects.TestKey -import com.instacart.formula.test.RxJavaTestableRuntime -import com.instacart.formula.test.TestableRuntime -import org.junit.Rule -import org.junit.Test -import org.junit.rules.RuleChain -import org.junit.rules.TestName - -class RxJavaRuntimeTest { - - val runtime: TestableRuntime = RxJavaTestableRuntime - - @get:Rule - val rule = RuleChain - .outerRule(TestName()) - .around(ClearPluginsRule()) - .around(runtime.rule) - - @Test - fun `adding duplicate child logs an exception`() { - val logs = mutableListOf() - RxJavaRuntime.setDefaultErrorHandler { logs.add(it.message.orEmpty()) } - - val result = Try { - val formula = DynamicParentFormula() - runtime.test(formula, Unit) - .output { addChild(TestKey("1")) } - .output { addChild(TestKey("1")) } - } - - val error = result.errorOrNull()?.cause - assertThat(error).isNull() - assertThat(logs).hasSize(1) - val log = logs.first() - val expectedLog = "There already is a child with same key: FormulaKey(scopeKey=null, type=class com.instacart.formula.subjects.KeyFormula, key=TestKey(id=1)). Override [Formula.key] function." - assertThat(log).isEqualTo(expectedLog) - RxJavaRuntime.setDefaultErrorHandler(null) - } -} diff --git a/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt b/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt new file mode 100644 index 00000000..17ef2681 --- /dev/null +++ b/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt @@ -0,0 +1,95 @@ +package com.instacart.formula.error + +import com.google.common.truth.Truth.assertThat +import com.instacart.formula.Action +import com.instacart.formula.DuplicateKeyException +import com.instacart.formula.internal.ClearPluginsRule +import com.instacart.formula.internal.Try +import com.instacart.formula.rxjava3.RxJavaRuntime +import com.instacart.formula.rxjava3.RxJavaRuntimeErrorHandler +import com.instacart.formula.subjects.DynamicParentFormula +import com.instacart.formula.subjects.OnlyUpdateFormula +import com.instacart.formula.subjects.TestKey +import com.instacart.formula.test.RxJavaTestableRuntime +import com.instacart.formula.test.TestableRuntime +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestName +import kotlin.IllegalStateException + +class RxJavaRuntimeErrorHandlerTest { + + val runtime: TestableRuntime = RxJavaTestableRuntime + + @get:Rule + val rule = RuleChain + .outerRule(TestName()) + .around(ClearPluginsRule()) + .around(runtime.rule) + + private val errorLogs = mutableListOf() + + private val duplicateKeyErrorHandler = object : RxJavaRuntimeErrorHandler { + override fun onError(error: Throwable): Boolean { + return when (error) { + is DuplicateKeyException -> { + errorLogs.add(error.message.orEmpty()) + true + } + + else -> { + false + } + } + } + } + + @Before + fun setUp() { + RxJavaRuntime.setDefaultErrorHandler(duplicateKeyErrorHandler) + } + + @After + fun tearDown() { + RxJavaRuntime.setDefaultErrorHandler(null) + } + + @Test + fun `emitting a generic error throws an exception`() { + val result = Try { + val formula = OnlyUpdateFormula { + events(Action.onInit()) { + throw IllegalStateException("crashed") + } + } + runtime.test(formula, Unit) + } + + val error = result.errorOrNull()?.cause + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error?.message).isEqualTo("crashed") + + assertThat(errorLogs).isEmpty() + } + + @Test + fun `adding duplicate child logs an exception`() { + val result = Try { + val formula = DynamicParentFormula() + runtime.test(formula, Unit) + .output { addChild(TestKey("1")) } + .output { addChild(TestKey("1")) } + } + + val error = result.errorOrNull()?.cause + assertThat(error).isNull() + assertThat(errorLogs).hasSize(1) + + val log = errorLogs.first() + val expectedLog = "There already is a child with same key: FormulaKey(scopeKey=null, type=class com.instacart.formula.subjects.KeyFormula, key=TestKey(id=1)). Override [Formula.key] function." + assertThat(log).isEqualTo(expectedLog) + } +}