From da695587d9ad4469eafbef7f9f134ebf152a1490 Mon Sep 17 00:00:00 2001 From: Dennis Deng Date: Fri, 1 Dec 2023 16:14:11 -0500 Subject: [PATCH] Add RxJavaRuntimeErrorHandler --- .../formula/rxjava3/RxJavaRuntime.kt | 19 +++- .../rxjava3/RxJavaRuntimeErrorHandler.kt | 10 ++ .../java/com/instacart/formula/Exceptions.kt | 6 ++ .../formula/internal/SingleRequestHolder.kt | 4 +- .../instacart/formula/FormulaRuntimeTest.kt | 5 +- .../error/RxJavaRuntimeErrorHandlerTest.kt | 95 +++++++++++++++++++ 6 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt create mode 100644 formula/src/main/java/com/instacart/formula/Exceptions.kt create mode 100644 formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt 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 09101a172..ca5c6a91d 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 @@ -10,6 +10,13 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.FormulaDisposableHelper object RxJavaRuntime { + + private var defaultErrorHandler: RxJavaRuntimeErrorHandler? = null + + fun setDefaultErrorHandler(errorHandler: RxJavaRuntimeErrorHandler?) { + this.defaultErrorHandler = errorHandler + } + fun start( input: Observable, formula: IFormula, @@ -22,12 +29,18 @@ object RxJavaRuntime { type = formula.type(), local = inspector, ) + + val onError = { error: Throwable -> + val handled = defaultErrorHandler?.onError(error) ?: false + if (!handled) emitter.onError(error) + }.takeIf { defaultErrorHandler != null } ?: emitter::onError + val runtimeFactory = { FormulaRuntime( threadChecker = threadChecker, formula = formula, onOutput = emitter::onNext, - onError = emitter::onError, + onError = onError, inspector = mergedInspector, isValidationEnabled = isValidationEnabled, ) @@ -45,7 +58,7 @@ object RxJavaRuntime { runtime = runtimeFactory() } runtime.onInput(input) - }, emitter::onError)) + }, onError)) val runnable = Runnable { threadChecker.check("Need to unsubscribe on the main thread.") @@ -56,4 +69,4 @@ object RxJavaRuntime { emitter.setDisposable(disposables) }.distinctUntilChanged() } -} \ No newline at end of file +} 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 000000000..aa5fb1ede --- /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/main/java/com/instacart/formula/Exceptions.kt b/formula/src/main/java/com/instacart/formula/Exceptions.kt new file mode 100644 index 000000000..347224eea --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/Exceptions.kt @@ -0,0 +1,6 @@ +package com.instacart.formula + +/** + * Thrown when a child formula is added with a duplicate key. + */ +class DuplicateKeyException(override val message: String?) : IllegalStateException() diff --git a/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt b/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt index 14a336fc7..3fc1ea229 100644 --- a/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt +++ b/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt @@ -1,5 +1,7 @@ package com.instacart.formula.internal +import com.instacart.formula.DuplicateKeyException + /** * Holder tracks when object has already been request and throws an error if requested again. This * is used to track duplicate requests for a particular key. @@ -9,7 +11,7 @@ internal class SingleRequestHolder(val value: T) { inline fun requestAccess(errorMessage: () -> String): T { if (requested) { - throw IllegalStateException(errorMessage()) + throw DuplicateKeyException(errorMessage()) } requested = true diff --git a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt index d8f528239..198a9b6fe 100644 --- a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt +++ b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt @@ -7,6 +7,7 @@ import com.instacart.formula.internal.ClearPluginsRule import com.instacart.formula.internal.TestInspector import com.instacart.formula.internal.Try import com.instacart.formula.rxjava3.RxAction +import com.instacart.formula.rxjava3.RxJavaRuntime import com.instacart.formula.subjects.ChildActionFiresParentEventOnStart import com.instacart.formula.subjects.ChildMessageNoParentStateChange import com.instacart.formula.subjects.ChildMessageTriggersEventTransitionInParent @@ -1108,7 +1109,9 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) { } val error = result.errorOrNull()?.cause - assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error).isInstanceOf(DuplicateKeyException::class.java) + val expectedMessage = "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(error?.message).isEqualTo(expectedMessage) } @Test 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 000000000..17ef2681e --- /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) + } +}