[tor-commits] [bridgedb/master] Add dynamic TestCase class generator.

isis at torproject.org isis at torproject.org
Sun Jan 12 06:06:35 UTC 2014


commit 0b89ae36724cfc4cbec4ebb310471d9871904674
Author: Isis Lovecruft <isis at torproject.org>
Date:   Fri Dec 20 02:51:07 2013 +0000

    Add dynamic TestCase class generator.
    
    This is not *strictly* necessary; however, the previous manner that I
    had devised for running the old unittests in lib/bridgedb/Tests.py had a
    small problem: it ran *all* the tests in lib/bridgedb/Tests.py as a
    single trial test. This meant that if a single test from Tests.py
    failed, the whole test would fail, which doesn't give us very
    fine-grained reporting, especially if there were multiple failures (the
    whole thing would fail when the first one failed).
    
    To fix that, it would be necessary to create one `TestCase` "test_*"
    method per original unittest. Each individual unittest would also need
    to be wrapped with a `twisted.trial.unittest.PyUnitResultAdapter`, in
    order for twisted.trial to be able to capture tracebacks and
    `failure.Failures` correctly. Additionally, it would be nice if we could
    simply take each `TestCase` method from the original unittests and reuse
    their method names (so that we know exactly which one of the original
    unittests is being run); therefore, the methods need to be wrapped yet
    again to reassign dynamic instancemethod names. (Normally, dynamically
    mucking around with Python's namespace is a BadIdeaâ„¢, but it turns out
    not only that this works, but that it *also* works with the dynamically
    discovered doctests from `test_Tests.generateTrialAdaptedDoctestSuite()`
    as well!)
    
    I mostly wanted to see if I could do it. Turns out, I can, and it seems
    to work well. This black-magic encrusted monster I've munged together,
    `DynamicTestCaseMeta`, works in the following manner:
    
      1. It generates dynamic methodnames with a given `methodPrefix`
    
      2. It generates corresponding methods for a
         `twisted.trial.unittest.TestCase` base class.
    
      3. Next, it wraps the generated methods. Sometimes, depending on the
         base class, it wraps them multiple times. It also allows further
         layers of wrapping, i.e. the `MonkeyPatcher` returned from
         `monkeypatchTests()` still works.
    
      4. Then, it assigns each generated methodname and method pair, using
         the special __new__() constructor, to the base class *when the base
         class is compiled* (i.e. when the test_Tests module is first
         imported into a Python interpreter). Therefore, each time you
         initialise a base class which uses `DynamicTestCaseMeta` as its
         __metaclass__, every initialised instance will have the *same*
         generated code, because the code which is generated is decided at
         compile time, not access time.
    
     * ADD the terrifying `test_Tests.DynamicTestCaseMeta` class generator.
    
    post scriptum: Take that, C++! :P
---
 lib/bridgedb/test/test_Tests.py |   98 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 98 insertions(+)

diff --git a/lib/bridgedb/test/test_Tests.py b/lib/bridgedb/test/test_Tests.py
index 2532be1..4e383d5 100644
--- a/lib/bridgedb/test/test_Tests.py
+++ b/lib/bridgedb/test/test_Tests.py
@@ -91,6 +91,104 @@ def monkeypatchTests():
     return patcher
 
 
+class DynamicTestCaseMeta(type):
+    """You how scary the seemingly-arbitrary constants in elliptic curve
+    cryptography seem? Well, I am over nine thousand times more scary. Dynamic
+    warez… beware! Be afraid; be very afraid.
+
+    :ivar testResult: An :class:`unittest.TestResult` adapted with
+                      :class:`twisted.trial.unittest.PyUnitResultAdapter`, for
+                      storing test failures and successes in.
+
+    A base class which uses this metaclass should define the following class
+    attributes:
+
+    :ivar testSuites: A list of :class:`unittest.TestSuite`s (or their
+                      :mod:`doctest` or :mod:`twisted.trial` equivalents).
+    :ivar methodPrefix: A string to prefix the generated method names
+                        with. (default: 'test_')
+    """
+
+    testResult = unittest.PyUnitResultAdapter(pyunit.TestResult())
+
+    def __new__(cls, name, bases, attrs):
+        """Construct the initialiser for a new
+        :class:`twisted.trial.unittest.TestCase`.
+
+        """
+        logging.debug("Metaclass __new__ constructor called for %r" % name)
+
+        if not 'testSuites' in attrs:
+            attrs['testSuites'] = list()
+        if not 'methodPrefix' in attrs:
+            attrs['methodPrefix'] = 'test_'
+
+        testSuites   = attrs['testSuites']
+        methodPrefix = attrs['methodPrefix']
+        logging.debug(
+            "Metaclass __new__() class %r(testSuites=%r, methodPrefix=%r)" %
+            (name, '\n\t'.join([str(ts) for ts in testSuites]), methodPrefix))
+
+        generatedMethods = cls.generateTestMethods(testSuites, methodPrefix)
+        attrs.update(generatedMethods)
+        #attrs['init'] = cls.__init__  # call the standard initialiser
+        return super(DynamicTestCaseMeta, cls).__new__(cls, name, bases, attrs)
+
+    @classmethod
+    def generateTestMethods(cls, testSuites, methodPrefix='test_'):
+        """Dynamically generate methods and their names for a
+        :class:`twisted.trial.unittest.TestCase`.
+
+        :param list testSuites: A list of :class:`unittest.TestSuite`s (or
+                                their :mod:`doctest` or :mod:`twisted.trial`
+                                equivalents).
+        :param str methodPrefix: A string to prefix the generated method names
+                                 with. (default: 'test_')
+        :rtype: dict
+        :returns: A dictionary of class attributes whose keys are dynamically
+                  generated method names (prefixed with **methodPrefix**), and
+                  whose corresponding values are dynamically generated methods
+                  (taken out of the class attribute ``testSuites``).
+        """
+        def testMethodFactory(test, name):
+            def createTestMethod(test):
+                def testMethod(*args, **kwargs):
+                    """When this function is generated, a methodname (beginning
+                    with whatever **methodPrefix** was set to) will also be
+                    generated, and the (methodname, method) pair will be
+                    assigned as attributes of the generated
+                    :class:`~twisted.trial.unittest.TestCase`.
+                    """
+                    # Get the number of failures before test.run():
+                    origFails = len(cls.testResult.original.failures)
+                    test.run(cls.testResult)
+                    # Fail the generated testMethod if the underlying failure
+                    # count has increased:
+                    if (len(cls.testResult.original.failures) > origFails):
+                        fail = cls.testResult.original.failures[origFails:][0]
+                        raise unittest.FailTest(''.join([str(fail[0]),
+                                                         str(fail[1])]))
+                    return cls.testResult
+                testMethod.__name__ = str(name)
+                return testMethod
+            return createTestMethod(test)
+
+        newAttrs = {}
+        for testSuite in testSuites:
+            for test in testSuite:
+                origName = test.id()
+                if origName.find('.') > 0:
+                    origFunc = origName.split('.')[-2:]
+                    origName = '_'.join(origFunc)
+                if origName.endswith('_py'):  # this happens with doctests
+                    origName = origName.strip('_py')
+                methName = str(methodPrefix + origName).replace('.', '_')
+                meth = testMethodFactory(test, methName)
+                logging.debug("Set %s.%s=%r" % (cls.__name__, methName, meth))
+                newAttrs[methName] = meth
+        return newAttrs
+
+
 class OldUnittests(unittest.TestCase):
     """A wrapper around :mod:`bridgedb.Tests` to produce :mod:`~twisted.trial`
     compatible output.





More information about the tor-commits mailing list