Navigation

Source code for logilab.common.testlib

# -*- coding: utf-8 -*-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of logilab-common.## logilab-common is free software: you can redistribute it and/or modify it under# the terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option) any# later version.## logilab-common is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with logilab-common. If not, see <http://www.gnu.org/licenses/>."""Run tests.This will find all modules whose name match a given prefix in the testdirectory, and run them. Various command line options provideadditional facilities.Command line options: -v verbose -- run tests in verbose mode with output to stdout -q quiet -- don't print anything except if a test fails -t testdir -- directory where the tests will be found -x exclude -- add a test to exclude -p profile -- profiled execution -d dbc -- enable design-by-contract -m match -- only run test matching the tag pattern which followIf no non-option arguments are present, prefixes used are 'test','regrtest', 'smoketest' and 'unittest'."""from__future__importprint_function__docformat__="restructuredtext en"# modified copy of some functions from test/regrtest.py from PyXml# disable camel case warning# pylint: disable=C0103fromcontextlibimportcontextmanagerimportsysimportos,os.pathasospimportreimportdifflibimporttempfileimportmathimportwarningsfromshutilimportrmtreefromoperatorimportitemgetterfrominspectimportisgeneratorfunctionfromsiximportPY2,add_metaclass,string_typesfromsix.movesimportbuiltins,range,configparser,inputfromlogilab.common.deprecationimportclass_deprecated,deprecatedimportunittestasunittest_legacyifnotgetattr(unittest_legacy,"__package__",None):try:importunittest2asunittestfromunittest2importSkipTestexceptImportError:raiseImportError("You have to install python-unittest2 to use %s"%__name__)else:importunittestasunittestfromunittestimportSkipTestfromfunctoolsimportwrapsfromlogilab.common.debuggerimportDebuggerfromlogilab.common.decoratorsimportcached,classpropertyfromlogilab.commonimporttextutils__all__=['unittest_main','find_tests','nocoverage','pause_trace']DEFAULT_PREFIXES=('test','regrtest','smoketest','unittest','func','validation')is_generator=deprecated('[lgc 0.63] use inspect.isgeneratorfunction')(isgeneratorfunction)# used by unittest to count the number of relevant levels in the traceback__unittest=1@deprecated('with_tempdir is deprecated, use {0}.TemporaryDirectory.'.format('tempfile'ifnotPY2else'backports.tempfile'))defwith_tempdir(callable):"""A decorator ensuring no temporary file left when the function return Work only for temporary file created with the tempfile module"""ifisgeneratorfunction(callable):defproxy(*args,**kwargs):old_tmpdir=tempfile.gettempdir()new_tmpdir=tempfile.mkdtemp(prefix="temp-lgc-")tempfile.tempdir=new_tmpdirtry:forxincallable(*args,**kwargs):yieldxfinally:try:rmtree(new_tmpdir,ignore_errors=True)finally:tempfile.tempdir=old_tmpdirreturnproxy@wraps(callable)defproxy(*args,**kargs):old_tmpdir=tempfile.gettempdir()new_tmpdir=tempfile.mkdtemp(prefix="temp-lgc-")tempfile.tempdir=new_tmpdirtry:returncallable(*args,**kargs)finally:try:rmtree(new_tmpdir,ignore_errors=True)finally:tempfile.tempdir=old_tmpdirreturnproxydefin_tempdir(callable):"""A decorator moving the enclosed function inside the tempfile.tempfdir """@wraps(callable)defproxy(*args,**kargs):old_cwd=os.getcwd()os.chdir(tempfile.tempdir)try:returncallable(*args,**kargs)finally:os.chdir(old_cwd)returnproxydefwithin_tempdir(callable):"""A decorator run the enclosed function inside a tmpdir removed after execution """proxy=with_tempdir(in_tempdir(callable))proxy.__name__=callable.__name__returnproxydeffind_tests(testdir,prefixes=DEFAULT_PREFIXES,suffix=".py",excludes=(),remove_suffix=True):""" Return a list of all applicable test modules. """tests=[]fornameinos.listdir(testdir):ifnotsuffixorname.endswith(suffix):forprefixinprefixes:ifname.startswith(prefix):ifremove_suffixandname.endswith(suffix):name=name[:-len(suffix)]ifnamenotinexcludes:tests.append(name)tests.sort()returntests## PostMortem Debug facilities #####defstart_interactive_mode(result):"""starts an interactive shell so that the user can inspect errors """debuggers=result.debuggersdescrs=result.error_descrs+result.fail_descrsiflen(debuggers)==1:# don't ask for test name if there's only one failuredebuggers[0].start()else:whileTrue:testindex=0print("Choose a test to debug:")# order debuggers in the same way than errors were printedprint("\n".join(['\t%s : %s'%(i,descr)fori,(_,descr)inenumerate(descrs)]))print("Type 'exit' (or ^D) to quit")print()try:todebug=input('Enter a test name: ')iftodebug.strip().lower()=='exit':print()breakelse:try:testindex=int(todebug)debugger=debuggers[descrs[testindex][0]]except(ValueError,IndexError):print("ERROR: invalid test number %r"%(todebug,))else:debugger.start()except(EOFError,KeyboardInterrupt):print()break# coverage pausing tools #####################################################@contextmanagerdefreplace_trace(trace=None):"""A context manager that temporary replaces the trace function"""oldtrace=sys.gettrace()sys.settrace(trace)try:yieldfinally:# specific hack to work around a bug in pycoverage, see# https://bitbucket.org/ned/coveragepy/issue/123if(oldtraceisnotNoneandnotcallable(oldtrace)andhasattr(oldtrace,'pytrace')):oldtrace=oldtrace.pytracesys.settrace(oldtrace)pause_trace=replace_tracedefnocoverage(func):"""Function decorator that pauses tracing functions"""ifhasattr(func,'uncovered'):returnfuncfunc.uncovered=Truedefnot_covered(*args,**kwargs):withpause_trace():returnfunc(*args,**kwargs)not_covered.uncovered=Truereturnnot_covered# test utils ################################################################### Add deprecation warnings about new api used by module level fixtures in unittest2# http://www.voidspace.org.uk/python/articles/unittest2.shtml#setupmodule-and-teardownmoduleclass_DebugResult(object):# simplify import statement among unittest flavors.."Used by the TestSuite to hold previous class when running in debug."_previousTestClass=None_moduleSetUpFailed=FalseshouldStop=False# backward compatibility: TestSuite might be imported from lgc.testlibTestSuite=unittest.TestSuiteclasskeywords(dict):"""Keyword args (**kwargs) support for generative tests."""classstarargs(tuple):"""Variable arguments (*args) for generative tests."""def__new__(cls,*args):returntuple.__new__(cls,args)unittest_main=unittest.mainclassInnerTestSkipped(SkipTest):"""raised when a test is skipped"""passdefparse_generative_args(params):args=[]varargs=()kwargs={}flags=0# 2 <=> starargs, 4 <=> kwargsforparaminparams:ifisinstance(param,starargs):varargs=paramifflags:raiseTypeError('found starargs after keywords !')flags|=2args+=list(varargs)elifisinstance(param,keywords):kwargs=paramifflags&4:raiseTypeError('got multiple keywords parameters')flags|=4elifflags&2orflags&4:raiseTypeError('found parameters after kwargs or args')else:args.append(param)returnargs,kwargsclassInnerTest(tuple):def__new__(cls,name,*data):instance=tuple.__new__(cls,data)instance.name=namereturninstanceclassTags(set):"""A set of tag able validate an expression"""def__init__(self,*tags,**kwargs):self.inherit=kwargs.pop('inherit',True)ifkwargs:raiseTypeError("%s are an invalid keyword argument for this function"%kwargs.keys())iflen(tags)==1andnotisinstance(tags[0],string_types):tags=tags[0]super(Tags,self).__init__(tags,**kwargs)def__getitem__(self,key):returnkeyinselfdefmatch(self,exp):returneval(exp,{},self)def__or__(self,other):returnTags(*super(Tags,self).__or__(other))# duplicate definition from unittest2 of the _deprecate decoratordef_deprecate(original_func):defdeprecated_func(*args,**kwargs):warnings.warn(('Please use %s instead.'%original_func.__name__),DeprecationWarning,2)returnoriginal_func(*args,**kwargs)returndeprecated_func

