Skip to content

Commit f5749a9

Browse files
committed
Introduces RenderTester.expectRemember().
1 parent fc71ed5 commit f5749a9

File tree

6 files changed

+414
-159
lines changed

6 files changed

+414
-159
lines changed

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ internal class RealRenderContext<out PropsT, StateT, OutputT>(
109109
frozen = false
110110
}
111111

112-
private fun checkNotFrozen(reason: () -> String = { "" }) = check(!frozen) {
113-
"RenderContext cannot be used after render method returns" +
114-
"${reason().takeUnless { it.isBlank() }?.let { " ($it)" }}"
112+
private fun checkNotFrozen(reason: () -> String) = check(!frozen) {
113+
"RenderContext cannot be used after render method returns: ${reason()}"
115114
}
116115
}

workflow-testing/api/workflow-testing.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ public abstract interface class com/squareup/workflow1/testing/RenderTestResult
2525

2626
public abstract class com/squareup/workflow1/testing/RenderTester {
2727
public fun <init> ()V
28+
public abstract fun expectRemember (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
29+
public static synthetic fun expectRemember$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
2830
public abstract fun expectSideEffect (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
2931
public static synthetic fun expectSideEffect$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
3032
public abstract fun render (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTestResult;
3133
public static synthetic fun render$default (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTestResult;
34+
public abstract fun requireExplicitRememberExpectations ()Lcom/squareup/workflow1/testing/RenderTester;
3235
public abstract fun requireExplicitSideEffectExpectations ()Lcom/squareup/workflow1/testing/RenderTester;
3336
public abstract fun requireExplicitWorkerExpectations ()Lcom/squareup/workflow1/testing/RenderTester;
3437
}
@@ -47,6 +50,13 @@ public final class com/squareup/workflow1/testing/RenderTester$ChildWorkflowMatc
4750
public static final field INSTANCE Lcom/squareup/workflow1/testing/RenderTester$ChildWorkflowMatch$NotMatched;
4851
}
4952

53+
public final class com/squareup/workflow1/testing/RenderTester$RememberInvocation {
54+
public fun <init> (Ljava/lang/String;Lkotlin/reflect/KType;Ljava/util/List;)V
55+
public final fun getInputs ()Ljava/util/List;
56+
public final fun getKey ()Ljava/lang/String;
57+
public final fun getResultType ()Lkotlin/reflect/KType;
58+
}
59+
5060
public final class com/squareup/workflow1/testing/RenderTester$RenderChildInvocation {
5161
public fun <init> (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Lkotlin/reflect/KTypeProjection;Lkotlin/reflect/KTypeProjection;Ljava/lang/String;)V
5262
public final fun getOutputType ()Lkotlin/reflect/KTypeProjection;
@@ -57,6 +67,8 @@ public final class com/squareup/workflow1/testing/RenderTester$RenderChildInvoca
5767
}
5868

5969
public final class com/squareup/workflow1/testing/RenderTesterKt {
70+
public static final fun expectRemember (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;Lkotlin/reflect/KType;[Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
71+
public static synthetic fun expectRemember$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;Lkotlin/reflect/KType;[Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
6072
public static final fun expectSideEffect (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester;
6173
public static final fun expectWorkflow (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
6274
public static final fun expectWorkflow (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;

workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.squareup.workflow1.WorkflowTracer
1818
import com.squareup.workflow1.applyTo
1919
import com.squareup.workflow1.identifier
2020
import com.squareup.workflow1.testing.RealRenderTester.Expectation
21+
import com.squareup.workflow1.testing.RealRenderTester.Expectation.ExpectedRemember
2122
import com.squareup.workflow1.testing.RealRenderTester.Expectation.ExpectedSideEffect
2223
import com.squareup.workflow1.testing.RealRenderTester.Expectation.ExpectedWorker
2324
import com.squareup.workflow1.testing.RealRenderTester.Expectation.ExpectedWorkflow
@@ -57,16 +58,6 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
5758
* property stores the [WorkflowAction] that was specified to handle that output.
5859
*/
5960
private var processedAction: WorkflowAction<PropsT, StateT, OutputT>? = null,
60-
/**
61-
* Tracks the identifier/key pairs of all calls to [renderChild], so it can emulate the behavior
62-
* of the real runtime and throw if a workflow is rendered twice in the same pass.
63-
*/
64-
private val renderedChildren: MutableList<Pair<WorkflowIdentifier, String>> = mutableListOf(),
65-
/**
66-
* Tracks the keys of all calls to [runningSideEffect], so it can emulate the behavior of the real
67-
* runtime and throw if a side effects is ran twice in the same pass.
68-
*/
69-
private val ranSideEffects: MutableList<String> = mutableListOf()
7061
) : RenderTester<PropsT, StateT, OutputT, RenderingT>(),
7162
BaseRenderContext<PropsT, StateT, OutputT>,
7263
RenderTestResult<PropsT, StateT, OutputT, RenderingT>,
@@ -101,25 +92,44 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
10192
) : Expectation<Nothing>() {
10293
override fun describe(): String = description
10394
}
95+
96+
data class ExpectedRemember(
97+
val matcher: (RememberInvocation) -> Boolean,
98+
val exactMatch: Boolean,
99+
val description: String,
100+
) : Expectation<Nothing>() {
101+
override fun describe(): String = description
102+
}
104103
}
105104

106105
private var frozen = false
107106

108107
private var explicitWorkerExpectationsRequired: Boolean = false
109108
private var explicitSideEffectExpectationsRequired: Boolean = false
109+
private var explicitRememberExpectationsRequired: Boolean = false
110110
private val stateAndOutput: Pair<StateT, WorkflowOutput<OutputT>?> by lazy {
111111
val action = processedAction ?: noAction()
112112
val (state, actionApplied) = action.applyTo(props, state)
113113
state to actionApplied.output
114114
}
115115

116-
private data class TestRememberKey(
117-
val key: String,
118-
val resultType: KType,
119-
val inputs: List<Any?>,
120-
)
116+
/**
117+
* Tracks the identifier/key pairs of all calls to [renderChild], so it can emulate the behavior
118+
* of the real runtime and throw if a workflow is rendered twice in the same pass.
119+
*/
120+
private val renderedChildren: MutableList<Pair<WorkflowIdentifier, String>> = mutableListOf()
121121

122-
private var rememberSet = mutableSetOf<TestRememberKey>()
122+
/**
123+
* Tracks the keys of all calls to [runningSideEffect], so it can emulate the behavior of the real
124+
* runtime and throw if duplicate keys are found.
125+
*/
126+
private val runSideEffects: MutableList<String> = mutableListOf()
127+
128+
/**
129+
* Tracks the invocations all calls to [remember], so it can emulate the behavior
130+
* of the real runtime and throw if duplicate keys are found.
131+
*/
132+
private val rememberSet = mutableSetOf<RememberInvocation>()
123133

124134
override val actionSink: Sink<WorkflowAction<PropsT, StateT, OutputT>> get() = this
125135
override val workflowTracer: WorkflowTracer? = null
@@ -140,6 +150,14 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
140150
expectations += ExpectedSideEffect(matcher, exactMatch, description)
141151
}
142152

153+
override fun expectRemember(
154+
description: String,
155+
exactMatch: Boolean,
156+
matcher: (RememberInvocation) -> Boolean
157+
): RenderTester<PropsT, StateT, OutputT, RenderingT> = apply {
158+
expectations += ExpectedRemember(matcher, exactMatch, description)
159+
}
160+
143161
override fun render(
144162
block: (RenderingT) -> Unit
145163
): RenderTestResult<PropsT, StateT, OutputT, RenderingT> {
@@ -153,6 +171,11 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
153171
expectSideEffect(description = "unexpected side effect", exactMatch = false) { true }
154172
}
155173

174+
if (!explicitRememberExpectationsRequired) {
175+
// Allow unexpected remember calls.
176+
expectRemember(description = "unexpected remembered value", exactMatch = false) { true }
177+
}
178+
156179
frozen = false
157180
// Clone the expectations to run a "dry" render pass.
158181
val noopContext = deepCloneForRender()
@@ -168,12 +191,13 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
168191
// Workers are always exact matches.
169192
is ExpectedWorker -> true
170193
is ExpectedSideEffect -> it.exactMatch
194+
is ExpectedRemember -> it.exactMatch
171195
}
172196
}
173197
if (unconsumedExactMatches.isNotEmpty()) {
174198
throw AssertionError(
175-
"Expected ${unconsumedExactMatches.size} more workflows, workers, or " +
176-
"side effects to be run:\n" +
199+
"Expected ${unconsumedExactMatches.size} more workflows, workers, " +
200+
"side effects, or remembers to be run:\n" +
177201
unconsumedExactMatches.joinToString(separator = "\n") { " ${it.describe()}" }
178202
)
179203
}
@@ -249,8 +273,8 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
249273
sideEffect: suspend CoroutineScope.() -> Unit
250274
) {
251275
checkNotFrozen { "runningSideEffect($key)" }
252-
require(key !in ranSideEffects) { "Expected side effect keys to be unique: \"$key\"" }
253-
ranSideEffects += key
276+
require(key !in runSideEffects) { "Expected side effect keys to be unique: \"$key\"" }
277+
runSideEffects += key
254278

255279
val description = "side effect with key \"$key\""
256280

@@ -285,10 +309,34 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
285309
calculation: () -> ResultT
286310
): ResultT {
287311
checkNotFrozen { "remember($key)" }
288-
val mapKey = TestRememberKey(key, resultType, inputs.asList())
289-
check(rememberSet.add(mapKey)) {
312+
val invocation = RememberInvocation(key, resultType, inputs.asList())
313+
check(rememberSet.add(invocation)) {
290314
"Expected combination of key, inputs and result type to be unique: \"$key\""
291315
}
316+
317+
val description = "remember with key \"$key\""
318+
319+
val matches = expectations.filterIsInstance<ExpectedRemember>()
320+
.mapNotNull { if (it.matcher(invocation)) it else null }
321+
if (matches.isEmpty()) {
322+
throw AssertionError("Unexpected $description")
323+
}
324+
325+
val exactMatches = matches.filter { it.exactMatch }
326+
if (exactMatches.size > 1) {
327+
throw AssertionError(
328+
"Multiple expectations matched $description: \n" +
329+
exactMatches.joinToString(separator = "\n") { " ${it.describe()}" }
330+
)
331+
}
332+
333+
// Inexact matches are not consumable.
334+
exactMatches.singleOrNull()
335+
?.let { expected ->
336+
expectations -= expected
337+
consumedExpectations += expected
338+
}
339+
292340
return calculation()
293341
}
294342

@@ -302,6 +350,11 @@ internal class RealRenderTester<PropsT, StateT, OutputT, RenderingT>(
302350
explicitSideEffectExpectationsRequired = true
303351
}
304352

353+
override fun requireExplicitRememberExpectations():
354+
RenderTester<PropsT, StateT, OutputT, RenderingT> = this.apply {
355+
explicitRememberExpectationsRequired = true
356+
}
357+
305358
override fun send(value: WorkflowAction<PropsT, StateT, OutputT>) {
306359
if (!frozen) {
307360
throw UnsupportedOperationException(

0 commit comments

Comments
 (0)