unittest-xml-reporting icon indicating copy to clipboard operation
unittest-xml-reporting copied to clipboard

Do _TestInfo objects really need a persistent unittest.TestResult reference? They are very hard to pickle...

Open crswong888 opened this issue 2 years ago • 0 comments

Reason

My current objective is to subclass unittest.TestCase to hook the run() method such that each test method runs from a multiprocessing.Process spawn on Windows 10. The trickiest part about this is that unittest.TestResult objects have a few unpicklable attributes, i.e., _original_stderr, _original_stdout, and sometimes stream.stream. Luckily, handling this case-by-case is fairly straightforward, especially with unittest.TextTestResult, by temporarily modifying the result object.

When my TestCase.run() hook receives _XMLTestResult objects, things become way messier because the result list attributes, e.g., successes, store references to _TestInfo objects collectively storing references to copies of all former states of the result object. This isn't a problem in the standard xmlrunner framework because all _TestInfo.test_result refer to the same object. However, since I'm pickling and unpickling the result, I end up with multiple, basically dormant copies. Therefore, I would have to loop through all copies to make each one pickleable. I hope this is all making sense so far...

Design

The first solution I came up with was to update the test_result attribute to the original _XMLTestResult object on the parent process each time a _TestInfo reference was appended to one of the many lists. Then, I would only have to modify the result object passed to run() to make everything picklable and not loop through a bunch of old copies. It worked but was ugly, and I didn't want my run() method requiring special treatment for _XMLTestResult objects.

Then, I noticed that the test_result attribute has no further use once test_finished() gets invoked. Thus, I decided that the simplest thing to do was delete test_result after the initial (and only?) invocation of test_finished() by forcibly overriding the class in the same module where my unittest.TestCase subclass is declared:

import xmlrunner

class _TestInfo(xmlrunner.result._TestInfo):
    """
    Instances of this class override the default value for the 'infoclass' attribute of
    xmlrunner.result._XMLTestResult objects.

    Upon pickling and unpickling, the 'test_result' attribute of the base class becomes an
    effectively static reference to an instance of unittest.TestResult (or any of its children),
    making it tricky to pickle again on the next test case. This class is merely a hook for deleting
    that attribute once it is no longer needed.
    """
    def test_finished(self):
        super().test_finished()
        delattr(self, 'test_result')

xmlrunner.result._TestInfo = _TestInfo

Impact

First off, is it safe for me to be deleting the test_result attribute in this way? If so, could we make this the official behavior? Either way, it seems inefficient to be using the result object for this purpose. There should be some alternative approach to confirm that a test has finished and compute the elapsed time. Instances of _TestInfo make only very mild use of their test_result attribute and could almost do away with it after construction.

crswong888 avatar Mar 11 '22 19:03 crswong888