Package logilab-common-0 ::
Package 36 ::
Package 1 ::
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.errmodules.append((filename[:-3], 1, 1))
267
268
270 """this is just presentation stuff"""
271 line1 = ['Ran %s test cases in %.2fs (%.2fs CPU)'
272 % (self.ran, self.ttime, self.ctime)]
273 if self.errors:
274 line1.append('%s errors' % self.errors)
275 if self.failures:
276 line1.append('%s failures' % self.failures)
277 if self.skipped:
278 line1.append('%s skipped' % self.skipped)
279 modulesok = self.modulescount - len(self.errmodules)
280 if self.errors or self.failures:
281 line2 = '%s modules OK (%s failed)' % (modulesok,
282 len(self.errmodules))
283 descr = ', '.join(['%s [%s/%s]' % info for info in self.errmodules])
284 line3 = '\nfailures: %s' % descr
285 elif modulesok:
286 line2 = 'All %s modules OK' % modulesok
287 line3 = ''
288 else:
289 return ''
290 return '%s\n%s%s' % (', '.join(line1), line2, line3)
291
292
293
295 """remove all modules from cache that come from `testdir`
296
297 This is used to avoid strange side-effects when using the
298 testall() mode of pytest.
299 For instance, if we run pytest on this tree::
300
301 A/test/test_utils.py
302 B/test/test_utils.py
303
304 we **have** to clean sys.modules to make sure the correct test_utils
305 module is ran in B
306 """
307 for modname, mod in sys.modules.items():
308 if mod is None:
309 continue
310 if not hasattr(mod, '__file__'):
311
312 continue
313 modfile = mod.__file__
314
315
316 if not osp.isabs(modfile) or modfile.startswith(testdir):
317 del sys.modules[modname]
318
319
320
322 """encaspulates testrun logic"""
323
329
331 """prints the report and returns appropriate exitcode"""
332
333 print "*" * 79
334 print self.report
335 return self.report.failures + self.report.errors
336
337
338 - def testall(self, exitfirst=False):
339 """walks trhough current working directory, finds something
340 which can be considered as a testdir and runs every test there
341 """
342 here = os.getcwd()
343 for dirname, dirs, _ in os.walk(here):
344 for skipped in ('CVS', '.svn', '.hg'):
345 if skipped in dirs:
346 dirs.remove(skipped)
347 basename = osp.basename(dirname)
348 if this_is_a_testdir(basename):
349 print "going into", dirname
350
351 self.testonedir(dirname, exitfirst)
352 dirs[:] = []
353 if self.report.ran == 0:
354 print "no test dir found testing here:", here
355
356
357
358 self.testonedir(here)
359
361 """finds each testfile in the `testdir` and runs it"""
362 for filename in abspath_listdir(testdir):
363 if this_is_a_testfile(filename):
364 if self.options.exitfirst and not self.options.restart:
365
366 try:
367 restartfile = open(testlib.FILE_RESTART, "w")
368 restartfile.close()
369 except Exception, e:
370 print >> sys.__stderr__, "Error while overwriting \
371 succeeded test file :", osp.join(os.getcwd(),testlib.FILE_RESTART)
372 raise e
373
374 prog = self.testfile(filename, batchmode=True)
375 if exitfirst and (prog is None or not prog.result.wasSuccessful()):
376 break
377 self.firstwrite = True
378
379 remove_local_modules_from_sys(testdir)
380
381
382 - def testfile(self, filename, batchmode=False):
383 """runs every test in `filename`
384
385 :param filename: an absolute path pointing to a unittest file
386 """
387 here = os.getcwd()
388 dirname = osp.dirname(filename)
389 if dirname:
390 os.chdir(dirname)
391
392 if self.options.exitfirst and not self.options.restart and self.firstwrite:
393 try:
394 restartfile = open(testlib.FILE_RESTART, "w")
395 restartfile.close()
396 except Exception, e:
397 print >> sys.__stderr__, "Error while overwriting \
398 succeeded test file :", osp.join(os.getcwd(),testlib.FILE_RESTART)
399 raise e
400 modname = osp.basename(filename)[:-3]
401 if batchmode:
402 from cStringIO import StringIO
403 outstream = StringIO()
404 else:
405 outstream = sys.stderr
406 try:
407 print >> outstream, (' %s ' % osp.basename(filename)).center(70, '=')
408 except TypeError:
409 print >> outstream, (' %s ' % osp.basename(filename)).center(70)
410 try:
411 try:
412 tstart, cstart = time(), clock()
413 testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg,
414 options=self.options, outstream=outstream)
415 tend, cend = time(), clock()
416 ttime, ctime = (tend - tstart), (cend - cstart)
417 if testprog.result.testsRun and batchmode:
418 print >> sys.stderr, outstream.getvalue()
419 self.report.feed(filename, testprog.result, ttime, ctime)
420 return testprog
421 except (KeyboardInterrupt, SystemExit):
422 raise
423 except Exception:
424 self.report.failed_to_test_module(filename)
425 print 'unhandled exception occured while testing', modname
426 import traceback
427 traceback.print_exc()
428 return None
429 finally:
430 if dirname:
431 os.chdir(here)
432
433
434
436
438 """try to find project's setting and load it"""
439 curdir = osp.abspath(dirname)
440 previousdir = curdir
441 while not osp.isfile(osp.join(curdir, 'settings.py')) and \
442 osp.isfile(osp.join(curdir, '__init__.py')):
443 newdir = osp.normpath(osp.join(curdir, os.pardir))
444 if newdir == curdir:
445 raise AssertionError('could not find settings.py')
446 previousdir = curdir
447 curdir = newdir
448
449 settings = load_module_from_modpath(modpath_from_file(osp.join(curdir, 'settings.py')))
450 from django.core.management import setup_environ
451 setup_environ(settings)
452 settings.DEBUG = False
453 self.settings = settings
454
455 if curdir not in sys.path:
456 sys.path.insert(1, curdir)
457
459
460 from django.test.utils import setup_test_environment
461 from django.test.utils import create_test_db
462 setup_test_environment()
463 create_test_db(verbosity=0)
464 self.dbname = self.settings.TEST_DATABASE_NAME
465
466
468
469 from django.test.utils import teardown_test_environment
470 from django.test.utils import destroy_test_db
471 teardown_test_environment()
472 print 'destroying', self.dbname
473 destroy_test_db(self.dbname, verbosity=0)
474
475
476 - def testall(self, exitfirst=False):
477 """walks trhough current working directory, finds something
478 which can be considered as a testdir and runs every test there
479 """
480 for dirname, dirs, _ in os.walk(os.getcwd()):
481 for skipped in ('CVS', '.svn', '.hg'):
482 if skipped in dirs:
483 dirs.remove(skipped)
484 if 'tests.py' in files:
485 self.testonedir(dirname, exitfirst)
486 dirs[:] = []
487 else:
488 basename = osp.basename(dirname)
489 if basename in ('test', 'tests'):
490 print "going into", dirname
491
492 self.testonedir(dirname, exitfirst)
493 dirs[:] = []
494
495
497 """finds each testfile in the `testdir` and runs it"""
498
499
500 testfiles = [fpath for fpath in abspath_listdir(testdir)
501 if this_is_a_testfile(fpath)]
502 if len(testfiles) > 1:
503 try:
504 testfiles.remove(osp.join(testdir, 'tests.py'))
505 except ValueError:
506 pass
507 for filename in testfiles:
508
509 prog = self.testfile(filename, batchmode=True)
510 if exitfirst and (prog is None or not prog.result.wasSuccessful()):
511 break
512
513 remove_local_modules_from_sys(testdir)
514
515
516 - def testfile(self, filename, batchmode=False):
517 """runs every test in `filename`
518
519 :param filename: an absolute path pointing to a unittest file
520 """
521 here = os.getcwd()
522 dirname = osp.dirname(filename)
523 if dirname:
524 os.chdir(dirname)
525 self.load_django_settings(dirname)
526 modname = osp.basename(filename)[:-3]
527 print >>sys.stderr, (' %s ' % osp.basename(filename)).center(70, '=')
528 try:
529 try:
530 tstart, cstart = time(), clock()
531 self.before_testfile()
532 testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg)
533 tend, cend = time(), clock()
534 ttime, ctime = (tend - tstart), (cend - cstart)
535 self.report.feed(filename, testprog.result, ttime, ctime)
536 return testprog
537 except SystemExit:
538 raise
539 except Exception, exc:
540 import traceback
541 traceback.print_exc()
542 self.report.failed_to_test_module(filename)
543 print 'unhandled exception occured while testing', modname
544 print 'error: %s' % exc
545 return None
546 finally:
547 self.after_testfile()
548 if dirname:
549 os.chdir(here)
550
551
553 """creates the OptionParser instance
554 """
555 from optparse import OptionParser
556 parser = OptionParser(usage=PYTEST_DOC)
557
558 parser.newargs = []
559 def rebuild_cmdline(option, opt, value, parser):
560 """carry the option to unittest_main"""
561 parser.newargs.append(opt)
562
563
564 def rebuild_and_store(option, opt, value, parser):
565 """carry the option to unittest_main and store
566 the value on current parser
567 """
568 parser.newargs.append(opt)
569 setattr(parser.values, option.dest, True)
570
571
572 parser.add_option('-t', dest='testdir', default=None,
573 help="directory where the tests will be found")
574 parser.add_option('-d', dest='dbc', default=False,
575 action="store_true", help="enable design-by-contract")
576
577 parser.add_option('-v', '--verbose', callback=rebuild_cmdline,
578 action="callback", help="Verbose output")
579 parser.add_option('-i', '--pdb', callback=rebuild_and_store,
580 dest="pdb", action="callback",
581 help="Enable test failure inspection (conflicts with --coverage)")
582 parser.add_option('-x', '--exitfirst', callback=rebuild_and_store,
583 dest="exitfirst", default=False,
584 action="callback", help="Exit on first failure "
585 "(only make sense when pytest run one test file)")
586 parser.add_option('-R', '--restart', callback=rebuild_and_store,
587 dest="restart", default=False,
588 action="callback",
589 help="Restart tests from where it failed (implies exitfirst) "
590 "(only make sense if tests previously ran with exitfirst only)")
591 parser.add_option('-c', '--capture', callback=rebuild_cmdline,
592 action="callback",
593 help="Captures and prints standard out/err only on errors "
594 "(only make sense when pytest run one test file)")
595 parser.add_option('-p', '--printonly',
596
597
598
599 action="store", dest="printonly", default=None,
600 help="Only prints lines matching specified pattern (implies capture) "
601 "(only make sense when pytest run one test file)")
602 parser.add_option('-s', '--skip',
603
604
605
606 action="store", dest="skipped", default=None,
607 help="test names matching this name will be skipped "
608 "to skip several patterns, use commas")
609 parser.add_option('-q', '--quiet', callback=rebuild_cmdline,
610 action="callback", help="Minimal output")
611 parser.add_option('-P', '--profile', default=None, dest='profile',
612 help="Profile execution and store data in the given file")
613 parser.add_option('-m', '--match', default=None, dest='tags_pattern',
614 help="only execute test whose tag macht the current pattern")
615
616 try:
617 from logilab.devtools.lib.coverage import Coverage
618 parser.add_option('--coverage', dest="coverage", default=False,
619 action="store_true",
620 help="run tests with pycoverage (conflicts with --pdb)")
621 except ImportError:
622 pass
623
624 if DJANGO_FOUND:
625 parser.add_option('-J', '--django', dest='django', default=False,
626 action="store_true",
627 help='use pytest for django test cases')
628 return parser
629
630
632 """Parse the command line and return (options processed), (options to pass to
633 unittest_main()), (explicitfile or None).
634 """
635
636 options, args = parser.parse_args()
637 if options.pdb and getattr(options, 'coverage', False):
638 parser.error("'pdb' and 'coverage' options are exclusive")
639 filenames = [arg for arg in args if arg.endswith('.py')]
640 if filenames:
641 if len(filenames) > 1:
642 parser.error("only one filename is acceptable")
643 explicitfile = filenames[0]
644 args.remove(explicitfile)
645 else:
646 explicitfile = None
647
648 testlib.ENABLE_DBC = options.dbc
649 newargs = parser.newargs
650 if options.printonly:
651 newargs.extend(['--printonly', options.printonly])
652 if options.skipped:
653 newargs.extend(['--skip', options.skipped])
654
655 if options.restart:
656 options.exitfirst = True
657
658
659 newargs += args
660 return options, explicitfile
661
662
663
665 parser = make_parser()
666 rootdir, testercls = project_root(parser)
667 options, explicitfile = parseargs(parser)
668
669 sys.argv[1:] = parser.newargs
670 covermode = getattr(options, 'coverage', None)
671 cvg = None
672 if not '' in sys.path:
673 sys.path.insert(0, '')
674 if covermode:
675
676 from logilab.devtools.lib.coverage import Coverage
677 cvg = Coverage([rootdir])
678 cvg.erase()
679 cvg.start()
680 if DJANGO_FOUND and options.django:
681 tester = DjangoTester(cvg, options)
682 else:
683 tester = testercls(cvg, options)
684 if explicitfile:
685 cmd, args = tester.testfile, (explicitfile,)
686 elif options.testdir:
687 cmd, args = tester.testonedir, (options.testdir, options.exitfirst)
688 else:
689 cmd, args = tester.testall, (options.exitfirst,)
690 try:
691 try:
692 if options.profile:
693 import hotshot
694 prof = hotshot.Profile(options.profile)
695 prof.runcall(cmd, *args)
696 prof.close()
697 print 'profile data saved in', options.profile
698 else:
699 cmd(*args)
700 except SystemExit:
701 raise
702 except:
703 import traceback
704 traceback.print_exc()
705 finally:
706 errcode = tester.show_report()
707 if covermode:
708 cvg.stop()
709 cvg.save()
710 here = osp.abspath(os.getcwd())
711 if this_is_a_testdir(here):
712 morfdir = osp.normpath(osp.join(here, '..'))
713 else:
714 morfdir = here
715 print "computing code coverage (%s), this might take some time" % \
716 morfdir
717 cvg.annotate([morfdir])
718 cvg.report([morfdir], False)
719 sys.exit(errcode)
720