-
Notifications
You must be signed in to change notification settings - Fork 105
1093: Launch Side Effects atomically so they are always started #1102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
workflow-testing/src/main/java/com/squareup/workflow1/testing/HeadlessIntegrationTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package com.squareup.workflow1.testing | ||
|
||
import app.cash.turbine.ReceiveTurbine | ||
import app.cash.turbine.test | ||
import com.squareup.workflow1.RuntimeConfig | ||
import com.squareup.workflow1.RuntimeConfigOptions | ||
import com.squareup.workflow1.Workflow | ||
import com.squareup.workflow1.WorkflowInterceptor | ||
import com.squareup.workflow1.renderWorkflowIn | ||
import com.squareup.workflow1.testing.WorkflowTurbine.Companion.WORKFLOW_TEST_DEFAULT_TIMEOUT_MS | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.cancel | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.flow.drop | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
import kotlinx.coroutines.test.runTest | ||
import kotlin.coroutines.CoroutineContext | ||
import kotlin.time.Duration.Companion.milliseconds | ||
|
||
/** | ||
* This is a test harness to run integration tests for a Workflow tree. The parameters passed here are | ||
* the same as those to start a Workflow runtime with [renderWorkflowIn] except for ignoring | ||
* state persistence as that is not needed for this style of test. | ||
* | ||
* The [coroutineContext] rather than a [CoroutineScope] is passed so that this harness handles the | ||
* scope for the Workflow runtime for you but you can still specify context for it. It defaults to | ||
* [Dispatchers.Main.immediate]. If you wish to use a dispatcher that skips delays, use a | ||
* [StandardTestDispatcher][kotlinx.coroutines.test.StandardTestDispatcher], so that the dispatcher | ||
* will still guarantee ordering. | ||
* | ||
* A [testTimeout] may be specified to override the default [WORKFLOW_TEST_DEFAULT_TIMEOUT_MS] for | ||
* any particular test. This is the max amount of time the test could spend waiting on a rendering. | ||
* | ||
* This will start the Workflow runtime (with params as passed) rooted at whatever Workflow | ||
* it is called on and then create a [WorkflowTurbine] for its renderings and run [testCase] on that. | ||
* [testCase] can thus drive the test scenario and assert against renderings. | ||
*/ | ||
public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.headlessIntegrationTest( | ||
props: StateFlow<PropsT>, | ||
coroutineContext: CoroutineContext = Dispatchers.Main.immediate, | ||
interceptors: List<WorkflowInterceptor> = emptyList(), | ||
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, | ||
onOutput: suspend (OutputT) -> Unit = {}, | ||
testTimeout: Long = WORKFLOW_TEST_DEFAULT_TIMEOUT_MS, | ||
testCase: suspend WorkflowTurbine<RenderingT>.() -> Unit | ||
) { | ||
val workflow = this | ||
|
||
runTest( | ||
context = coroutineContext, | ||
timeout = testTimeout.milliseconds | ||
) { | ||
// We use a sub-scope so that we can cancel the Workflow runtime when we are done with it so that | ||
// tests don't all have to do that themselves. | ||
val workflowRuntimeScope = CoroutineScope(coroutineContext) | ||
val renderings = renderWorkflowIn( | ||
workflow = workflow, | ||
props = props, | ||
scope = workflowRuntimeScope, | ||
interceptors = interceptors, | ||
runtimeConfig = runtimeConfig, | ||
onOutput = onOutput | ||
) | ||
|
||
val firstRendering = renderings.value.rendering | ||
|
||
// Drop one as its provided separately via `firstRendering`. | ||
renderings.drop(1).map { | ||
it.rendering | ||
}.test { | ||
val workflowTurbine = WorkflowTurbine( | ||
firstRendering, | ||
this | ||
) | ||
workflowTurbine.testCase() | ||
cancelAndIgnoreRemainingEvents() | ||
} | ||
workflowRuntimeScope.cancel() | ||
} | ||
} | ||
|
||
/** | ||
* Version of [headlessIntegrationTest] that does not require props. For Workflows that have [Unit] | ||
* props type. | ||
*/ | ||
@OptIn(ExperimentalCoroutinesApi::class) | ||
public fun <OutputT, RenderingT> Workflow<Unit, OutputT, RenderingT>.headlessIntegrationTest( | ||
coroutineContext: CoroutineContext = UnconfinedTestDispatcher(), | ||
interceptors: List<WorkflowInterceptor> = emptyList(), | ||
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, | ||
onOutput: suspend (OutputT) -> Unit = {}, | ||
testTimeout: Long = WORKFLOW_TEST_DEFAULT_TIMEOUT_MS, | ||
testCase: suspend WorkflowTurbine<RenderingT>.() -> Unit | ||
): Unit = headlessIntegrationTest( | ||
props = MutableStateFlow(Unit).asStateFlow(), | ||
coroutineContext = coroutineContext, | ||
interceptors = interceptors, | ||
runtimeConfig = runtimeConfig, | ||
onOutput = onOutput, | ||
testTimeout = testTimeout, | ||
testCase = testCase | ||
) | ||
|
||
/** | ||
* Simple wrapper around a [ReceiveTurbine] of [RenderingT] to provide convenience helper methods specific | ||
* to Workflow renderings. | ||
* | ||
* @property firstRendering The first rendering of the Workflow runtime is made synchronously. This is | ||
* provided separately if any assertions or operations are needed from it. | ||
*/ | ||
public class WorkflowTurbine<RenderingT>( | ||
public val firstRendering: RenderingT, | ||
private val receiveTurbine: ReceiveTurbine<RenderingT> | ||
) { | ||
private var usedFirst = false | ||
|
||
/** | ||
* Suspend waiting for the next rendering to be produced by the Workflow runtime. Note this includes | ||
* the first (synchronously made) rendering. | ||
* | ||
* @return the rendering. | ||
*/ | ||
public suspend fun awaitNextRendering(): RenderingT { | ||
if (!usedFirst) { | ||
usedFirst = true | ||
return firstRendering | ||
} | ||
return receiveTurbine.awaitItem() | ||
} | ||
|
||
public suspend fun skipRenderings(count: Int) { | ||
val skippedCount = if (!usedFirst) { | ||
usedFirst = true | ||
count - 1 | ||
} else { | ||
count | ||
} | ||
|
||
if (skippedCount > 0) { | ||
receiveTurbine.skipItems(skippedCount) | ||
} | ||
} | ||
|
||
/** | ||
* Suspend waiting for the next rendering to be produced by the Workflow runtime that satisfies the | ||
* [predicate]. | ||
* | ||
* @return the rendering. | ||
*/ | ||
public suspend fun awaitNextRenderingSatisfying( | ||
predicate: (RenderingT) -> Boolean | ||
): RenderingT { | ||
var rendering = awaitNextRendering() | ||
while (!predicate(rendering)) { | ||
rendering = awaitNextRendering() | ||
} | ||
return rendering | ||
} | ||
|
||
/** | ||
* Suspend waiting for the next rendering which satisfies [precondition], can successfully be mapped | ||
* using [map] and satisfies the [satisfying] predicate when called on the [T] rendering after it | ||
* has been mapped. | ||
* | ||
* @return the mapped rendering as [T] | ||
*/ | ||
public suspend fun <T> awaitNext( | ||
precondition: (RenderingT) -> Boolean = { true }, | ||
map: (RenderingT) -> T, | ||
satisfying: T.() -> Boolean = { true } | ||
): T = | ||
map( | ||
awaitNextRenderingSatisfying { | ||
precondition(it) && | ||
with(map(it)) { | ||
this.satisfying() | ||
} | ||
} | ||
) | ||
|
||
public companion object { | ||
/** | ||
* Default timeout to use while waiting for renderings. | ||
*/ | ||
public const val WORKFLOW_TEST_DEFAULT_TIMEOUT_MS: Long = 60_000L | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was just out-dated. Not true for some time (since GUWT and side effects I think), but we hadn't updated documentation.