PageRenderTime 292ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/python/nose/plugins/plugintest.py

https://github.com/mozilla/affiliates-lib
Python | 343 lines | 336 code | 1 blank | 6 comment | 3 complexity | 0e9d241da417a3af9cce03490d28debe MD5 | raw file
  1. """
  2. Testing Plugins
  3. ===============
  4. The plugin interface is well-tested enough to safely unit test your
  5. use of its hooks with some level of confidence. However, there is also
  6. a mixin for unittest.TestCase called PluginTester that's designed to
  7. test plugins in their native runtime environment.
  8. Here's a simple example with a do-nothing plugin and a composed suite.
  9. >>> import unittest
  10. >>> from nose.plugins import Plugin, PluginTester
  11. >>> class FooPlugin(Plugin):
  12. ... pass
  13. >>> class TestPluginFoo(PluginTester, unittest.TestCase):
  14. ... activate = '--with-foo'
  15. ... plugins = [FooPlugin()]
  16. ... def test_foo(self):
  17. ... for line in self.output:
  18. ... # i.e. check for patterns
  19. ... pass
  20. ...
  21. ... # or check for a line containing ...
  22. ... assert "ValueError" in self.output
  23. ... def makeSuite(self):
  24. ... class TC(unittest.TestCase):
  25. ... def runTest(self):
  26. ... raise ValueError("I hate foo")
  27. ... return unittest.TestSuite([TC()])
  28. ...
  29. >>> res = unittest.TestResult()
  30. >>> case = TestPluginFoo('test_foo')
  31. >>> case(res)
  32. >>> res.errors
  33. []
  34. >>> res.failures
  35. []
  36. >>> res.wasSuccessful()
  37. True
  38. >>> res.testsRun
  39. 1
  40. And here is a more complex example of testing a plugin that has extra
  41. arguments and reads environment variables.
  42. >>> import unittest, os
  43. >>> from nose.plugins import Plugin, PluginTester
  44. >>> class FancyOutputter(Plugin):
  45. ... name = "fancy"
  46. ... def configure(self, options, conf):
  47. ... Plugin.configure(self, options, conf)
  48. ... if not self.enabled:
  49. ... return
  50. ... self.fanciness = 1
  51. ... if options.more_fancy:
  52. ... self.fanciness = 2
  53. ... if 'EVEN_FANCIER' in self.env:
  54. ... self.fanciness = 3
  55. ...
  56. ... def options(self, parser, env=os.environ):
  57. ... self.env = env
  58. ... parser.add_option('--more-fancy', action='store_true')
  59. ... Plugin.options(self, parser, env=env)
  60. ...
  61. ... def report(self, stream):
  62. ... stream.write("FANCY " * self.fanciness)
  63. ...
  64. >>> class TestFancyOutputter(PluginTester, unittest.TestCase):
  65. ... activate = '--with-fancy' # enables the plugin
  66. ... plugins = [FancyOutputter()]
  67. ... args = ['--more-fancy']
  68. ... env = {'EVEN_FANCIER': '1'}
  69. ...
  70. ... def test_fancy_output(self):
  71. ... assert "FANCY FANCY FANCY" in self.output, (
  72. ... "got: %s" % self.output)
  73. ... def makeSuite(self):
  74. ... class TC(unittest.TestCase):
  75. ... def runTest(self):
  76. ... raise ValueError("I hate fancy stuff")
  77. ... return unittest.TestSuite([TC()])
  78. ...
  79. >>> res = unittest.TestResult()
  80. >>> case = TestFancyOutputter('test_fancy_output')
  81. >>> case(res)
  82. >>> res.errors
  83. []
  84. >>> res.failures
  85. []
  86. >>> res.wasSuccessful()
  87. True
  88. >>> res.testsRun
  89. 1
  90. """
  91. import re
  92. import sys
  93. from warnings import warn
  94. try:
  95. from cStringIO import StringIO
  96. except ImportError:
  97. from StringIO import StringIO
  98. __all__ = ['PluginTester', 'run']
  99. class PluginTester(object):
  100. """A mixin for testing nose plugins in their runtime environment.
  101. Subclass this and mix in unittest.TestCase to run integration/functional
  102. tests on your plugin. When setUp() is called, the stub test suite is
  103. executed with your plugin so that during an actual test you can inspect the
  104. artifacts of how your plugin interacted with the stub test suite.
  105. - activate
  106. - the argument to send nosetests to activate the plugin
  107. - suitepath
  108. - if set, this is the path of the suite to test. Otherwise, you
  109. will need to use the hook, makeSuite()
  110. - plugins
  111. - the list of plugins to make available during the run. Note
  112. that this does not mean these plugins will be *enabled* during
  113. the run -- only the plugins enabled by the activate argument
  114. or other settings in argv or env will be enabled.
  115. - args
  116. - a list of arguments to add to the nosetests command, in addition to
  117. the activate argument
  118. - env
  119. - optional dict of environment variables to send nosetests
  120. """
  121. activate = None
  122. suitepath = None
  123. args = None
  124. env = {}
  125. argv = None
  126. plugins = []
  127. ignoreFiles = None
  128. def makeSuite(self):
  129. """returns a suite object of tests to run (unittest.TestSuite())
  130. If self.suitepath is None, this must be implemented. The returned suite
  131. object will be executed with all plugins activated. It may return
  132. None.
  133. Here is an example of a basic suite object you can return ::
  134. >>> import unittest
  135. >>> class SomeTest(unittest.TestCase):
  136. ... def runTest(self):
  137. ... raise ValueError("Now do something, plugin!")
  138. ...
  139. >>> unittest.TestSuite([SomeTest()]) # doctest: +ELLIPSIS
  140. <unittest...TestSuite tests=[<...SomeTest testMethod=runTest>]>
  141. """
  142. raise NotImplementedError
  143. def _execPlugin(self):
  144. """execute the plugin on the internal test suite.
  145. """
  146. from nose.config import Config
  147. from nose.core import TestProgram
  148. from nose.plugins.manager import PluginManager
  149. suite = None
  150. stream = StringIO()
  151. conf = Config(env=self.env,
  152. stream=stream,
  153. plugins=PluginManager(plugins=self.plugins))
  154. if self.ignoreFiles is not None:
  155. conf.ignoreFiles = self.ignoreFiles
  156. if not self.suitepath:
  157. suite = self.makeSuite()
  158. self.nose = TestProgram(argv=self.argv, config=conf, suite=suite,
  159. exit=False)
  160. self.output = AccessDecorator(stream)
  161. def setUp(self):
  162. """runs nosetests with the specified test suite, all plugins
  163. activated.
  164. """
  165. self.argv = ['nosetests', self.activate]
  166. if self.args:
  167. self.argv.extend(self.args)
  168. if self.suitepath:
  169. self.argv.append(self.suitepath)
  170. self._execPlugin()
  171. class AccessDecorator(object):
  172. stream = None
  173. _buf = None
  174. def __init__(self, stream):
  175. self.stream = stream
  176. stream.seek(0)
  177. self._buf = stream.read()
  178. stream.seek(0)
  179. def __contains__(self, val):
  180. return val in self._buf
  181. def __iter__(self):
  182. return self.stream
  183. def __str__(self):
  184. return self._buf
  185. def blankline_separated_blocks(text):
  186. block = []
  187. for line in text.splitlines(True):
  188. block.append(line)
  189. if not line.strip():
  190. yield "".join(block)
  191. block = []
  192. if block:
  193. yield "".join(block)
  194. def remove_stack_traces(out):
  195. # this regexp taken from Python 2.5's doctest
  196. traceback_re = re.compile(r"""
  197. # Grab the traceback header. Different versions of Python have
  198. # said different things on the first traceback line.
  199. ^(?P<hdr> Traceback\ \(
  200. (?: most\ recent\ call\ last
  201. | innermost\ last
  202. ) \) :
  203. )
  204. \s* $ # toss trailing whitespace on the header.
  205. (?P<stack> .*?) # don't blink: absorb stuff until...
  206. ^ (?P<msg> \w+ .*) # a line *starts* with alphanum.
  207. """, re.VERBOSE | re.MULTILINE | re.DOTALL)
  208. blocks = []
  209. for block in blankline_separated_blocks(out):
  210. blocks.append(traceback_re.sub(r"\g<hdr>\n...\n\g<msg>", block))
  211. return "".join(blocks)
  212. def simplify_warnings(out):
  213. warn_re = re.compile(r"""
  214. # Cut the file and line no, up to the warning name
  215. ^.*:\d+:\s
  216. (?P<category>\w+): \s+ # warning category
  217. (?P<detail>.+) $ \n? # warning message
  218. ^ .* $ # stack frame
  219. """, re.VERBOSE | re.MULTILINE)
  220. return warn_re.sub(r"\g<category>: \g<detail>", out)
  221. def remove_timings(out):
  222. return re.sub(
  223. r"Ran (\d+ tests?) in [0-9.]+s", r"Ran \1 in ...s", out)
  224. def munge_nose_output_for_doctest(out):
  225. """Modify nose output to make it easy to use in doctests."""
  226. out = remove_stack_traces(out)
  227. out = simplify_warnings(out)
  228. out = remove_timings(out)
  229. return out.strip()
  230. def run(*arg, **kw):
  231. """
  232. Specialized version of nose.run for use inside of doctests that
  233. test test runs.
  234. This version of run() prints the result output to stdout. Before
  235. printing, the output is processed by replacing the timing
  236. information with an ellipsis (...), removing traceback stacks, and
  237. removing trailing whitespace.
  238. Use this version of run wherever you are writing a doctest that
  239. tests nose (or unittest) test result output.
  240. Note: do not use doctest: +ELLIPSIS when testing nose output,
  241. since ellipses ("test_foo ... ok") in your expected test runner
  242. output may match multiple lines of output, causing spurious test
  243. passes!
  244. """
  245. from nose import run
  246. from nose.config import Config
  247. from nose.plugins.manager import PluginManager
  248. buffer = StringIO()
  249. if 'config' not in kw:
  250. plugins = kw.pop('plugins', [])
  251. if isinstance(plugins, list):
  252. plugins = PluginManager(plugins=plugins)
  253. env = kw.pop('env', {})
  254. kw['config'] = Config(env=env, plugins=plugins)
  255. if 'argv' not in kw:
  256. kw['argv'] = ['nosetests', '-v']
  257. kw['config'].stream = buffer
  258. # Set up buffering so that all output goes to our buffer,
  259. # or warn user if deprecated behavior is active. If this is not
  260. # done, prints and warnings will either be out of place or
  261. # disappear.
  262. stderr = sys.stderr
  263. stdout = sys.stdout
  264. if kw.pop('buffer_all', False):
  265. sys.stdout = sys.stderr = buffer
  266. restore = True
  267. else:
  268. restore = False
  269. warn("The behavior of nose.plugins.plugintest.run() will change in "
  270. "the next release of nose. The current behavior does not "
  271. "correctly account for output to stdout and stderr. To enable "
  272. "correct behavior, use run_buffered() instead, or pass "
  273. "the keyword argument buffer_all=True to run().",
  274. DeprecationWarning, stacklevel=2)
  275. try:
  276. run(*arg, **kw)
  277. finally:
  278. if restore:
  279. sys.stderr = stderr
  280. sys.stdout = stdout
  281. out = buffer.getvalue()
  282. print munge_nose_output_for_doctest(out)
  283. def run_buffered(*arg, **kw):
  284. kw['buffer_all'] = True
  285. run(*arg, **kw)
  286. if __name__ == '__main__':
  287. import doctest
  288. doctest.testmod()