-
Notifications
You must be signed in to change notification settings - Fork 106
Clean up RenderWorkflowIn Tests #1265
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
Conversation
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.
LGTM, but it would be wise to get @zach-klippenstein or someone else with stronger Coroutine chops to LG.
testTracer | ||
) | ||
|
||
private val runtimeOptions: Sequence<Pair<RuntimeConfig, WorkflowTracer?>> = cartesianProduct( |
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.
Neat
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.
Glad we're finally using the official test scopes for this!
* We *collect* emitted renderings on the [UnconfinedTestDispatcher] of the [runTest]. | ||
* The point here is that when the runtime sets the value on the StateFlow - a non-suspending | ||
* operation - and then it calls [onOutput] - a suspending operation - the [onOutput] handler | ||
* will not be immediately dispatched (it is waiting for dispatch from the scheduler), but the |
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.
the [onOutput] handler will not be immediately dispatched (it is waiting for dispatch from the scheduler)
The workflow runtime calls onOutput
directly, it's just a function, so the dispatcher is not involved at all. The reason this timing is the way it is is because the runtime sets the MSF's value
property before calling onOutput
. Or am I misreading this explanation?
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.
onOutput
is a suspend fun
, so it will go through dispatch. It's a good point though that perhaps we don't need it to be a suspend fun
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 thinking we made onOutput
a suspend fun
because we wanted it to be able to cancel the runtime's scope? cc @rjrjr if you remember.
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.
No idea. I doubt we did it by accident.
* collector of the renderings [StateFlow] will be dispatched and update the 'emitted' renderings. | ||
* Then when we let the runtime's scheduler go ahead, it will have already been populated. | ||
*/ | ||
@Test fun onOutput_called_after_rendering_emitted_and_collected() { |
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.
Maybe I'm missing something but it looks like what this test is really testing is the behavior of Unconfined
dispatchers. The behavior it validates is implied by the previous test when the renderings flow is collected with an unconfined dispatcher. Doesn't seem worth writing our own tests for core coroutine features.
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.
heh, ya you're point is valid. I was trying to make a point with the test more than actually test anything.
runtimeConfig = runtimeConfig, | ||
workflowTracer = workflowTracer, | ||
) { it: String -> | ||
// The list collecting the renderings already contains it by the time onOutput is fired. |
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.
Having expressed my doubt that this test is really worth having at all, if we do want it, it might make the test more clear and explicit to use a pattern that is often used to validate synchronization expectations in concurrent code. I can't remember if we use this elsewhere in workflow tests, but basically you just do this (in the test method, the class, or an external helper class, doesn't matter):
val actual = AtomicInteger(0)
fun expect(expected: Int) {
val actual = actual.getAndIncrement()
assertEquals(expected, actual, "Expected $expected, was $actual")
}
And then you pepper the concurrent code with calls something like this:
onOutput = { it: String ->
expect(1)
}
…
launch {
renderings.collect {
expect(0)
}
}
It's easy to see what you're testing because the code says explicitly what order the expect
calls are expected to occur in.
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.
That's a nice pattern! I will see if I can make use of in future refactors.
In the course of #1263 I encountered some issues with these tests.
onOutput_called_after_rendering_emitted
andoutput_is_emitted_before_next_render_pass
which were stating that they were testing the opposite things. In fact, there was a bug inonOutput_called_after_rendering_emitted
where the assertion that should have failed the test was uncaught and only killed the coroutineTestScope
rather than bubbling up to fail the test. So that test was 'failing' silently.onOutput
after the rendering is emitted..value
on theStateFlow
ofRenderingAndSnapshot
s. This is a non-suspending operation, whereas callingonOutput
is suspending. So really,onOutput
will be called before any collectors of the renderingsStateFlow
can be dispatched to handle the update, but if the.value
of thatStateFlow
of renderings was fetched inonOutput
then it would be "up-to-date".Anyway, as a result of discovering this I have gone through and audited the
RenderWorklfowInTest
tests, updating them to:runTest
so that by default anything cancelling theCoroutineScope
provided will also fail the test.testScope
andpausedTestScope
in favour of using the scope provided byrunTest
(and thebackgroundScope
when we have an operation - like the runtime that we don't care if its stopped before the test is done).