| 1 | """%prog [OPTIONS] [testfile [testpattern]] |
|---|
| 2 | |
|---|
| 3 | examples: |
|---|
| 4 | |
|---|
| 5 | pytest path/to/mytests.py |
|---|
| 6 | pytest path/to/mytests.py TheseTests |
|---|
| 7 | pytest path/to/mytests.py TheseTests.test_thisone |
|---|
| 8 | |
|---|
| 9 | pytest one (will run both test_thisone and test_thatone) |
|---|
| 10 | pytest path/to/mytests.py -s not (will skip test_notthisone) |
|---|
| 11 | |
|---|
| 12 | pytest --coverage test_foo.py |
|---|
| 13 | (only if logilab.devtools is available) |
|---|
| 14 | """ |
|---|
| 15 | |
|---|
| 16 | import os, sys |
|---|
| 17 | import os.path as osp |
|---|
| 18 | from time import time, clock |
|---|
| 19 | |
|---|
| 20 | from logilab.common.fileutils import abspath_listdir |
|---|
| 21 | from logilab.common import testlib |
|---|
| 22 | import doctest |
|---|
| 23 | import unittest |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | import imp |
|---|
| 27 | |
|---|
| 28 | import __builtin__ |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | try: |
|---|
| 32 | import django |
|---|
| 33 | from logilab.common.modutils import modpath_from_file, load_module_from_modpath |
|---|
| 34 | DJANGO_FOUND = True |
|---|
| 35 | except ImportError: |
|---|
| 36 | DJANGO_FOUND = False |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | ## coverage hacks, do not read this, do not read this, do not read this |
|---|
| 40 | |
|---|
| 41 | # hey, but this is an aspect, right ?!!! |
|---|
| 42 | class TraceController(object): |
|---|
| 43 | nesting = 0 |
|---|
| 44 | |
|---|
| 45 | def pause_tracing(cls): |
|---|
| 46 | if not cls.nesting: |
|---|
| 47 | cls.tracefunc = getattr(sys, '__settrace__', sys.settrace) |
|---|
| 48 | cls.oldtracer = getattr(sys, '__tracer__', None) |
|---|
| 49 | sys.__notrace__ = True |
|---|
| 50 | cls.tracefunc(None) |
|---|
| 51 | cls.nesting += 1 |
|---|
| 52 | pause_tracing = classmethod(pause_tracing) |
|---|
| 53 | |
|---|
| 54 | def resume_tracing(cls): |
|---|
| 55 | cls.nesting -= 1 |
|---|
| 56 | assert cls.nesting >= 0 |
|---|
| 57 | if not cls.nesting: |
|---|
| 58 | cls.tracefunc(cls.oldtracer) |
|---|
| 59 | delattr(sys, '__notrace__') |
|---|
| 60 | resume_tracing = classmethod(resume_tracing) |
|---|
| 61 | |
|---|
| 62 | |
|---|
| 63 | pause_tracing = TraceController.pause_tracing |
|---|
| 64 | resume_tracing = TraceController.resume_tracing |
|---|
| 65 | |
|---|
| 66 | |
|---|
| 67 | def nocoverage(func): |
|---|
| 68 | if hasattr(func, 'uncovered'): |
|---|
| 69 | return func |
|---|
| 70 | func.uncovered = True |
|---|
| 71 | def not_covered(*args, **kwargs): |
|---|
| 72 | pause_tracing() |
|---|
| 73 | try: |
|---|
| 74 | return func(*args, **kwargs) |
|---|
| 75 | finally: |
|---|
| 76 | resume_tracing() |
|---|
| 77 | not_covered.uncovered = True |
|---|
| 78 | return not_covered |
|---|
| 79 | |
|---|
| 80 | |
|---|
| 81 | ## end of coverage hacks |
|---|
| 82 | |
|---|
| 83 | |
|---|
| 84 | # monkeypatch unittest and doctest (ouch !) |
|---|
| 85 | unittest.TestCase = testlib.TestCase |
|---|
| 86 | unittest.main = testlib.unittest_main |
|---|
| 87 | unittest._TextTestResult = testlib.SkipAwareTestResult |
|---|
| 88 | unittest.TextTestRunner = testlib.SkipAwareTextTestRunner |
|---|
| 89 | unittest.TestLoader = testlib.NonStrictTestLoader |
|---|
| 90 | unittest.TestProgram = testlib.SkipAwareTestProgram |
|---|
| 91 | if sys.version_info >= (2, 4): |
|---|
| 92 | doctest.DocTestCase.__bases__ = (testlib.TestCase,) |
|---|
| 93 | else: |
|---|
| 94 | unittest.FunctionTestCase.__bases__ = (testlib.TestCase,) |
|---|
| 95 | |
|---|
| 96 | |
|---|
| 97 | |
|---|
| 98 | def this_is_a_testfile(filename): |
|---|
| 99 | """returns True if `filename` seems to be a test file""" |
|---|
| 100 | filename = osp.basename(filename) |
|---|
| 101 | return ((filename.startswith('unittest') |
|---|
| 102 | or filename.startswith('test') |
|---|
| 103 | or filename.startswith('smoketest')) |
|---|
| 104 | and filename.endswith('.py')) |
|---|
| 105 | |
|---|
| 106 | |
|---|
| 107 | def this_is_a_testdir(dirpath): |
|---|
| 108 | """returns True if `filename` seems to be a test directory""" |
|---|
| 109 | return osp.basename(dirpath) in ('test', 'tests', 'unittests') |
|---|
| 110 | |
|---|
| 111 | |
|---|
| 112 | def project_root(projdir=os.getcwd()): |
|---|
| 113 | """try to find project's root and add it to sys.path""" |
|---|
| 114 | curdir = osp.abspath(projdir) |
|---|
| 115 | previousdir = curdir |
|---|
| 116 | while this_is_a_testdir(curdir) or \ |
|---|
| 117 | osp.isfile(osp.join(curdir, '__init__.py')): |
|---|
| 118 | newdir = osp.normpath(osp.join(curdir, os.pardir)) |
|---|
| 119 | if newdir == curdir: |
|---|
| 120 | break |
|---|
| 121 | previousdir = curdir |
|---|
| 122 | curdir = newdir |
|---|
| 123 | return previousdir |
|---|
| 124 | |
|---|
| 125 | |
|---|
| 126 | class GlobalTestReport(object): |
|---|
| 127 | """this class holds global test statistics""" |
|---|
| 128 | def __init__(self): |
|---|
| 129 | self.ran = 0 |
|---|
| 130 | self.skipped = 0 |
|---|
| 131 | self.failures = 0 |
|---|
| 132 | self.errors = 0 |
|---|
| 133 | self.ttime = 0 |
|---|
| 134 | self.ctime = 0 |
|---|
| 135 | self.modulescount = 0 |
|---|
| 136 | self.errmodules = [] |
|---|
| 137 | |
|---|
| 138 | def feed(self, filename, testresult, ttime, ctime): |
|---|
| 139 | """integrates new test information into internal statistics""" |
|---|
| 140 | ran = testresult.testsRun |
|---|
| 141 | self.ran += ran |
|---|
| 142 | self.skipped += len(getattr(testresult, 'skipped', ())) |
|---|
| 143 | self.failures += len(testresult.failures) |
|---|
| 144 | self.errors += len(testresult.errors) |
|---|
| 145 | self.ttime += ttime |
|---|
| 146 | self.ctime += ctime |
|---|
| 147 | self.modulescount += 1 |
|---|
| 148 | if not testresult.wasSuccessful(): |
|---|
| 149 | problems = len(testresult.failures) + len(testresult.errors) |
|---|
| 150 | self.errmodules.append((filename[:-3], problems, ran)) |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | def failed_to_test_module(self, filename): |
|---|
| 154 | """called when the test module could not be imported by unittest |
|---|
| 155 | """ |
|---|
| 156 | self.errors += 1 |
|---|
| 157 | self.errmodules.append((filename[:-3], 1, 1)) |
|---|
| 158 | |
|---|
| 159 | |
|---|
| 160 | def __str__(self): |
|---|
| 161 | """this is just presentation stuff""" |
|---|
| 162 | line1 = ['Ran %s test cases in %.2fs (%.2fs CPU)' |
|---|
| 163 | % (self.ran, self.ttime, self.ctime)] |
|---|
| 164 | if self.errors: |
|---|
| 165 | line1.append('%s errors' % self.errors) |
|---|
| 166 | if self.failures: |
|---|
| 167 | line1.append('%s failures' % self.failures) |
|---|
| 168 | if self.skipped: |
|---|
| 169 | line1.append('%s skipped' % self.skipped) |
|---|
| 170 | modulesok = self.modulescount - len(self.errmodules) |
|---|
| 171 | if self.errors or self.failures: |
|---|
| 172 | line2 = '%s modules OK (%s failed)' % (modulesok, |
|---|
| 173 | len(self.errmodules)) |
|---|
| 174 | descr = ', '.join(['%s [%s/%s]' % info for info in self.errmodules]) |
|---|
| 175 | line3 = '\nfailures: %s' % descr |
|---|
| 176 | else: |
|---|
| 177 | line2 = 'All %s modules OK' % modulesok |
|---|
| 178 | line3 = '' |
|---|
| 179 | return '%s\n%s%s' % (', '.join(line1), line2, line3) |
|---|
| 180 | |
|---|
| 181 | |
|---|
| 182 | |
|---|
| 183 | def remove_local_modules_from_sys(testdir): |
|---|
| 184 | """remove all modules from cache that come from `testdir` |
|---|
| 185 | |
|---|
| 186 | This is used to avoid strange side-effects when using the |
|---|
| 187 | testall() mode of pytest. |
|---|
| 188 | For instance, if we run pytest on this tree:: |
|---|
| 189 | |
|---|
| 190 | A/test/test_utils.py |
|---|
| 191 | B/test/test_utils.py |
|---|
| 192 | |
|---|
| 193 | we **have** to clean sys.modules to make sure the correct test_utils |
|---|
| 194 | module is ran in B |
|---|
| 195 | """ |
|---|
| 196 | for modname, mod in sys.modules.items(): |
|---|
| 197 | if mod is None: |
|---|
| 198 | continue |
|---|
| 199 | if not hasattr(mod, '__file__'): |
|---|
| 200 | # this is the case of some built-in modules like sys, imp, marshal |
|---|
| 201 | continue |
|---|
| 202 | modfile = mod.__file__ |
|---|
| 203 | # if modfile is not an asbolute path, it was probably loaded locally |
|---|
| 204 | # during the tests |
|---|
| 205 | if not osp.isabs(modfile) or modfile.startswith(testdir): |
|---|
| 206 | del sys.modules[modname] |
|---|
| 207 | |
|---|
| 208 | |
|---|
| 209 | |
|---|
| 210 | class PyTester(object): |
|---|
| 211 | """encaspulates testrun logic""" |
|---|
| 212 | |
|---|
| 213 | def __init__(self, cvg): |
|---|
| 214 | self.tested_files = [] |
|---|
| 215 | self.report = GlobalTestReport() |
|---|
| 216 | self.cvg = cvg |
|---|
| 217 | |
|---|
| 218 | |
|---|
| 219 | def show_report(self): |
|---|
| 220 | """prints the report and returns appropriate exitcode""" |
|---|
| 221 | # everything has been ran, print report |
|---|
| 222 | print "*" * 79 |
|---|
| 223 | print self.report |
|---|
| 224 | return self.report.failures + self.report.errors |
|---|
| 225 | |
|---|
| 226 | |
|---|
| 227 | def testall(self, exitfirst=False): |
|---|
| 228 | """walks trhough current working directory, finds something |
|---|
| 229 | which can be considered as a testdir and runs every test there |
|---|
| 230 | """ |
|---|
| 231 | for dirname, dirs, files in os.walk(os.getcwd()): |
|---|
| 232 | for skipped in ('CVS', '.svn', '.hg'): |
|---|
| 233 | if skipped in dirs: |
|---|
| 234 | dirs.remove(skipped) |
|---|
| 235 | basename = osp.basename(dirname) |
|---|
| 236 | if basename in ('test', 'tests'): |
|---|
| 237 | print "going into", dirname |
|---|
| 238 | # we found a testdir, let's explore it ! |
|---|
| 239 | self.testonedir(dirname, exitfirst) |
|---|
| 240 | dirs[:] = [] |
|---|
| 241 | |
|---|
| 242 | |
|---|
| 243 | def testonedir(self, testdir, exitfirst=False): |
|---|
| 244 | """finds each testfile in the `testdir` and runs it""" |
|---|
| 245 | for filename in abspath_listdir(testdir): |
|---|
| 246 | if this_is_a_testfile(filename): |
|---|
| 247 | # run test and collect information |
|---|
| 248 | prog = self.testfile(filename, batchmode=True) |
|---|
| 249 | if exitfirst and (prog is None or not prog.result.wasSuccessful()): |
|---|
| 250 | break |
|---|
| 251 | # clean local modules |
|---|
| 252 | remove_local_modules_from_sys(testdir) |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | def testfile(self, filename, batchmode=False): |
|---|
| 256 | """runs every test in `filename` |
|---|
| 257 | |
|---|
| 258 | :param filename: an absolute path pointing to a unittest file |
|---|
| 259 | """ |
|---|
| 260 | here = os.getcwd() |
|---|
| 261 | dirname = osp.dirname(filename) |
|---|
| 262 | if dirname: |
|---|
| 263 | os.chdir(dirname) |
|---|
| 264 | modname = osp.basename(filename)[:-3] |
|---|
| 265 | try: |
|---|
| 266 | print >>sys.stderr, (' %s ' % osp.basename(filename)).center(70, '=') |
|---|
| 267 | except TypeError: # < py 2.4 bw compat |
|---|
| 268 | print >>sys.stderr, (' %s ' % osp.basename(filename)).center(70) |
|---|
| 269 | try: |
|---|
| 270 | try: |
|---|
| 271 | tstart, cstart = time(), clock() |
|---|
| 272 | testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg) |
|---|
| 273 | tend, cend = time(), clock() |
|---|
| 274 | ttime, ctime = (tend - tstart), (cend - cstart) |
|---|
| 275 | self.report.feed(filename, testprog.result, ttime, ctime) |
|---|
| 276 | return testprog |
|---|
| 277 | except (KeyboardInterrupt, SystemExit): |
|---|
| 278 | raise |
|---|
| 279 | except Exception, exc: |
|---|
| 280 | self.report.failed_to_test_module(filename) |
|---|
| 281 | print 'unhandled exception occured while testing', modname |
|---|
| 282 | import traceback |
|---|
| 283 | traceback.print_exc() |
|---|
| 284 | return None |
|---|
| 285 | finally: |
|---|
| 286 | if dirname: |
|---|
| 287 | os.chdir(here) |
|---|
| 288 | |
|---|
| 289 | |
|---|
| 290 | |
|---|
| 291 | class DjangoTester(PyTester): |
|---|
| 292 | |
|---|
| 293 | def __init__(self, cvg): |
|---|
| 294 | super(DjangoTester, self).__init__(cvg) |
|---|
| 295 | |
|---|
| 296 | |
|---|
| 297 | def load_django_settings(self, dirname): |
|---|
| 298 | """try to find project's setting and load it""" |
|---|
| 299 | curdir = osp.abspath(dirname) |
|---|
| 300 | previousdir = curdir |
|---|
| 301 | while not osp.isfile(osp.join(curdir, 'settings.py')) and \ |
|---|
| 302 | osp.isfile(osp.join(curdir, '__init__.py')): |
|---|
| 303 | newdir = osp.normpath(osp.join(curdir, os.pardir)) |
|---|
| 304 | if newdir == curdir: |
|---|
| 305 | raise AssertionError('could not find settings.py') |
|---|
| 306 | previousdir = curdir |
|---|
| 307 | curdir = newdir |
|---|
| 308 | # late django initialization |
|---|
| 309 | settings = load_module_from_modpath(modpath_from_file(osp.join(curdir, 'settings.py'))) |
|---|
| 310 | from django.core.management import setup_environ |
|---|
| 311 | setup_environ(settings) |
|---|
| 312 | settings.DEBUG = False |
|---|
| 313 | self.settings = settings |
|---|
| 314 | # add settings dir to pythonpath since it's the project's root |
|---|
| 315 | if curdir not in sys.path: |
|---|
| 316 | sys.path.insert(1, curdir) |
|---|
| 317 | |
|---|
| 318 | def before_testfile(self): |
|---|
| 319 | # Those imports must be done **after** setup_environ was called |
|---|
| 320 | from django.test.utils import setup_test_environment |
|---|
| 321 | from django.test.utils import create_test_db |
|---|
| 322 | setup_test_environment() |
|---|
| 323 | create_test_db(verbosity=0) |
|---|
| 324 | self.dbname = self.settings.TEST_DATABASE_NAME |
|---|
| 325 | |
|---|
| 326 | |
|---|
| 327 | def after_testfile(self): |
|---|
| 328 | # Those imports must be done **after** setup_environ was called |
|---|
| 329 | from django.test.utils import teardown_test_environment |
|---|
| 330 | from django.test.utils import destroy_test_db |
|---|
| 331 | teardown_test_environment() |
|---|
| 332 | print 'destroying', self.dbname |
|---|
| 333 | destroy_test_db(self.dbname, verbosity=0) |
|---|
| 334 | |
|---|
| 335 | |
|---|
| 336 | def testall(self, exitfirst=False): |
|---|
| 337 | """walks trhough current working directory, finds something |
|---|
| 338 | which can be considered as a testdir and runs every test there |
|---|
| 339 | """ |
|---|
| 340 | for dirname, dirs, files in os.walk(os.getcwd()): |
|---|
| 341 | for skipped in ('CVS', '.svn', '.hg'): |
|---|
| 342 | if skipped in dirs: |
|---|
| 343 | dirs.remove(skipped) |
|---|
| 344 | if 'tests.py' in files: |
|---|
| 345 | self.testonedir(dirname, exitfirst) |
|---|
| 346 | dirs[:] = [] |
|---|
| 347 | else: |
|---|
| 348 | basename = osp.basename(dirname) |
|---|
| 349 | if basename in ('test', 'tests'): |
|---|
| 350 | print "going into", dirname |
|---|
| 351 | # we found a testdir, let's explore it ! |
|---|
| 352 | self.testonedir(dirname, exitfirst) |
|---|
| 353 | dirs[:] = [] |
|---|
| 354 | |
|---|
| 355 | |
|---|
| 356 | def testonedir(self, testdir, exitfirst=False): |
|---|
| 357 | """finds each testfile in the `testdir` and runs it""" |
|---|
| 358 | # special django behaviour : if tests are splited in several files, |
|---|
| 359 | # remove the main tests.py file and tests each test file separately |
|---|
| 360 | testfiles = [fpath for fpath in abspath_listdir(testdir) |
|---|
| 361 | if this_is_a_testfile(fpath)] |
|---|
| 362 | if len(testfiles) > 1: |
|---|
| 363 | try: |
|---|
| 364 | testfiles.remove(osp.join(testdir, 'tests.py')) |
|---|
| 365 | except ValueError: |
|---|
| 366 | pass |
|---|
| 367 | for filename in testfiles: |
|---|
| 368 | # run test and collect information |
|---|
| 369 | prog = self.testfile(filename, batchmode=True) |
|---|
| 370 | if exitfirst and (prog is None or not prog.result.wasSuccessful()): |
|---|
| 371 | break |
|---|
| 372 | # clean local modules |
|---|
| 373 | remove_local_modules_from_sys(testdir) |
|---|
| 374 | |
|---|
| 375 | |
|---|
| 376 | def testfile(self, filename, batchmode=False): |
|---|
| 377 | """runs every test in `filename` |
|---|
| 378 | |
|---|
| 379 | :param filename: an absolute path pointing to a unittest file |
|---|
| 380 | """ |
|---|
| 381 | here = os.getcwd() |
|---|
| 382 | dirname = osp.dirname(filename) |
|---|
| 383 | if dirname: |
|---|
| 384 | os.chdir(dirname) |
|---|
| 385 | self.load_django_settings(dirname) |
|---|
| 386 | modname = osp.basename(filename)[:-3] |
|---|
| 387 | print >>sys.stderr, (' %s ' % osp.basename(filename)).center(70, '=') |
|---|
| 388 | try: |
|---|
| 389 | try: |
|---|
| 390 | tstart, cstart = time(), clock() |
|---|
| 391 | self.before_testfile() |
|---|
| 392 | testprog = testlib.unittest_main(modname, batchmode=batchmode, cvg=self.cvg) |
|---|
| 393 | tend, cend = time(), clock() |
|---|
| 394 | ttime, ctime = (tend - tstart), (cend - cstart) |
|---|
| 395 | self.report.feed(filename, testprog.result, ttime, ctime) |
|---|
| 396 | return testprog |
|---|
| 397 | except SystemExit: |
|---|
| 398 | raise |
|---|
| 399 | except Exception, exc: |
|---|
| 400 | import traceback |
|---|
| 401 | traceback.print_exc() |
|---|
| 402 | self.report.failed_to_test_module(filename) |
|---|
| 403 | print 'unhandled exception occured while testing', modname |
|---|
| 404 | print 'error: %s' % exc |
|---|
| 405 | return None |
|---|
| 406 | finally: |
|---|
| 407 | self.after_testfile() |
|---|
| 408 | if dirname: |
|---|
| 409 | os.chdir(here |
|---|