[docs]classTestCase(unittest.TestCase):"""A unittest.TestCase extension with some additional methods."""maxDiff=Nonetags=Tags()def__init__(self,methodName='runTest'):super(TestCase,self).__init__(methodName)self.__exc_info=sys.exc_infoself.__testMethodName=self._testMethodNameself._current_test_descr=Noneself._options_=None@classproperty@cacheddefdatadir(cls):# pylint: disable=E0213"""helper attribute holding the standard test's data directory NOTE: this is a logilab's standard """mod=sys.modules[cls.__module__]returnosp.join(osp.dirname(osp.abspath(mod.__file__)),'data')# cache it (use a class method to cache on class since TestCase is# instantiated for each test run)

[docs]@classmethoddefdatapath(cls,*fname):"""joins the object's datadir and `fname`"""returnosp.join(cls.datadir,*fname)

[docs]defset_description(self,descr):"""sets the current test's description. This can be useful for generative tests because it allows to specify a description per yield """self._current_test_descr=descr

defquiet_run(self,result,func,*args,**kwargs):try:func(*args,**kwargs)except(KeyboardInterrupt,SystemExit):raiseexceptunittest.SkipTestase:ifhasattr(result,'addSkip'):result.addSkip(self,str(e))else:warnings.warn("TestResult has no addSkip method, skips not reported",RuntimeWarning,2)result.addSuccess(self)returnFalseexcept:result.addError(self,self.__exc_info())returnFalsereturnTruedef_get_test_method(self):"""return the test method"""returngetattr(self,self._testMethodName)

