In this part I will look into implementing a proper command for "quit". Currently it's possible to quit the interpreter using "quit()" or ctrl-c. The problem is that "quit()" calls the native quit of Python (courtesy of Python command) and results in a nasty exception (ValueError: I/O operation on closed file).
I will solve this issue by writing a custom command that encapsulates the quit functionality and overrides this old, not desired behavior. I will also extend scenario tester to include a way to restart the application so that it's easy to verify that the application did indeed quit. Later on this feature could be used to test various other features such as persistency (it might be nice to be able to save a session and to be able to restore it).
Scenario for Quit
I think a scenario such as this might work just fine in this case:
The idea is that I test for the value of variable "a". As the application does not support any form of persistency yet it's expected that the value of a should be "null" if the application is restarted.
I admit the test is a bit questionable. How to guarantee that the application really quit as "restart" should end up quitting the application anyway even if "quit" was broken? I probably should add some sort way to test the state of the application ("--- running" and "--- not running" come to my mind first). Let's add that check there too:
Obviously the new test should fail. Before we can make it pass, we need to extend the scenario tester to support the new semantics.
Extending Scenario Tester
We need to add the following features to the scenario tester:
- "--- restart" - This command should cause the tester to reset the application and rerun it. It should continue reading input/output as usual.
- "--- running" - This command should verify that the application is still running. If it's not, it should give and an error (NotRunningError).
- "--- not running" - This command should verify that the application is not running. If it's still running, it should give an error (RunningError). This is the opposite of "--- running".
The "---" parts are meant as direct commands to the scenario tester. They are not meant to be read as a part of the actual user input/expected output. Following these specifications I ended up with these tests.
I defined lines containing "---" prefix as Meta considering they are metadata. Initially I tried alternatives such as "Assert" but that particular option broke down with "restart" which is not an assertion but more of a command. Meta contains both of the cases.
You can find my tests and implementation here. I refactored scenario tester somewhat and decomposed it into smaller pieces that are easier to manage. Later on it might make sense to treat scenario tester as a project of its own and treat the line and exception related parts as separate modules.
Now that the scenario tester is up to par it's good to focus on the quit command itself.
Implementation
The "quit" command needs to be able to match both "quit" and "quit()". On execute it should just raise a SystemExit. Codewise something along this should work:
There is only one problem. The interpreter has been designed so that it should catch all exceptions and return 'null' in case it caught something. In this case it should let SystemExit pass through. Yet again it's just a matter of specifying that using a test:
An implementation could be something along this:
Now the interpreter passes all its tests. The quit scenario fails still, however. This is due to missing test for None. Currently the output of Meta lines (None) gets evaluated as "null". Let's change this behavior:
Test:
Implementation:
After these changes all tests should pass fine now and "quit" should be ready. Give it a go.
Summary
In this relatively brief part of series we managed to extend the semantics of the scenario tester and come up with a neat way to quit the application. The "quit()" alternative feels a bit redundant and perhaps should be removed later. Otherwise it seems fine enough for now.
I will look into providing means to implement VPython based commands in the next part of the series. This includes some dabbling with Python threads and some minor changes to the command architecture to avoid redundancy.
You may find the source code of this part here.