Merge lp:~duncf/pyjunitxml/cmdline-runner into lp:pyjunitxml
- cmdline-runner
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 27 |
Proposed branch: | lp:~duncf/pyjunitxml/cmdline-runner |
Merge into: | lp:pyjunitxml |
Diff against target: |
689 lines (+597/-4) 10 files modified
junitxml/__init__.py (+9/-3) junitxml/__main__.py (+12/-0) junitxml/main.py (+167/-0) junitxml/runner.py (+71/-0) junitxml/tests/__init__.py (+8/-0) junitxml/tests/test_junitxml.py (+5/-1) junitxml/tests/test_main.py (+198/-0) junitxml/tests/test_runner.py (+119/-0) pyjunitxml (+7/-0) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~duncf/pyjunitxml/cmdline-runner |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins | Approve | ||
Review via email:
|
Commit message
Description of the change
Adds support for running unit tests under junitxml directly from the command line. Tests can be specified by name, or (if available) they can be discovered automatically.
This allows for existing unit tests to be run directly by a continuous integration system without needing to write custom wrapper scripts. It also allows for test discovery so that new unit tests can be run without needing to update a wrapper script.
To use, run "python -m junitxml", "python -m junitxml.main" or "python -c 'import junitxml.
New code is thoroughly unit tested, and has been tested on Python 2.4, 2.5, 2.6, 2.7, 3.1 and 3.2 with and without unittest2 installed.

Robert Collins (lifeless) wrote : | # |

Robert Collins (lifeless) wrote : | # |
Ok, I've reviewed this and it looks fine. Thanks for writing it. I'd like to check that your contribution is also under the LGPL-3 (which the rest of the package is).
If you can confirm that, I'll add appropriate headers to the files and merge it.
Cheers,
Rob
- 29. By Duncan Findlay
-
Add pyjunitxml script to avoid the tricky python-
version- specific ways of starting junitxml.

Duncan Findlay (duncf) wrote : | # |
Added a pending change I had lying around. Turns out when building using many versions of python, having to remember the different ways to start pyjunitxml is annoying, so I added a script to do it, similar to how unittest2 works.

Duncan Findlay (duncf) wrote : | # |
On Dec 3, 2011, at 12:07 AM, Robert Collins wrote:
> I'd like to check that your contribution is also under the LGPL-3 (which the rest of the package is).
>
> If you can confirm that, I'll add appropriate headers to the files and merge it.
My contribution is under LGPL-3.
Duncan

