From b2e9f1e24db95d46406ca994252fa444c7fd39bc Mon Sep 17 00:00:00 2001 From: Stephen Edwards Date: Thu, 5 Dec 2024 14:53:02 -0500 Subject: [PATCH 1/2] Add WorkflowTracer Through some Runtime Internals --- workflow-core/api/workflow-core.api | 13 +++ .../squareup/workflow1/BaseRenderContext.kt | 6 +- .../com/squareup/workflow1/WorkerWorkflow.kt | 9 +- .../com/squareup/workflow1/WorkflowTracer.kt | 36 ++++++++ workflow-runtime/api/workflow-runtime.api | 5 +- .../com/squareup/workflow1/RenderWorkflow.kt | 11 ++- .../squareup/workflow1/WorkflowInterceptor.kt | 4 + .../workflow1/internal/RealRenderContext.kt | 4 +- .../workflow1/internal/SubtreeManager.kt | 23 +++-- .../workflow1/internal/WorkflowNode.kt | 29 ++++--- .../workflow1/internal/WorkflowRunner.kt | 5 +- .../workflow1/RenderWorkflowInTest.kt | 83 +++++++++++++------ .../SimpleLoggingWorkflowInterceptorTest.kt | 2 + .../workflow1/WorkflowInterceptorTest.kt | 5 ++ .../ChainedWorkflowInterceptorTest.kt | 3 + .../internal/RealRenderContextTest.kt | 14 +++- .../workflow1/internal/SubtreeManagerTest.kt | 3 +- .../workflow1/internal/WorkflowNodeTest.kt | 2 + .../workflow1/internal/WorkflowRunnerTest.kt | 3 +- .../workflow1/testing/RealRenderTester.kt | 2 + workflow-ui/core-android/api/core-android.api | 12 +-- .../workflow1/ui/AndroidRenderWorkflow.kt | 7 ++ 22 files changed, 219 insertions(+), 62 deletions(-) create mode 100644 workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index a419493fc..8afbb5c61 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -35,6 +35,7 @@ public abstract interface class com/squareup/workflow1/BaseRenderContext { public abstract fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; public abstract fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; public abstract fun getActionSink ()Lcom/squareup/workflow1/Sink; + public abstract fun getWorkflowTracer ()Lcom/squareup/workflow1/WorkflowTracer; public abstract fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V } @@ -167,6 +168,7 @@ public final class com/squareup/workflow1/StatefulWorkflow$RenderContext : com/s public fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; public fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; public fun getActionSink ()Lcom/squareup/workflow1/Sink; + public fun getWorkflowTracer ()Lcom/squareup/workflow1/WorkflowTracer; public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V } @@ -192,6 +194,7 @@ public final class com/squareup/workflow1/StatelessWorkflow$RenderContext : com/ public fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; public fun eventHandler (Ljava/lang/String;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; public fun getActionSink ()Lcom/squareup/workflow1/Sink; + public fun getWorkflowTracer ()Lcom/squareup/workflow1/WorkflowTracer; public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V } @@ -307,6 +310,16 @@ public final class com/squareup/workflow1/WorkflowOutput { public fun toString ()Ljava/lang/String; } +public abstract interface class com/squareup/workflow1/WorkflowTracer { + public abstract fun beginSection (Ljava/lang/String;)V + public abstract fun endSection ()V +} + +public final class com/squareup/workflow1/WorkflowTracerKt { + public static final fun trace (Lcom/squareup/workflow1/WorkflowTracer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public static final fun trace (Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + public final class com/squareup/workflow1/Workflows { public static final fun RenderContext (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/StatefulWorkflow;)Lcom/squareup/workflow1/StatefulWorkflow$RenderContext; public static final fun RenderContext (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/StatelessWorkflow;)Lcom/squareup/workflow1/StatelessWorkflow$RenderContext; diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt index 103256fc3..e26614557 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt @@ -55,6 +55,8 @@ public interface BaseRenderContext { */ public val actionSink: Sink> + public val workflowTracer: WorkflowTracer? + /** * Ensures [child] is running as a child of this workflow, and returns the result of its * `render` method. @@ -437,6 +439,8 @@ internal fun key: String = "", handler: (T) -> WorkflowAction ) { - val workerWorkflow = WorkerWorkflow(workerType, key) + val workerWorkflow = workflowTracer.trace("CreateWorkerWorkflow") { + WorkerWorkflow(workerType, key) + } renderChild(workerWorkflow, props = worker, key = key, handler = handler) } diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt index 5219b5c86..64c6a33e3 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt @@ -30,11 +30,16 @@ import kotlin.reflect.KType */ internal class WorkerWorkflow( val workerType: KType, - private val key: String + private val key: String, + workflowTracer: WorkflowTracer? = null ) : StatefulWorkflow, Int, OutputT, Unit>(), ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(workerType) + override val realIdentifier: WorkflowIdentifier = + workflowTracer.trace("ComputeRealIdentifier" ) { + unsnapshottableIdentifier(workerType) + } + override fun describeRealIdentifier(): String = workerType.toString() override fun initialState( diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt new file mode 100644 index 000000000..139c88373 --- /dev/null +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt @@ -0,0 +1,36 @@ +package com.squareup.workflow1 + +/** + * This is a very simple tracing interface that can be passed into a workflow runtime in order + * to inject span tracing throughout the workflow core and runtime internals. + */ +public interface WorkflowTracer { + public fun beginSection(label: String): Unit + public fun endSection(): Unit +} + +/** + * Convenience function to wrap [block] with a trace span as defined by [WorkflowTracer]. + * Only calls [label] if there is an active [WorkflowTracer] use this for any label other than + * a constant. + */ +public inline fun WorkflowTracer?.trace(label: () -> String, block: () -> T): T { + val optimizedLabel = if (this !== null) { + label() + } else { + "" + } + return trace(optimizedLabel, block) +} + +/** + * Convenience function to wrap [block] with a trace span as defined by [WorkflowTracer]. + */ +public inline fun WorkflowTracer?.trace(label: String, block: () -> T): T { + this?.beginSection(label) + try { + return block() + } finally { + this?.endSection() + } +} diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index 42f8d1f00..5591211f0 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -10,8 +10,8 @@ public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup } public final class com/squareup/workflow1/RenderWorkflowKt { - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; } public final class com/squareup/workflow1/RenderingAndSnapshot { @@ -104,6 +104,7 @@ public abstract interface class com/squareup/workflow1/WorkflowInterceptor$Workf public abstract fun getRenderKey ()Ljava/lang/String; public abstract fun getRuntimeConfig ()Ljava/util/Set; public abstract fun getSessionId ()J + public abstract fun getWorkflowTracer ()Lcom/squareup/workflow1/WorkflowTracer; public abstract fun isRootWorkflow ()Z } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt index 78eb60c9a..f49bd1f03 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -110,12 +110,21 @@ public fun renderWorkflowIn( initialSnapshot: TreeSnapshot? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + workflowTracer: WorkflowTracer? = null, onOutput: suspend (OutputT) -> Unit ): StateFlow> { val chainedInterceptor = interceptors.chained() val runner = - WorkflowRunner(scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig) + WorkflowRunner( + scope, + workflow, + props, + initialSnapshot, + chainedInterceptor, + runtimeConfig, + workflowTracer + ) // Rendering is synchronous, so we can run the first render pass before launching the runtime // coroutine to calculate the initial rendering. diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt index 2363b7ad9..e86b5901e 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt @@ -160,6 +160,9 @@ public interface WorkflowInterceptor { /** The [RuntimeConfig] of the runtime this session is executing in. */ public val runtimeConfig: RuntimeConfig + + /** The optional [WorkflowTracer] of the runtime this session is executing in. */ + public val workflowTracer: WorkflowTracer? } /** @@ -314,6 +317,7 @@ private class InterceptedRenderContext( private val interceptor: RenderContextInterceptor ) : BaseRenderContext, Sink> { override val actionSink: Sink> get() = this + override val workflowTracer: WorkflowTracer? = baseRenderContext.workflowTracer override fun send(value: WorkflowAction) { interceptor.onActionSent(value) { interceptedAction -> diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt index 1e4d60128..9129bb638 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt @@ -4,13 +4,15 @@ import com.squareup.workflow1.BaseRenderContext import com.squareup.workflow1.Sink import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowTracer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.SendChannel internal class RealRenderContext( private val renderer: Renderer, private val sideEffectRunner: SideEffectRunner, - private val eventActionsChannel: SendChannel> + private val eventActionsChannel: SendChannel>, + override val workflowTracer: WorkflowTracer? ) : BaseRenderContext, Sink> { interface Renderer { diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt index 8311456bc..f5e6fe45f 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt @@ -9,7 +9,9 @@ import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.identifier +import com.squareup.workflow1.trace import kotlinx.coroutines.selects.SelectBuilder import kotlin.coroutines.CoroutineContext @@ -91,6 +93,7 @@ internal class SubtreeManager( childResult: ActionApplied<*> ) -> ActionProcessingResult, private val runtimeConfig: RuntimeConfig, + private val workflowTracer: WorkflowTracer?, private val workflowSession: WorkflowSession? = null, private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, private val idCounter: IdCounter? = null @@ -121,17 +124,22 @@ internal class SubtreeManager( handler: (ChildOutputT) -> WorkflowAction ): ChildRenderingT { // Prevent duplicate workflows with the same key. - children.forEachStaging { - require(!(it.matches(child, key))) { - "Expected keys to be unique for ${child.identifier}: key=\"$key\"" + workflowTracer.trace("CheckingUniqueMatches") { + children.forEachStaging { + require(!(it.matches(child, key))) { + "Expected keys to be unique for ${child.identifier}: key=\"$key\"" + } } } // Start tracking this case so we can be ready to render it. - val stagedChild = children.retainOrCreate( - predicate = { it.matches(child, key) }, - create = { createChildNode(child, props, key, handler) } - ) + val stagedChild = + workflowTracer.trace("RetainingChildren") { + children.retainOrCreate( + predicate = { it.matches(child, key) }, + create = { createChildNode(child, props, key, handler) } + ) + } stagedChild.setHandler(handler) return stagedChild.render(child.asStatefulWorkflow(), props) } @@ -188,6 +196,7 @@ internal class SubtreeManager( snapshot = childTreeSnapshots, baseContext = contextForChildren, runtimeConfig = runtimeConfig, + workflowTracer = workflowTracer, emitAppliedActionToParent = ::acceptChildActionResult, parent = workflowSession, interceptor = interceptor, diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt index cd6a40599..96355b3ff 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt @@ -14,9 +14,11 @@ import com.squareup.workflow1.WorkflowExperimentalApi import com.squareup.workflow1.WorkflowIdentifier import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.applyTo import com.squareup.workflow1.intercept import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner +import com.squareup.workflow1.trace import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -51,6 +53,7 @@ internal class WorkflowNode( baseContext: CoroutineContext, // Providing default value so we don't need to specify in test. override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + override val workflowTracer: WorkflowTracer? = null, private val emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult = { it }, override val parent: WorkflowSession? = null, @@ -74,6 +77,7 @@ internal class WorkflowNode( contextForChildren = coroutineContext, emitActionToParent = ::applyAction, runtimeConfig = runtimeConfig, + workflowTracer = workflowTracer, workflowSession = this, interceptor = interceptor, idCounter = idCounter @@ -87,7 +91,8 @@ internal class WorkflowNode( private val baseRenderContext = RealRenderContext( renderer = subtreeManager, sideEffectRunner = this, - eventActionsChannel = eventActionsChannel + eventActionsChannel = eventActionsChannel, + workflowTracer = workflowTracer, ) private val context = RenderContext(baseRenderContext, workflow) @@ -212,12 +217,14 @@ internal class WorkflowNode( .render(props, state, context) baseRenderContext.freeze() - // Tear down workflows and workers that are obsolete. - subtreeManager.commitRenderedChildren() - // Side effect jobs are launched lazily, since they can send actions to the sink, and can only - // be started after context is frozen. - sideEffects.forEachStaging { it.job.start() } - sideEffects.commitStaging { it.job.cancel() } + workflowTracer.trace("UpdateRuntimeTree") { + // Tear down workflows and workers that are obsolete. + subtreeManager.commitRenderedChildren() + // Side effect jobs are launched lazily, since they can send actions to the sink, and can only + // be started after context is frozen. + sideEffects.forEachStaging { it.job.start() } + sideEffects.commitStaging { it.job.cancel() } + } return rendering } @@ -261,8 +268,10 @@ internal class WorkflowNode( key: String, sideEffect: suspend CoroutineScope.() -> Unit ): SideEffectNode { - val scope = this + CoroutineName("sideEffect[$key] for $id") - val job = scope.launch(start = LAZY, block = sideEffect) - return SideEffectNode(key, job) + return workflowTracer.trace("CreateSideEffectNode") { + val scope = this + CoroutineName("sideEffect[$key] for $id") + val job = scope.launch(start = LAZY, block = sideEffect) + SideEffectNode(key, job) + } } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt index e19f190d8..9eb66bb1b 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt @@ -10,6 +10,7 @@ import com.squareup.workflow1.TreeSnapshot import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowTracer import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -28,7 +29,8 @@ internal class WorkflowRunner( props: StateFlow, snapshot: TreeSnapshot?, private val interceptor: WorkflowInterceptor, - private val runtimeConfig: RuntimeConfig + private val runtimeConfig: RuntimeConfig, + private val workflowTracer: WorkflowTracer? ) { private val workflow = protoWorkflow.asStatefulWorkflow() private val idCounter = IdCounter() @@ -55,6 +57,7 @@ internal class WorkflowRunner( snapshot = snapshot, baseContext = scope.coroutineContext, runtimeConfig = runtimeConfig, + workflowTracer = workflowTracer, interceptor = interceptor, idCounter = idCounter ) diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt index 4f91fef74..73fdf7116 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -69,7 +69,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = pausedTestScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertEquals("props: foo", renderings.value.rendering) } @@ -88,7 +89,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = pausedTestScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertEquals("props: foo", renderings.value.rendering) } @@ -112,7 +114,8 @@ class RenderWorkflowInTest { workflow, testScope, MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} testScope.advanceUntilIdle() @@ -141,7 +144,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} testScope.advanceUntilIdle() @@ -160,7 +164,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertEquals("props: foo", renderings.value.rendering) @@ -239,7 +244,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig1 + runtimeConfig = runtimeConfig1, + workflowTracer = null, ) {} // Interact with the workflow to change the state. @@ -266,6 +272,7 @@ class RenderWorkflowInTest { scope = restoreScope, props = props, initialSnapshot = snapshot, + workflowTracer = null, runtimeConfig = runtimeConfig2 ) {} runtimeMatrixTestRunner.assertEquals( @@ -302,7 +309,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} val emitted = mutableListOf>() @@ -360,7 +368,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) { receivedOutputs += it } @@ -402,7 +411,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) { it: String -> receivedOutputs += it assertTrue(emittedRenderings.contains(it)) @@ -436,7 +446,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) { onOutputCalls++ } assertEquals(0, renderings.value.rendering) assertEquals(0, onOutputCalls) @@ -473,7 +484,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} } assertTrue(testScope.isActive) @@ -499,7 +511,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} } assertFalse(sideEffectWasRan) @@ -532,7 +545,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} } assertTrue(sideEffectWasRan) @@ -565,7 +579,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} } assertFalse(sideEffectWasRan) @@ -592,7 +607,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertTrue(testScope.isActive) @@ -622,7 +638,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertTrue(testScope.isActive) @@ -651,7 +668,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertNull(cancellationException) assertTrue(testScope.isActive) @@ -681,7 +699,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertTrue(testScope.isActive) assertTrue(renderCount == 1) @@ -714,7 +733,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} assertNull(cancellationException) assertTrue(testScope.isActive) @@ -735,7 +755,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} // Collect in separate scope so we actually test that the parent scope is failed when it's @@ -768,7 +789,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = pausedTestScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} pausedTestScope.launch { @@ -796,7 +818,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = pausedTestScope, props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) { throw ExpectedException() } @@ -837,6 +860,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, + workflowTracer = null, onOutput = { events += "output($it)" } ) .onEach { events += "rendering(${it.rendering})" } @@ -883,7 +907,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope + exceptionHandler, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} .value .snapshot @@ -921,7 +946,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope + exceptionHandler, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} val renderings = ras.map { it.rendering } .produceIn(testScope) @@ -969,7 +995,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope + exceptionHandler, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} val renderings = ras.map { it.rendering } .produceIn(testScope) @@ -1016,7 +1043,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} val emitted = mutableListOf>() @@ -1057,7 +1085,8 @@ class RenderWorkflowInTest { workflow = workflow, scope = testScope, props = props, - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowTracer = null, ) {} val emitted = mutableListOf>() diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt index acebb5f8f..2af982fd3 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt @@ -88,11 +88,13 @@ internal class SimpleLoggingWorkflowInterceptorTest { override val sessionId: Long get() = 42 override val parent: WorkflowSession? get() = null override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + override val workflowTracer: WorkflowTracer? = null } private object FakeRenderContext : BaseRenderContext { override val actionSink: Sink> get() = fail() + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt index 4e7c26a21..5a233c00a 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt @@ -62,6 +62,7 @@ internal class WorkflowInterceptorTest { val intercepted = recorder.intercept(TestWorkflow, TestWorkflow.session) val fakeContext = object : BaseRenderContext { override val actionSink: Sink> get() = fail() + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, @@ -103,6 +104,7 @@ internal class WorkflowInterceptorTest { val fakeContext = object : BaseRenderContext { override val actionSink: Sink> = Sink { value -> actions += value } + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, @@ -136,6 +138,7 @@ internal class WorkflowInterceptorTest { val intercepted = recorder.intercept(workflow, workflow.session) val fakeContext = object : BaseRenderContext { override val actionSink: Sink> get() = fail() + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, @@ -197,6 +200,7 @@ internal class WorkflowInterceptorTest { val intercepted = recorder.intercept(workflow, workflow.session) val fakeContext = object : BaseRenderContext { override val actionSink: Sink> get() = fail() + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, @@ -224,6 +228,7 @@ internal class WorkflowInterceptorTest { override val sessionId: Long = 0 override val parent: WorkflowSession? = null override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + override val workflowTracer: WorkflowTracer? = null } private object TestWorkflow : StatefulWorkflow() { diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt index 6a35d557c..fb6d3fe5d 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt @@ -14,6 +14,7 @@ import com.squareup.workflow1.WorkflowIdentifier import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.identifier import com.squareup.workflow1.parse import com.squareup.workflow1.rendering @@ -322,6 +323,7 @@ internal class ChainedWorkflowInterceptorTest { private object FakeRenderContext : BaseRenderContext { override val actionSink: Sink> get() = fail() + override val workflowTracer: WorkflowTracer? = null override fun renderChild( child: Workflow, @@ -348,5 +350,6 @@ internal class ChainedWorkflowInterceptorTest { override val sessionId: Long = 0 override val parent: WorkflowSession? = null override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + override val workflowTracer: WorkflowTracer? = null } } diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt index 4c58b9be2..35cdc97d4 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt @@ -393,11 +393,21 @@ internal class RealRenderContextTest { private fun createdPoisonedContext(): RealRenderContext { val workerRunner = PoisonRunner() - return RealRenderContext(PoisonRenderer(), workerRunner, eventActionsChannel) + return RealRenderContext( + PoisonRenderer(), + workerRunner, + eventActionsChannel, + workflowTracer = null + ) } private fun createTestContext(): RealRenderContext { val workerRunner = TestRunner() - return RealRenderContext(TestRenderer(), workerRunner, eventActionsChannel) + return RealRenderContext( + TestRenderer(), + workerRunner, + eventActionsChannel, + workflowTracer = null + ) } } diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt index 9c37ef111..3653fb2d3 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt @@ -314,6 +314,7 @@ internal class SubtreeManagerTest { runtimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, emitActionToParent = { action, childResult -> ActionApplied(WorkflowOutput(action), childResult.stateChanged) - } + }, + workflowTracer = null ) } diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt index 8ff1c4229..82f4bdc00 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt @@ -19,6 +19,7 @@ import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.action import com.squareup.workflow1.contraMap import com.squareup.workflow1.identifier @@ -1339,5 +1340,6 @@ internal class WorkflowNodeTest { override val renderKey: String = "" override val parent: WorkflowSession? = null override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + override val workflowTracer: WorkflowTracer? = null } } diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt index df5e2d469..7efab5a76 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt @@ -344,6 +344,7 @@ internal class WorkflowRunnerTest { props, snapshot = null, interceptor = NoopWorkflowInterceptor, - runtimeConfig + runtimeConfig, + workflowTracer = null ) } diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt index 067d1dca2..39f8b4a7d 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt @@ -13,6 +13,7 @@ import com.squareup.workflow1.WorkflowIdentifierType import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.applyTo import com.squareup.workflow1.identifier import com.squareup.workflow1.testing.RealRenderTester.Expectation @@ -107,6 +108,7 @@ internal class RealRenderTester( state to actionApplied.output } override val actionSink: Sink> get() = this + override val workflowTracer: WorkflowTracer? = null override fun expectWorkflow( description: String, diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index d0218530c..6e43b8975 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -1,11 +1,11 @@ public final class com/squareup/workflow1/ui/AndroidRenderWorkflowKt { public static final fun removeWorkflowState (Landroidx/lifecycle/SavedStateHandle;)V - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; } public abstract interface class com/squareup/workflow1/ui/AndroidScreen : com/squareup/workflow1/ui/Screen { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt index 2ba4defea..1d0c19417 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt @@ -6,6 +6,7 @@ import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfigOptions import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.renderWorkflowIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -86,6 +87,7 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + workflowTracer: WorkflowTracer? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow { return renderWorkflowIn( @@ -95,6 +97,7 @@ public fun renderWorkflowIn( savedStateHandle = savedStateHandle, interceptors = interceptors, runtimeConfig = runtimeConfig, + workflowTracer = workflowTracer, onOutput = onOutput ) } @@ -172,6 +175,7 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + workflowTracer: WorkflowTracer? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow = renderWorkflowIn( workflow, @@ -180,6 +184,7 @@ public fun renderWorkflowIn( savedStateHandle, interceptors, runtimeConfig, + workflowTracer, onOutput ) @@ -272,6 +277,7 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + workflowTracer: WorkflowTracer? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow { val restoredSnap = savedStateHandle?.get(KEY)?.snapshot @@ -282,6 +288,7 @@ public fun renderWorkflowIn( restoredSnap, interceptors, runtimeConfig, + workflowTracer, onOutput ) From 4a0b76fc45cb9ae55ce494e2f719dda42ed2eaa2 Mon Sep 17 00:00:00 2001 From: Stephen Edwards Date: Fri, 6 Dec 2024 10:51:00 -0500 Subject: [PATCH 2/2] Add Tests for WorkflowTracer --- workflow-core/api/workflow-core.api | 1 - .../com/squareup/workflow1/WorkerWorkflow.kt | 2 +- .../com/squareup/workflow1/WorkflowTracer.kt | 35 ++- .../com/squareup/workflow1/RenderWorkflow.kt | 19 +- .../workflow1/internal/SubtreeManager.kt | 4 +- .../workflow1/internal/WorkflowChildNode.kt | 7 +- .../workflow1/RenderWorkflowInTest.kt | 233 +++++++++++++----- 7 files changed, 201 insertions(+), 100 deletions(-) diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index 8afbb5c61..6fb107d5f 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -317,7 +317,6 @@ public abstract interface class com/squareup/workflow1/WorkflowTracer { public final class com/squareup/workflow1/WorkflowTracerKt { public static final fun trace (Lcom/squareup/workflow1/WorkflowTracer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; - public static final fun trace (Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; } public final class com/squareup/workflow1/Workflows { diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt index 64c6a33e3..dedfa5902 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt @@ -36,7 +36,7 @@ internal class WorkerWorkflow( ImpostorWorkflow { override val realIdentifier: WorkflowIdentifier = - workflowTracer.trace("ComputeRealIdentifier" ) { + workflowTracer.trace("ComputeRealIdentifier") { unsnapshottableIdentifier(workerType) } diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt index 139c88373..49dd279f4 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowTracer.kt @@ -10,27 +10,22 @@ public interface WorkflowTracer { } /** - * Convenience function to wrap [block] with a trace span as defined by [WorkflowTracer]. - * Only calls [label] if there is an active [WorkflowTracer] use this for any label other than - * a constant. + * Convenience function to wrap [block] with a trace span as defined by [WorkflowTracer]. This + * wraps very frequently evaluated code and we should only use constants for [label], with no + * interpolation. */ -public inline fun WorkflowTracer?.trace(label: () -> String, block: () -> T): T { - val optimizedLabel = if (this !== null) { - label() +public inline fun WorkflowTracer?.trace( + label: String, + block: () -> T +): T { + return if (this == null) { + block() } else { - "" - } - return trace(optimizedLabel, block) -} - -/** - * Convenience function to wrap [block] with a trace span as defined by [WorkflowTracer]. - */ -public inline fun WorkflowTracer?.trace(label: String, block: () -> T): T { - this?.beginSection(label) - try { - return block() - } finally { - this?.endSection() + beginSection(label) + try { + return block() + } finally { + endSection() + } } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt index f49bd1f03..62bac9eb5 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -115,16 +115,15 @@ public fun renderWorkflowIn( ): StateFlow> { val chainedInterceptor = interceptors.chained() - val runner = - WorkflowRunner( - scope, - workflow, - props, - initialSnapshot, - chainedInterceptor, - runtimeConfig, - workflowTracer - ) + val runner = WorkflowRunner( + scope, + workflow, + props, + initialSnapshot, + chainedInterceptor, + runtimeConfig, + workflowTracer + ) // Rendering is synchronous, so we can run the first render pass before launching the runtime // coroutine to calculate the initial rendering. diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt index f5e6fe45f..7ec3bd6ec 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt @@ -126,7 +126,7 @@ internal class SubtreeManager( // Prevent duplicate workflows with the same key. workflowTracer.trace("CheckingUniqueMatches") { children.forEachStaging { - require(!(it.matches(child, key))) { + require(!(it.matches(child, key, workflowTracer))) { "Expected keys to be unique for ${child.identifier}: key=\"$key\"" } } @@ -136,7 +136,7 @@ internal class SubtreeManager( val stagedChild = workflowTracer.trace("RetainingChildren") { children.retainOrCreate( - predicate = { it.matches(child, key) }, + predicate = { it.matches(child, key, workflowTracer) }, create = { createChildNode(child, props, key, handler) } ) } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt index b6a833a95..4f5d357fe 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt @@ -3,7 +3,9 @@ package com.squareup.workflow1.internal import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowTracer import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.trace /** * Representation of a child workflow that has been rendered by another workflow. @@ -32,8 +34,9 @@ internal class WorkflowChildNode< */ fun matches( otherWorkflow: Workflow<*, *, *>, - key: String - ): Boolean = id.matches(otherWorkflow, key) + key: String, + workflowTracer: WorkflowTracer? + ): Boolean = workflowTracer.trace("matches") { id.matches(otherWorkflow, key) } /** * Updates the handler function that will be invoked by [acceptChildOutput]. diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt index 73fdf7116..5d770e6a3 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -28,6 +28,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import okio.ByteString import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class, WorkflowExperimentalRuntime::class) class RenderWorkflowInTest { @@ -42,16 +44,35 @@ class RenderWorkflowInTest { */ private lateinit var testScope: TestScope - private val runtimeOptions: Sequence = arrayOf( - RuntimeConfigOptions.RENDER_PER_ACTION, - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(CONFLATE_STALE_RENDERINGS), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES) + private val traces: StringBuilder = StringBuilder() + private val testTracer: WorkflowTracer = object : WorkflowTracer { + var prefix: String = "" + override fun beginSection(label: String) { + traces.appendLine("${prefix}Starting$label") + prefix += " " + } + + override fun endSection() { + prefix = prefix.substring(0, prefix.length - 2) + traces.appendLine("${prefix}Ending") + } + } + + private val runtimeOptions: Sequence> = arrayOf( + RuntimeConfigOptions.RENDER_PER_ACTION to null, + RuntimeConfigOptions.RENDER_PER_ACTION to testTracer, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to null, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to testTracer, + setOf(CONFLATE_STALE_RENDERINGS) to null, + setOf(CONFLATE_STALE_RENDERINGS) to testTracer, + setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES) to null, + setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES) to testTracer, ).asSequence() - private val runtimeTestRunner = ParameterizedTestRunner() + private val runtimeTestRunner = ParameterizedTestRunner>() private fun setup() { + traces.clear() pausedTestScope = TestScope() testScope = TestScope(UnconfinedTestDispatcher()) } @@ -60,7 +81,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } // Don't allow the workflow runtime to actually start. @@ -70,7 +91,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertEquals("props: foo", renderings.value.rendering) } @@ -80,7 +101,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } @@ -90,7 +111,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertEquals("props: foo", renderings.value.rendering) } @@ -101,7 +122,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var sideEffectWasRan = false val workflow = Workflow.stateless { runningSideEffect("test") { @@ -115,7 +136,7 @@ class RenderWorkflowInTest { testScope, MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} testScope.advanceUntilIdle() @@ -128,7 +149,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var sideEffectWasRan = false val childWorkflow = Workflow.stateless { runningSideEffect("test") { @@ -145,7 +166,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} testScope.advanceUntilIdle() @@ -157,7 +178,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } val renderings = renderWorkflowIn( @@ -165,7 +186,7 @@ class RenderWorkflowInTest { scope = testScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertEquals("props: foo", renderings.value.rendering) @@ -287,7 +308,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> lateinit var sink: Sink var snapped = false @@ -310,7 +331,7 @@ class RenderWorkflowInTest { scope = testScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} val emitted = mutableListOf>() @@ -355,7 +376,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = Channel() val workflow = Workflow.stateless { runningWorker( @@ -369,7 +390,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) { receivedOutputs += it } @@ -387,7 +408,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = Channel() val workflow = Workflow.stateful( initialState = "initial", @@ -412,7 +433,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) { it: String -> receivedOutputs += it assertTrue(emittedRenderings.contains(it)) @@ -434,11 +455,62 @@ class RenderWorkflowInTest { } } + @Test fun tracer_includes_expected_sections() { + val runtimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + val workflowTracer = testTracer + setup() + val trigger = Channel() + val workflow = Workflow.stateful( + initialState = "initial", + render = { renderState -> + runningWorker( + trigger.consumeAsFlow() + .asWorker() + ) { + action("") { + state = it + setOutput(it) + } + } + renderState + } + ) + + val emittedRenderings = mutableListOf() + val receivedOutputs = mutableListOf() + val renderings = renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = workflowTracer, + ) { it: String -> + receivedOutputs += it + assertTrue(emittedRenderings.contains(it)) + } + assertTrue(receivedOutputs.isEmpty()) + + val scope = CoroutineScope(Unconfined) + scope.launch { + renderings.collect { rendering: RenderingAndSnapshot -> + emittedRenderings += rendering.rendering + } + } + + trigger.trySend("foo").isSuccess + + trigger.trySend("bar").isSuccess + + scope.cancel() + + assertEquals(EXPECTED_TRACE, traces.toString().trim()) + } + @Test fun onOutput_is_not_called_when_no_output_emitted() { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val workflow = Workflow.stateless { props -> props } var onOutputCalls = 0 val props = MutableStateFlow(0) @@ -447,7 +519,7 @@ class RenderWorkflowInTest { scope = testScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) { onOutputCalls++ } assertEquals(0, renderings.value.rendering) assertEquals(0, onOutputCalls) @@ -475,7 +547,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val workflow = Workflow.stateless { throw ExpectedException() } @@ -485,7 +557,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} } assertTrue(testScope.isActive) @@ -497,7 +569,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var sideEffectWasRan = false val workflow = Workflow.stateless { runningSideEffect("test") { @@ -512,7 +584,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} } assertFalse(sideEffectWasRan) @@ -524,7 +596,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var sideEffectWasRan = false var cancellationException: Throwable? = null val childWorkflow = Workflow.stateless { @@ -546,7 +618,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} } assertTrue(sideEffectWasRan) @@ -562,7 +634,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var sideEffectWasRan = false val childWorkflow = Workflow.stateless { runningSideEffect("test") { @@ -580,7 +652,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} } assertFalse(sideEffectWasRan) @@ -591,7 +663,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = CompletableDeferred() // Throws an exception when trigger is completed. val workflow = Workflow.stateful( @@ -608,7 +680,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertTrue(testScope.isActive) @@ -624,7 +696,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = CompletableDeferred() // Throws an exception when trigger is completed. val workflow = Workflow.stateless { @@ -639,7 +711,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertTrue(testScope.isActive) @@ -655,7 +727,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var cancellationException: Throwable? = null val workflow = Workflow.stateless { runningSideEffect(key = "test1") { @@ -669,7 +741,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertNull(cancellationException) assertTrue(testScope.isActive) @@ -684,7 +756,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = CompletableDeferred() var renderCount = 0 val workflow = Workflow.stateless { @@ -700,7 +772,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertTrue(testScope.isActive) assertTrue(renderCount == 1) @@ -720,7 +792,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var cancellationException: Throwable? = null val workflow = Workflow.stateless { runningSideEffect(key = "failing") { @@ -734,7 +806,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} assertNull(cancellationException) assertTrue(testScope.isActive) @@ -749,14 +821,14 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val workflow = Workflow.stateless {} val renderings = renderWorkflowIn( workflow = workflow, scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} // Collect in separate scope so we actually test that the parent scope is failed when it's @@ -774,7 +846,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> var cancellationException: Throwable? = null val workflow = Workflow.stateless { runningSideEffect(key = "test") { @@ -790,7 +862,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} pausedTestScope.launch { @@ -808,7 +880,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val trigger = CompletableDeferred() // Emits a Unit when trigger is completed. val workflow = Workflow.stateless { @@ -819,7 +891,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) { throw ExpectedException() } @@ -838,7 +910,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val outputTrigger = CompletableDeferred() // A workflow whose state and rendering is the last output that it emitted. val workflow = Workflow.stateful( @@ -860,7 +932,7 @@ class RenderWorkflowInTest { scope = pausedTestScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, onOutput = { events += "output($it)" } ) .onEach { events += "rendering(${it.rendering})" } @@ -888,7 +960,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> val workflow = Workflow.stateful( snapshot = { Snapshot.of { @@ -908,7 +980,7 @@ class RenderWorkflowInTest { scope = testScope + exceptionHandler, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} .value .snapshot @@ -926,7 +998,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> @Suppress("EqualsOrHashCode", "unused") class FailRendering(val value: Int) { override fun equals(other: Any?): Boolean { @@ -947,7 +1019,7 @@ class RenderWorkflowInTest { scope = testScope + exceptionHandler, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} val renderings = ras.map { it.rendering } .produceIn(testScope) @@ -975,7 +1047,7 @@ class RenderWorkflowInTest { runtimeTestRunner.runParametrizedTest( paramSource = runtimeOptions, before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> @Suppress("EqualsOrHashCode") data class FailRendering(val value: Int) { override fun hashCode(): Int { @@ -996,7 +1068,7 @@ class RenderWorkflowInTest { scope = testScope + exceptionHandler, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} val renderings = ras.map { it.rendering } .produceIn(testScope) @@ -1023,11 +1095,13 @@ class RenderWorkflowInTest { @Test fun for_render_on_state_change_only_we_do_not_render_if_state_not_changed() { runtimeTestRunner.runParametrizedTest( paramSource = arrayOf( - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to null, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to testTracer, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) to null, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) to testTracer, ).asSequence(), before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink @@ -1044,7 +1118,7 @@ class RenderWorkflowInTest { scope = testScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} val emitted = mutableListOf>() @@ -1065,11 +1139,13 @@ class RenderWorkflowInTest { @Test fun for_render_on_state_change_only_we_render_if_state_changed() { runtimeTestRunner.runParametrizedTest( paramSource = arrayOf( - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to null, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES) to testTracer, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) to null, + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS) to testTracer, ).asSequence(), before = ::setup, - ) { runtimeConfig: RuntimeConfig -> + ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) -> check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink @@ -1086,7 +1162,7 @@ class RenderWorkflowInTest { scope = testScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = null, + workflowTracer = workflowTracer, ) {} val emitted = mutableListOf>() @@ -1105,4 +1181,33 @@ class RenderWorkflowInTest { } private class ExpectedException : RuntimeException() + + companion object { + internal val EXPECTED_TRACE: String = """ +StartingCreateWorkerWorkflow +Ending +StartingCheckingUniqueMatches +Ending +StartingRetainingChildren +Ending +StartingCreateSideEffectNode +Ending +StartingUpdateRuntimeTree +Ending +StartingUpdateRuntimeTree +Ending +StartingCreateWorkerWorkflow +Ending +StartingCheckingUniqueMatches +Ending +StartingRetainingChildren + Startingmatches + Ending +Ending +StartingUpdateRuntimeTree +Ending +StartingUpdateRuntimeTree +Ending + """.trim() + } }