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() |
---|