[svn r57321] merging the event branch:
* moving in test, misc, code, io directories and py/__init__.py * py/bin/_find.py does not print to stderr anymore * a few fixes to conftest files in other dirs some more fixes and adjustments pending --HG-- branch : trunk
This commit is contained in:
413
py/test/collect.py
Normal file
413
py/test/collect.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
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
|
||||
Directory
|
||||
CustomCollector # provided via conftest's
|
||||
CustomItem # provided via conftest's
|
||||
CustomItem # provided via conftest's
|
||||
Directory
|
||||
...
|
||||
|
||||
"""
|
||||
import py
|
||||
|
||||
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 SetupState(object):
|
||||
""" shared state for setting up/tearing down test items or collectors. """
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
|
||||
def teardown_all(self):
|
||||
while self.stack:
|
||||
col = self.stack.pop()
|
||||
col.teardown()
|
||||
|
||||
def teardown_exact(self, item):
|
||||
if self.stack and self.stack[-1] == item:
|
||||
col = self.stack.pop()
|
||||
col.teardown()
|
||||
|
||||
def prepare(self, colitem):
|
||||
""" setup objects along the collector chain to the test-method
|
||||
Teardown any unneccessary previously setup objects."""
|
||||
|
||||
needed_collectors = colitem.listchain()
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
break
|
||||
col = self.stack.pop()
|
||||
col.teardown()
|
||||
for col in needed_collectors[len(self.stack):]:
|
||||
#print "setting up", col
|
||||
col.setup()
|
||||
self.stack.append(col)
|
||||
|
||||
class ReprMetaInfo(object):
|
||||
def __init__(self, fspath=None, lineno=None, modpath=None):
|
||||
self.fspath = fspath
|
||||
self.lineno = lineno
|
||||
self.modpath = modpath
|
||||
|
||||
def verboseline(self, basedir=None):
|
||||
params = self.__dict__.copy()
|
||||
if self.fspath:
|
||||
if basedir is not None:
|
||||
params['fspath'] = getrelpath(basedir, self.fspath)
|
||||
if self.lineno is not None:
|
||||
params['lineno'] = self.lineno + 1
|
||||
|
||||
if self.fspath and self.lineno and self.modpath:
|
||||
line = "%(fspath)s:%(lineno)s: %(modpath)s"
|
||||
elif self.fspath and self.modpath:
|
||||
line = "%(fspath)s: %(modpath)s"
|
||||
elif self.fspath and self.lineno:
|
||||
line = "%(fspath)s:%(lineno)s"
|
||||
else:
|
||||
line = "[nometainfo]"
|
||||
return line % params
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Nodes in the collection tree.
|
||||
Collector nodes have children and
|
||||
Item nodes are terminal.
|
||||
|
||||
All nodes of the collection tree carry a _config
|
||||
attribute for these reasons:
|
||||
- to access custom Collection Nodes from a project
|
||||
(defined in conftest's)
|
||||
- to pickle themselves relatively to the "topdir"
|
||||
- configuration/options for setup/teardown
|
||||
stdout/stderr capturing and execution of test items
|
||||
"""
|
||||
ReprMetaInfo = ReprMetaInfo
|
||||
# XXX we keep global SetupState here because
|
||||
# pycollect's Generators participate
|
||||
# in setup/teardown procedures during collect.
|
||||
_setupstate = SetupState()
|
||||
def __init__(self, name, parent=None, config=None):
|
||||
self.name = name
|
||||
self.parent = parent
|
||||
if config is None:
|
||||
config = getattr(parent, '_config')
|
||||
self._config = config
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#
|
||||
# note to myself: Pickling is uh.
|
||||
#
|
||||
def __getstate__(self):
|
||||
return (self.name, self.parent)
|
||||
def __setstate__(self, (name, parent)):
|
||||
newnode = parent.join(name)
|
||||
assert newnode is not None, (self, name, parent)
|
||||
self.__dict__.update(newnode.__dict__)
|
||||
#self.__init__(name=name, parent=parent)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None))
|
||||
|
||||
# methods for ordering nodes
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Node):
|
||||
return False
|
||||
return self.name == other.name and self.parent == other.parent
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.parent))
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Node):
|
||||
return -1
|
||||
s1 = self._getsortvalue()
|
||||
s2 = other._getsortvalue()
|
||||
return cmp(s1, s2)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self. """
|
||||
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):
|
||||
cur = self
|
||||
for name in namelist:
|
||||
if name:
|
||||
next = cur.join(name)
|
||||
if next is None:
|
||||
existingnames = cur.listdir()
|
||||
msg = ("Collector %r does not have name %r "
|
||||
"existing names are: %s" %
|
||||
(cur, name, existingnames))
|
||||
raise AssertionError(msg)
|
||||
cur = next
|
||||
return cur
|
||||
|
||||
def _keywords(self):
|
||||
return [self.name]
|
||||
|
||||
def _skipbykeyword(self, keywordexpr):
|
||||
""" return True if they given keyword expression means to
|
||||
skip this collector/item.
|
||||
"""
|
||||
if not keywordexpr:
|
||||
return
|
||||
chain = self.listchain()
|
||||
for key in filter(None, keywordexpr.split()):
|
||||
eor = key[:1] == '-'
|
||||
if eor:
|
||||
key = key[1:]
|
||||
if not (eor ^ self._matchonekeyword(key, chain)):
|
||||
return True
|
||||
|
||||
def _matchonekeyword(self, key, chain):
|
||||
elems = key.split(".")
|
||||
# XXX O(n^2), anyone cares?
|
||||
chain = [item._keywords() for item in chain if item._keywords()]
|
||||
for start, _ in enumerate(chain):
|
||||
if start + len(elems) > len(chain):
|
||||
return False
|
||||
for num, elem in enumerate(elems):
|
||||
for keyword in chain[num + start]:
|
||||
ok = False
|
||||
if elem in keyword:
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
break
|
||||
if num == len(elems) - 1 and ok:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getsortvalue(self):
|
||||
return self.name
|
||||
|
||||
def _prunetraceback(self, traceback):
|
||||
return traceback
|
||||
|
||||
def _totrail(self):
|
||||
""" provide a trail relative to the topdir,
|
||||
which can be used to reconstruct the
|
||||
collector (possibly on a different host
|
||||
starting from a different topdir).
|
||||
"""
|
||||
chain = self.listchain()
|
||||
topdir = self._config.topdir
|
||||
relpath = chain[0].fspath.relto(topdir)
|
||||
if not relpath:
|
||||
if chain[0].fspath == topdir:
|
||||
relpath = "."
|
||||
else:
|
||||
raise ValueError("%r not relative to topdir %s"
|
||||
%(chain[0].fspath, topdir))
|
||||
return relpath, tuple([x.name for x in chain[1:]])
|
||||
|
||||
def _fromtrail(trail, config):
|
||||
relpath, names = trail
|
||||
fspath = config.topdir.join(relpath)
|
||||
col = config.getfsnode(fspath)
|
||||
return col._getitembynames(names)
|
||||
_fromtrail = staticmethod(_fromtrail)
|
||||
|
||||
def _repr_failure_py(self, excinfo, outerr):
|
||||
excinfo.traceback = self._prunetraceback(excinfo.traceback)
|
||||
repr = excinfo.getrepr(funcargs=True,
|
||||
showlocals=self._config.option.showlocals)
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
repr.addsection("Captured std%s" % secname, content.rstrip())
|
||||
return repr
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
shortfailurerepr = "F"
|
||||
|
||||
class Collector(Node):
|
||||
"""
|
||||
Collector instances generate children through
|
||||
their listdir() and join() methods and thus
|
||||
form a tree. attributes::
|
||||
|
||||
parent: attribute pointing to the parent collector
|
||||
(or None if this is the root collector)
|
||||
name: basename of this collector object
|
||||
"""
|
||||
Directory = configproperty('Directory')
|
||||
Module = configproperty('Module')
|
||||
DoctestFile = configproperty('DoctestFile')
|
||||
|
||||
def run(self):
|
||||
""" deprecated: use listdir(). """
|
||||
py.std.warnings.warn("deprecated: use listdir()", category=DeprecationWarning)
|
||||
return self.listdir()
|
||||
|
||||
def multijoin(self, namelist):
|
||||
""" return a list of colitems for the given namelist. """
|
||||
return [self.join(name) for name in namelist]
|
||||
|
||||
def listdir(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.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def join(self, name):
|
||||
""" return a child collector or item for the given name.
|
||||
If the return value is None there is no such child.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo, outerr):
|
||||
return self._repr_failure_py(excinfo, outerr)
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None):
|
||||
fspath = py.path.local(fspath)
|
||||
super(FSCollector, self).__init__(fspath.basename, parent, config=config)
|
||||
self.fspath = fspath
|
||||
|
||||
def __getstate__(self):
|
||||
if self.parent is None:
|
||||
# the root node needs to pickle more context info
|
||||
topdir = self._config.topdir
|
||||
relpath = self.fspath.relto(topdir)
|
||||
if not relpath:
|
||||
if self.fspath == topdir:
|
||||
relpath = "."
|
||||
else:
|
||||
raise ValueError("%r not relative to topdir %s"
|
||||
%(self.fspath, topdir))
|
||||
return (self.name, self._config, relpath)
|
||||
else:
|
||||
return (self.name, self.parent)
|
||||
|
||||
def __setstate__(self, picklestate):
|
||||
if len(picklestate) == 3:
|
||||
# root node
|
||||
name, config, relpath = picklestate
|
||||
if not config._initialized:
|
||||
raise ValueError("incomplete unpickling of "
|
||||
"config object, need call to _initafterpickle()?")
|
||||
fspath = config.topdir.join(relpath)
|
||||
fsnode = config.getfsnode(fspath)
|
||||
self.__dict__.update(fsnode.__dict__)
|
||||
else:
|
||||
name, parent = picklestate
|
||||
self.__init__(parent.fspath.join(name), parent=parent)
|
||||
|
||||
|
||||
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 listdir(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):
|
||||
# not use self.Directory here as
|
||||
# dir/conftest.py shall be able to
|
||||
# define Directory(dir) already
|
||||
Directory = self._config.getvalue('Directory', p)
|
||||
res = Directory(p, parent=self)
|
||||
name2items[name] = res
|
||||
return res
|
||||
|
||||
from py.__.test.runner import basic_run_report, forked_run_report
|
||||
class Item(Node):
|
||||
""" a basic test item. """
|
||||
def _getrunner(self):
|
||||
if self._config.option.boxed:
|
||||
return forked_run_report
|
||||
return basic_run_report
|
||||
|
||||
def repr_metainfo(self):
|
||||
try:
|
||||
return self.ReprMetaInfo(self.fspath, modpath=self.__class__.__name__)
|
||||
except AttributeError:
|
||||
code = py.code.Code(self.execute)
|
||||
return self.ReprMetaInfo(code.path, code.firstlineno)
|
||||
|
||||
def execute(self):
|
||||
""" execute this test item."""
|
||||
|
||||
|
||||
def getrelpath(curdir, dest):
|
||||
try:
|
||||
base = curdir.common(dest)
|
||||
if not base: # can be the case on windows
|
||||
return dest
|
||||
curdir2base = curdir.relto(base)
|
||||
reldest = dest.relto(base)
|
||||
if curdir2base:
|
||||
n = curdir2base.count(curdir.sep) + 1
|
||||
else:
|
||||
n = 0
|
||||
l = ['..'] * n
|
||||
if reldest:
|
||||
l.append(reldest)
|
||||
target = dest.sep.join(l)
|
||||
return target
|
||||
except AttributeError:
|
||||
return dest
|
||||
Reference in New Issue
Block a user