The run() routine, while it would seem would be pretty simple, gets a bit messy. Here is what it looks like.
This means that I have had to write some pretty interesting tests, since I don't really want to also test digest() itself, but do want to have a fake version of it simulate raising the errors/exceptions. And, after writing 5 other tests, I am finalizing the test for the block which starts except Exception as e:. I have it checking to see that digest() is called 4 times, with "line 1", "line 2", "line 3" and finally "line 4", with lines 2 raising an Exception with no cause, and line 3 raising an Exception with a previous cause (which would be the case when either the method for parsing IPv4 or IPv6 gets a database error, which they catch and handle). The resulting output for that test would look something like this:
Being the professional I am, I want the test for this situation to make sure of the following:
- That
run() calls digest() 4 times, once each with each of the 4 lines which the test "feeds" it in the correct order, with the newline stripped off the end. - That run
() returned a 0 return value, since that is what it is designed to do. (Not sure under what conditions I might return a non-zero value, but...) - That the standard output is empty.
- That the error output contains:
- A message reading
Unexpected exception while parsing record: with the main exception message. - The contents of the line on which the error occurred, since it may be that it is some record format for which my parsing does not properly handle.
- The traceback, so I know where the error was caused, including the file and line number where the error was raised.
- The additional information if the exception was caused by something else, like a database error raising an exception, instead of the far simpler case of my code saying something like "Hey, we had stuff left over which I don't know how to handle", which will again have line numbers where pointing to where the original problem occurred.
The problem is, this gets tricky. For one, if you look at the output I included above, there are lots of intermediate functions/methods which will be involved. Secondly, line numbers change as code is edited or reformatted. Thankfully, there is a trick I am doing, where I have put the following to lines immediately following the fake method which is "mocking" the real digest() method.
expected_frame1_lineno = inspect.currentframe().f_lineno - 5 # NOTE!!! REFORMATTING/REORDERING CAN CAUSE THIS TO NEED TO BE ADJUSTED
expected_frame2_lineno = inspect.currentframe().f_lineno - 4 # NOTE!!! REFORMATTING/REORDERING CAN CAUSE THIS TO NEED TO BE ADJUSTED
Notice the comment. I am warning myself or anybody else in the future that these are critical lines. But, to get the actual line numbers from the output, I am using something called regular expressions, also called regex or regexp (Wikipedia). In short, it is a way of using a combination of regular characters and special sequences of characters to match patterns in the error text. So, to "eat" multiple lines, I use the character sequence ".*" (without the quotes) along with the re.DOTALL flag to match the character which signifies a new line, and to match and capture a line number, I use "(\d+)" (again, without the quotes). It does mean that in the error text, to match text in parenthesis, I have to do "\(this\)" (you get the idea...). And all this gets wrapped together with literal text to match, which leads to a really complicated regular expression which actually spans 25 lines of code. Only, as I write this, my regular expression is not working.