457 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			457 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
"""
 | 
						|
Python related collection nodes.  Here is an example of 
 | 
						|
a tree of collectors and test items that this modules provides:: 
 | 
						|
 | 
						|
        Module                  # File
 | 
						|
            Class 
 | 
						|
                Instance   
 | 
						|
                    Function  
 | 
						|
                    Generator 
 | 
						|
                        ... 
 | 
						|
            Function 
 | 
						|
            Generator 
 | 
						|
                Function 
 | 
						|
 | 
						|
        DoctestFile              # File
 | 
						|
            DoctestFileContent   # acts as Item 
 | 
						|
 | 
						|
""" 
 | 
						|
import py
 | 
						|
from py.__.test.collect import configproperty, warnoldcollect
 | 
						|
from py.__.code.source import findsource
 | 
						|
pydir = py.path.local(py.__file__).dirpath()
 | 
						|
 | 
						|
class PyobjMixin(object):
 | 
						|
    def obj(): 
 | 
						|
        def fget(self):
 | 
						|
            try: 
 | 
						|
                return self._obj   
 | 
						|
            except AttributeError: 
 | 
						|
                self._obj = obj = self._getobj() 
 | 
						|
                return obj 
 | 
						|
        def fset(self, value): 
 | 
						|
            self._obj = value 
 | 
						|
        return property(fget, fset, None, "underlying python object") 
 | 
						|
    obj = obj()
 | 
						|
 | 
						|
    def _getobj(self):
 | 
						|
        return getattr(self.parent.obj, self.name)
 | 
						|
 | 
						|
    def getmodpath(self, stopatmodule=True, includemodule=False):
 | 
						|
        """ return python path relative to the containing module. """
 | 
						|
        chain = self.listchain()
 | 
						|
        chain.reverse()
 | 
						|
        parts = []
 | 
						|
        for node in chain:
 | 
						|
            if isinstance(node, Instance):
 | 
						|
                continue
 | 
						|
            name = node.name 
 | 
						|
            if isinstance(node, Module):
 | 
						|
                assert name.endswith(".py")
 | 
						|
                name = name[:-3]
 | 
						|
                if stopatmodule:
 | 
						|
                    if includemodule:
 | 
						|
                        parts.append(name)
 | 
						|
                    break
 | 
						|
            parts.append(name)
 | 
						|
        parts.reverse()
 | 
						|
        s = ".".join(parts)
 | 
						|
        return s.replace(".[", "[")
 | 
						|
 | 
						|
    def getfslineno(self):
 | 
						|
        try:
 | 
						|
            return self._fslineno
 | 
						|
        except AttributeError:
 | 
						|
            pass
 | 
						|
        obj = self.obj
 | 
						|
        # let decorators etc specify a sane ordering
 | 
						|
        if hasattr(obj, 'place_as'):
 | 
						|
            obj = obj.place_as
 | 
						|
        try:
 | 
						|
            code = py.code.Code(obj)
 | 
						|
        except TypeError:
 | 
						|
            # fallback to 
 | 
						|
            fn = (py.std.inspect.getsourcefile(obj) or
 | 
						|
                  py.std.inspect.getfile(obj))
 | 
						|
            fspath = fn and py.path.local(fn) or None
 | 
						|
            if fspath:
 | 
						|
                try:
 | 
						|
                    _, lineno = findsource(obj)
 | 
						|
                except IOError:
 | 
						|
                    lineno = None
 | 
						|
            else:
 | 
						|
                lineno = None
 | 
						|
        else:
 | 
						|
            fspath = code.path
 | 
						|
            lineno = code.firstlineno 
 | 
						|
        self._fslineno = fspath, lineno
 | 
						|
        return fspath, lineno
 | 
						|
 | 
						|
    def repr_metainfo(self):
 | 
						|
        fspath, lineno = self.getfslineno()
 | 
						|
        modpath = self.getmodpath()
 | 
						|
        return self.ReprMetaInfo(
 | 
						|
            fspath=fspath, 
 | 
						|
            lineno=lineno,
 | 
						|
            modpath=modpath,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): 
 | 
						|
    Class = configproperty('Class')
 | 
						|
    Instance = configproperty('Instance')
 | 
						|
    Function = configproperty('Function')
 | 
						|
    Generator = configproperty('Generator')
 | 
						|
    
 | 
						|
    def funcnamefilter(self, name): 
 | 
						|
        return name.startswith('test') 
 | 
						|
    def classnamefilter(self, name): 
 | 
						|
        return name.startswith('Test')
 | 
						|
 | 
						|
    def collect(self):
 | 
						|
        l = self._deprecated_collect()
 | 
						|
        if l is not None:
 | 
						|
            return l
 | 
						|
        name2items = self._buildname2items()
 | 
						|
        colitems = name2items.values()
 | 
						|
        colitems.sort()
 | 
						|
        return colitems
 | 
						|
 | 
						|
    def _buildname2items(self): 
 | 
						|
        # NB. we avoid random getattrs and peek in the __dict__ instead
 | 
						|
        d = {}
 | 
						|
        dicts = [getattr(self.obj, '__dict__', {})]
 | 
						|
        for basecls in py.std.inspect.getmro(self.obj.__class__):
 | 
						|
            dicts.append(basecls.__dict__)
 | 
						|
        seen = {}
 | 
						|
        for dic in dicts:
 | 
						|
            for name, obj in dic.items():
 | 
						|
                if name in seen:
 | 
						|
                    continue
 | 
						|
                seen[name] = True
 | 
						|
                res = self.makeitem(name, obj)
 | 
						|
                if res is not None:
 | 
						|
                    d[name] = res 
 | 
						|
        return d
 | 
						|
 | 
						|
    def _deprecated_join(self, name):
 | 
						|
        if self.__class__.join != py.test.collect.Collector.join:
 | 
						|
            warnoldcollect()
 | 
						|
            return self.join(name)
 | 
						|
 | 
						|
    def makeitem(self, name, obj):
 | 
						|
        res = self.config.api.pytest_pymodule_makeitem(
 | 
						|
            modcol=self, name=name, obj=obj)
 | 
						|
        if res:
 | 
						|
            return res
 | 
						|
        if (self.classnamefilter(name)) and \
 | 
						|
            py.std.inspect.isclass(obj):
 | 
						|
            res = self._deprecated_join(name)
 | 
						|
            if res is not None:
 | 
						|
                return res 
 | 
						|
            return self.Class(name, parent=self)
 | 
						|
        elif self.funcnamefilter(name) and callable(obj): 
 | 
						|
            res = self._deprecated_join(name)
 | 
						|
            if res is not None:
 | 
						|
                return res 
 | 
						|
            if obj.func_code.co_flags & 32: # generator function 
 | 
						|
                return self.Generator(name, parent=self)
 | 
						|
            else: 
 | 
						|
                return self.Function(name, parent=self)
 | 
						|
 | 
						|