[docs]defoptval(self,option,default=None):"""return the option value or default if the option is not define"""returngetattr(self._options_,option,default)

def__call__(self,result=None,runcondition=None,options=None):"""rewrite TestCase.__call__ to support generative tests This is mostly a copy/paste from unittest.py (i.e same variable names, same logic, except for the generative tests part) """ifresultisNone:result=self.defaultTestResult()self._options_=options# if result.cvg:# result.cvg.start()testMethod=self._get_test_method()if(getattr(self.__class__,"__unittest_skip__",False)orgetattr(testMethod,"__unittest_skip__",False)):# If the class or method was skipped.try:skip_why=(getattr(self.__class__,'__unittest_skip_why__','')orgetattr(testMethod,'__unittest_skip_why__',''))ifhasattr(result,'addSkip'):result.addSkip(self,skip_why)else:warnings.warn("TestResult has no addSkip method, skips not reported",RuntimeWarning,2)result.addSuccess(self)finally:result.stopTest(self)returnifrunconditionandnotruncondition(testMethod):return# test is skippedresult.startTest(self)try:ifnotself.quiet_run(result,self.setUp):returngenerative=isgeneratorfunction(testMethod)# generative testsifgenerative:self._proceed_generative(result,testMethod,runcondition)else:status=self._proceed(result,testMethod)success=(status==0)ifnotself.quiet_run(result,self.tearDown):returnifnotgenerativeandsuccess:result.addSuccess(self)finally:# if result.cvg:# result.cvg.stop()result.stopTest(self)def_proceed_generative(self,result,testfunc,runcondition=None):# cancel startTest()'s incrementresult.testsRun-=1success=Truetry:forparamsintestfunc():ifrunconditionandnotruncondition(testfunc,skipgenerator=False):ifnot(isinstance(params,InnerTest)andruncondition(params)):continueifnotisinstance(params,(tuple,list)):params=(params,)func=params[0]args,kwargs=parse_generative_args(params[1:])# increment test counter manuallyresult.testsRun+=1status=self._proceed(result,func,args,kwargs)ifstatus==0:result.addSuccess(self)success=Trueelse:success=False# XXX Don't stop anymore if an error occured#if status == 2:# result.shouldStop = Trueifresult.shouldStop:# either on error or on exitfirst + errorbreakexceptself.failureException:result.addFailure(self,self.__exc_info())success=FalseexceptSkipTestase:result.addSkip(self,e)except:# if an error occurs between two yieldresult.addError(self,self.__exc_info())success=Falsereturnsuccessdef_proceed(self,result,testfunc,args=(),kwargs=None):"""proceed the actual test returns 0 on success, 1 on failure, 2 on error Note: addSuccess can't be called here because we have to wait for tearDown to be successfully executed to declare the test as successful """kwargs=kwargsor{}try:testfunc(*args,**kwargs)exceptself.failureException:result.addFailure(self,self.__exc_info())return1exceptKeyboardInterrupt:raiseexceptInnerTestSkippedase:result.addSkip(self,e)return1exceptSkipTestase:result.addSkip(self,e)return0except:result.addError(self,self.__exc_info())return2return0

[docs]definnerSkip(self,msg=None):"""mark a generative test as skipped for the <msg> reason"""msg=msgor'test was skipped'raiseInnerTestSkipped(msg)

