Package logilab-common-0 ::
Package 39 ::
Package 0 ::
Module pytest
|
|
1 """pytest is a tool that eases test running and debugging.
2
3 To be able to use pytest, you should either write tests using
4 the logilab.common.testlib's framework or the unittest module of the
5 Python's standard library.
6
7 You can customize pytest's behaviour by defining a ``pytestconf.py`` file
8 somewhere in your test directory. In this file, you can add options or
9 change the way tests are run.
10
11 To add command line options, you must define a ``update_parser`` function in
12 your ``pytestconf.py`` file. The function must accept a single parameter
13 that will be the OptionParser's instance to customize.
14
15 If you wish to customize the tester, you'll have to define a class named
16 ``CustomPyTester``. This class should extend the default `PyTester` class
17 defined in the pytest module. Take a look at the `PyTester` and `DjangoTester`
18 classes for more information about what can be done.
19
20 For instance, if you wish to add a custom -l option to specify a loglevel, you
21 could define the following ``pytestconf.py`` file ::
22
23 import logging
24 from logilab.common.pytest import PyTester
25
26 def update_parser(parser):
27 parser.add_option('-l', '--loglevel', dest='loglevel', action='store',
28 choices=('debug', 'info', 'warning', 'error', 'critical'),
29 default='critical', help="the default log level possible choices are "
30 "('debug', 'info', 'warning', 'error', 'critical')")
31 return parser
32
33
34 class CustomPyTester(PyTester):
35 def __init__(self, cvg, options):
36 super(CustomPyTester, self).__init__(cvg, options)
37 loglevel = options.loglevel.upper()
38 logger = logging.getLogger('erudi')
39 logger.setLevel(logging.getLevelName(loglevel))
40
41
42 In your TestCase class you can then get the value of a specific option with
43 the ``optval`` method::
44
45 class MyTestCase(TestCase):
46 def test_foo(self):
47 loglevel = self.optval('loglevel')
48 # ...
49
50
51 You can also tag your tag your test for fine filtering
52
53 With those tag::
54
55 from logilab.common.testlib import tag, TestCase
56
57 class Exemple(TestCase):
58
59 @tag('rouge', 'carre')
60 def toto(self):
61 pass
62
63 @tag('carre', 'vert')
64 def tata(self):
65 pass
66
67 @tag('rouge')
68 def titi(test):
69 pass
70
71 you can filter the function with a simpe python expression
72
73 * ``toto`` and ``titi`` match ``rouge``
74
75 * ``toto``, ``tata`` and ``titi``, match ``rouge or carre``
76
77 * ``tata`` and ``titi`` match``rouge ^ carre``
78
79 * ``titi`` match ``rouge and not carre``
80
81
82
83
84
85
86 :copyright: 2000-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
87 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
88 :license: General Public License version 2 - http://www.gnu.org/licenses
89 """
90 __docformat__ = "restructuredtext en"
91
92 PYTEST_DOC = """%prog [OPTIONS] [testfile [testpattern]]
93
94 examples:
95
96 pytest path/to/mytests.py
97 pytest path/to/mytests.py TheseTests
98 pytest path/to/mytests.py TheseTests.test_thisone
99 pytest path/to/mytests.py -m '(not long and database) or regr'
100
101 pytest one (will run both test_thisone and test_thatone)
102 pytest path/to/mytests.py -s not (will skip test_notthisone)
103
104 pytest --coverage test_foo.py
105 (only if logilab.devtools is available)
106 """
107
108 import os, sys, re
109 import os.path as osp
110 from time import time, clock
111
112
113 from logilab.common.fileutils import abspath_listdir
114 from logilab.common import testlib
115 import doctest
116 import unittest
117
118
119 import imp
120
121 import __builtin__
122
123
124 try:
125 import django
126 from logilab.common.modutils import modpath_from_file, load_module_from_modpath
127 DJANGO_FOUND = True
128 except ImportError:
129 DJANGO_FOUND = False
130
131 CONF_FILE = 'pytestconf.py'
132
133
134
135
137 nesting = 0
138
140 if not cls.nesting:
141 cls.tracefunc = staticmethod(getattr(sys, '__settrace__', sys.settrace))
142 cls.oldtracer = getattr(sys, '__tracer__', None)
143 sys.__notrace__ = True
144 cls.tracefunc(None)
145 cls.nesting += 1
146 pause_tracing = classmethod(pause_tracing)
147
149 cls.nesting -= 1
150 assert cls.nesting >= 0
151 if not cls.nesting:
152 cls.tracefunc(cls.oldtracer)
153 delattr(sys, '__notrace__')
154 resume_tracing = classmethod(resume_tracing)
155
156
157 pause_tracing = TraceController.pause_tracing
158 resume_tracing = TraceController.resume_tracing
159
160
162 if hasattr(func, 'uncovered'):
163 return func
164 func.uncovered = True
165 def not_covered(*args, **kwargs):
166 pause_tracing()
167 try:
168 return func(*args, **kwargs)
169 finally:
170 resume_tracing()
171 not_covered.uncovered = True
172 return not_covered
173
174
175
176
177
178
179 unittest.TestCase = testlib.TestCase
180 unittest.main = testlib.unittest_main
181 unittest._TextTestResult = testlib.SkipAwareTestResult
182 unittest.TextTestRunner = testlib.SkipAwareTextTestRunner
183 unittest.TestLoader = testlib.NonStrictTestLoader
184 unittest.TestProgram = testlib.SkipAwareTestProgram
185 if sys.version_info >= (2, 4):
186 doctest.DocTestCase.__bases__ = (testlib.TestCase,)
187 else:
188 unittest.FunctionTestCase.__bases__ = (testlib.TestCase,)
189
190
191
192 TESTFILE_RE = re.compile("^((unit)?test.*|smoketest)\.py$")
194 """returns True if `filename` seems to be a test file"""
195 return TESTFILE_RE.match(osp.basename(filename))
196
197 TESTDIR_RE = re.compile("^(unit)?tests?$")
199 """returns True if `filename` seems to be a test directory"""
200 return TESTDIR_RE.match(osp.basename(dirpath))
201
202
204 """loads a ``pytestconf.py`` file and update default parser
205 and / or tester.
206 """
207 namespace = {}
208 execfile(path, namespace)
209 if 'update_parser' in namespace:
210 namespace['update_parser'](parser)
211 return namespace.get('CustomPyTester', PyTester)
212
213
215 """try to find project's root and add it to sys.path"""
216 curdir = osp.abspath(projdir)
217 previousdir = curdir
218 testercls = PyTester
219 conf_file_path = osp.join(curdir, CONF_FILE)
220 if osp.isfile(conf_file_path):
221 testercls = load_pytest_conf(conf_file_path, parser)
222 while this_is_a_testdir(curdir) or \
223 osp.isfile(osp.join(curdir, '__init__.py')):
224 newdir = osp.normpath(osp.join(curdir, os.pardir))
225 if newdir == curdir:
226 break
227 previousdir = curdir
228 curdir = newdir
229 conf_file_path = osp.join(curdir, CONF_FILE)
230 if osp.isfile(conf_file_path):
231 testercls = load_pytest_conf(conf_file_path, parser)
232 return previousdir, testercls
233
234
236 """this class holds global test statistics"""
238 self.ran = 0
239 self.skipped = 0
240 self.failures = 0
241 self.errors = 0
242 self.ttime = 0
243 self.ctime = 0
244 self.modulescount = 0
245 self.errmodules = []
246
247 - def feed(self, filename, testresult, ttime, ctime):
248 """integrates new test information into internal statistics"""
249 ran = testresult.testsRun
250 self.ran += ran
251 self.skipped += len(getattr(testresult, 'skipped', ()))
252 self.failures += len(testresult.failures)
253 self.errors += len(testresult.errors)
254 self.ttime += ttime
255 self.ctime += ctime
256 self.modulescount += 1
257 if not testresult.wasSuccessful():
258 problems = len(testresult.failures) + len(testresult.errors)
259 self.errmodules.append((filename[:-3], problems, ran))
260
261
263 """called when the test module could not be imported by unittest
264 """
265 self.errors += 1
266 self.modulescount += 1
267 self.ran += 1
268 self.errmodules.append((filename[:-3], 1, 1))
269
271 self.modulescount += 1
272 self.ran += 1
273 self.errmodules.append((filename[:-3], 0, 0))
274
276 """this is just presentation stuff"""
277 line1 = ['Ran %s test cases in %.2fs (%.2fs CPU)'
278 % (self.ran, self.ttime, self.ctime)]
279 if self.errors:
280 line1.append('%s errors' % self.errors)
281 if self.failures:
282 line1.append('%s failures' % self.failures)
283 if self.skipped:
284 line1.append('%s skipped' % self.skipped)
285 modulesok = self.modulescount - len(self.errmodules)
286 if self.errors or self.failures:
287 line2 = '%s modules OK (%s failed)' % (modulesok,
288 len(self.errmodules))
289 descr = ', '.join(['%s [%s/%s]' % info for info in self.errmodules])
290 line3 = '\nfailures: %s' % descr
291 elif modulesok:
292 line2 = 'All %s modules OK' % modulesok
293 line3 = ''
294 else:
295 return ''
296 return '%s\n%s%s' % (', '.join(line1), line2, line3)
297
298
299
301 """remove all modules from cache that come from `testdir`
302
303 This is used to avoid strange side-effects when using the
304 testall() mode of pytest.
305 For instance, if we run pytest on this tree::
306
307 A/test/test_utils.py
308 B/test/test_utils.py
309
310 we **have** to clean sys.modules to make sure the correct test_utils
311 module is ran in B
312 """
313 for modname, mod in sys.modules.items():
314 if mod is None:
315 continue
316 if not hasattr(mod, '__file__'):
317
318 continue
319 modfile = mod.__file__
320
321
322 if not osp.isabs(modfile) or modfile.startswith(testdir):
323 del sys.modules[modname]
324
325
326
328 """encaspulates testrun logic"""
329
335
337 """prints the report and returns appropriate exitcode"""
338
339 print "*" * 79
340 print self.report
341 return self.report.failures + self.report.errors
342
343
344 - def testall(self, exitfirst=False):
345 """walks trhough current working directory, finds something
346 which can be considered as a testdir and runs every test there
347 """
348 here = os.getcwd()
349 for dirname, dirs, _ in os.walk(here):
350 for skipped in ('CVS', '.svn', '.hg'):
351 if skipped in dirs:
352 dirs.remove(skipped)
353 basename = osp.basename(dirname)
354 if this_is_a_testdir(basename):
355 print "going into", dirname
356
357 self.testonedir(dirname, exitfirst)
358 dirs[:] = []
359 if self.report.ran == 0:
360 print "no test dir found testing here:", here
361
362
363
364 self.testonedir(here)
365
367 """finds each testfile in the `testdir` and runs it"""
368 for filename in abspath_listdir(testdir):
369 if this_is_a_testfile(filename):
370 if self.options.exitfirst and not self.options.restart:
371
372 try:
373 restartfile = open(testlib.FILE_RESTART, "w")
374 restartfile.close()
375 except Exception, e:
376 print >> sys.__stderr__, "Error while overwriting \
377 succeeded test file :", osp.join(os.getcwd(),testlib.FILE_RESTART)
378 raise e
379
380 prog = self.testfile(filename, batchmode=True)
381 if exitfirst and (prog is None or not prog.result.wasSuccessful()):
382 break
383 self.firstwrite = True
384
385 remove_local_modules_from_sys(testdir)
386
387
388 - def testfile(self, filename, batchmode=False):
389 """runs every test in `filename`
390
391 :param filename: an absolute path pointing to a unittest file
392 """
393 here = os.getcwd()
394 dirname = osp.dirname(filename)
395 if dirname:
396 os.chdir(dirname)
397
398 if self.options.exitfirst and not self.options.restart and self.firstwrite:
399 try:
400 restartfile = open(testlib.FILE_RESTART, "w")
401 restartfile.close()
402 except Exception, e:
403 print >> sys.__stderr__, "Error while overwriting \
404 succeeded test file :", osp.join(os.getcwd(),testlib.FILE_RESTART)
405 raise e
406 modname = osp.basename(filename)[:-3]
407 if batchmode:
408 from cStringIO import StringIO
409 outstream = StringIO()
410 else:
411 outstream = sys.stderr
412 try:
413 print >> outstream, (' %s ' % osp.basename(filename)).center(70, '=')
414 except TypeError:
415 print >> outstream, (' %s ' % osp.basename(filename)).center(70)
416 try:
417 tstart, cstart = time(), clock()
418 try:
419 testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg,
420 options=self.options, outstream=outstream)
421 except (KeyboardInterrupt, SystemExit):
422 raise
423 except testlib.TestSkipped:
424 print "Module skipped:", filename
425 self.report.skip_module(filename)
426 return None
427 except Exception:
428 self.report.failed_to_test_module(filename)
429 print >> outstream, 'unhandled exception occured while testing', modname
430 import traceback
431 traceback.print_exc(file=outstream)
432 if batchmode:
433 print >> sys.stderr, outstream.getvalue()
434 return None
435
436 tend, cend = time(), clock()
437 ttime, ctime = (tend - tstart), (cend - cstart)
438 if testprog.result.testsRun and batchmode:
439 print >> sys.stderr, outstream.getvalue()
440 self.report.feed(filename, testprog.result, ttime, ctime)
441 return testprog
442 finally:
443 if dirname:
444 os.chdir(here)
445
446
447
449
451 """try to find project's setting and load it"""
452 curdir = osp.abspath(dirname)
453 previousdir = curdir
454 while not osp.isfile(osp.join(curdir, 'settings.py')) and \
455 osp.isfile(osp.join(curdir, '__init__.py')):
456 newdir = osp.normpath(osp.join(curdir, os.pardir))
457 if newdir == curdir:
458 raise AssertionError('could not find settings.py')
459 previousdir = curdir
460 curdir = newdir
461
462 settings = load_module_from_modpath(modpath_from_file(osp.join(curdir, 'settings.py')))
463 from django.core.management import setup_environ
464 setup_environ(settings)
465 settings.DEBUG = False
466 self.settings = settings
467
468 if curdir not in sys.path:
469 sys.path.insert(1, curdir)
470
472
473 from django.test.utils import setup_test_environment
474 from django.test.utils import create_test_db
475 setup_test_environment()
476 create_test_db(verbosity=0)
477 self.dbname = self.settings.TEST_DATABASE_NAME
478
479
481
482 from django.test.utils import teardown_test_environment
483 from django.test.utils import destroy_test_db
484 teardown_test_environment()
485 print 'destroying', self.dbname
486 destroy_test_db(self.dbname, verbosity=0)
487
488
489 - def testall(self, exitfirst=False):
490 """walks trhough current working directory, finds something
491 which can be considered as a testdir and runs every test there
492 """
493 for dirname, dirs, _ in os.walk(os.getcwd()):
494 for skipped in ('CVS', '.svn', '.hg'):
495 if skipped in dirs:
496 dirs.remove(skipped)
497 if 'tests.py' in files:
498 self.testonedir(dirname, exitfirst)
499 dirs[:] = []
500 else:
501 basename = osp.basename(dirname)
502 if basename in ('test', 'tests'):
503 print "going into", dirname
504
505 self.testonedir(dirname, exitfirst)
506 dirs[:] = []
507
508
510 """finds each testfile in the `testdir` and runs it"""
511
512
513 testfiles = [fpath for fpath in abspath_listdir(testdir)
514 if this_is_a_testfile(fpath)]
515 if len(testfiles) > 1:
516 try:
517 testfiles.remove(osp.join(testdir, 'tests.py'))
518 except ValueError:
519 pass
520 for filename in testfiles:
521
522 prog = self.testfile(filename, batchmode=True)
523 if exitfirst and (prog is None or not prog.result.wasSuccessful()):
524 break
525
526 remove_local_modules_from_sys(testdir)
527
528
529 - def testfile(self, filename, batchmode=False):
530 """runs every test in `filename`
531
532 :param filename: an absolute path pointing to a unittest file
533 """
534 here = os.getcwd()
535 dirname = osp.dirname(filename)
536 if dirname:
537 os.chdir(dirname)
538 self.load_django_settings(dirname)
539 modname = osp.basename(filename)[:-3]
540 print >>sys.stderr, (' %s ' % osp.basename(filename)).center(70, '=')
541 try:
542 try:
543 tstart, cstart = time(), clock()
544 self.before_testfile()
545 testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg)
546 tend, cend = time(), clock()
547 ttime, ctime = (tend - tstart), (cend - cstart)
548 self.report.feed(filename, testprog.result, ttime, ctime)
549 return testprog
550 except SystemExit:
551 raise
552 except Exception, exc:
553 import traceback
554 traceback.print_exc()
555 self.report.failed_to_test_module(filename)
556 print 'unhandled exception occured while testing', modname
557 print 'error: %s' % exc
558 return None
559 finally:
560 self.after_testfile()
561 if dirname:
562 os.chdir(here)
563
564
566 """creates the OptionParser instance
567 """
568 from optparse import OptionParser
569 parser = OptionParser(usage=PYTEST_DOC)
570
571 parser.newargs = []
572 def rebuild_cmdline(option, opt, value, parser):
573 """carry the option to unittest_main"""
574 parser.newargs.append(opt)
575
576
577 def rebuild_and_store(option, opt, value, parser):
578 """carry the option to unittest_main and store
579 the value on current parser
580 """
581 parser.newargs.append(opt)
582 setattr(parser.values, option.dest, True)
583
584
585 parser.add_option('-t', dest='testdir', default=None,
586 help="directory where the tests will be found")
587 parser.add_option('-d', dest='dbc', default=False,
588 action="store_true", help="enable design-by-contract")
589
590 parser.add_option('-v', '--verbose', callback=rebuild_cmdline,
591 action="callback", help="Verbose output")
592 parser.add_option('-i', '--pdb', callback=rebuild_and_store,
593 dest="pdb", action="callback",
594 help="Enable test failure inspection (conflicts with --coverage)")
595 parser.add_option('-x', '--exitfirst', callback=rebuild_and_store,
596 dest="exitfirst", default=False,
597 action="callback", help="Exit on first failure "
598 "(only make sense when pytest run one test file)")
599 parser.add_option('-R', '--restart', callback=rebuild_and_store,
600 dest="restart", default=False,
601 action="callback",
602 help="Restart tests from where it failed (implies exitfirst) "
603 "(only make sense if tests previously ran with exitfirst only)")
604 parser.add_option('-c', '--capture', callback=rebuild_cmdline,
605 action="callback",
606 help="Captures and prints standard out/err only on errors "
607 "(only make sense when pytest run one test file)")
608 parser.add_option('--color', callback=rebuild_cmdline,
609 action="callback",
610 help="colorize tracebacks")
611 parser.add_option('-p', '--printonly',
612
613
614
615 action="store", dest="printonly", default=None,
616 help="Only prints lines matching specified pattern (implies capture) "
617 "(only make sense when pytest run one test file)")
618 parser.add_option('-s', '--skip',
619
620
621
622 action="store", dest="skipped", default=None,
623 help="test names matching this name will be skipped "
624 "to skip several patterns, use commas")
625 parser.add_option('-q', '--quiet', callback=rebuild_cmdline,
626 action="callback", help="Minimal output")
627 parser.add_option('-P', '--profile', default=None, dest='profile',
628 help="Profile execution and store data in the given file")
629 parser.add_option('-m', '--match', default=None, dest='tags_pattern',
630 help="only execute test whose tag macht the current pattern")
631
632 try:
633 from logilab.devtools.lib.coverage import Coverage
634 parser.add_option('--coverage', dest="coverage", default=False,
635 action="store_true",
636 help="run tests with pycoverage (conflicts with --pdb)")
637 except ImportError:
638 pass
639
640 if DJANGO_FOUND:
641 parser.add_option('-J', '--django', dest='django', default=False,
642 action="store_true",
643 help='use pytest for django test cases')
644 return parser
645
646
648 """Parse the command line and return (options processed), (options to pass to
649 unittest_main()), (explicitfile or None).
650 """
651
652 options, args = parser.parse_args()
653 if options.pdb and getattr(options, 'coverage', False):
654 parser.error("'pdb' and 'coverage' options are exclusive")
655 filenames = [arg for arg in args if arg.endswith('.py')]
656 if filenames:
657 if len(filenames) > 1:
658 parser.error("only one filename is acceptable")
659 explicitfile = filenames[0]
660 args.remove(explicitfile)
661 else:
662 explicitfile = None
663
664 testlib.ENABLE_DBC = options.dbc
665 newargs = parser.newargs
666 if options.printonly:
667 newargs.extend(['--printonly', options.printonly])
668 if options.skipped:
669 newargs.extend(['--skip', options.skipped])
670
671 if options.restart:
672 options.exitfirst = True
673
674
675 newargs += args
676 return options, explicitfile
677
678
679
681 parser = make_parser()
682 rootdir, testercls = project_root(parser)
683 options, explicitfile = parseargs(parser)
684
685 sys.argv[1:] = parser.newargs
686 covermode = getattr(options, 'coverage', None)
687 cvg = None
688 if not '' in sys.path:
689 sys.path.insert(0, '')
690 if covermode:
691
692 from logilab.devtools.lib.coverage import Coverage
693 cvg = Coverage([rootdir])
694 cvg.erase()
695 cvg.start()
696 if DJANGO_FOUND and options.django:
697 tester = DjangoTester(cvg, options)
698 else:
699 tester = testercls(cvg, options)
700 if explicitfile:
701 cmd, args = tester.testfile, (explicitfile,)
702 elif options.testdir:
703 cmd, args = tester.testonedir, (options.testdir, options.exitfirst)
704 else:
705 cmd, args = tester.testall, (options.exitfirst,)
706 try:
707 try:
708 if options.profile:
709 import hotshot
710 prof = hotshot.Profile(options.profile)
711 prof.runcall(cmd, *args)
712 prof.close()
713 print 'profile data saved in', options.profile
714 else:
715 cmd(*args)
716 except SystemExit:
717 raise
718 except:
719 import traceback
720 traceback.print_exc()
721 finally:
722 errcode = tester.show_report()
723 if covermode:
724 cvg.stop()
725 cvg.save()
726 here = osp.abspath(os.getcwd())
727 if this_is_a_testdir(here):
728 morfdir = osp.normpath(osp.join(here, '..'))
729 else:
730 morfdir = here
731 print "computing code coverage (%s), this might take some time" % \
732 morfdir
733 cvg.annotate([morfdir])
734 cvg.report([morfdir], False)
735 sys.exit(errcode)
736