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