Robert Collins (lifeless) wrote : | # |
Merged and pushed. Thanks, and thanks for your patience while I got
accustomed to parenthood :)
Preview Diff
1 | === modified file 'junitxml/__init__.py' |
2 | --- junitxml/__init__.py 2011-11-18 18:50:00 +0000 |
3 | +++ junitxml/__init__.py 2011-12-07 01:45:28 +0000 |
4 | @@ -9,8 +9,11 @@ |
5 | |
6 | import datetime |
7 | import re |
8 | -import time |
9 | -import unittest |
10 | + |
11 | +try: |
12 | + import unittest2 as unittest |
13 | +except ImportError: |
14 | + import unittest |
15 | |
16 | # same format as sys.version_info: "A tuple containing the five components of |
17 | # the version number: major, minor, micro, releaselevel, and serial. All |
18 | @@ -89,7 +92,7 @@ |
19 | |
20 | class JUnitXmlResult(unittest.TestResult): |
21 | """A TestResult which outputs JUnit compatible XML.""" |
22 | - |
23 | + |
24 | def __init__(self, stream): |
25 | """Create a JUnitXmlResult. |
26 | |
27 | @@ -219,3 +222,6 @@ |
28 | self._test_case_string(test) |
29 | self._results.append('/>\n') |
30 | |
31 | +if __name__ == '__main__': |
32 | + import junitxml.main |
33 | + junitxml.main.main() |
34 | |
35 | === added file 'junitxml/__main__.py' |
36 | --- junitxml/__main__.py 1970-01-01 00:00:00 +0000 |
37 | +++ junitxml/__main__.py 2011-12-07 01:45:28 +0000 |
38 | @@ -0,0 +1,12 @@ |
39 | +"""Command line functionality for junitxml. |
40 | + |
41 | +:Author: Duncan Findlay <duncan@duncf.ca> |
42 | +""" |
43 | +import sys |
44 | + |
45 | +import junitxml.main |
46 | + |
47 | +if __name__ == '__main__': |
48 | + if sys.argv[0].endswith('__main__.py'): |
49 | + sys.argv[0] = 'python -m junitxml' |
50 | + junitxml.main.main() |
51 | |
52 | === added file 'junitxml/main.py' |
53 | --- junitxml/main.py 1970-01-01 00:00:00 +0000 |
54 | +++ junitxml/main.py 2011-12-07 01:45:28 +0000 |
55 | @@ -0,0 +1,167 @@ |
56 | +"""Command line functionality for junitxml. |
57 | + |
58 | +Runs specific tests or does automatic discovery with output in XML format. |
59 | + |
60 | +:Author: Duncan Findlay <duncan@duncf.ca> |
61 | +""" |
62 | +import optparse |
63 | +import sys |
64 | + |
65 | +# If we're using Python < 2.7, we want unittest2 if we can get it, otherwise |
66 | +# unittest will suffice. |
67 | +try: |
68 | + import unittest2 as unittest |
69 | +except ImportError: |
70 | + import unittest |
71 | + |
72 | +import junitxml |
73 | +import junitxml.runner |
74 | + |
75 | +ADDITIONAL_HELP_NO_DISCOVERY = """\ |
76 | + |
77 | +<xml file> is the name of a file used for output. |
78 | +<tests> is a list of any number of test modules, classes and test |
79 | +methods. If no tests are specified, automatic discovery is performed. |
80 | + |
81 | +Example for executing specific tests: |
82 | + %(prog_name)s -o junit.xml test_module |
83 | + %(prog_name)s test_module.TestClass |
84 | + %(prog_name)s test_module.TestClass.test_method |
85 | +""" |
86 | + |
87 | +ADDITIONAL_HELP_DISCOVERY = """\ |
88 | + |
89 | +<xml file> is the name of a file used for output. |
90 | +[tests] can be a list of any number of test modules, classes and test |
91 | +methods. If no tests are specified, automatic discovery is performed. |
92 | + |
93 | +Example for executing specific tests: |
94 | + %(prog_name)s -o junit.xml test_module |
95 | + %(prog_name)s test_module.TestClass |
96 | + %(prog_name)s test_module.TestClass.test_method |
97 | + |
98 | +Example for test discovery: |
99 | + %(prog_name)s |
100 | + %(prog_name)s -o junit.xml -s tests/ -p '*.py' |
101 | + %(prog_name)s -t myproj/ -s myproj/tests/ -p '*.py' |
102 | + |
103 | +For test discovery all test modules must be importable from the top |
104 | +level directory of the project. |
105 | + |
106 | +It is an error to specify discovery options and specific tests. |
107 | +""" |
108 | + |
109 | +class XmlTestProgram(object): |
110 | + """Command line program for running tests with XML output.""" |
111 | + |
112 | + loader = unittest.defaultTestLoader |
113 | + runner_class = junitxml.runner.JUnitXmlTestRunner |
114 | + |
115 | + def __init__(self, can_discover=None): |
116 | + self.tests = None |
117 | + self.output_filename = None |
118 | + |
119 | + self._can_discover = can_discover |
120 | + if self._can_discover is None: |
121 | + self._can_discover = bool(hasattr(self.loader, 'discover')) |
122 | + |
123 | + if self._can_discover: |
124 | + self._usage = 'Usage: %prog [options] [tests]' |
125 | + self._help = ADDITIONAL_HELP_DISCOVERY |
126 | + else: |
127 | + self._usage = 'Usage: %prog [options] <tests>' |
128 | + self._help = ADDITIONAL_HELP_NO_DISCOVERY |
129 | + |
130 | + def parse_args(self, argv=None): |
131 | + """Parse command line arguments.""" |
132 | + parser = optparse.OptionParser( |
133 | + usage=self._usage, add_help_option=False) |
134 | + |
135 | + parser.add_option('-h', '--help', dest='help', action='store_true', |
136 | + help='Show option summary and exit') |
137 | + parser.add_option( |
138 | + '-o', '--output-file', dest='output', |
139 | + help='Specify name of output XML file. (Default: %default)', |
140 | + default='./junit.xml') |
141 | + |
142 | + if self._can_discover: |
143 | + discovery_group = optparse.OptionGroup( |
144 | + parser, 'Discovery options', |
145 | + 'Used to control discovery (when no tests specified).') |
146 | + |
147 | + discovery_group.add_option( |
148 | + '-s', '--start-directory', dest='start', |
149 | + default=None, help="Directory to start discovery " |
150 | + "('.' default)") |
151 | + discovery_group.add_option( |
152 | + '-p', '--pattern', dest='pattern', default=None, |
153 | + help="Pattern to match tests ('test*.py' default)") |
154 | + discovery_group.add_option( |
155 | + '-t', '--top-level-directory', dest='top', default=None, |
156 | + help='Top level directory of project (defaults to start ' |
157 | + 'directory)') |
158 | + parser.add_option_group(discovery_group) |
159 | + |
160 | + if argv is None: |
161 | + argv = sys.argv[1:] |
162 | + options, args = parser.parse_args(argv) |
163 | + |
164 | + if options.help: |
165 | + parser.print_help() |
166 | + sys.stdout.write(self._help % ({'prog_name': sys.argv[0]})) |
167 | + sys.exit(1) |
168 | + |
169 | + self.output_filename = options.output |
170 | + |
171 | + if self._can_discover and args and \ |
172 | + (options.start or options.pattern or options.top): |
173 | + parser.error( |
174 | + 'Cannot specify discovery options and specific tests.') |
175 | + elif args: |
176 | + self.tests = self._load_tests(args) |
177 | + elif self._can_discover: |
178 | + self.tests = self._do_discovery(options.start, options.pattern, |
179 | + options.top) |
180 | + else: |
181 | + parser.error('Must specify tests to run.') |
182 | + |
183 | + def _do_discovery(self, start_dir, pattern, top_level_dir): |
184 | + assert self._can_discover |
185 | + |
186 | + if start_dir is None: |
187 | + start_dir = '.' |
188 | + if pattern is None: |
189 | + pattern = 'test*.py' |
190 | + if top_level_dir is None: |
191 | + top_level_dir = start_dir |
192 | + |
193 | + return self.loader.discover(start_dir, pattern, top_level_dir) |
194 | + |
195 | + def _load_tests(self, test_names): |
196 | + return self.loader.loadTestsFromNames(test_names) |
197 | + |
198 | + def run(self): |
199 | + """Run the specified tests. |
200 | + |
201 | + :Returns: TestResult object. |
202 | + """ |
203 | + stream = open(self.output_filename, 'w') |
204 | + try: |
205 | + test_runner = self.runner_class(xml_stream=stream) |
206 | + return test_runner.run(self.tests) |
207 | + finally: |
208 | + stream.close() |
209 | + |
210 | + |
211 | +def main(args=None, prog=None): |
212 | + if args is None: |
213 | + args = sys.argv[1:] |
214 | + if prog is None: |
215 | + prog = XmlTestProgram() |
216 | + |
217 | + prog.parse_args(args) |
218 | + result = prog.run() |
219 | + sys.exit(int(not result.wasSuccessful())) |
220 | + |
221 | +if __name__ == '__main__': |
222 | + main() |
223 | |
224 | === added file 'junitxml/runner.py' |
225 | --- junitxml/runner.py 1970-01-01 00:00:00 +0000 |
226 | +++ junitxml/runner.py 2011-12-07 01:45:28 +0000 |
227 | @@ -0,0 +1,71 @@ |
228 | +"""Simple Test Runner for XML output. |
229 | + |
230 | +Writes XML output and reports test status to stderr. |
231 | + |
232 | +:Author: Duncan Findlay <duncan@duncf.ca> |
233 | +""" |
234 | +import sys |
235 | +import time |
236 | + |
237 | +import junitxml |
238 | + |
239 | +class JUnitXmlTestRunner(object): |
240 | + |
241 | + """Simple Test Runner that writes XML output and reports status. |
242 | + |
243 | + Provides high-level status suitable for command-line operation as well as |
244 | + XML output. |
245 | + """ |
246 | + |
247 | + resultclass = junitxml.JUnitXmlResult |
248 | + |
249 | + def __init__(self, xml_stream, txt_stream=None, **kwargs): |
250 | + if txt_stream is None: |
251 | + txt_stream = sys.stderr |
252 | + self._txt_stream = txt_stream |
253 | + self._xml_stream = xml_stream |
254 | + |
255 | + def _make_result(self): |
256 | + return self.resultclass(self._xml_stream) |
257 | + |
258 | + def run(self, test): |
259 | + result = self._make_result() |
260 | + result.startTestRun() |
261 | + |
262 | + start_time = time.time() |
263 | + test.run(result) |
264 | + end_time = time.time() |
265 | + |
266 | + result.stopTestRun() |
267 | + |
268 | + self._write_summary(result, end_time - start_time) |
269 | + return result |
270 | + |
271 | + def _write_summary(self, result, time_elapsed): |
272 | + |
273 | + plural = '' |
274 | + if result.testsRun != 1: |
275 | + plural = 's' |
276 | + |
277 | + self._txt_stream.write('Ran %d test%s in %.3fs\n\n' % |
278 | + (result.testsRun, plural, time_elapsed)) |
279 | + test_info = [] |
280 | + |
281 | + for result_attr, desc in ( |
282 | + ('failures', 'failures'), ('errors', 'errors'), |
283 | + ('skipped', 'skipped'), ('expectedFailures', 'expected failures'), |
284 | + ('unexpectedSuccesses', 'unexpected successes')): |
285 | + |
286 | + num = len(getattr(result, result_attr, [])) |
287 | + if num > 0: |
288 | + test_info.append('%s=%s' % (desc, num)) |
289 | + |
290 | + test_info_str = '' |
291 | + if test_info: |
292 | + test_info_str = ' (%s)' % (', '.join(test_info),) |
293 | + |
294 | + if result.wasSuccessful(): |
295 | + self._txt_stream.write('OK%s\n' % (test_info_str,)) |
296 | + else: |
297 | + self._txt_stream.write('FAILED%s\n' % (test_info_str,)) |
298 | + |
299 | |
300 | === modified file 'junitxml/tests/__init__.py' |
301 | --- junitxml/tests/__init__.py 2009-08-01 07:45:44 +0000 |
302 | +++ junitxml/tests/__init__.py 2011-12-07 01:45:28 +0000 |
303 | @@ -8,9 +8,17 @@ |
304 | |
305 | from junitxml.tests import ( |
306 | test_junitxml, |
307 | + test_main, |
308 | + test_runner |
309 | ) |
310 | |
311 | def test_suite(): |
312 | return unittest.TestLoader().loadTestsFromNames([ |
313 | 'junitxml.tests.test_junitxml', |
314 | + 'junitxml.tests.test_main', |
315 | + 'junitxml.tests.test_runner' |
316 | ]) |
317 | + |
318 | +if __name__ == '__main__': |
319 | + suite = test_suite() |
320 | + unittest.TextTestRunner(verbosity=2).run(suite) |
321 | |
322 | === modified file 'junitxml/tests/test_junitxml.py' |
323 | --- junitxml/tests/test_junitxml.py 2011-11-18 18:50:00 +0000 |
324 | +++ junitxml/tests/test_junitxml.py 2011-12-07 01:45:28 +0000 |
325 | @@ -12,9 +12,13 @@ |
326 | import datetime |
327 | import re |
328 | import sys |
329 | -import unittest |
330 | import xml.dom.minidom |
331 | |
332 | +try: |
333 | + import unittest2 as unittest |
334 | +except ImportError: |
335 | + import unittest |
336 | + |
337 | import junitxml |
338 | |
339 | class TestImports(unittest.TestCase): |
340 | |
341 | === added file 'junitxml/tests/test_main.py' |
342 | --- junitxml/tests/test_main.py 1970-01-01 00:00:00 +0000 |
343 | +++ junitxml/tests/test_main.py 2011-12-07 01:45:28 +0000 |
344 | @@ -0,0 +1,198 @@ |
345 | +"""Test "main" functionality for junitxml. |
346 | + |
347 | +:Author: Duncan Findlay <duncan@duncf.ca> |
348 | +""" |
349 | +import os |
350 | +import shutil |
351 | +import sys |
352 | +import tempfile |
353 | +import xml.dom.minidom |
354 | +try: |
355 | + from cStringIO import StringIO |
356 | +except ImportError: |
357 | + from io import StringIO |
358 | + |
359 | +try: |
360 | + import unittest2 as unittest |
361 | +except ImportError: |
362 | + import unittest |
363 | + |
364 | +import junitxml.main |
365 | + |
366 | +def _skip_if(condition): |
367 | + """Decorator for skipping tests no matter what version of unittest.""" |
368 | + if condition: |
369 | + def decorator(func): |
370 | + if hasattr(unittest, 'skip'): |
371 | + # If we don't have discovery, we probably don't skip, but we'll |
372 | + # try anyways... |
373 | + return unittest.skip('Discovery not supported.')(func) |
374 | + else: |
375 | + return None |
376 | + return decorator |
377 | + else: |
378 | + # Condition is false, return the do-nothing decorator. |
379 | + def decorator(func): |
380 | + return func |
381 | + return decorator |
382 | + |
383 | + |
384 | +class FakeLoader(object): |
385 | + """Fake TestLoader to stub out test loading.""" |
386 | + |
387 | + def discover(self, start_dir, pattern=None, top_level_dir=None): |
388 | + self._did_discovery = (start_dir, pattern, top_level_dir) |
389 | + return unittest.TestSuite() |
390 | + |
391 | + def loadTestsFromNames(self, names, module=None): |
392 | + self._loaded_tests = (names, module) |
393 | + return unittest.TestSuite() |
394 | + |
395 | + |
396 | +class RedirectedTestCase(unittest.TestCase): |
397 | + |
398 | + """Redirects test output away from stdout/stderr.""" |
399 | + |
400 | + def setUp(self): |
401 | + self._stderr = StringIO() |
402 | + self._stdout = StringIO() |
403 | + self._old_stderr = sys.stderr |
404 | + self._old_stdout = sys.stdout |
405 | + sys.stderr = self._stderr |
406 | + sys.stdout = self._stdout |
407 | + |
408 | + def tearDown(self): |
409 | + sys.stderr = self._old_stderr |
410 | + sys.stdout = self._old_stdout |
411 | + |
412 | + |
413 | +class TestArgs(RedirectedTestCase): |
414 | + |
415 | + def _test_bad_args(self, args, can_discover=None): |
416 | + # XmlTestPrograrm uses some magic to figure out whether it can do test |
417 | + # discovery. We want to manually control that, sometimes. |
418 | + try: |
419 | + if can_discover is not None: |
420 | + prog = junitxml.main.XmlTestProgram(can_discover=can_discover) |
421 | + junitxml.main.main(args, prog=prog) |
422 | + else: |
423 | + junitxml.main.main(args) |
424 | + self.fail('No exception thrown') |
425 | + except SystemExit: |
426 | + e = sys.exc_info()[1] |
427 | + self.assertEqual(e.code, 2, self._stderr.getvalue()) |
428 | + except: |
429 | + self.fail('No SystemExit exception thrown') |
430 | + |
431 | + def test_bad_opts(self): |
432 | + """Bad option combinations are rejected.""" |
433 | + # Mix of tests and discovery opts. |
434 | + self._test_bad_args(['-s', '..', 'my_test']) |
435 | + self._test_bad_args(['-p', 'foo*.py', 'my_test']) |
436 | + self._test_bad_args(['-t', '..', 'my_test']) |
437 | + self._test_bad_args(['--top', '..', 'my_test']) |
438 | + |
439 | + # Incomplete options. |
440 | + self._test_bad_args(['-o']) |
441 | + self._test_bad_args(['-p']) |
442 | + self._test_bad_args(['-s']) |
443 | + self._test_bad_args(['-t']) |
444 | + |
445 | + def test_help(self): |
446 | + """Help is displayed with --help.""" |
447 | + try: |
448 | + junitxml.main.main(['--help']) |
449 | + except SystemExit: |
450 | + e = sys.exc_info()[1] |
451 | + self.assertEqual(e.code, 1) |
452 | + except: |
453 | + self.fail('No SystemExit exception thrown.') |
454 | + self.assertTrue('Example ' in self._stdout.getvalue()) |
455 | + |
456 | + def test_no_discovery(self): |
457 | + """Discovery options are rejected if discovery is not available.""" |
458 | + self._test_bad_args([], can_discover=False) |
459 | + self._test_bad_args(['-s', '..'], can_discover=False) |
460 | + self._test_bad_args(['-t', '..'], can_discover=False) |
461 | + self._test_bad_args(['-p', '*.*'], can_discover=False) |
462 | + |
463 | + |
464 | +class TestLoad(RedirectedTestCase): |
465 | + |
466 | + def setUp(self): |
467 | + super(TestLoad, self).setUp() |
468 | + self._tmpdir = tempfile.mkdtemp(prefix='unittest_junitxml_') |
469 | + self._output_file = os.path.join(self._tmpdir, 'junit.xml') |
470 | + |
471 | + def tearDown(self): |
472 | + super(TestLoad, self).tearDown() |
473 | + shutil.rmtree(self._tmpdir) |
474 | + |
475 | + def test_loaded_tests(self): |
476 | + """Verify named tests are properly loaded.""" |
477 | + prog = junitxml.main.XmlTestProgram() |
478 | + prog.loader = FakeLoader() |
479 | + prog.parse_args(['-o', self._output_file, 'my_test1', 'my_test2']) |
480 | + result = prog.run() |
481 | + |
482 | + document = xml.dom.minidom.parse(self._output_file) |
483 | + self.assertEqual(document.documentElement.tagName, 'testsuite') |
484 | + self.assertEqual(document.documentElement.getAttribute('tests'), '0') |
485 | + |
486 | + self.assertEqual(result.wasSuccessful(), True) |
487 | + self.assertTrue(hasattr(prog.loader, '_loaded_tests')) |
488 | + self.assertEqual(prog.loader._loaded_tests, |
489 | + (['my_test1', 'my_test2'], None)) |
490 | + |
491 | + def test_discovery_all_args(self): |
492 | + """Verify cmdline opts are used for discovery.""" |
493 | + prog = junitxml.main.XmlTestProgram(can_discover=True) |
494 | + prog.loader = FakeLoader() |
495 | + prog.parse_args(['-o', self._output_file, '-s', self._tmpdir, |
496 | + '--pattern', '*.py', '-t', '.']) |
497 | + result = prog.run() |
498 | + |
499 | + document = xml.dom.minidom.parse(self._output_file) |
500 | + self.assertEqual(document.documentElement.tagName, 'testsuite') |
501 | + self.assertEqual(document.documentElement.getAttribute('tests'), '0') |
502 | + |
503 | + self.assertEqual(result.wasSuccessful(), True) |
504 | + self.assertTrue(hasattr(prog.loader, '_did_discovery')) |
505 | + self.assertEqual(prog.loader._did_discovery, |
506 | + (self._tmpdir, '*.py', '.')) |
507 | + |
508 | + def test_discovery_top_dir(self): |
509 | + """Verify top-level dir properly defaults to start directory.""" |
510 | + prog = junitxml.main.XmlTestProgram(can_discover=True) |
511 | + prog.loader = FakeLoader() |
512 | + prog.parse_args(['-o', self._output_file, '--start-dir', self._tmpdir]) |
513 | + result = prog.run() |
514 | + |
515 | + document = xml.dom.minidom.parse(self._output_file) |
516 | + self.assertEqual(document.documentElement.tagName, 'testsuite') |
517 | + self.assertEqual(document.documentElement.getAttribute('tests'), '0') |
518 | + |
519 | + self.assertEqual(result.wasSuccessful(), True) |
520 | + self.assertTrue(hasattr(prog.loader, '_did_discovery')) |
521 | + self.assertEqual(prog.loader._did_discovery, |
522 | + (self._tmpdir, 'test*.py', self._tmpdir)) |
523 | + |
524 | + def test_discovery_no_args(self): |
525 | + """Verify good defaults are used for discovery when not specified.""" |
526 | + prog = junitxml.main.XmlTestProgram(can_discover=True) |
527 | + prog.loader = FakeLoader() |
528 | + prog.parse_args(['-o', self._output_file]) |
529 | + result = prog.run() |
530 | + |
531 | + document = xml.dom.minidom.parse(self._output_file) |
532 | + self.assertEqual(document.documentElement.tagName, 'testsuite') |
533 | + self.assertEqual(document.documentElement.getAttribute('tests'), '0') |
534 | + |
535 | + self.assertEqual(result.wasSuccessful(), True) |
536 | + self.assertTrue(hasattr(prog.loader, '_did_discovery')) |
537 | + self.assertEqual(prog.loader._did_discovery, |
538 | + ('.', 'test*.py', '.')) |
539 | + |
540 | + |
541 | +if __name__ == '__main__': |
542 | + unittest.main() |
543 | |
544 | === added file 'junitxml/tests/test_runner.py' |
545 | --- junitxml/tests/test_runner.py 1970-01-01 00:00:00 +0000 |
546 | +++ junitxml/tests/test_runner.py 2011-12-07 01:45:28 +0000 |
547 | @@ -0,0 +1,119 @@ |
548 | +"""Test XmlTestRunner functionality for junitxml. |
549 | + |
550 | +:Author: Duncan Findlay <duncan@duncf.ca> |
551 | +""" |
552 | +import xml.dom.minidom |
553 | +try: |
554 | + from cStringIO import StringIO |
555 | +except ImportError: |
556 | + from io import StringIO |
557 | + |
558 | +try: |
559 | + import unittest2 as unittest |
560 | +except ImportError: |
561 | + import unittest |
562 | + |
563 | +import junitxml.runner |
564 | + |
565 | + |
566 | +# Old versions of unittest don't have these "fancy" types of results. |
567 | +_FANCY_UNITTEST = (hasattr(unittest, 'skip') and |
568 | + hasattr(unittest, 'expectedFailure')) |
569 | + |
570 | + |
571 | +class TestXMLTestRunner(unittest.TestCase): |
572 | + |
573 | + class DummyTestCase(unittest.TestCase): |
574 | + |
575 | + def test_pass(self): |
576 | + pass |
577 | + |
578 | + def test_fail(self): |
579 | + self.fail() |
580 | + |
581 | + def test_error(self): |
582 | + raise Exception() |
583 | + |
584 | + if _FANCY_UNITTEST: |
585 | + |
586 | + @unittest.skip('skipped') |
587 | + def test_skip(self): |
588 | + pass |
589 | + |
590 | + @unittest.expectedFailure |
591 | + def test_xfail(self): |
592 | + self.fail('all is good') |
593 | + |
594 | + @unittest.expectedFailure |
595 | + def test_unexpected_success(self): |
596 | + pass |
597 | + |
598 | + def _run_runner(self, test_suite): |
599 | + xml_out = StringIO() |
600 | + console = StringIO() |
601 | + |
602 | + runner = junitxml.runner.JUnitXmlTestRunner( |
603 | + xml_stream=xml_out, txt_stream=console) |
604 | + result = runner.run(test_suite) |
605 | + |
606 | + return (result, xml_out, console) |
607 | + |
608 | + def test_xml_output(self): |
609 | + """Tests that runner properly gives XML output.""" |
610 | + test_suite = unittest.TestLoader().loadTestsFromTestCase( |
611 | + self.DummyTestCase) |
612 | + |
613 | + result, xml_out, console = self._run_runner(test_suite) |
614 | + |
615 | + num_tests = test_suite.countTestCases() |
616 | + |
617 | + # Make sure the XML output looks correct. |
618 | + value = xml_out.getvalue() |
619 | + document = xml.dom.minidom.parseString(value) |
620 | + |
621 | + self.assertEqual(document.documentElement.tagName, 'testsuite') |
622 | + self.assertEqual(document.documentElement.getAttribute('tests'), |
623 | + str(num_tests)) |
624 | + |
625 | + def test_console_output_fail(self): |
626 | + """Tests that failure is reported properly on stderr.""" |
627 | + test_suite = unittest.TestLoader().loadTestsFromTestCase( |
628 | + self.DummyTestCase) |
629 | + |
630 | + result, xml_out, console = self._run_runner(test_suite) |
631 | + |
632 | + num_tests = test_suite.countTestCases() |
633 | + |
634 | + # Make sure the console output looks correct. |
635 | + value = console.getvalue() |
636 | + self.assertTrue('Ran %d tests in ' % (num_tests,) in value, |
637 | + 'Output was:\n%s' % (value,)) |
638 | + self.assertTrue('FAILED (failures=1' in value, |
639 | + 'Output was:\n%s' % (value,)) |
640 | + self.assertTrue('errors=1' in value, |
641 | + 'Output was:\n%s' % (value,)) |
642 | + |
643 | + if _FANCY_UNITTEST: |
644 | + self.assertTrue('expected failures=1' in value, |
645 | + 'Output was:\n%s' % (value,)) |
646 | + self.assertTrue('skipped=1' in value, |
647 | + 'Output was:\n%s' % (value,)) |
648 | + self.assertTrue('unexpected successes=1' in value, |
649 | + 'Output was:\n%s' % (value,)) |
650 | + |
651 | + def test_console_output_ok(self): |
652 | + """Tests that success is reported properly on stderr.""" |
653 | + test_suite = unittest.TestSuite() |
654 | + test_suite.addTest(self.DummyTestCase('test_pass')) |
655 | + |
656 | + result, xml_out, console = self._run_runner(test_suite) |
657 | + |
658 | + value = console.getvalue() |
659 | + self.assertTrue('Ran 1 test in ' in value, |
660 | + 'Output was:\n%s' % (value,)) |
661 | + self.assertTrue('OK\n' in value, |
662 | + 'Output was:\n%s' % (value,)) |
663 | + |
664 | + |
665 | +if __name__ == '__main__': |
666 | + unittest.main() |
667 | |
668 | === added file 'pyjunitxml' |
669 | --- pyjunitxml 1970-01-01 00:00:00 +0000 |
670 | +++ pyjunitxml 2011-12-07 01:45:28 +0000 |
671 | @@ -0,0 +1,7 @@ |
672 | +#!/usr/bin/env python |
673 | + |
674 | +"""Script to run unit tests under junitxml.""" |
675 | + |
676 | +if __name__ == '__main__': |
677 | + import junitxml.main |
678 | + junitxml.main.main() |
679 | |
680 | === modified file 'setup.py' |
681 | --- setup.py 2010-09-11 22:36:30 +0000 |
682 | +++ setup.py 2011-12-07 01:45:28 +0000 |
683 | @@ -9,5 +9,6 @@ |
684 | maintainer_email="robertc@robertcollins.net", |
685 | url="https://launchpad.net/pyjunitxml", |
686 | packages=['junitxml', 'junitxml.tests'], |
687 | + scripts=['pyjunitxml'], |
688 | license="LGPL-3", |
689 | ) |
Thanks for this, sorry for being slack - RealLife was interfering (new baby). I should get to it soon now.