class Module(py.test.collect.File, PyCollectorMixin):
 | 
						|
    def _getobj(self):
 | 
						|
        return self._memoizedcall('_obj', self._importtestmodule)
 | 
						|
 | 
						|
    def _importtestmodule(self):
 | 
						|
        # we assume we are only called once per module 
 | 
						|
        mod = self.fspath.pyimport()
 | 
						|
        #print "imported test module", mod
 | 
						|
        self.config.pluginmanager.consider_module(mod)
 | 
						|
        return mod
 | 
						|
 | 
						|
    def setup(self): 
 | 
						|
        if getattr(self.obj, 'disabled', 0):
 | 
						|
            py.test.skip("%r is disabled" %(self.obj,))
 | 
						|
        if not self.config.option.nomagic:
 | 
						|
            #print "*" * 20, "INVOKE assertion", self
 | 
						|
            py.magic.invoke(assertion=1)
 | 
						|
        mod = self.obj
 | 
						|
        #self.config.pluginmanager.register(mod)
 | 
						|
        if hasattr(mod, 'setup_module'): 
 | 
						|
            self.obj.setup_module(mod)
 | 
						|
 | 
						|
    def teardown(self): 
 | 
						|
        if hasattr(self.obj, 'teardown_module'): 
 | 
						|
            self.obj.teardown_module(self.obj) 
 | 
						|
        if not self.config.option.nomagic:
 | 
						|
            #print "*" * 20, "revoke assertion", self
 | 
						|
            py.magic.revoke(assertion=1)
 | 
						|
        #self.config.pluginmanager.unregister(self.obj)
 | 
						|
 | 
						|
