[Enhancement] improve FlowTest + Exception handling interactions #573
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description
Quick Background
A FlowTest sequence normally proceeds from View to View, exactly as if a user was clicking from one screen to the next.
But we also have redirects that are often invisible to the user (e.g. only Native Segwit is enabled, so skip the script type selection View).
There are three types of possible redirects:
(newest) The View's
__init__
determines that the user should be redirected elsewhere and callsView.set_redirect()
. In this case the View'srun()
is never called. This method is preferred but is not widely implemented yet.(most common) The View's
run()
immediately returns a new Destination without ever calling itsrun_screen()
. This was how v0.5.0+ redirects were originally implemented.An unhandled exception occurs during any phase (in
__init__()
, inrun()
before or afterrun_screen()
, or within the associatedScreen.display()
). TheController
catches the exception and redirects the user to theUnhandledExceptionView
.The Problem
A
FlowStep
can specifyis_redirect=True
to indicate that the expected View should execute, but is expected to immediately return a new Destination without ever running its Screen.But lack of clarity on the above three distinct redirect scenarios made that one single
is_redirect
attr ineffective and confusing inFlowTest.run_sequence()
. This was wholly my fault, by the way! The original implementation more or less properly handled the middle case (redirect triggered inView.run()
), maybe handled the first case correctly (triggered viaView.set_redirect()
), and definitely did NOT handle the third case correctly (UnhandledExceptionView
).Yes, FlowTests have been passing all this time, but partially due to silent, unnoticed failures. A typical example is that the last or penultimate FlowStep triggers an
UnhandledExceptionView
but the heart of the FlowTest is already over and so there's no next step (next expected View) that would fail.But if the penultimate FlowStep lands on
UnhandledExceptionView
, shouldn't the FINAL step detect some kind of mismatch? This is where the second aspect of the problem comes in: when do wesequence.pop(0)
the current FlowStep out of the sequence?In this example, that final FlowStep was being improperly popped before the FlowTest could verify that we got the expected View. So the FlowTest was totally unaware that our sequence ended on the wrong View.
The Fix
First, just getting mental clarity on those three possible redirect scenarios was huge.
Second, remove ALL the
sequence.pop(0)
calls that created too many confusing hell scenarios.Instead, wrap it all in a
try...finally
block to guarantee thatsequence.pop(0)
is always called after each iteration AND that it's only called once.The explicit handling for each of the redirect scenarios:
__init__.set_redirect()
obviously happens before the Screen is run, so put that logic at the front.Redirects coming out of
View.run()
are obvious because we can check thecall_count
onrun_screen()
.And
UnhandledExceptionView
doesn't actually need any special handling. If the FlowTest was expecting that to be triggered, it would specifyis_redirect=True
and the next FlowStep would be theUnhandledExceptionView
. If the FlowTest was NOT expecting it, either theis_redirect=True
would be missing (in which case we raise the newFlowTestUnexpectedRedirectException
) or the next FlowStep's expected View won't match when we run the next iteration in the sequence (resulting in aFlowTestUnexpectedViewException
).Additional:
FlowTestRunScreenNotExecutedException
removed as its purpose has been superseded byFlowTestUnexpectedRedirectException
. Raised when a redirect occurs but the FlowStep did not specifyis_redirect=True
.FlowTestMissingRedirectException
added for the opposite scenario (FlowStep specifiedis_redirect=True
but no redirect happened.FlowTest.run_sequence().run_screen()
no longer makes any changes to thesequence
. Just (simulate) running the f'n Screen.MainMenuView
but with no user input result specified, the FlowTest would execute the View and its Screen, but would actually create a quiet Exception that we just weren't aware of and wouldn't care about anyway (the View runs as if the Screen returnedNone
, which then causes anIndexError
when the View tries to accessNone
to see which item in itsbutton_list
the user ostensibly selected).sequence
list is empty.This pull request is categorized as a:
Checklist
pytest
and made sure all unit tests pass before sumbitting the PRIf you modified or added functionality/workflow, did you add new unit tests?
I have tested this PR on the following platforms/os:
Note: Keep your changes limited in scope; if you uncover other issues or improvements along the way, ideally submit those as a separate PR. The more complicated the PR the harder to review, test, and merge.