[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [bridgedb/master] Add dynamic TestCase class generator.
commit 0b89ae36724cfc4cbec4ebb310471d9871904674
Author: Isis Lovecruft <isis@xxxxxxxxxxxxxx>
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.
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits