| 1 | """ |
|---|
| 2 | ErrorClass Plugins |
|---|
| 3 | ------------------ |
|---|
| 4 | |
|---|
| 5 | ErrorClass plugins provide an easy way to add support for custom |
|---|
| 6 | handling of particular classes of exceptions. |
|---|
| 7 | |
|---|
| 8 | An ErrorClass plugin defines one or more ErrorClasses and how each is |
|---|
| 9 | handled and reported on. Each error class is stored in a different |
|---|
| 10 | attribute on the result, and reported separately. Each error class must |
|---|
| 11 | indicate the exceptions that fall under that class, the label to use |
|---|
| 12 | for reporting, and whether exceptions of the class should be |
|---|
| 13 | considered as failures for the whole test run. |
|---|
| 14 | |
|---|
| 15 | ErrorClasses use a declarative syntax. Assign an ErrorClass to the |
|---|
| 16 | attribute you wish to add to the result object, defining the |
|---|
| 17 | exceptions, label and isfailure attributes. For example, to declare an |
|---|
| 18 | ErrorClassPlugin that defines TodoErrors (and subclasses of TodoError) |
|---|
| 19 | as an error class with the label 'TODO' that is considered a failure, |
|---|
| 20 | do this: |
|---|
| 21 | |
|---|
| 22 | >>> class Todo(Exception): |
|---|
| 23 | ... pass |
|---|
| 24 | >>> class TodoError(ErrorClassPlugin): |
|---|
| 25 | ... todo = ErrorClass(Todo, label='TODO', isfailure=True) |
|---|
| 26 | |
|---|
| 27 | The MetaErrorClass metaclass translates the ErrorClass declarations |
|---|
| 28 | into the tuples used by the error handling and reporting functions in |
|---|
| 29 | the result. This is an internal format and subject to change; you |
|---|
| 30 | should always use the declarative syntax for attaching ErrorClasses to |
|---|
| 31 | an ErrorClass plugin. |
|---|
| 32 | |
|---|
| 33 | >>> TodoError.errorClasses # doctest: +ELLIPSIS |
|---|
| 34 | ((<class ...Todo...>, ('todo', 'TODO', True)),) |
|---|
| 35 | |
|---|
| 36 | Let's see the plugin in action. First some boilerplate. |
|---|
| 37 | |
|---|
| 38 | >>> import sys |
|---|
| 39 | >>> import unittest |
|---|
| 40 | >>> buf = unittest._WritelnDecorator(sys.stdout) |
|---|
| 41 | |
|---|
| 42 | Now define a test case that raises a Todo. |
|---|
| 43 | |
|---|
| 44 | >>> class TestTodo(unittest.TestCase): |
|---|
| 45 | ... def runTest(self): |
|---|
| 46 | ... raise Todo("I need to test something") |
|---|
| 47 | >>> case = TestTodo() |
|---|
| 48 | |
|---|
| 49 | Prepare the result using our plugin. Normally this happens during the |
|---|
| 50 | course of test execution within nose -- you won't be doing this |
|---|
| 51 | yourself. For the purposes of this testing document, I'm stepping |
|---|
| 52 | through the internal process of nose so you can see what happens at |
|---|
| 53 | each step. |
|---|
| 54 | |
|---|
| 55 | >>> plugin = TodoError() |
|---|
| 56 | >>> result = unittest._TextTestResult(stream=buf, |
|---|
| 57 | ... descriptions=0, verbosity=2) |
|---|
| 58 | >>> plugin.prepareTestResult(result) |
|---|
| 59 | |
|---|
| 60 | Now run the test. TODO is printed. |
|---|
| 61 | |
|---|
| 62 | >>> case(result) # doctest: +ELLIPSIS |
|---|
| 63 | runTest (....TestTodo) ... TODO: I need to test something |
|---|
| 64 | |
|---|
| 65 | Errors and failures are empty, but todo has our test: |
|---|
| 66 | |
|---|
| 67 | >>> result.errors |
|---|
| 68 | [] |
|---|
| 69 | >>> result.failures |
|---|
| 70 | [] |
|---|
| 71 | >>> result.todo # doctest: +ELLIPSIS |
|---|
| 72 | [(<....TestTodo testMethod=runTest>, '...Todo: I need to test something\\n')] |
|---|
| 73 | >>> result.printErrors() # doctest: +ELLIPSIS |
|---|
| 74 | <BLANKLINE> |
|---|
| 75 | ====================================================================== |
|---|
| 76 | TODO: runTest (....TestTodo) |
|---|
| 77 | ---------------------------------------------------------------------- |
|---|
| 78 | Traceback (most recent call last): |
|---|
| 79 | ... |
|---|
| 80 | Todo: I need to test something |
|---|
| 81 | <BLANKLINE> |
|---|
| 82 | |
|---|
| 83 | Since we defined a Todo as a failure, the run was not successful. |
|---|
| 84 | |
|---|
| 85 | >>> result.wasSuccessful() |
|---|
| 86 | False |
|---|
| 87 | """ |
|---|
| 88 | |
|---|
| 89 | from new import instancemethod |
|---|
| 90 | from nose.plugins.base import Plugin |
|---|
| 91 | from nose.result import TextTestResult |
|---|
| 92 | from nose.util import isclass |
|---|
| 93 | |
|---|
| 94 | class MetaErrorClass(type): |
|---|
| 95 | """Metaclass for ErrorClassPlugins that allows error classes to be |
|---|
| 96 | set up in a declarative manner. |
|---|
| 97 | """ |
|---|
| 98 | def __init__(self, name, bases, attr): |
|---|
| 99 | errorClasses = [] |
|---|
| 100 | for name, detail in attr.items(): |
|---|
| 101 | if isinstance(detail, ErrorClass): |
|---|
| 102 | attr.pop(name) |
|---|
| 103 | for cls in detail: |
|---|
| 104 | errorClasses.append( |
|---|
| 105 | (cls, (name, detail.label, detail.isfailure))) |
|---|
| 106 | super(MetaErrorClass, self).__init__(name, bases, attr) |
|---|
| 107 | self.errorClasses = tuple(errorClasses) |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | class ErrorClass(object): |
|---|
| 111 | def __init__(self, *errorClasses, **kw): |
|---|
| 112 | self.errorClasses = errorClasses |
|---|
| 113 | try: |
|---|
| 114 | for key in ('label', 'isfailure'): |
|---|
| 115 | setattr(self, key, kw.pop(key)) |
|---|
| 116 | except KeyError: |
|---|
| 117 | raise TypeError("%r is a required named argument for ErrorClass" |
|---|
| 118 | % key) |
|---|
| 119 | |
|---|
| 120 | def __iter__(self): |
|---|
| 121 | return iter(self.errorClasses) |
|---|
| 122 | |
|---|
| 123 | |
|---|
| 124 | class ErrorClassPlugin(Plugin): |
|---|
| 125 | """ |
|---|
| 126 | Base class for ErrorClass plugins. Subclass this class and declare the |
|---|
| 127 | exceptions that you wish to handle as attributes of the subclass. |
|---|
| 128 | """ |
|---|
| 129 | __metaclass__ = MetaErrorClass |
|---|
| 130 | score = 1000 |
|---|
| 131 | errorClasses = () |
|---|
| 132 | |
|---|
| 133 | def addError(self, test, err): |
|---|
| 134 | err_cls, a, b = err |
|---|
| 135 | if not isclass(err_cls): |
|---|
| 136 | return |
|---|
| 137 | classes = [e[0] for e in self.errorClasses] |
|---|
| 138 | if filter(lambda c: issubclass(err_cls, c), classes): |
|---|
| 139 | return True |
|---|
| 140 | |
|---|
| 141 | def prepareTestResult(self, result): |
|---|
| 142 | if not hasattr(result, 'errorClasses'): |
|---|
| 143 | self.patchResult(result) |
|---|
| 144 | for cls, (storage_attr, label, isfail) in self.errorClasses: |
|---|
| 145 | if cls not in result.errorClasses: |
|---|
| 146 | storage = getattr(result, storage_attr, []) |
|---|
| 147 | setattr(result, storage_attr, storage) |
|---|
| 148 | result.errorClasses[cls] = (storage, label, isfail) |
|---|
| 149 | |
|---|
| 150 | def patchResult(self, result): |
|---|
| 151 | result._orig_addError, result.addError = \ |
|---|
| 152 | result.addError, add_error_patch(result) |
|---|
| 153 | result._orig_wasSuccessful, result.wasSuccessful = \ |
|---|
| 154 | result.wasSuccessful, wassuccessful_patch(result) |
|---|
| 155 | if hasattr(result, 'printErrors'): |
|---|
| 156 | result._orig_printErrors, result.printErrors = \ |
|---|
| 157 | result.printErrors, print_errors_patch(result) |
|---|
| 158 | result.errorClasses = {} |
|---|
| 159 | |
|---|
| 160 | |
|---|
| 161 | def add_error_patch(result): |
|---|
| 162 | """Create a new addError method to patch into a result instance |
|---|
| 163 | that recognizes the errorClasses attribute and deals with |
|---|
| 164 | errorclasses correctly. |
|---|
| 165 | """ |
|---|
| 166 | return instancemethod( |
|---|
| 167 | TextTestResult.addError.im_func, result, result.__class__) |
|---|
| 168 | |
|---|
| 169 | |
|---|
| 170 | def print_errors_patch(result): |
|---|
| 171 | """Create a new printErrors method that prints errorClasses items |
|---|
| 172 | as well. |
|---|
| 173 | """ |
|---|
| 174 | return instancemethod( |
|---|
| 175 | TextTestResult.printErrors.im_func, result, result.__class__) |
|---|
| 176 | |
|---|
| 177 | |
|---|
| 178 | def wassuccessful_patch(result): |
|---|
| 179 | """Create a new wasSuccessful method that checks errorClasses for |
|---|
| 180 | exceptions that were put into other slots than error or failure |
|---|
| 181 | but that still count as not success. |
|---|
| 182 | """ |
|---|
| 183 | return instancemethod( |
|---|
| 184 | TextTestResult.wasSuccessful.im_func, result, result.__class__) |
|---|
| 185 | |
|---|
| 186 | |
|---|
| 187 | if __name__ == '__main__': |
|---|
| 188 | import doctest |
|---|
| 189 | doctest.testmod() |
|---|