class Class(PyCollectorMixin, py.test.collect.Collector): 
 | 
						|
 | 
						|
    def collect(self):
 | 
						|
        l = self._deprecated_collect()
 | 
						|
        if l is not None:
 | 
						|
            return l
 | 
						|
        return [self.Instance(name="()", parent=self)]
 | 
						|
 | 
						|
    def setup(self): 
 | 
						|
        if getattr(self.obj, 'disabled', 0):
 | 
						|
            py.test.skip("%r is disabled" %(self.obj,))
 | 
						|
        setup_class = getattr(self.obj, 'setup_class', None)
 | 
						|
        if setup_class is not None: 
 | 
						|
            setup_class = getattr(setup_class, 'im_func', setup_class) 
 | 
						|
            setup_class(self.obj) 
 | 
						|
 | 
						|
    def teardown(self): 
 | 
						|
        teardown_class = getattr(self.obj, 'teardown_class', None) 
 | 
						|
        if teardown_class is not None: 
 | 
						|
            teardown_class = getattr(teardown_class, 'im_func', teardown_class) 
 | 
						|
            teardown_class(self.obj) 
 | 
						|
 | 
						|
    def _getsortvalue(self):  
 | 
						|
        return self.getfslineno()
 | 
						|
 | 
						|
class Instance(PyCollectorMixin, py.test.collect.Collector): 
 | 
						|
    def _getobj(self): 
 | 
						|
        return self.parent.obj()  
 | 
						|
    def Function(self): 
 | 
						|
        return getattr(self.obj, 'Function', 
 | 
						|
                       PyCollectorMixin.Function.__get__(self)) # XXX for python 2.2
 | 
						|
    def _keywords(self):
 | 
						|
        return []
 | 
						|
    Function = property(Function)
 | 
						|
 | 
						|
    #def __repr__(self):
 | 
						|
    #    return "<%s of '%s'>" %(self.__class__.__name__, 
 | 
						|
    #                         self.parent.obj.__name__)
 | 
						|
 | 
						|
    def newinstance(self):  
 | 
						|
        self.obj = self._getobj()
 | 
						|
        return self.obj
 | 
						|
 | 
						|
class FunctionMixin(PyobjMixin):
 | 
						|
    """ mixin for the code common to Function and Generator.
 | 
						|
    """
 | 
						|
    def _getsortvalue(self):  
 | 
						|
        return self.getfslineno()
 | 
						|
 | 
						|
    def setup(self): 
 | 
						|
        """ perform setup for this test function. """
 | 
						|
        if hasattr(self.obj, 'im_self'):
 | 
						|
            name = 'setup_method' 
 | 
						|
        else: 
 | 
						|
            name = 'setup_function' 
 | 
						|
        if isinstance(self.parent, Instance):
 | 
						|
            obj = self.parent.newinstance()
 | 
						|
            self.obj = self._getobj()
 | 
						|
        else:
 | 
						|
            obj = self.parent.obj 
 | 
						|
        setup_func_or_method = getattr(obj, name, None)
 | 
						|
        if setup_func_or_method is not None: 
 | 
						|
            return setup_func_or_method(self.obj) 
 | 
						|
 | 
						|
    def teardown(self): 
 | 
						|
        """ perform teardown for this test function. """
 | 
						|
        if hasattr(self.obj, 'im_self'):
 | 
						|
            name = 'teardown_method' 
 | 
						|
        else: 
 | 
						|
            name = 'teardown_function' 
 | 
						|
        obj = self.parent.obj 
 | 
						|
        teardown_func_or_meth = getattr(obj, name, None)
 | 
						|
        if teardown_func_or_meth is not None: 
 | 
						|
            teardown_func_or_meth(self.obj) 
 | 
						|
 | 
						|
    def _prunetraceback(self, traceback):
 | 
						|
        if hasattr(self, '_obj') and not self.config.option.fulltrace: 
 | 
						|
            code = py.code.Code(self.obj) 
 | 
						|
            path, firstlineno = code.path, code.firstlineno 
 | 
						|
            ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
 | 
						|
            if ntraceback == traceback:
 | 
						|
                ntraceback = ntraceback.cut(path=path)
 | 
						|
                if ntraceback == traceback:
 | 
						|
                    ntraceback = ntraceback.cut(excludepath=pydir)
 | 
						|
            traceback = ntraceback.filter()
 | 
						|
        return traceback 
 | 
						|
 | 
						|
    def repr_failure(self, excinfo, outerr):
 | 
						|
        return self._repr_failure_py(excinfo, outerr)
 | 
						|
 | 
						|
    shortfailurerepr = "F"
 | 
						|
 | 
						|
