root / logilab.pylintinstaller / logilab / common / pytest.py

Revision 202:d67e86292521, 19.9 kB (checked in by tziade@…, 9 months ago)

added logilab.pylintinstaller

Line 
1"""%prog [OPTIONS] [testfile [testpattern]]
2
3examples:
4
5pytest path/to/mytests.py
6pytest path/to/mytests.py TheseTests
7pytest path/to/mytests.py TheseTests.test_thisone
8
9pytest one (will run both test_thisone and test_thatone)
10pytest path/to/mytests.py -s not (will skip test_notthisone)
11
12pytest --coverage test_foo.py
13  (only if logilab.devtools is available)
14"""
15
16import os, sys
17import os.path as osp
18from time import time, clock
19
20from logilab.common.fileutils import abspath_listdir
21from logilab.common import testlib
22import doctest
23import unittest
24
25
26import imp
27
28import __builtin__
29
30
31try:
32    import django
33    from logilab.common.modutils import modpath_from_file, load_module_from_modpath
34    DJANGO_FOUND = True
35except 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 ?!!!
42class 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
63pause_tracing = TraceController.pause_tracing
64resume_tracing = TraceController.resume_tracing
65
66
67def 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 !)
85unittest.TestCase = testlib.TestCase
86unittest.main = testlib.unittest_main
87unittest._TextTestResult = testlib.SkipAwareTestResult
88unittest.TextTestRunner = testlib.SkipAwareTextTestRunner
89unittest.TestLoader = testlib.NonStrictTestLoader
90unittest.TestProgram = testlib.SkipAwareTestProgram
91if sys.version_info >= (2, 4):
92    doctest.DocTestCase.__bases__ = (testlib.TestCase,)
93else:
94    unittest.FunctionTestCase.__bases__ = (testlib.TestCase,)
95
96
97
98def 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
107def 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
112def 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
126class 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
183def 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
210class 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
291class 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