errorclass.py 7.1 KB

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