class Generator(FunctionMixin, PyCollectorMixin, py.test.collect.Collector): 
 | 
						|
    def collect(self):
 | 
						|
        # test generators are collectors yet participate in 
 | 
						|
        # the test-item setup and teardown protocol. 
 | 
						|
        # otherwise we could avoid global setupstate
 | 
						|
        self.config._setupstate.prepare(self) 
 | 
						|
        l = []
 | 
						|
        seen = {}
 | 
						|
        for i, x in py.builtin.enumerate(self.obj()): 
 | 
						|
            name, call, args = self.getcallargs(x)
 | 
						|
            if not callable(call): 
 | 
						|
                raise TypeError("%r yielded non callable test %r" %(self.obj, call,))
 | 
						|
            if name is None:
 | 
						|
                name = "[%d]" % i
 | 
						|
            else:
 | 
						|
                name = "['%s']" % name
 | 
						|
            if name in seen:
 | 
						|
                raise ValueError("%r generated tests with non-unique name %r" %(self, name))
 | 
						|
            seen[name] = True
 | 
						|
            l.append(self.Function(name, self, args=args, callobj=call))
 | 
						|
        return l
 | 
						|
        
 | 
						|
    def getcallargs(self, obj):
 | 
						|
        if not isinstance(obj, (tuple, list)):
 | 
						|
            obj = (obj,)
 | 
						|
        # explict naming
 | 
						|
        if isinstance(obj[0], basestring):
 | 
						|
            name = obj[0]
 | 
						|
            obj = obj[1:]
 | 
						|
        else:
 | 
						|
            name = None
 | 
						|
        call, args = obj[0], obj[1:]
 | 
						|
        return name, call, args 
 | 
						|
 | 
						|
#
 | 
						|
#  Test Items 
 | 
						|
#
 | 
						|
_dummy = object()
 | 
						|
class Function(FunctionMixin, py.test.collect.Item): 
 | 
						|
    """ a Function Item is responsible for setting up  
 | 
						|
        and executing a Python callable test object.
 | 
						|
    """
 | 
						|
    def __init__(self, name, parent=None, config=None, args=(), callobj=_dummy):
 | 
						|
        super(Function, self).__init__(name, parent, config=config) 
 | 
						|
        self._finalizers = []
 | 
						|
        self._args = args
 | 
						|
        self.funcargs = {}
 | 
						|
        if callobj is not _dummy: 
 | 
						|
            self._obj = callobj 
 | 
						|
 | 
						|
    def addfinalizer(self, func):
 | 
						|
        self._finalizers.append(func)
 | 
						|
        
 | 
						|
    def teardown(self):
 | 
						|
        finalizers = self._finalizers
 | 
						|
        while finalizers:
 | 
						|
            call = finalizers.pop()
 | 
						|
            call()
 | 
						|
        super(Function, self).teardown()
 | 
						|
 | 
						|
    def readkeywords(self):
 | 
						|
        d = super(Function, self).readkeywords()
 | 
						|
        d.update(self.obj.func_dict)
 | 
						|
        return d
 | 
						|
 | 
						|
    def runtest(self):
 | 
						|
        """ execute the given test function. """
 | 
						|
        if not self._deprecated_testexecution():
 | 
						|
            self.setupargs() # XXX move to setup() / consider funcargs plugin
 | 
						|
            ret = self.config.api.pytest_pyfunc_call(
 | 
						|
                pyfuncitem=self, args=self._args, kwargs=self.funcargs)
 | 
						|
 | 
						|
    def setupargs(self):
 | 
						|
        if self._args:
 | 
						|
            # generator case: we don't do anything then
 | 
						|
            pass
 | 
						|
        else:
 | 
						|
            # standard Python Test function/method case  
 | 
						|
            funcobj = self.obj 
 | 
						|
            startindex = getattr(funcobj, 'im_self', None) and 1 or 0 
 | 
						|
            argnames = py.std.inspect.getargs(self.obj.func_code)[0]
 | 
						|
            for i, argname in py.builtin.enumerate(argnames):
 | 
						|
                if i < startindex:
 | 
						|
                    continue 
 | 
						|
                request = self.getrequest(argname) 
 | 
						|
                try:
 | 
						|
                    self.funcargs[argname] = request.call_next_provider()
 | 
						|
                except request.Error:
 | 
						|
                    numdefaults = len(funcobj.func_defaults or ()) 
 | 
						|
                    if i + numdefaults >= len(argnames):
 | 
						|
                        continue # our args have defaults XXX issue warning? 
 | 
						|
                    else:
 | 
						|
                        request._raiselookupfailed()
 | 
						|
 | 
						|
    def __eq__(self, other):
 | 
						|
        try:
 | 
						|
            return (self.name == other.name and 
 | 
						|
                    self._args == other._args and
 | 
						|
                    self.parent == other.parent and
 | 
						|
                    self.obj == other.obj)
 | 
						|
        except AttributeError:
 | 
						|
            pass
 | 
						|
        return False
 | 
						|
    def __ne__(self, other):
 | 
						|
        return not self == other
 | 
						|
 | 
						|
    def getrequest(self, argname):
 | 
						|
        return FuncargRequest(pyfuncitem=self, argname=argname) 
 | 
						|
        
 | 
						|
 | 
						|
