Skip to content

Commit

Permalink
Increase runtime coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Mar 15, 2024
1 parent bc48eef commit a76f428
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 30 deletions.
68 changes: 38 additions & 30 deletions formula/src/main/java/com/instacart/formula/FormulaRuntime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class FormulaRuntime<Input : Any, Output : Any>(
/**
* Global transition effect queue which executes side-effects after all formulas are idle.
*/
private var globalEffectQueue = LinkedList<Effect>()
private val globalEffectQueue = LinkedList<Effect>()

/**
* Determines if we are iterating through [globalEffectQueue]. It prevents us from
Expand Down Expand Up @@ -207,45 +207,42 @@ class FormulaRuntime<Input : Any, Output : Any>(
if (isRunning) return

try {
val manager = checkNotNull(manager)
val manager = requireManager()

if (evaluate) {
var shouldRun = true
while (shouldRun) {
val localInputId = inputId
if (!manager.isTerminated()) {
isRunning = true
inspector?.onRunStarted(true)
isRunning = true
inspector?.onRunStarted(true)

val currentInput = checkNotNull(input)
runFormula(manager, currentInput)
isRunning = false
val currentInput = requireInput()
runFormula(manager, currentInput)
isRunning = false

inspector?.onRunFinished()
inspector?.onRunFinished()

/**
* If termination happened during runFormula() execution, let's perform
* termination side-effects here.
*/
if (manager.isTerminated()) {
shouldRun = false
terminateManager(manager)

/**
* If termination happened during runFormula() execution, let's perform
* termination side-effects here.
* If runtime has been terminated, there is nothing else to do.
*/
if (manager.isTerminated()) {
shouldRun = false
terminateManager(manager)

// If runtime has been terminated, we are stopping and do
// not need to do anything else.
if (!isRuntimeTerminated) {
// Terminated manager with input change indicates that formula
// key changed and we are resetting formula state. We need to
// start a new formula manager.
if (localInputId != inputId) {
input?.let(this::startNewManager)
}
}
} else {
shouldRun = localInputId != inputId
if (!isRuntimeTerminated) {
/**
* Terminated manager with non-terminated runtime indicates that
* formula input has triggered a key change and we are resetting
* formula state. We need to start a new formula manager here.
*/
startNewManager(requireInput())
}
} else {
shouldRun = false
shouldRun = localInputId != inputId
}
}
}
Expand All @@ -256,9 +253,10 @@ class FormulaRuntime<Input : Any, Output : Any>(
} catch (e: Throwable) {
isRunning = false

manager?.markAsTerminated()
val manager = requireManager()
manager.markAsTerminated()
onError(e)
manager?.let(this::terminateManager)
manager.let(this::terminateManager)
}
}

Expand Down Expand Up @@ -360,4 +358,14 @@ class FormulaRuntime<Input : Any, Output : Any>(
defaultDispatcher = defaultDispatcher,
)
}

// Visible for testing
internal fun requireInput(): Input {
return checkNotNull(input)
}

// Visible for testing
internal fun requireManager(): FormulaManagerImpl<Input, *, Output> {
return checkNotNull(manager)
}
}
106 changes: 106 additions & 0 deletions formula/src/test/java/com/instacart/formula/DirectRuntimeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.instacart.formula

import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.instacart.formula.internal.Try
import com.instacart.formula.test.TestEventCallback
import com.instacart.formula.types.InputIdentityFormula
import org.junit.Test

/**
* [FormulaRuntimeTest] runs both `toObservable` and `toFlow` internally to ensure that both
* implementations function identically. This test is used to capture various edge-cases
* within [FormulaRuntime] that are not possible via indirect tests.
*/
class DirectRuntimeTest {

@Test fun `requireInput will throw illegal state exception if it is null`() {
val root = InputIdentityFormula<Int>()

val onOutput = TestEventCallback<Int>()
val onError = TestEventCallback<Throwable>()
val runtime = FormulaRuntime(
formula = root,
onOutput = onOutput,
onError = onError,
config = RuntimeConfig()
)

val result = Try {
runtime.requireInput()
}
assertThat(result.errorOrNull()).isInstanceOf(IllegalStateException::class.java)
}

@Test fun `requireManager will throw illegal state exception if it is null`() {
val root = InputIdentityFormula<Int>()

val onOutput = TestEventCallback<Int>()
val onError = TestEventCallback<Throwable>()
val runtime = FormulaRuntime(
formula = root,
onOutput = onOutput,
onError = onError,
config = RuntimeConfig()
)

val result = Try {
runtime.requireManager()
}
assertThat(result.errorOrNull()).isInstanceOf(IllegalStateException::class.java)
}

@Test
fun `input change when runtime is terminated does nothing`() {
val root = InputIdentityFormula<Int>()

val onOutput = TestEventCallback<Int>()
val onError = TestEventCallback<Throwable>()
val runtime = FormulaRuntime(
formula = root,
onOutput = onOutput,
onError = onError,
config = RuntimeConfig()
)

runtime.onInput(0)
runtime.terminate()
runtime.onInput(1)

assertThat(onOutput.values()).containsExactly(0).inOrder()
}

@Test fun `it is safe to call terminate before first input initializes formula`() {
val root = InputIdentityFormula<Int>()

val onOutput = TestEventCallback<Int>()
val onError = TestEventCallback<Throwable>()
val runtime = FormulaRuntime(
formula = root,
onOutput = onOutput,
onError = onError,
config = RuntimeConfig()
)

runtime.terminate()
}

@Test fun `it is safe to call terminate multiple times`() {
val root = InputIdentityFormula<Int>()

val onOutput = TestEventCallback<Int>()
val onError = TestEventCallback<Throwable>()
val runtime = FormulaRuntime(
formula = root,
onOutput = onOutput,
onError = onError,
config = RuntimeConfig()
)

runtime.onInput(0)
runtime.terminate()
runtime.terminate()
runtime.terminate()
}

}
65 changes: 65 additions & 0 deletions formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,72 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) {

// Check that termination was called
assertThat(terminationCallback.values()).containsExactly(0).inOrder()
}

@Test fun `runtime termination triggered while formula is running`() {
val terminationCallback = TestEventCallback<Int>()
var observer: TestFormulaObserver<Int, *, *>? = null
val root = object : StatelessFormula<Int, Int>() {
override fun Snapshot<Int, Unit>.evaluate(): Evaluation<Int> {
return Evaluation(
output = input,
actions = context.actions {
Action.onTerminate().onEvent {
transition {
terminationCallback.invoke(input)
}
}

if (input == 0) {
Action.onInit().onEvent {
// This is outside of effect to trigger termination while running
observer?.dispose()
none()
}
}
}
)
}
}

observer = runtime.test(root)
observer.input(0)

// Check that termination was called
assertThat(terminationCallback.values()).containsExactly(0).inOrder()
}

@Test fun `runtime termination triggered by an effect`() {
val terminationCallback = TestEventCallback<Int>()
var observer: TestFormulaObserver<Int, *, *>? = null
val root = object : StatelessFormula<Int, Int>() {
override fun Snapshot<Int, Unit>.evaluate(): Evaluation<Int> {
return Evaluation(
output = input,
actions = context.actions {
Action.onTerminate().onEvent {
transition {
terminationCallback.invoke(input)
}
}

if (input == 0) {
Action.onInit().onEvent {
transition {
observer?.dispose()
}
}
}
}
)
}
}

observer = runtime.test(root)
observer.input(0)

// Check that termination was called
assertThat(terminationCallback.values()).containsExactly(0).inOrder()
}

@Test
Expand Down

0 comments on commit a76f428

Please sign in to comment.