-
Notifications
You must be signed in to change notification settings - Fork 80
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
Introduce SideEffect #1021 #1174
Conversation
b131954
to
b847a7a
Compare
b847a7a
to
4103c3f
Compare
@@ -99,6 +102,14 @@ public struct ExpectedWorker { | |||
} | |||
} | |||
|
|||
public struct ExpectedSideEffect { |
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.
I think right now we don’t have a way to just send a WorkflowAction
to RenderTester
. So we can’t write tests that act like a side-effect did something (and we couldn’t convert ExpectedWorker
to be built on ExpectedSideEffect
. We should consider adding an action to ExpectedSideEffect
or a way to send an action to RenderTester
directly.
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.
Good point @bencochran .
or a way to send an action to RenderTester directly.
Any ideas on the best way to do that?
- Pass in
context
, so that we canmakeSink()
and send event. - Pass in
inout state
so that we can just modify the state directly. - Add a generic
ActionType
toExpectedSideEffect
and pass in asink
ofActionType
.
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.
@bencochran, @AquaGeek: Please take a look at ExpectedSideEffect
when you get a chance. I've made it take in a closure which accepts the context.
Currently, the only init
takes in key
and action
, however, I wanted to make sure we can pass in arbitrary block of code in the future to ensure it's in line with the runSideEffect
api.
@@ -230,7 +232,7 @@ | |||
fileprivate final class RenderTestContext<T: Workflow>: RenderContextType { | |||
typealias WorkflowType = T | |||
|
|||
private var (lifetime, token) = Lifetime.make() | |||
private var (lifetime, token) = ReactiveSwift.Lifetime.make() |
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.
Do we dislike the need to disambiguate enough to use a different name than Lifetime
? I couldn’t come up with anything better when I originally typed my version of this, but wanted to raise the question in case others had thoughts.
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.
I don't think we need to rename just to disambiguate. As we continue to remove the ReactiveSwift
dependency from the core library - this will become less of an issue.
4103c3f
to
4efbd42
Compare
Been meaning to review this, hopefully will get to it today or Monday since I want to start working on this for Kotlin soon. |
4efbd42
to
c68e408
Compare
@@ -99,6 +104,24 @@ public struct ExpectedWorker { | |||
} | |||
} | |||
|
|||
public struct ExpectedSideEffect<WorkflowType: Workflow> { | |||
let key: AnyHashable | |||
let action: ((RenderContext<WorkflowType>) -> Void)? |
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.
What's the purpose of this closure?
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.
Currently, the only init takes in key and action, however, I wanted to make sure we can pass in arbitrary block of code in the future to ensure it's in line with the
runSideEffect
api.
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.
But what is the block intended to do? If you're writing a test for a workflow that directly invokes runSideEffect
, what kinds of things would you actually want to assert on?
- That
runSideEffect
was called with a given key. - That
runSideEffect
was not called with a given key. - Given that a side effect was ran with a given key, that the side effect lambda actually performed some side effect (e.g. called
logger.log()
with some message). - Given that a side effect was ran with a given key, that the side effect lambda registers the correct logic to be executed in the
onEnded
handler. - Given that a side effect was ran with a given key, that the side effect lambda sent some action to the event sink.
1 can definitely be done with this API, presumably 2 can as well (by not expecting the key?). I'm not sure how close the Swift render testing API is to the kotlin one yet, but if the testing infrastructure runs the action immediately, then 3 and 5 can be done with existing APIs (3 by asserting the side effect fake was invoked after the render call, 5 using verifyAction(Result)
). The only special case is 4, which requires an API to end the lifetime. Some ways to do this:
- Pass a flag in
ExpectedSideEffect
that, if set, will cause theonEnded
handlers to be invoked immediately. - An additional API on
RenderTestResult
called something likeendSideEffect(key: String): RenderTestResult
that executes the end actions for the given side effect and returns aRenderTestResult
that allows verifying if any actions were sent to the sink in theonEnded
handler.
I think 1 is the simpler option, although both could be tricky (if the side effect immediately sends an action, and then sends another action when ending, the initial action will always be evaluated first and a second render pass would be needed to evaluate the second, and that's hard to express in the render tester API since it, by definition, tests a single render pass). Perhaps we don't actually need to need to provide the ability to test side effect implementations in this API, and can use the more general "integration" testing API?
In any case, this lambda parameter doesn't seem to address any of these use cases so I'm also not sure what purpose it serves.
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 is in response to @bencochran 's feedback here: #1174 (comment)
ExpectedWorker
allows you to send an Output
back to the RenderContext
when executed. Trying to provide the same ability here.
The action
is actually not public, it's not meant to be used directly. The two public initializers are:
public init(key: AnyHashable)
public init<ActionType: WorkflowAction>(key: AnyHashable, action: ActionType) where ActionType.WorkflowType == WorkflowType
I wanted to make sure ExpectedSideEffect
is implemented similarly to how SideEffects
are implemented and hence in the future we can provide additional ways to pass WorkflowAction
s to the RenderContext
.
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.
I'm worried we never quite converged our testing APIs, and now they're diverging more. Need to dig into this more to understand where the differences are. I could have sworn we had an issue to track testing APIs but I can't find it.
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.
I could have sworn we had an issue to track testing APIs but I can't find it.
#702?
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.
@zach-klippenstein: #702 is slated for v1.0. Happy to look into it after the GUWT changes.
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.
I think the public parts of ExpectedSideEffect
as written here fits with the overall current shape of our RenderTester
. Though, yeah, I’d prefer we converged in the direction of #702 and what Kotlin has.
I’m not sure of the need internally to hold a ((RenderContext<WorkflowType>) -> Void)?
vs an AnyWorkflowAction<WorkflowType>?
but that’s implementation detail that I’m less concerned with, I think?
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.
My plan is to work on the Kotlin side of this once I've split the repo out, this week. If yall are happy with this then SGTM, I'll not look too hard, see what I come up with in Kotlin, hopefully it's the same! And if not, I'll be in a much better position to debate 😉
The only part unclear to me is the purpose of the closure on |
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.
Discussed offline. I'm fine with where this is.
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.
Shouldn't this PR close one or more of the issues from the guwt label? https://github.com/square/workflow/issues?q=is%3Aissue+is%3Aopen+label%3Aguwt
@@ -99,6 +104,24 @@ public struct ExpectedWorker { | |||
} | |||
} | |||
|
|||
public struct ExpectedSideEffect<WorkflowType: Workflow> { | |||
let key: AnyHashable | |||
let action: ((RenderContext<WorkflowType>) -> Void)? |
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.
But what is the block intended to do? If you're writing a test for a workflow that directly invokes runSideEffect
, what kinds of things would you actually want to assert on?
- That
runSideEffect
was called with a given key. - That
runSideEffect
was not called with a given key. - Given that a side effect was ran with a given key, that the side effect lambda actually performed some side effect (e.g. called
logger.log()
with some message). - Given that a side effect was ran with a given key, that the side effect lambda registers the correct logic to be executed in the
onEnded
handler. - Given that a side effect was ran with a given key, that the side effect lambda sent some action to the event sink.
1 can definitely be done with this API, presumably 2 can as well (by not expecting the key?). I'm not sure how close the Swift render testing API is to the kotlin one yet, but if the testing infrastructure runs the action immediately, then 3 and 5 can be done with existing APIs (3 by asserting the side effect fake was invoked after the render call, 5 using verifyAction(Result)
). The only special case is 4, which requires an API to end the lifetime. Some ways to do this:
- Pass a flag in
ExpectedSideEffect
that, if set, will cause theonEnded
handlers to be invoked immediately. - An additional API on
RenderTestResult
called something likeendSideEffect(key: String): RenderTestResult
that executes the end actions for the given side effect and returns aRenderTestResult
that allows verifying if any actions were sent to the sink in theonEnded
handler.
I think 1 is the simpler option, although both could be tricky (if the side effect immediately sends an action, and then sends another action when ending, the initial action will always be evaluated first and a second render pass would be needed to evaluate the second, and that's hard to express in the render tester API since it, by definition, tests a single render pass). Perhaps we don't actually need to need to provide the ability to test side effect implementations in this API, and can use the more general "integration" testing API?
In any case, this lambda parameter doesn't seem to address any of these use cases so I'm also not sure what purpose it serves.
@@ -75,6 +75,10 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType { | |||
fatalError() | |||
} | |||
|
|||
public func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) { |
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 needs documentation, especially discussing that the side effect is allowed to send actions to the context's Sink
, that the side effect is expected to clean itself up when the Lifetime
is ended, that the Lifetime
won't end until a render pass in which this function is not called with the given key, etc.
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.
Also worth noting that the given closure will not be executed on subsequent runs of the same side-effect key.
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.
Agree with the calls for documentation (sorry, didn’t do any of that in my prototype 🙈), but overall looks good to me
@@ -75,6 +75,10 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType { | |||
fatalError() | |||
} | |||
|
|||
public func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) { |
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.
Also worth noting that the given closure will not be executed on subsequent runs of the same side-effect key.
@@ -419,7 +445,7 @@ extension WorkflowNode.SubtreeManager { | |||
.observe(on: QueueScheduler.workflowExecution) | |||
.start { [weak self] event in | |||
switch event { | |||
case .value(let output): | |||
case let .value(output): |
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.
nit: I prefer the let
inside the case for the reason outlined in the Google Swift style guide
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 will require a swiftformat
rules change, will do it in a subsequent PR.
@@ -99,6 +104,24 @@ public struct ExpectedWorker { | |||
} | |||
} | |||
|
|||
public struct ExpectedSideEffect<WorkflowType: Workflow> { | |||
let key: AnyHashable | |||
let action: ((RenderContext<WorkflowType>) -> Void)? |
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.
I think the public parts of ExpectedSideEffect
as written here fits with the overall current shape of our RenderTester
. Though, yeah, I’d prefer we converged in the direction of #702 and what Kotlin has.
I’m not sure of the need internally to hold a ((RenderContext<WorkflowType>) -> Void)?
vs an AnyWorkflowAction<WorkflowType>?
but that’s implementation detail that I’m less concerned with, I think?
c68e408
to
f3cc9b0
Compare
f3cc9b0
to
0fa0ade
Compare
First part of Kotlin implementation of square/workflow#1021. Kotlin counterpart to square/workflow#1174. This implementation intentionally does not run the side effect coroutine on the `workerContext` `CoroutineContext` that is threaded through the runtime for testing infrastructure. Initially, workers ran in the same context as the workflow runtime. The behavior of running workers on a different dispatcher by default (`Unconfined`) was introduced in square/workflow#851 as an optimization to reduce the overhead for running workers that only perform wiring tasks with other async libraries. This was a theoretical optimization, since running on the `Unconfined` dispatcher inherently involves less dispatching work, but the overhead of dispatching wiring coroutines was never actually shown to be a problem. Additionally, because tests often need to be in full control of dispatchers, the ability to override the context used for workers was introduced in square/workflow#940, which introduced `workerContext`. I am dropping that complexity here because it adds a decent amount of complexity to worker/side effect machinery without any proven value. It is also complexity that needs to be documented, and is probably just more confusing than anything. The old behavior for workers is maintained for now to reduce the risk of this change, but side effects will always run in the workflow runtime's context. This is nice and simple and unsurprising and easy to reason about.
Introduces a
SideEffect
hook that will be used to perform the functionality currently provided byWorker
s.This is a subset of @bencochran's implementation in the bc/guwt-prototype branch.