514 lines
16 KiB
Python
514 lines
16 KiB
Python
"""
|
|
Collect test items at filesystem and python module levels.
|
|
|
|
Collectors and test items form a tree. The difference
|
|
between a collector and a test item as seen from the session
|
|
is smalll. Collectors usually return a list of child
|
|
collectors/items whereas items usually return None
|
|
indicating a successful test run.
|
|
|
|
The is a schematic example of a tree of collectors and test items::
|
|
|
|
Directory
|
|
Module
|
|
Class
|
|
Instance
|
|
Function
|
|
Generator
|
|
...
|
|
Function
|
|
Generator
|
|
Function
|
|
Directory
|
|
...
|
|
|
|
"""
|
|
from __future__ import generators
|
|
import py
|
|
from py.__.test.outcome import Skipped
|
|
|
|
def configproperty(name):
|
|
def fget(self):
|
|
#print "retrieving %r property from %s" %(name, self.fspath)
|
|
return self._config.getvalue(name, self.fspath)
|
|
return property(fget)
|
|
|
|
class Collector(object):
|
|
""" Collector instances are iteratively generated
|
|
(through their run() and join() methods)
|
|
and form a tree. attributes::
|
|
|
|
parent: attribute pointing to the parent collector
|
|
(or None if it is the root collector)
|
|
name: basename of this collector object
|
|
"""
|
|
def __init__(self, name, parent=None):
|
|
self.name = name
|
|
self.parent = parent
|
|
self._config = getattr(parent, '_config', py.test.config)
|
|
if parent is not None:
|
|
if hasattr(parent, 'config'):
|
|
py.test.pdb()
|
|
self.fspath = getattr(parent, 'fspath', None)
|
|
|
|
Module = configproperty('Module')
|
|
DoctestFile = configproperty('DoctestFile')
|
|
Directory = configproperty('Directory')
|
|
Class = configproperty('Class')
|
|
Instance = configproperty('Instance')
|
|
Function = configproperty('Function')
|
|
Generator = configproperty('Generator')
|
|
|
|
_stickyfailure = None
|
|
|
|
def __repr__(self):
|
|
return "<%s %r>" %(self.__class__.__name__, self.name)
|
|
|
|
def __eq__(self, other):
|
|
# XXX a rather strict check for now to not confuse
|
|
# the SetupState.prepare() logic
|
|
return self is other
|
|
|
|
def __hash__(self):
|
|
return hash((self.name, self.parent))
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
def __cmp__(self, other):
|
|
s1 = self._getsortvalue()
|
|
s2 = other._getsortvalue()
|
|
#print "cmp", s1, s2
|
|
return cmp(s1, s2)
|
|
|
|
|
|
def run(self):
|
|
""" returns a list of names available from this collector.
|
|
You can return an empty list. Callers of this method
|
|
must take care to catch exceptions properly. The session
|
|
object guards its calls to ``colitem.run()`` in its
|
|
``session.runtraced(colitem)`` method, including
|
|
catching of stdout.
|
|
"""
|
|
raise NotImplementedError("abstract")
|
|
|
|
def join(self, name):
|
|
""" return a child item for the given name. Usually the
|
|
session feeds the join method with each name obtained
|
|
from ``colitem.run()``. If the return value is None
|
|
it means the ``colitem`` was not able to resolve
|
|
with the given name.
|
|
"""
|
|
|
|
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 object")
|
|
obj = obj()
|
|
|
|
def _getobj(self):
|
|
return getattr(self.parent.obj, self.name)
|
|
|
|
def multijoin(self, namelist):
|
|
""" return a list of colitems for the given namelist. """
|
|
return [self.join(name) for name in namelist]
|
|
|
|
def _getpathlineno(self):
|
|
return self.fspath, py.std.sys.maxint
|
|
|
|
def setup(self):
|
|
pass
|
|
|
|
def teardown(self):
|
|
pass
|
|
|
|
def listchain(self):
|
|
""" return list of all parent collectors up to ourself. """
|
|
l = [self]
|
|
while 1:
|
|
x = l[-1]
|
|
if x.parent is not None:
|
|
l.append(x.parent)
|
|
else:
|
|
l.reverse()
|
|
return l
|
|
|
|
def listnames(self):
|
|
return [x.name for x in self.listchain()]
|
|
|
|
def _getitembynames(self, namelist):
|
|
if isinstance(namelist, str):
|
|
namelist = namelist.split("/")
|
|
cur = self
|
|
for name in namelist:
|
|
if name:
|
|
next = cur.join(name)
|
|
assert next is not None, (cur, name, namelist)
|
|
cur = next
|
|
return cur
|
|
|
|
def _haskeyword(self, keyword):
|
|
return keyword in self.name
|
|
|
|
def _getmodpath(self):
|
|
""" return dotted module path (relative to the containing). """
|
|
inmodule = False
|
|
newl = []
|
|
for x in self.listchain():
|
|
if not inmodule and not isinstance(x, Module):
|
|
continue
|
|
if not inmodule:
|
|
inmodule = True
|
|
continue
|
|
if newl and x.name[:1] in '([':
|
|
newl[-1] += x.name
|
|
else:
|
|
newl.append(x.name)
|
|
return ".".join(newl)
|
|
|
|
def _skipbykeyword(self, keyword):
|
|
""" raise Skipped() exception if the given keyword
|
|
matches for this collector.
|
|
"""
|
|
if not keyword:
|
|
return
|
|
chain = self.listchain()
|
|
for key in filter(None, keyword.split()):
|
|
eor = key[:1] == '-'
|
|
if eor:
|
|
key = key[1:]
|
|
if not (eor ^ self._matchonekeyword(key, chain)):
|
|
py.test.skip("test not selected by keyword %r" %(keyword,))
|
|
|
|
def _matchonekeyword(self, key, chain):
|
|
for subitem in chain:
|
|
if subitem._haskeyword(key):
|
|
return True
|
|
return False
|
|
|
|
def _tryiter(self, yieldtype=None, reporterror=None, keyword=None):
|
|
""" yield stop item instances from flattening the collector.
|
|
XXX deprecated: this way of iteration is not safe in all
|
|
cases.
|
|
"""
|
|
if yieldtype is None:
|
|
yieldtype = py.test.collect.Item
|
|
if isinstance(self, yieldtype):
|
|
try:
|
|
self._skipbykeyword(keyword)
|
|
yield self
|
|
except Skipped:
|
|
if reporterror is not None:
|
|
excinfo = py.code.ExceptionInfo()
|
|
reporterror((excinfo, self))
|
|
else:
|
|
if not isinstance(self, py.test.collect.Item):
|
|
try:
|
|
if reporterror is not None:
|
|
reporterror((None, self))
|
|
for x in self.run():
|
|
for y in self.join(x)._tryiter(yieldtype,
|
|
reporterror, keyword):
|
|
yield y
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
if reporterror is not None:
|
|
excinfo = py.code.ExceptionInfo()
|
|
reporterror((excinfo, self))
|
|
|
|
def _getsortvalue(self):
|
|
return self.name
|
|
|
|
_captured_out = _captured_err = None
|
|
def startcapture(self):
|
|
return None # by default collectors don't capture output
|
|
|
|
def finishcapture(self):
|
|
return None # by default collectors don't capture output
|
|
|
|
def _getouterr(self):
|
|
return self._captured_out, self._captured_err
|
|
|
|
def _get_collector_trail(self):
|
|
""" Shortcut
|
|
"""
|
|
return self._config.get_collector_trail(self)
|
|
|
|
class FSCollector(Collector):
|
|
def __init__(self, fspath, parent=None):
|
|
fspath = py.path.local(fspath)
|
|
super(FSCollector, self).__init__(fspath.basename, parent)
|
|
self.fspath = fspath
|
|
|
|
class Directory(FSCollector):
|
|
def filefilter(self, path):
|
|
if path.check(file=1):
|
|
b = path.purebasename
|
|
ext = path.ext
|
|
return (b.startswith('test_') or
|
|
b.endswith('_test')) and ext in ('.txt', '.py')
|
|
|
|
def recfilter(self, path):
|
|
if path.check(dir=1, dotfile=0):
|
|
return path.basename not in ('CVS', '_darcs', '{arch}')
|
|
|
|
def run(self):
|
|
files = []
|
|
dirs = []
|
|
for p in self.fspath.listdir():
|
|
if self.filefilter(p):
|
|
files.append(p.basename)
|
|
elif self.recfilter(p):
|
|
dirs.append(p.basename)
|
|
files.sort()
|
|
dirs.sort()
|
|
return files + dirs
|
|
|
|
def join(self, name):
|
|
name2items = self.__dict__.setdefault('_name2items', {})
|
|
try:
|
|
res = name2items[name]
|
|
except KeyError:
|
|
p = self.fspath.join(name)
|
|
res = None
|
|
if p.check(file=1):
|
|
if p.ext == '.py':
|
|
res = self.Module(p, parent=self)
|
|
elif p.ext == '.txt':
|
|
res = self.DoctestFile(p, parent=self)
|
|
elif p.check(dir=1):
|
|
Directory = py.test.config.getvalue('Directory', p)
|
|
res = Directory(p, parent=self)
|
|
name2items[name] = res
|
|
return res
|
|
|
|
class PyCollectorMixin(object):
|
|
def funcnamefilter(self, name):
|
|
return name.startswith('test')
|
|
def classnamefilter(self, name):
|
|
return name.startswith('Test')
|
|
|
|
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 makeitem(self, name, obj, usefilters=True):
|
|
if (not usefilters or self.classnamefilter(name)) and \
|
|
py.std.inspect.isclass(obj):
|
|
return self.Class(name, parent=self)
|
|
elif (not usefilters or self.funcnamefilter(name)) and callable(obj):
|
|
if obj.func_code.co_flags & 32: # generator function
|
|
return self.Generator(name, parent=self)
|
|
else:
|
|
return self.Function(name, parent=self)
|
|
|
|
def _prepare(self):
|
|
if not hasattr(self, '_name2items'):
|
|
ex = getattr(self, '_name2items_exception', None)
|
|
if ex is not None:
|
|
raise ex[0], ex[1], ex[2]
|
|
try:
|
|
self._name2items = self._buildname2items()
|
|
except (SystemExit, KeyboardInterrupt):
|
|
raise
|
|
except:
|
|
self._name2items_exception = py.std.sys.exc_info()
|
|
raise
|
|
|
|
def run(self):
|
|
self._prepare()
|
|
itemlist = self._name2items.values()
|
|
itemlist.sort()
|
|
return [x.name for x in itemlist]
|
|
|
|
def join(self, name):
|
|
self._prepare()
|
|
return self._name2items.get(name, None)
|
|
|
|
|
|
class Module(FSCollector, PyCollectorMixin):
|
|
def run(self):
|
|
if getattr(self.obj, 'disabled', 0):
|
|
return []
|
|
return PyCollectorMixin.run(self)
|
|
|
|
def join(self, name):
|
|
res = super(Module, self).join(name)
|
|
if res is None:
|
|
attr = getattr(self.obj, name, None)
|
|
if attr is not None:
|
|
res = self.makeitem(name, attr, usefilters=False)
|
|
return res
|
|
|
|
def startcapture(self):
|
|
self._config._startcapture(self, path=self.fspath)
|
|
|
|
def finishcapture(self):
|
|
self._config._finishcapture(self)
|
|
|
|
def __repr__(self):
|
|
return "<%s %r>" % (self.__class__.__name__, self.name)
|
|
|
|
def obj(self):
|
|
try:
|
|
return self._obj
|
|
except AttributeError:
|
|
failure = getattr(self, '_stickyfailure', None)
|
|
if failure is not None:
|
|
raise failure[0], failure[1], failure[2]
|
|
try:
|
|
self._obj = obj = self.fspath.pyimport()
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
self._stickyfailure = py.std.sys.exc_info()
|
|
raise
|
|
return obj
|
|
obj = property(obj, None, None, "module object")
|
|
|
|
def setup(self):
|
|
if hasattr(self.obj, 'setup_module'):
|
|
self.obj.setup_module(self.obj)
|
|
|
|
def teardown(self):
|
|
if hasattr(self.obj, 'teardown_module'):
|
|
self.obj.teardown_module(self.obj)
|
|
|
|
|
|
class Class(PyCollectorMixin, Collector):
|
|
|
|
def run(self):
|
|
if getattr(self.obj, 'disabled', 0):
|
|
return []
|
|
return ["()"]
|
|
|
|
def join(self, name):
|
|
assert name == '()'
|
|
return self.Instance(name, self)
|
|
|
|
def setup(self):
|
|
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):
|
|
# try to locate the class in the source - not nice, but probably
|
|
# the most useful "solution" that we have
|
|
try:
|
|
file = (py.std.inspect.getsourcefile(self.obj) or
|
|
py.std.inspect.getfile(self.obj))
|
|
if not file:
|
|
raise IOError
|
|
lines, lineno = py.std.inspect.findsource(self.obj)
|
|
return py.path.local(file), lineno
|
|
except IOError:
|
|
pass
|
|
# fall back...
|
|
for x in self._tryiter((py.test.collect.Generator, py.test.collect.Item)):
|
|
return x._getsortvalue()
|
|
|
|
class Instance(PyCollectorMixin, Collector):
|
|
def _getobj(self):
|
|
return self.parent.obj()
|
|
def Function(self):
|
|
return getattr(self.obj, 'Function',
|
|
Collector.Function.__get__(self)) # XXX for python 2.2
|
|
Function = property(Function)
|
|
|
|
|
|
class FunctionMixin(object):
|
|
""" mixin for the code common to Function and Generator.
|
|
"""
|
|
def _getpathlineno(self):
|
|
code = py.code.Code(self.obj)
|
|
return code.path, code.firstlineno
|
|
|
|
def _getsortvalue(self):
|
|
return self._getpathlineno()
|
|
|
|
def setup(self):
|
|
""" perform setup for this test function. """
|
|
if getattr(self.obj, 'im_self', None):
|
|
name = 'setup_method'
|
|
else:
|
|
name = 'setup_function'
|
|
obj = self.parent.obj
|
|
meth = getattr(obj, name, None)
|
|
if meth is not None:
|
|
return meth(self.obj)
|
|
|
|
def teardown(self):
|
|
""" perform teardown for this test function. """
|
|
if getattr(self.obj, 'im_self', None):
|
|
name = 'teardown_method'
|
|
else:
|
|
name = 'teardown_function'
|
|
obj = self.parent.obj
|
|
meth = getattr(obj, name, None)
|
|
if meth is not None:
|
|
return meth(self.obj)
|
|
|
|
class Generator(FunctionMixin, PyCollectorMixin, Collector):
|
|
def run(self):
|
|
self._prepare()
|
|
itemlist = self._name2items
|
|
return [itemlist["[%d]" % num].name for num in xrange(len(itemlist))]
|
|
|
|
def _buildname2items(self):
|
|
d = {}
|
|
# slightly hackish to invoke setup-states on
|
|
# collection ...
|
|
self.Function._state.prepare(self)
|
|
for i, x in py.builtin.enumerate(self.obj()):
|
|
call, args = self.getcallargs(x)
|
|
if not callable(call):
|
|
raise TypeError("yielded test %r not callable" %(call,))
|
|
name = "[%d]" % i
|
|
d[name] = self.Function(name, self, args, obj=call, sort_value = i)
|
|
return d
|
|
|
|
def getcallargs(self, obj):
|
|
if isinstance(obj, (tuple, list)):
|
|
call, args = obj[0], obj[1:]
|
|
else:
|
|
call, args = obj, ()
|
|
return call, args
|
|
|
|
class DoctestFile(PyCollectorMixin, FSCollector):
|
|
def run(self):
|
|
return [self.fspath.basename]
|
|
|
|
def join(self, name):
|
|
from py.__.test.doctest import DoctestText
|
|
if name == self.fspath.basename:
|
|
item = DoctestText(self.fspath.basename, parent=self)
|
|
item._content = self.fspath.read()
|
|
return item
|