Merge lp:~duncf/pyjunitxml/cmdline-runner into lp:pyjunitxml

Proposed by Duncan Findlay
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
Reviewer Review Type Date Requested Status
Robert Collins Approve
Review via email: mp+71637@code.launchpad.net

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.main;junitxml.main.main()'" (depending on Python version). Tests can be specified by name. If no tests are specified, discovery is used. Discovery is supported for Python >= 2.7 and >= 3.2 or with the unittest2 package installed. Output file can be specified with the -o option (defaults to junit.xml). Number of tests run and final results are also reported on stderr. -h/--help provides a summary of available options (help differs depending on whether discovery is possible).

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.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Thanks for this, sorry for being slack - RealLife was interfering (new baby). I should get to it soon now.

Revision history for this message
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

review: Approve
lp:~duncf/pyjunitxml/cmdline-runner updated
29. By Duncan Findlay

Add pyjunitxml script to avoid the tricky python-version-specific ways of starting junitxml.

Revision history for this message
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.

Revision history for this message
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

Revision history for this message
Robert Collins (lifeless) wrote :

Merged and pushed. Thanks, and thanks for your patience while I got
accustomed to parenthood :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 )

Subscribers

People subscribed via source and target branches

to all changes: