commit 0b89ae36724cfc4cbec4ebb310471d9871904674 Author: Isis Lovecruft isis@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.
tor-commits@lists.torproject.org