TestCase.assertItemsEqual=deprecated('assertItemsEqual is deprecated, use assertCountEqual')(TestCase.assertItemsEqual)importdoctestclassSkippedSuite(unittest.TestSuite):deftest(self):"""just there to trigger test execution"""self.skipped_test('doctest module has no DocTestSuite class')classDocTestFinder(doctest.DocTestFinder):def__init__(self,*args,**kwargs):self.skipped=kwargs.pop('skipped',())doctest.DocTestFinder.__init__(self,*args,**kwargs)def_get_test(self,obj,name,module,globs,source_lines):"""override default _get_test method to be able to skip tests according to skipped attribute's value """ifgetattr(obj,'__name__','')inself.skipped:returnNonereturndoctest.DocTestFinder._get_test(self,obj,name,module,globs,source_lines)@add_metaclass(class_deprecated)classDocTest(TestCase):"""trigger module doctest I don't know how to make unittest.main consider the DocTestSuite instance without this hack """__deprecation_warning__='use stdlib doctest module with unittest API directly'skipped=()def__call__(self,result=None,runcondition=None,options=None):\
# pylint: disable=W0613try:finder=DocTestFinder(skipped=self.skipped)suite=doctest.DocTestSuite(self.module,test_finder=finder)# XXX iirkdoctest.DocTestCase._TestCase__exc_info=sys.exc_infoexceptAttributeError:suite=SkippedSuite()# doctest may gork the builtins dictionnary# This happen to the "_" entry used by gettextold_builtins=builtins.__dict__.copy()try:returnsuite.run(result)finally:builtins.__dict__.clear()builtins.__dict__.update(old_builtins)run=__call__deftest(self):"""just there to trigger test execution"""classMockConnection:"""fake DB-API 2.0 connexion AND cursor (i.e. cursor() return self)"""def__init__(self,results):self.received=[]self.states=[]self.results=resultsdefcursor(self):"""Mock cursor method"""returnselfdefexecute(self,query,args=None):"""Mock execute method"""self.received.append((query,args))deffetchone(self):"""Mock fetchone method"""returnself.results[0]deffetchall(self):"""Mock fetchall method"""returnself.resultsdefcommit(self):"""Mock commiy method"""self.states.append(('commit',len(self.received)))defrollback(self):"""Mock rollback method"""self.states.append(('rollback',len(self.received)))defclose(self):"""Mock close method"""passdefmock_object(**params):"""creates an object using params to set attributes >>> option = mock_object(verbose=False, index=range(5)) >>> option.verbose False >>> option.index [0, 1, 2, 3, 4] """returntype('Mock',(),params)()defcreate_files(paths,chroot):"""Creates directories and files found in <path>. :param paths: list of relative paths to files or directories :param chroot: the root directory in which paths will be created >>> from os.path import isdir, isfile >>> isdir('/tmp/a') False >>> create_files(['a/b/foo.py', 'a/b/c/', 'a/b/c/d/e.py'], '/tmp') >>> isdir('/tmp/a') True >>> isdir('/tmp/a/b/c') True >>> isfile('/tmp/a/b/c/d/e.py') True >>> isfile('/tmp/a/b/foo.py') True """dirs,files=set(),set()forpathinpaths:path=osp.join(chroot,path)filename=osp.basename(path)# path is a directory pathiffilename=='':dirs.add(path)# path is a filename pathelse:dirs.add(osp.dirname(path))files.add(path)fordirpathindirs:ifnotosp.isdir(dirpath):os.makedirs(dirpath)forfilepathinfiles:open(filepath,'w').close()classAttrObject:# XXX cf mock_objectdef__init__(self,**kwargs):self.__dict__.update(kwargs)deftag(*args,**kwargs):"""descriptor adding tag to a function"""defdesc(func):assertnothasattr(func,'tags')func.tags=Tags(*args,**kwargs)returnfuncreturndescdefrequire_version(version):""" Compare version of python interpreter to the given one. Skip the test if older. """defcheck_require_version(f):version_elements=version.split('.')try:compare=tuple([int(v)forvinversion_elements])exceptValueError:raiseValueError('%s is not a correct version : should be X.Y[.Z].'%version)current=sys.version_info[:3]ifcurrent<compare:defnew_f(self,*args,**kwargs):self.skipTest('Need at least %s version of python. Current version is %s.'%(version,'.'.join([str(element)forelementincurrent])))new_f.__name__=f.__name__returnnew_felse:returnfreturncheck_require_versiondefrequire_module(module):""" Check if the given module is loaded. Skip the test if not. """defcheck_require_module(f):try:__import__(module)returnfexceptImportError:defnew_f(self,*args,**kwargs):self.skipTest('%s can not be imported.'%module)new_f.__name__=f.__name__returnnew_freturncheck_require_module