class FuncargRequest:
 | 
						|
    _argprefix = "pytest_funcarg__"
 | 
						|
 | 
						|
    class Error(LookupError):
 | 
						|
        """ error on performing funcarg request. """ 
 | 
						|
        
 | 
						|
    def __init__(self, pyfuncitem, argname):
 | 
						|
        # XXX make pyfuncitem _pyfuncitem 
 | 
						|
        self._pyfuncitem = pyfuncitem
 | 
						|
        self.argname = argname 
 | 
						|
        self.function = pyfuncitem.obj
 | 
						|
        self.config = pyfuncitem.config
 | 
						|
        self.fspath = pyfuncitem.fspath
 | 
						|
        self._plugins = self._getplugins()
 | 
						|
        self._methods = self.config.pluginmanager.listattr(
 | 
						|
            plugins=self._plugins, 
 | 
						|
            attrname=self._argprefix + str(argname)
 | 
						|
        )
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return "<FuncargRequest %r for %r>" %(self.argname, self._pyfuncitem)
 | 
						|
 | 
						|
 | 
						|
    def _getplugins(self):
 | 
						|
        plugins = []
 | 
						|
        current = self._pyfuncitem
 | 
						|
        while not isinstance(current, Module):
 | 
						|
            current = current.parent
 | 
						|
            if isinstance(current, (Instance, Module)):
 | 
						|
                plugins.insert(0, current.obj)
 | 
						|
        return self.config.pluginmanager.getplugins() + plugins 
 | 
						|
 | 
						|
    def call_next_provider(self):
 | 
						|
        if not self._methods:
 | 
						|
            raise self.Error("no provider methods left")
 | 
						|
        nextmethod = self._methods.pop()
 | 
						|
        return nextmethod(request=self)
 | 
						|
 | 
						|
    def addfinalizer(self, finalizer):
 | 
						|
        self._pyfuncitem.addfinalizer(finalizer)
 | 
						|
 | 
						|
    def maketempdir(self):
 | 
						|
        basetemp = self.config.getbasetemp()
 | 
						|
        tmp = py.path.local.make_numbered_dir(
 | 
						|
            prefix=self.function.__name__ + "_", 
 | 
						|
            keep=0, rootdir=basetemp)
 | 
						|
        return tmp
 | 
						|
 | 
						|
    def _raiselookupfailed(self):
 | 
						|
        available = []
 | 
						|
        for plugin in self._plugins:
 | 
						|
            for name in vars(plugin.__class__):
 | 
						|
                if name.startswith(self._argprefix):
 | 
						|
                    name = name[len(self._argprefix):]
 | 
						|
                    if name not in available:
 | 
						|
                        available.append(name) 
 | 
						|
        metainfo = self._pyfuncitem.repr_metainfo()
 | 
						|
        msg = "funcargument %r not found for: %s" %(self.argname,metainfo.verboseline())
 | 
						|
        msg += "\n available funcargs: %s" %(", ".join(available),)
 | 
						|
        raise LookupError(msg)
 | 
						|
 | 
						|
 | 
						|
        
 |